diff --git a/.changelog/011.yaml b/.changelog/011.yaml new file mode 100644 index 0000000..d95b4a5 --- /dev/null +++ b/.changelog/011.yaml @@ -0,0 +1,7 @@ +name: console +ts: 2026-03-23 21:41:45.646042+00:00 +type: fix +author: Espen Albert +changelog_message: 'fix: also wrap logging for sub apps' +message: 'fix: also wrap logging for sub apps' +short_sha: a411a8 diff --git a/ask_shell/_internal/rich_progress_test.py b/ask_shell/_internal/rich_progress_test.py index 13c427b..a53fa02 100644 --- a/ask_shell/_internal/rich_progress_test.py +++ b/ask_shell/_internal/rich_progress_test.py @@ -1,5 +1,7 @@ import logging +import pytest + from ask_shell._internal.rich_live import get_live, pause_live from ask_shell._internal.rich_progress import ( get_default_progress_manager, @@ -70,3 +72,22 @@ def test_task_should_update_progress_with_logging(capture_console): assert "Test Task" in out assert "20%" in out assert "60%" in out + + +def test_task_context_system_exit_zero_logs_success(caplog): + caplog.set_level(logging.INFO) + with pytest.raises(SystemExit): + with new_task("se0", total=1): + raise SystemExit(0) + assert "✅" in caplog.text + + +def test_task_context_exception_with_exit_code_attr_zero_logs_success(caplog): + class QuietExit(Exception): + exit_code = 0 + + caplog.set_level(logging.INFO) + with pytest.raises(QuietExit): + with new_task("qe0", total=1): + raise QuietExit() + assert "✅" in caplog.text diff --git a/ask_shell/_internal/typer_command.py b/ask_shell/_internal/typer_command.py index 17b61b0..23aa7e1 100644 --- a/ask_shell/_internal/typer_command.py +++ b/ask_shell/_internal/typer_command.py @@ -88,6 +88,42 @@ def wrapper(*args, **kwargs): return decorator +def _wrap_typer_tree_commands( + app: typer.Typer, + *, + settings: AskShellSettings, + log_path_prefix: str, + skip_except_hook: bool, + use_app_name_command_for_logs: bool, + render_rich_error_on_sys_exit: bool, +) -> None: + for command in app.registered_commands: + command.callback = track_progress_decorator( + skip_except_hook=skip_except_hook, + settings=settings, + use_app_name_command_for_logs=use_app_name_command_for_logs, + app_name=log_path_prefix, + command_name=command.name or command.callback.__name__, # type: ignore + skip_rich_exception=not render_rich_error_on_sys_exit, + )( + command.callback # type: ignore + ) + for group in app.registered_groups: + nested = group.typer_instance + if nested is None: + continue + segment = group.name or nested.info.name or "group" + child_prefix = f"{log_path_prefix}/{segment}" + _wrap_typer_tree_commands( + nested, + settings=settings, + log_path_prefix=child_prefix, + skip_except_hook=skip_except_hook, + use_app_name_command_for_logs=use_app_name_command_for_logs, + render_rich_error_on_sys_exit=render_rich_error_on_sys_exit, + ) + + def remove_secrets(message: str, secrets: list[str]) -> str: for secret in secrets: message = message.replace(secret, "***") @@ -141,18 +177,15 @@ def configure_logging( render_rich_error_on_sys_exit: bool = False, ) -> logging.Handler: settings = settings or AskShellSettings.from_env() - app_name = app.info.name or "typer_app" - for command in app.registered_commands: - command.callback = track_progress_decorator( - skip_except_hook=skip_except_hook, - settings=settings, - use_app_name_command_for_logs=use_app_name_command_for_logs, - app_name=app_name, - command_name=command.name or command.callback.__name__, # type: ignore - skip_rich_exception=not render_rich_error_on_sys_exit, - )( - command.callback # type: ignore - ) + root_prefix = app.info.name or "typer_app" + _wrap_typer_tree_commands( + app, + settings=settings, + log_path_prefix=root_prefix, + skip_except_hook=skip_except_hook, + use_app_name_command_for_logs=use_app_name_command_for_logs, + render_rich_error_on_sys_exit=render_rich_error_on_sys_exit, + ) handler = RichHandler(rich_tracebacks=False, level=settings.log_level, console=get_live_console()) logging.basicConfig( level=settings.log_level, diff --git a/ask_shell/_internal/typer_command_test.py b/ask_shell/_internal/typer_command_test.py index 389a0b7..c021102 100644 --- a/ask_shell/_internal/typer_command_test.py +++ b/ask_shell/_internal/typer_command_test.py @@ -1,27 +1,35 @@ -import logging - -from ask_shell._internal.typer_command import hide_secrets - - -def test_hide_secrets(caplog, tmp_path): - root_logger = logging.getLogger() - assert root_logger.handlers - handler = root_logger.handlers[0] - assert isinstance(handler, logging.StreamHandler) - secrets = { - "SECRET_KEY": "my_secret_value", - "ANOTHER_KEY": "another", - "token": "adsfadf", - "ok": "some-value", - "my-secret-path": str(tmp_path), - } - hide_secrets(handler, secrets) - expect_hidden = {value for key, value in secrets.items() if key not in {"ok", "my-secret-path"}} - expect_shown = {value for key, value in secrets.items() if key in {"ok", "my-secret-path"}} - all_vars_logged = ",".join(f"{key}={value}" for key, value in secrets.items()) - root_logger.warning(f"Logging all variables: {all_vars_logged}") - output = caplog.text - found_hidden = {value for value in expect_hidden if value in output} - assert not found_hidden - found_shown: set[str] = {value for value in expect_shown if value in output} - assert found_shown == expect_shown, f"Expected to find {expect_shown}, but found {found_shown}" +import pytest +import typer + +from ask_shell._internal import typer_command + + +def test_configure_logging_wraps_commands_in_nested_typers() -> None: + root = typer.Typer(name="root") + sub = typer.Typer(help="sub") + + @sub.command("leaf") + def leaf() -> None: + return None + + root.add_typer(sub, name="grp") + nested = root.registered_groups[0].typer_instance + assert nested is not None + before = nested.registered_commands[0].callback + typer_command.configure_logging(root, skip_except_hook=True) + after = nested.registered_commands[0].callback + assert after is not before + + +def test_configure_logging_skips_group_without_typer_instance(monkeypatch: pytest.MonkeyPatch) -> None: + root = typer.Typer(name="r") + sub = typer.Typer(help="s") + + @sub.command("x") + def x() -> None: + return None + + root.add_typer(sub, name="g") + group_info = root.registered_groups[0] + monkeypatch.setattr(group_info, "typer_instance", None) + typer_command.configure_logging(root, skip_except_hook=True) diff --git a/docs/console/index.md b/docs/console/index.md index 08f14ed..193c83d 100644 --- a/docs/console/index.md +++ b/docs/console/index.md @@ -58,7 +58,7 @@ def add_renderable(renderable: ConsoleRenderable | RichCast | str, *, order: int ### function: `configure_logging` -- [source](../../ask_shell/_internal/typer_command.py#L133) +- [source](../../ask_shell/_internal/typer_command.py#L169) > **Since:** 0.3.0 ```python