From 5b46753e1f1df52948517f2d24ddd1df56d9e5e1 Mon Sep 17 00:00:00 2001 From: VenishPaneliya <141703684+VenishPaneliya@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:09:54 +0530 Subject: [PATCH] Keep the sub-app's deprecated flag in add_typer() add_typer() declared `deprecated` as a plain False while every other setting in the same signature uses the Default() sentinel, including `hidden` on the line above it. solve_typer_info_defaults() relies on that sentinel to tell "the caller passed this to add_typer()" apart from "the caller said nothing". A bare False is not a DefaultPlaceholder, so Priority 1 always matched and the callback and sub-app instance were never consulted: a sub-app that marked itself deprecated lost the flag as soon as it was added to a parent. Passing deprecated=True to add_typer() directly already worked, and the sibling `hidden` propagates correctly, so only the default was wrong. Typer.command() also uses a bare False, but it builds a CommandInfo, which never goes through solve_typer_info_defaults(), so it is left as is. --- tests/test_deprecation.py | 35 +++++++++++++++++++++++++++++++++++ typer/main.py | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index 8f21bbf34f..c331385ad7 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -1,7 +1,10 @@ import pytest import typer +import typer.core from typer.testing import CliRunner +from tests.utils import needs_rich + runner = CliRunner() @@ -23,3 +26,35 @@ def cmd( match="The 'is_flag' and 'flag_value' parameters are not supported by Typer" ): add_command() + + +@pytest.mark.parametrize( + ("use_rich", "expected"), + [ + pytest.param(False, "(DEPRECATED)"), + pytest.param(True, "(deprecated)", marks=needs_rich), + ], +) +def test_add_typer_keeps_sub_app_deprecated( + monkeypatch: pytest.MonkeyPatch, use_rich: bool, expected: str +) -> None: + """A sub-app that marks itself deprecated stays deprecated once added. + + ``add_typer()`` has to leave its own ``deprecated`` default as a + ``Default()`` placeholder, otherwise it reads as an explicit argument and + overrides the value the sub-app set on itself. + """ + monkeypatch.setattr(typer.core, "HAS_RICH", use_rich) + + app = typer.Typer() + sub_app = typer.Typer(deprecated=True) + + @sub_app.command() + def sub_command() -> None: + """Sub command.""" # pragma: no cover + + app.add_typer(sub_app, name="sub") + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert expected in result.output diff --git a/typer/main.py b/typer/main.py index 700648da66..05f7143844 100644 --- a/typer/main.py +++ b/typer/main.py @@ -1073,7 +1073,7 @@ def add_typer( Mark this command as deprecated in the help outputs. `False` by default. """ ), - ] = False, + ] = Default(False), # Rich settings rich_help_panel: Annotated[ str | None,