From 172a4894695e4e2ca191900d469aa436f3882bbe Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 11 Dec 2024 16:24:45 +0100 Subject: [PATCH 01/20] extend test to cover issue reported in #1058 --- tests/test_rich_markup_mode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_rich_markup_mode.py b/tests/test_rich_markup_mode.py index d9fe5bae4b..e8d1925a96 100644 --- a/tests/test_rich_markup_mode.py +++ b/tests/test_rich_markup_mode.py @@ -20,6 +20,7 @@ def main(arg: str): assert "Hello World" in result.stdout result = runner.invoke(app, ["--help"]) + assert "ARG [required]" in result.stdout assert all(c not in result.stdout for c in rounded) From 91641165db1272c051a1257637734b21c3492eab Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Dec 2024 12:03:24 +0100 Subject: [PATCH 02/20] set rich_markup_mode as a setting on ctx.obj to only perform escaping when necessary --- typer/cli.py | 5 +++++ typer/core.py | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index 3fe3d3ee7f..aa6bc100d9 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -301,6 +301,11 @@ def docs( if not typer_obj: typer.echo("No Typer app found", err=True) raise typer.Abort() + if hasattr(typer_obj, "rich_markup_mode"): + if not hasattr(ctx, "obj") or ctx.obj is None: + ctx.ensure_object(dict) + if isinstance(ctx.obj, dict): + ctx.obj['TYPER_RICH_MARKUP_MODE'] = typer_obj.rich_markup_mode click_obj = typer.main.get_command(typer_obj) docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title) clean_docs = f"{docs.strip()}\n" diff --git a/typer/core.py b/typer/core.py index 4dc24ada70..227e882940 100644 --- a/typer/core.py +++ b/typer/core.py @@ -371,7 +371,10 @@ def get_help_record(self, ctx: click.Context) -> Optional[Tuple[str, str]]: if extra: extra_str = "; ".join(extra) extra_str = f"[{extra_str}]" - if rich is not None: + rich_markup_mode = None + if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): + rich_markup_mode = ctx.obj.get('TYPER_RICH_MARKUP_MODE', None) + if rich is not None and rich_markup_mode == "rich": # This is needed for when we want to export to HTML extra_str = rich.markup.escape(extra_str).strip() @@ -565,7 +568,11 @@ def _write_opts(opts: Sequence[str]) -> str: if extra: extra_str = "; ".join(extra) extra_str = f"[{extra_str}]" - if rich is not None: + + rich_markup_mode = None + if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): + rich_markup_mode = ctx.obj.get('TYPER_RICH_MARKUP_MODE', None) + if rich is not None and rich_markup_mode == "rich": # This is needed for when we want to export to HTML extra_str = rich.markup.escape(extra_str).strip() @@ -690,6 +697,10 @@ def main( def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: if not rich or self.rich_markup_mode is None: + if not hasattr(ctx, "obj") or ctx.obj is None: + ctx.ensure_object(dict) + if isinstance(ctx.obj, dict): + ctx.obj['TYPER_RICH_MARKUP_MODE'] = self.rich_markup_mode return super().format_help(ctx, formatter) return rich_utils.rich_format_help( obj=self, From c6a7347e73456265ab51abd776d992c3a7e12524 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:28:37 +0000 Subject: [PATCH 03/20] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/cli.py | 2 +- typer/core.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index aa6bc100d9..087efef625 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -305,7 +305,7 @@ def docs( if not hasattr(ctx, "obj") or ctx.obj is None: ctx.ensure_object(dict) if isinstance(ctx.obj, dict): - ctx.obj['TYPER_RICH_MARKUP_MODE'] = typer_obj.rich_markup_mode + ctx.obj["TYPER_RICH_MARKUP_MODE"] = typer_obj.rich_markup_mode click_obj = typer.main.get_command(typer_obj) docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title) clean_docs = f"{docs.strip()}\n" diff --git a/typer/core.py b/typer/core.py index 227e882940..39b55d2684 100644 --- a/typer/core.py +++ b/typer/core.py @@ -373,7 +373,7 @@ def get_help_record(self, ctx: click.Context) -> Optional[Tuple[str, str]]: extra_str = f"[{extra_str}]" rich_markup_mode = None if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): - rich_markup_mode = ctx.obj.get('TYPER_RICH_MARKUP_MODE', None) + rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) if rich is not None and rich_markup_mode == "rich": # This is needed for when we want to export to HTML extra_str = rich.markup.escape(extra_str).strip() @@ -571,7 +571,7 @@ def _write_opts(opts: Sequence[str]) -> str: rich_markup_mode = None if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): - rich_markup_mode = ctx.obj.get('TYPER_RICH_MARKUP_MODE', None) + rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) if rich is not None and rich_markup_mode == "rich": # This is needed for when we want to export to HTML extra_str = rich.markup.escape(extra_str).strip() @@ -700,7 +700,7 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non if not hasattr(ctx, "obj") or ctx.obj is None: ctx.ensure_object(dict) if isinstance(ctx.obj, dict): - ctx.obj['TYPER_RICH_MARKUP_MODE'] = self.rich_markup_mode + ctx.obj["TYPER_RICH_MARKUP_MODE"] = self.rich_markup_mode return super().format_help(ctx, formatter) return rich_utils.rich_format_help( obj=self, From 542d6bdd74f27de85037572a06068a9f1148fb9a Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Dec 2024 14:22:22 +0100 Subject: [PATCH 04/20] deal with DefaultPlaceholder value --- typer/cli.py | 6 +++++- typer/core.py | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index 087efef625..2f12af059b 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -10,6 +10,7 @@ from click import Command, Group, Option from . import __version__ +from .models import DefaultPlaceholder try: import rich @@ -305,7 +306,10 @@ def docs( if not hasattr(ctx, "obj") or ctx.obj is None: ctx.ensure_object(dict) if isinstance(ctx.obj, dict): - ctx.obj["TYPER_RICH_MARKUP_MODE"] = typer_obj.rich_markup_mode + if isinstance(typer_obj.rich_markup_mode, DefaultPlaceholder): + ctx.obj["TYPER_RICH_MARKUP_MODE"] = typer_obj.rich_markup_mode.value + else: + ctx.obj["TYPER_RICH_MARKUP_MODE"] = typer_obj.rich_markup_mode click_obj = typer.main.get_command(typer_obj) docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title) clean_docs = f"{docs.strip()}\n" diff --git a/typer/core.py b/typer/core.py index 39b55d2684..22c610e319 100644 --- a/typer/core.py +++ b/typer/core.py @@ -26,6 +26,8 @@ import click.types import click.utils +from .models import DefaultPlaceholder + if sys.version_info >= (3, 8): from typing import Literal else: @@ -700,7 +702,10 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non if not hasattr(ctx, "obj") or ctx.obj is None: ctx.ensure_object(dict) if isinstance(ctx.obj, dict): - ctx.obj["TYPER_RICH_MARKUP_MODE"] = self.rich_markup_mode + if isinstance(self.rich_markup_mode, DefaultPlaceholder): + ctx.obj["TYPER_RICH_MARKUP_MODE"] = self.rich_markup_mode.value + else: + ctx.obj["TYPER_RICH_MARKUP_MODE"] = self.rich_markup_mode return super().format_help(ctx, formatter) return rich_utils.rich_format_help( obj=self, From 72eb74bf4789b7405fa2ee2e2e8476cccffc5754 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Dec 2024 14:43:45 +0100 Subject: [PATCH 05/20] add another failing test --- tests/assets/cli/multi_app_norich.py | 40 +++++++++ tests/assets/cli/multiapp-docs-norich.md | 102 +++++++++++++++++++++++ tests/test_cli/test_doc.py | 24 ++++++ 3 files changed, 166 insertions(+) create mode 100644 tests/assets/cli/multi_app_norich.py create mode 100644 tests/assets/cli/multiapp-docs-norich.md diff --git a/tests/assets/cli/multi_app_norich.py b/tests/assets/cli/multi_app_norich.py new file mode 100644 index 0000000000..d61a963b69 --- /dev/null +++ b/tests/assets/cli/multi_app_norich.py @@ -0,0 +1,40 @@ +import typer + +sub_app = typer.Typer() + +variable = "Some text" + + +@sub_app.command() +def hello(name: str = "World", age: int = typer.Option(0, help="The age of the user")): + """ + Say Hello + """ + typer.echo(f"Hello {name}") + + +@sub_app.command() +def hi(user: str = typer.Argument("World", help="The name of the user to greet")): + """ + Say Hi + """ + + +@sub_app.command() +def bye(): + """ + Say bye + """ + typer.echo("sub bye") + + +app = typer.Typer(help="Demo App", epilog="The end", rich_markup_mode=None) +app.add_typer(sub_app, name="sub") + + +@app.command() +def top(): + """ + Top command + """ + typer.echo("top") diff --git a/tests/assets/cli/multiapp-docs-norich.md b/tests/assets/cli/multiapp-docs-norich.md new file mode 100644 index 0000000000..67d02568db --- /dev/null +++ b/tests/assets/cli/multiapp-docs-norich.md @@ -0,0 +1,102 @@ +# `multiapp` + +Demo App + +**Usage**: + +```console +$ multiapp [OPTIONS] COMMAND [ARGS]... +``` + +**Options**: + +* `--install-completion`: Install completion for the current shell. +* `--show-completion`: Show completion for the current shell, to copy it or customize the installation. +* `--help`: Show this message and exit. + +The end + +**Commands**: + +* `top`: Top command +* `sub` + +## `multiapp top` + +Top command + +**Usage**: + +```console +$ multiapp top [OPTIONS] +``` + +**Options**: + +* `--help`: Show this message and exit. + +## `multiapp sub` + +**Usage**: + +```console +$ multiapp sub [OPTIONS] COMMAND [ARGS]... +``` + +**Options**: + +* `--help`: Show this message and exit. + +**Commands**: + +* `hello`: Say Hello +* `hi`: Say Hi +* `bye`: Say bye + +### `multiapp sub hello` + +Say Hello + +**Usage**: + +```console +$ multiapp sub hello [OPTIONS] +``` + +**Options**: + +* `--name TEXT`: [default: World] +* `--age INTEGER`: The age of the user [default: 0] +* `--help`: Show this message and exit. + +### `multiapp sub hi` + +Say Hi + +**Usage**: + +```console +$ multiapp sub hi [OPTIONS] [USER] +``` + +**Arguments**: + +* `[USER]`: The name of the user to greet [default: World] + +**Options**: + +* `--help`: Show this message and exit. + +### `multiapp sub bye` + +Say bye + +**Usage**: + +```console +$ multiapp sub bye [OPTIONS] +``` + +**Options**: + +* `--help`: Show this message and exit. diff --git a/tests/test_cli/test_doc.py b/tests/test_cli/test_doc.py index f0d1d9b850..24b0a2da8a 100644 --- a/tests/test_cli/test_doc.py +++ b/tests/test_cli/test_doc.py @@ -28,6 +28,30 @@ def test_doc(): assert "**Arguments**" in result.stdout +def test_doc_no_rich(): + result = subprocess.run( + [ + sys.executable, + "-m", + "coverage", + "run", + "-m", + "typer", + "tests.assets.cli.multi_app_norich", + "utils", + "docs", + "--name", + "multiapp", + ], + capture_output=True, + encoding="utf-8", + ) + docs_path: Path = Path(__file__).parent.parent / "assets/cli/multiapp-docs.md" + docs = docs_path.read_text() + assert docs in result.stdout + assert "**Arguments**" in result.stdout + + def test_doc_output(tmp_path: Path): out_file: Path = tmp_path / "out.md" result = subprocess.run( From c02f0fd55c773cebd3416a27797a9c37e4185061 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Dec 2024 14:51:31 +0100 Subject: [PATCH 06/20] Ensure that rich_to_html is only called when escaping has happened correctly --- typer/cli.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index 2f12af059b..c5a047019b 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -203,6 +203,9 @@ def get_docs_for_click( title: Optional[str] = None, ) -> str: docs = "#" * (1 + indent) + rich_markup_mode = None + if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): + rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) command_name = name or obj.name if call_prefix: command_name = f"{call_prefix} {command_name}" @@ -210,7 +213,7 @@ def get_docs_for_click( title = f"`{command_name}`" if command_name else "CLI" docs += f" {title}\n\n" if obj.help: - docs += f"{_parse_html(obj.help)}\n\n" + docs += f"{_parse_html(obj.help, rich_markup_mode)}\n\n" usage_pieces = obj.collect_usage_pieces(ctx) if usage_pieces: docs += "**Usage**:\n\n" @@ -234,7 +237,7 @@ def get_docs_for_click( for arg_name, arg_help in args: docs += f"* `{arg_name}`" if arg_help: - docs += f": {_parse_html(arg_help)}" + docs += f": {_parse_html(arg_help, rich_markup_mode)}" docs += "\n" docs += "\n" if opts: @@ -242,7 +245,7 @@ def get_docs_for_click( for opt_name, opt_help in opts: docs += f"* `{opt_name}`" if opt_help: - docs += f": {_parse_html(opt_help)}" + docs += f": {_parse_html(opt_help, rich_markup_mode)}" docs += "\n" docs += "\n" if obj.epilog: @@ -258,7 +261,7 @@ def get_docs_for_click( docs += f"* `{command_obj.name}`" command_help = command_obj.get_short_help_str() if command_help: - docs += f": {_parse_html(command_help)}" + docs += f": {_parse_html(command_help, rich_markup_mode)}" docs += "\n" docs += "\n" for command in commands: @@ -273,10 +276,10 @@ def get_docs_for_click( return docs -def _parse_html(input_text: str) -> str: - if not has_rich: # pragma: no cover - return input_text - return rich_utils.rich_to_html(input_text) +def _parse_html(input_text: str, rich_markup_mode: None | str) -> str: + if has_rich and rich_markup_mode and rich_markup_mode == "rich": # pragma: no cover + return rich_utils.rich_to_html(input_text) + return input_text @utils_app.command() From dd7edfd8e24cd599167c57d8f16210ef581ba85a Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Dec 2024 14:58:51 +0100 Subject: [PATCH 07/20] Fix type annotation --- typer/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/cli.py b/typer/cli.py index c5a047019b..2d8507c030 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -276,7 +276,7 @@ def get_docs_for_click( return docs -def _parse_html(input_text: str, rich_markup_mode: None | str) -> str: +def _parse_html(input_text: str, rich_markup_mode: Optional[str]) -> str: if has_rich and rich_markup_mode and rich_markup_mode == "rich": # pragma: no cover return rich_utils.rich_to_html(input_text) return input_text From c687a85655ef9d159484b8cf7efbd480464da861 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Dec 2024 15:49:44 +0100 Subject: [PATCH 08/20] cleanup --- tests/assets/cli/multi_app_norich.py | 3 - tests/assets/cli/multiapp-docs-norich.md | 102 ----------------------- tests/test_cli/test_doc.py | 34 ++++---- 3 files changed, 17 insertions(+), 122 deletions(-) delete mode 100644 tests/assets/cli/multiapp-docs-norich.md diff --git a/tests/assets/cli/multi_app_norich.py b/tests/assets/cli/multi_app_norich.py index d61a963b69..32917f666d 100644 --- a/tests/assets/cli/multi_app_norich.py +++ b/tests/assets/cli/multi_app_norich.py @@ -10,7 +10,6 @@ def hello(name: str = "World", age: int = typer.Option(0, help="The age of the u """ Say Hello """ - typer.echo(f"Hello {name}") @sub_app.command() @@ -25,7 +24,6 @@ def bye(): """ Say bye """ - typer.echo("sub bye") app = typer.Typer(help="Demo App", epilog="The end", rich_markup_mode=None) @@ -37,4 +35,3 @@ def top(): """ Top command """ - typer.echo("top") diff --git a/tests/assets/cli/multiapp-docs-norich.md b/tests/assets/cli/multiapp-docs-norich.md deleted file mode 100644 index 67d02568db..0000000000 --- a/tests/assets/cli/multiapp-docs-norich.md +++ /dev/null @@ -1,102 +0,0 @@ -# `multiapp` - -Demo App - -**Usage**: - -```console -$ multiapp [OPTIONS] COMMAND [ARGS]... -``` - -**Options**: - -* `--install-completion`: Install completion for the current shell. -* `--show-completion`: Show completion for the current shell, to copy it or customize the installation. -* `--help`: Show this message and exit. - -The end - -**Commands**: - -* `top`: Top command -* `sub` - -## `multiapp top` - -Top command - -**Usage**: - -```console -$ multiapp top [OPTIONS] -``` - -**Options**: - -* `--help`: Show this message and exit. - -## `multiapp sub` - -**Usage**: - -```console -$ multiapp sub [OPTIONS] COMMAND [ARGS]... -``` - -**Options**: - -* `--help`: Show this message and exit. - -**Commands**: - -* `hello`: Say Hello -* `hi`: Say Hi -* `bye`: Say bye - -### `multiapp sub hello` - -Say Hello - -**Usage**: - -```console -$ multiapp sub hello [OPTIONS] -``` - -**Options**: - -* `--name TEXT`: [default: World] -* `--age INTEGER`: The age of the user [default: 0] -* `--help`: Show this message and exit. - -### `multiapp sub hi` - -Say Hi - -**Usage**: - -```console -$ multiapp sub hi [OPTIONS] [USER] -``` - -**Arguments**: - -* `[USER]`: The name of the user to greet [default: World] - -**Options**: - -* `--help`: Show this message and exit. - -### `multiapp sub bye` - -Say bye - -**Usage**: - -```console -$ multiapp sub bye [OPTIONS] -``` - -**Options**: - -* `--help`: Show this message and exit. diff --git a/tests/test_cli/test_doc.py b/tests/test_cli/test_doc.py index 24b0a2da8a..1da8bb73ef 100644 --- a/tests/test_cli/test_doc.py +++ b/tests/test_cli/test_doc.py @@ -28,7 +28,8 @@ def test_doc(): assert "**Arguments**" in result.stdout -def test_doc_no_rich(): +def test_doc_output(tmp_path: Path): + out_file: Path = tmp_path / "out.md" result = subprocess.run( [ sys.executable, @@ -37,22 +38,25 @@ def test_doc_no_rich(): "run", "-m", "typer", - "tests.assets.cli.multi_app_norich", + "tests.assets.cli.multi_app", "utils", "docs", "--name", "multiapp", + "--output", + str(out_file), ], capture_output=True, encoding="utf-8", ) docs_path: Path = Path(__file__).parent.parent / "assets/cli/multiapp-docs.md" docs = docs_path.read_text() - assert docs in result.stdout - assert "**Arguments**" in result.stdout + written_docs = out_file.read_text() + assert docs in written_docs + assert "Docs saved to:" in result.stdout -def test_doc_output(tmp_path: Path): +def test_doc_title_output(tmp_path: Path): out_file: Path = tmp_path / "out.md" result = subprocess.run( [ @@ -67,21 +71,22 @@ def test_doc_output(tmp_path: Path): "docs", "--name", "multiapp", + "--title", + "Awesome CLI", "--output", str(out_file), ], capture_output=True, encoding="utf-8", ) - docs_path: Path = Path(__file__).parent.parent / "assets/cli/multiapp-docs.md" + docs_path: Path = Path(__file__).parent.parent / "assets/cli/multiapp-docs-title.md" docs = docs_path.read_text() written_docs = out_file.read_text() assert docs in written_docs assert "Docs saved to:" in result.stdout -def test_doc_title_output(tmp_path: Path): - out_file: Path = tmp_path / "out.md" +def test_doc_no_rich(): result = subprocess.run( [ sys.executable, @@ -90,24 +95,19 @@ def test_doc_title_output(tmp_path: Path): "run", "-m", "typer", - "tests.assets.cli.multi_app", + "tests.assets.cli.multi_app_norich", "utils", "docs", "--name", "multiapp", - "--title", - "Awesome CLI", - "--output", - str(out_file), ], capture_output=True, encoding="utf-8", ) - docs_path: Path = Path(__file__).parent.parent / "assets/cli/multiapp-docs-title.md" + docs_path: Path = Path(__file__).parent.parent / "assets/cli/multiapp-docs.md" docs = docs_path.read_text() - written_docs = out_file.read_text() - assert docs in written_docs - assert "Docs saved to:" in result.stdout + assert docs in result.stdout + assert "**Arguments**" in result.stdout def test_doc_not_existing(): From 0a9c25373209ec27e4a2e5ff6262f020cbfce893 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Dec 2024 15:55:57 +0100 Subject: [PATCH 09/20] small refactor --- typer/cli.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index 2d8507c030..d31cc474dc 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -203,9 +203,6 @@ def get_docs_for_click( title: Optional[str] = None, ) -> str: docs = "#" * (1 + indent) - rich_markup_mode = None - if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): - rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) command_name = name or obj.name if call_prefix: command_name = f"{call_prefix} {command_name}" @@ -213,7 +210,7 @@ def get_docs_for_click( title = f"`{command_name}`" if command_name else "CLI" docs += f" {title}\n\n" if obj.help: - docs += f"{_parse_html(obj.help, rich_markup_mode)}\n\n" + docs += f"{_parse_html(ctx, obj.help)}\n\n" usage_pieces = obj.collect_usage_pieces(ctx) if usage_pieces: docs += "**Usage**:\n\n" @@ -237,7 +234,7 @@ def get_docs_for_click( for arg_name, arg_help in args: docs += f"* `{arg_name}`" if arg_help: - docs += f": {_parse_html(arg_help, rich_markup_mode)}" + docs += f": {_parse_html(ctx, arg_help)}" docs += "\n" docs += "\n" if opts: @@ -245,7 +242,7 @@ def get_docs_for_click( for opt_name, opt_help in opts: docs += f"* `{opt_name}`" if opt_help: - docs += f": {_parse_html(opt_help, rich_markup_mode)}" + docs += f": {_parse_html(ctx, opt_help)}" docs += "\n" docs += "\n" if obj.epilog: @@ -261,7 +258,7 @@ def get_docs_for_click( docs += f"* `{command_obj.name}`" command_help = command_obj.get_short_help_str() if command_help: - docs += f": {_parse_html(command_help, rich_markup_mode)}" + docs += f": {_parse_html(ctx, command_help)}" docs += "\n" docs += "\n" for command in commands: @@ -276,7 +273,10 @@ def get_docs_for_click( return docs -def _parse_html(input_text: str, rich_markup_mode: Optional[str]) -> str: +def _parse_html(ctx: click.Context, input_text: str) -> str: + rich_markup_mode = None + if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): + rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) if has_rich and rich_markup_mode and rich_markup_mode == "rich": # pragma: no cover return rich_utils.rich_to_html(input_text) return input_text From b6c2f8a8175b7c7ff1b84cd7b4cfe793a3e00eb5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:57:29 +0000 Subject: [PATCH 10/20] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/typer/core.py b/typer/core.py index 77e589f750..6254708477 100644 --- a/typer/core.py +++ b/typer/core.py @@ -26,9 +26,8 @@ import click.types import click.utils -from .models import DefaultPlaceholder - from ._typing import Literal +from .models import DefaultPlaceholder MarkupMode = Literal["markdown", "rich", None] From 12a023dfe0389c3fe387a098c1c54444820b2bc6 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 26 Aug 2025 14:19:55 +0200 Subject: [PATCH 11/20] refactor to avoid doing the same operations multiple times --- typer/cli.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index d31cc474dc..a6b0c3f80f 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -209,8 +209,12 @@ def get_docs_for_click( if not title: title = f"`{command_name}`" if command_name else "CLI" docs += f" {title}\n\n" + rich_markup_mode = None + if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): + rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) + to_parse = has_rich and rich_markup_mode and rich_markup_mode == "rich" if obj.help: - docs += f"{_parse_html(ctx, obj.help)}\n\n" + docs += f"{_parse_html(to_parse, obj.help)}\n\n" usage_pieces = obj.collect_usage_pieces(ctx) if usage_pieces: docs += "**Usage**:\n\n" @@ -234,7 +238,7 @@ def get_docs_for_click( for arg_name, arg_help in args: docs += f"* `{arg_name}`" if arg_help: - docs += f": {_parse_html(ctx, arg_help)}" + docs += f": {_parse_html(to_parse, arg_help)}" docs += "\n" docs += "\n" if opts: @@ -242,7 +246,7 @@ def get_docs_for_click( for opt_name, opt_help in opts: docs += f"* `{opt_name}`" if opt_help: - docs += f": {_parse_html(ctx, opt_help)}" + docs += f": {_parse_html(to_parse, opt_help)}" docs += "\n" docs += "\n" if obj.epilog: @@ -258,7 +262,7 @@ def get_docs_for_click( docs += f"* `{command_obj.name}`" command_help = command_obj.get_short_help_str() if command_help: - docs += f": {_parse_html(ctx, command_help)}" + docs += f": {_parse_html(to_parse, command_help)}" docs += "\n" docs += "\n" for command in commands: @@ -273,11 +277,8 @@ def get_docs_for_click( return docs -def _parse_html(ctx: click.Context, input_text: str) -> str: - rich_markup_mode = None - if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): - rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) - if has_rich and rich_markup_mode and rich_markup_mode == "rich": # pragma: no cover +def _parse_html(to_parse: bool, input_text: str) -> str: + if to_parse: return rich_utils.rich_to_html(input_text) return input_text From 5e24bc113c675ec55a1780143494b047da7b7638 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 26 Aug 2025 14:30:44 +0200 Subject: [PATCH 12/20] fix lint issue --- typer/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/cli.py b/typer/cli.py index a6b0c3f80f..574cf1bdf2 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -212,7 +212,7 @@ def get_docs_for_click( rich_markup_mode = None if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) - to_parse = has_rich and rich_markup_mode and rich_markup_mode == "rich" + to_parse: bool = bool(has_rich and (rich_markup_mode == "rich")) if obj.help: docs += f"{_parse_html(to_parse, obj.help)}\n\n" usage_pieces = obj.collect_usage_pieces(ctx) From 0a348da93467289e0ded5cd6eca7e007e923cd3d Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 26 Aug 2025 14:40:29 +0200 Subject: [PATCH 13/20] use global VAR for key --- typer/cli.py | 7 ++++--- typer/core.py | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index 574cf1bdf2..650b071b61 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -8,6 +8,7 @@ import typer import typer.core from click import Command, Group, Option +from core import MARKUP_MODE_KEY from . import __version__ from .models import DefaultPlaceholder @@ -211,7 +212,7 @@ def get_docs_for_click( docs += f" {title}\n\n" rich_markup_mode = None if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): - rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) + rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None) to_parse: bool = bool(has_rich and (rich_markup_mode == "rich")) if obj.help: docs += f"{_parse_html(to_parse, obj.help)}\n\n" @@ -311,9 +312,9 @@ def docs( ctx.ensure_object(dict) if isinstance(ctx.obj, dict): if isinstance(typer_obj.rich_markup_mode, DefaultPlaceholder): - ctx.obj["TYPER_RICH_MARKUP_MODE"] = typer_obj.rich_markup_mode.value + ctx.obj[MARKUP_MODE_KEY] = typer_obj.rich_markup_mode.value else: - ctx.obj["TYPER_RICH_MARKUP_MODE"] = typer_obj.rich_markup_mode + ctx.obj[MARKUP_MODE_KEY] = typer_obj.rich_markup_mode click_obj = typer.main.get_command(typer_obj) docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title) clean_docs = f"{docs.strip()}\n" diff --git a/typer/core.py b/typer/core.py index 6254708477..9513143544 100644 --- a/typer/core.py +++ b/typer/core.py @@ -30,6 +30,7 @@ from .models import DefaultPlaceholder MarkupMode = Literal["markdown", "rich", None] +MARKUP_MODE_KEY = "TYPER_RICH_MARKUP_MODE" try: import rich @@ -371,7 +372,7 @@ def get_help_record(self, ctx: click.Context) -> Optional[Tuple[str, str]]: extra_str = f"[{extra_str}]" rich_markup_mode = None if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): - rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) + rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None) if rich is not None and rich_markup_mode == "rich": # This is needed for when we want to export to HTML extra_str = rich.markup.escape(extra_str).strip() @@ -586,7 +587,7 @@ def _write_opts(opts: Sequence[str]) -> str: rich_markup_mode = None if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): - rich_markup_mode = ctx.obj.get("TYPER_RICH_MARKUP_MODE", None) + rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None) if rich is not None and rich_markup_mode == "rich": # This is needed for when we want to export to HTML extra_str = rich.markup.escape(extra_str).strip() @@ -716,9 +717,9 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non ctx.ensure_object(dict) if isinstance(ctx.obj, dict): if isinstance(self.rich_markup_mode, DefaultPlaceholder): - ctx.obj["TYPER_RICH_MARKUP_MODE"] = self.rich_markup_mode.value + ctx.obj[MARKUP_MODE_KEY] = self.rich_markup_mode.value else: - ctx.obj["TYPER_RICH_MARKUP_MODE"] = self.rich_markup_mode + ctx.obj[MARKUP_MODE_KEY] = self.rich_markup_mode return super().format_help(ctx, formatter) return rich_utils.rich_format_help( obj=self, From 6d42930e2106c8edd1e23c6172a260dfe7ef15fe Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 26 Aug 2025 14:42:05 +0200 Subject: [PATCH 14/20] fix --- typer/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/cli.py b/typer/cli.py index 650b071b61..2dfc9c7d05 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -8,7 +8,7 @@ import typer import typer.core from click import Command, Group, Option -from core import MARKUP_MODE_KEY +from .core import MARKUP_MODE_KEY from . import __version__ from .models import DefaultPlaceholder From 9a502ea91315a6bc9c32a280af106017bbf87760 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:42:13 +0000 Subject: [PATCH 15/20] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/cli.py b/typer/cli.py index 2dfc9c7d05..3ae154ad19 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -8,9 +8,9 @@ import typer import typer.core from click import Command, Group, Option -from .core import MARKUP_MODE_KEY from . import __version__ +from .core import MARKUP_MODE_KEY from .models import DefaultPlaceholder try: From 6354ea242679334ac6fb87ad440c63b2431890ff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 10:25:47 +0000 Subject: [PATCH 16/20] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typer/cli.py b/typer/cli.py index 15c9109d6f..ac68c2c14f 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -283,6 +283,8 @@ def _parse_html(to_parse: bool, input_text: str) -> str: return rich_utils.rich_to_html(input_text) return input_text + + def _parse_html(input_text: str) -> str: if not has_rich: # pragma: no cover return input_text From 77f95ddaff4739b7f6c7ee9f5959ff0df024b5fc Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 1 Sep 2025 12:27:22 +0200 Subject: [PATCH 17/20] remove duplicate leftover from merge conflict --- typer/cli.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index ac68c2c14f..a024c2ed42 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -285,14 +285,6 @@ def _parse_html(to_parse: bool, input_text: str) -> str: return input_text -def _parse_html(input_text: str) -> str: - if not has_rich: # pragma: no cover - return input_text - from . import rich_utils - - return rich_utils.rich_to_html(input_text) - - @utils_app.command() def docs( ctx: typer.Context, From 4f7470fb93e0890edeb7e34c40c04cd0340daedd Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 1 Sep 2025 12:28:29 +0200 Subject: [PATCH 18/20] follow diff more cleanly --- typer/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index a024c2ed42..2629ea9481 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -278,11 +278,11 @@ def get_docs_for_click( def _parse_html(to_parse: bool, input_text: str) -> str: - if to_parse: - from . import rich_utils + if not to_parse: + return input_text + from . import rich_utils - return rich_utils.rich_to_html(input_text) - return input_text + return rich_utils.rich_to_html(input_text) @utils_app.command() From 5971be07e1ad1a2b1ebc63363fb94acd5d1228f7 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 6 Jan 2026 11:53:59 +0100 Subject: [PATCH 19/20] simplify code now that the markup model can't be a Default instance anymore --- typer/cli.py | 5 +---- typer/core.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index d1a0a07387..891f0fd6e6 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -303,10 +303,7 @@ def docs( if not hasattr(ctx, "obj") or ctx.obj is None: ctx.ensure_object(dict) if isinstance(ctx.obj, dict): - if isinstance(typer_obj.rich_markup_mode, DefaultPlaceholder): - ctx.obj[MARKUP_MODE_KEY] = typer_obj.rich_markup_mode.value - else: - ctx.obj[MARKUP_MODE_KEY] = typer_obj.rich_markup_mode + ctx.obj[MARKUP_MODE_KEY] = typer_obj.rich_markup_mode click_obj = typer.main.get_command(typer_obj) docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title) clean_docs = f"{docs.strip()}\n" diff --git a/typer/core.py b/typer/core.py index 3fa125cc46..3365e74669 100644 --- a/typer/core.py +++ b/typer/core.py @@ -740,10 +740,7 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non if not hasattr(ctx, "obj") or ctx.obj is None: ctx.ensure_object(dict) if isinstance(ctx.obj, dict): - if isinstance(self.rich_markup_mode, DefaultPlaceholder): - ctx.obj[MARKUP_MODE_KEY] = self.rich_markup_mode.value - else: - ctx.obj[MARKUP_MODE_KEY] = self.rich_markup_mode + ctx.obj[MARKUP_MODE_KEY] = self.rich_markup_mode return super().format_help(ctx, formatter) from . import rich_utils From 7d44e6cea172903b20a9022dbd2338eb86e2c0ac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:54:35 +0000 Subject: [PATCH 20/20] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/cli.py | 1 - typer/core.py | 1 - 2 files changed, 2 deletions(-) diff --git a/typer/cli.py b/typer/cli.py index 891f0fd6e6..ef83bf87a0 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -11,7 +11,6 @@ from . import __version__ from .core import HAS_RICH, MARKUP_MODE_KEY -from .models import DefaultPlaceholder default_app_names = ("app", "cli", "main") default_func_names = ("main", "cli", "app") diff --git a/typer/core.py b/typer/core.py index 3365e74669..d0d888ccf0 100644 --- a/typer/core.py +++ b/typer/core.py @@ -24,7 +24,6 @@ import click.utils from ._typing import Literal -from .models import DefaultPlaceholder MarkupMode = Literal["markdown", "rich", None] MARKUP_MODE_KEY = "TYPER_RICH_MARKUP_MODE"