diff --git a/tests/assets/cli/multi_app_norich.py b/tests/assets/cli/multi_app_norich.py new file mode 100644 index 0000000000..32917f666d --- /dev/null +++ b/tests/assets/cli/multi_app_norich.py @@ -0,0 +1,37 @@ +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 + """ + + +@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 + """ + + +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 + """ diff --git a/tests/test_cli/test_doc.py b/tests/test_cli/test_doc.py index f0d1d9b850..1da8bb73ef 100644 --- a/tests/test_cli/test_doc.py +++ b/tests/test_cli/test_doc.py @@ -86,6 +86,30 @@ def test_doc_title_output(tmp_path: Path): assert "Docs saved to:" 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_not_existing(): result = subprocess.run( [ diff --git a/tests/test_rich_markup_mode.py b/tests/test_rich_markup_mode.py index df5d0323f0..2d61d12893 100644 --- a/tests/test_rich_markup_mode.py +++ b/tests/test_rich_markup_mode.py @@ -23,6 +23,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) diff --git a/typer/cli.py b/typer/cli.py index 16b37c515d..525e2e5067 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -10,7 +10,8 @@ from click import Command, Group, Option from . import __version__ -from .core import HAS_RICH +from .core import HAS_RICH, MARKUP_MODE_KEY +from .models import DefaultPlaceholder default_app_names = ("app", "cli", "main") default_func_names = ("main", "cli", "app") @@ -199,8 +200,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(MARKUP_MODE_KEY, None) + to_parse: bool = bool(HAS_RICH and (rich_markup_mode == "rich")) if obj.help: - docs += f"{_parse_html(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" @@ -224,7 +229,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(to_parse, arg_help)}" docs += "\n" docs += "\n" if opts: @@ -232,7 +237,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(to_parse, opt_help)}" docs += "\n" docs += "\n" if obj.epilog: @@ -248,7 +253,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(to_parse, command_help)}" docs += "\n" docs += "\n" for command in commands: @@ -263,8 +268,8 @@ def get_docs_for_click( return docs -def _parse_html(input_text: str) -> str: - if not HAS_RICH: # pragma: no cover +def _parse_html(to_parse: bool, input_text: str) -> str: + if not to_parse: return input_text from . import rich_utils @@ -294,6 +299,14 @@ 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): + 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 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 048f28c137..e8bfbc399e 100644 --- a/typer/core.py +++ b/typer/core.py @@ -28,8 +28,10 @@ import click.utils from ._typing import Literal +from .models import DefaultPlaceholder MarkupMode = Literal["markdown", "rich", None] +MARKUP_MODE_KEY = "TYPER_RICH_MARKUP_MODE" HAS_RICH = importlib.util.find_spec("rich") is not None @@ -369,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 HAS_RICH: + rich_markup_mode = None + if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): + rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None) + if HAS_RICH and rich_markup_mode == "rich": # This is needed for when we want to export to HTML from . import rich_utils @@ -585,7 +590,10 @@ def _write_opts(opts: Sequence[str]) -> str: if extra: extra_str = "; ".join(extra) extra_str = f"[{extra_str}]" - if HAS_RICH: + rich_markup_mode = None + if hasattr(ctx, "obj") and isinstance(ctx.obj, dict): + rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None) + if HAS_RICH and rich_markup_mode == "rich": # This is needed for when we want to export to HTML from . import rich_utils @@ -729,6 +737,13 @@ def main( def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: if not HAS_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): + 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 return super().format_help(ctx, formatter) from . import rich_utils