Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -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
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
27 changes: 20 additions & 7 deletions typer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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"
Expand All @@ -224,15 +229,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(to_parse, arg_help)}"
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(to_parse, opt_help)}"
docs += "\n"
docs += "\n"
if obj.epilog:
Expand All @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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"
Expand Down
19 changes: 17 additions & 2 deletions typer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
Loading