Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
172a489
extend test to cover issue reported in #1058
svlandeg Dec 11, 2024
9164116
set rich_markup_mode as a setting on ctx.obj to only perform escaping…
svlandeg Dec 12, 2024
c6a7347
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Dec 12, 2024
542d6bd
deal with DefaultPlaceholder value
svlandeg Dec 12, 2024
72eb74b
add another failing test
svlandeg Dec 12, 2024
c02f0fd
Ensure that rich_to_html is only called when escaping has happened co…
svlandeg Dec 12, 2024
dd7edfd
Fix type annotation
svlandeg Dec 12, 2024
c687a85
cleanup
svlandeg Dec 12, 2024
0a9c253
small refactor
svlandeg Dec 12, 2024
860d0df
Merge branch 'master' into fix/escape
svlandeg Dec 19, 2024
be06986
Merge branch 'master' into fix/escape
svlandeg Aug 26, 2025
b6c2f8a
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Aug 26, 2025
12a023d
refactor to avoid doing the same operations multiple times
svlandeg Aug 26, 2025
5e24bc1
fix lint issue
svlandeg Aug 26, 2025
92b0f78
Merge branch 'master' into fix/escape
svlandeg Aug 26, 2025
0a348da
use global VAR for key
svlandeg Aug 26, 2025
6d42930
fix
svlandeg Aug 26, 2025
9a502ea
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Aug 26, 2025
c1bbfe5
Merge branch 'master' into fix/escape
svlandeg Sep 1, 2025
6354ea2
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 1, 2025
77f95dd
remove duplicate leftover from merge conflict
svlandeg Sep 1, 2025
4f7470f
follow diff more cleanly
svlandeg Sep 1, 2025
b186f2a
Merge branch 'master' into fix/escape
svlandeg Sep 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions tests/assets/cli/multi_app_norich.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member Author

@svlandeg svlandeg Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of this new test is this: checking that rich_markup_mode=None, with rich installed, still produces correct results (showing [required], default values, etc and not accidentally 'removing' them because of incorrect escaping behaviour)

app.add_typer(sub_app, name="sub")


@app.command()
def top():
"""
Top command
"""
24 changes: 24 additions & 0 deletions tests/test_cli/test_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand Down
1 change: 1 addition & 0 deletions tests/test_rich_markup_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails with master, as pointed out in #1058 (it'll have a slash)

assert all(c not in result.stdout for c in rounded)


Expand Down
28 changes: 20 additions & 8 deletions typer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from click import Command, Group, Option

from . import __version__
from .models import DefaultPlaceholder

try:
import rich
Expand Down Expand Up @@ -202,14 +203,17 @@ 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}"
if not title:
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"
Expand All @@ -233,15 +237,15 @@ 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:
docs += "**Options**:\n\n"
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:
Expand All @@ -257,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:
Expand All @@ -272,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: 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


@utils_app.command()
Expand All @@ -301,6 +305,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["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"
Expand Down
20 changes: 18 additions & 2 deletions typer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -371,7 +373,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()

Expand Down Expand Up @@ -565,7 +570,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()

Expand Down Expand Up @@ -690,6 +699,13 @@ 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):
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,
Expand Down
Loading