diff --git a/tests/test_single_command_callback.py b/tests/test_single_command_callback.py new file mode 100644 index 0000000000..f848cf6660 --- /dev/null +++ b/tests/test_single_command_callback.py @@ -0,0 +1,59 @@ +import typer +from typer.testing import CliRunner + +runner: CliRunner = CliRunner() + + +def test_result_callback_single_command() -> None: + # A list to capture the result from the callback + captured_results: list[str] = [] + + def my_callback(value: str) -> None: + captured_results.append(value) + + # Create app with a result_callback + app: typer.Typer = typer.Typer(result_callback=my_callback) + + @app.command() + def main() -> str: + return "single_command_result" + + # Invoke the app (using the single command fast-path) + result = runner.invoke(app, []) + + # Verify the command ran successfully + assert result.exit_code == 0 + + # CRITICAL: Verify the callback was actually executed + assert "single_command_result" in captured_results, ( + "Result callback was not triggered for single command!" + ) + + +def test_result_callback_single_command_placeholder() -> None: + from typer.models import DefaultPlaceholder + + # A list to capture the result from the callback + captured_results: list[str] = [] + + def my_callback(value: str) -> None: + captured_results.append(value) + + # Create app and manually inject a DefaultPlaceholder for the result_callback + app = typer.Typer() + app.info.result_callback = DefaultPlaceholder(my_callback) + + @app.command() + def main() -> str: + return "placeholder_result" + + # Invoke the app + result = runner.invoke(app, []) + + # Verify the command ran successfully + assert result.exit_code == 0 + + # Verify the callback was actually executed via the placeholder path + assert "placeholder_result" in captured_results, ( + "Result callback was not triggered via placeholder!" + ) diff --git a/typer/main.py b/typer/main.py index 0bea4c2315..824b402e6a 100644 --- a/typer/main.py +++ b/typer/main.py @@ -1200,6 +1200,25 @@ def get_command(typer_instance: Typer) -> click.Command: if typer_instance._add_completion: click_command.params.append(click_install_param) click_command.params.append(click_show_param) + + click_callback = click_command.callback + use_result_callback = None + if typer_instance.info.result_callback: + if isinstance(typer_instance.info.result_callback, DefaultPlaceholder): + use_result_callback = typer_instance.info.result_callback.value + else: + use_result_callback = typer_instance.info.result_callback + + if click_callback and use_result_callback: + + def callback_wrapper(*args: Any, **kwargs: Any) -> Any: + result = click_callback(*args, **kwargs) + ctx = click.get_current_context() + return ctx.invoke(use_result_callback, result) + + update_wrapper(callback_wrapper, click_callback) + click_command.callback = callback_wrapper + return click_command raise RuntimeError( "Could not get a command for this Typer instance"