Skip to content

Commit c0aeb6e

Browse files
svlandegpre-commit-ci[bot]tiangolopre-commit-ci-lite[bot]
authored
🐛 Fix escaping in help text when rich is installed but not used (#1089)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 66acaed commit c0aeb6e

5 files changed

Lines changed: 91 additions & 9 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import typer
2+
3+
sub_app = typer.Typer()
4+
5+
variable = "Some text"
6+
7+
8+
@sub_app.command()
9+
def hello(name: str = "World", age: int = typer.Option(0, help="The age of the user")):
10+
"""
11+
Say Hello
12+
"""
13+
14+
15+
@sub_app.command()
16+
def hi(user: str = typer.Argument("World", help="The name of the user to greet")):
17+
"""
18+
Say Hi
19+
"""
20+
21+
22+
@sub_app.command()
23+
def bye():
24+
"""
25+
Say bye
26+
"""
27+
28+
29+
app = typer.Typer(help="Demo App", epilog="The end", rich_markup_mode=None)
30+
app.add_typer(sub_app, name="sub")
31+
32+
33+
@app.command()
34+
def top():
35+
"""
36+
Top command
37+
"""

tests/test_cli/test_doc.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,30 @@ def test_doc_title_output(tmp_path: Path):
8686
assert "Docs saved to:" in result.stdout
8787

8888

89+
def test_doc_no_rich():
90+
result = subprocess.run(
91+
[
92+
sys.executable,
93+
"-m",
94+
"coverage",
95+
"run",
96+
"-m",
97+
"typer",
98+
"tests.assets.cli.multi_app_norich",
99+
"utils",
100+
"docs",
101+
"--name",
102+
"multiapp",
103+
],
104+
capture_output=True,
105+
encoding="utf-8",
106+
)
107+
docs_path: Path = Path(__file__).parent.parent / "assets/cli/multiapp-docs.md"
108+
docs = docs_path.read_text()
109+
assert docs in result.stdout
110+
assert "**Arguments**" in result.stdout
111+
112+
89113
def test_doc_not_existing():
90114
result = subprocess.run(
91115
[

tests/test_rich_markup_mode.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def main(arg: str):
2121
assert "Hello World" in result.stdout
2222

2323
result = runner.invoke(app, ["--help"])
24+
assert "ARG [required]" in result.stdout
2425
assert all(c not in result.stdout for c in rounded)
2526

2627

typer/cli.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from click import Command, Group, Option
1111

1212
from . import __version__
13-
from .core import HAS_RICH
13+
from .core import HAS_RICH, MARKUP_MODE_KEY
1414

1515
default_app_names = ("app", "cli", "main")
1616
default_func_names = ("main", "cli", "app")
@@ -199,8 +199,12 @@ def get_docs_for_click(
199199
if not title:
200200
title = f"`{command_name}`" if command_name else "CLI"
201201
docs += f" {title}\n\n"
202+
rich_markup_mode = None
203+
if hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
204+
rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None)
205+
to_parse: bool = bool(HAS_RICH and (rich_markup_mode == "rich"))
202206
if obj.help:
203-
docs += f"{_parse_html(obj.help)}\n\n"
207+
docs += f"{_parse_html(to_parse, obj.help)}\n\n"
204208
usage_pieces = obj.collect_usage_pieces(ctx)
205209
if usage_pieces:
206210
docs += "**Usage**:\n\n"
@@ -224,15 +228,15 @@ def get_docs_for_click(
224228
for arg_name, arg_help in args:
225229
docs += f"* `{arg_name}`"
226230
if arg_help:
227-
docs += f": {_parse_html(arg_help)}"
231+
docs += f": {_parse_html(to_parse, arg_help)}"
228232
docs += "\n"
229233
docs += "\n"
230234
if opts:
231235
docs += "**Options**:\n\n"
232236
for opt_name, opt_help in opts:
233237
docs += f"* `{opt_name}`"
234238
if opt_help:
235-
docs += f": {_parse_html(opt_help)}"
239+
docs += f": {_parse_html(to_parse, opt_help)}"
236240
docs += "\n"
237241
docs += "\n"
238242
if obj.epilog:
@@ -248,7 +252,7 @@ def get_docs_for_click(
248252
docs += f"* `{command_obj.name}`"
249253
command_help = command_obj.get_short_help_str()
250254
if command_help:
251-
docs += f": {_parse_html(command_help)}"
255+
docs += f": {_parse_html(to_parse, command_help)}"
252256
docs += "\n"
253257
docs += "\n"
254258
for command in commands:
@@ -263,8 +267,8 @@ def get_docs_for_click(
263267
return docs
264268

265269

266-
def _parse_html(input_text: str) -> str:
267-
if not HAS_RICH: # pragma: no cover
270+
def _parse_html(to_parse: bool, input_text: str) -> str:
271+
if not to_parse:
268272
return input_text
269273
from . import rich_utils
270274

@@ -294,6 +298,11 @@ def docs(
294298
if not typer_obj:
295299
typer.echo("No Typer app found", err=True)
296300
raise typer.Abort()
301+
if hasattr(typer_obj, "rich_markup_mode"):
302+
if not hasattr(ctx, "obj") or ctx.obj is None:
303+
ctx.ensure_object(dict)
304+
if isinstance(ctx.obj, dict):
305+
ctx.obj[MARKUP_MODE_KEY] = typer_obj.rich_markup_mode
297306
click_obj = typer.main.get_command(typer_obj)
298307
docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title)
299308
clean_docs = f"{docs.strip()}\n"

typer/core.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from ._typing import Literal
2727

2828
MarkupMode = Literal["markdown", "rich", None]
29+
MARKUP_MODE_KEY = "TYPER_RICH_MARKUP_MODE"
2930

3031
HAS_RICH = importlib.util.find_spec("rich") is not None
3132
HAS_SHELLINGHAM = importlib.util.find_spec("shellingham") is not None
@@ -366,7 +367,10 @@ def get_help_record(self, ctx: click.Context) -> Optional[tuple[str, str]]:
366367
if extra:
367368
extra_str = "; ".join(extra)
368369
extra_str = f"[{extra_str}]"
369-
if HAS_RICH:
370+
rich_markup_mode = None
371+
if hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
372+
rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None)
373+
if HAS_RICH and rich_markup_mode == "rich":
370374
# This is needed for when we want to export to HTML
371375
from . import rich_utils
372376

@@ -585,7 +589,10 @@ def _write_opts(opts: Sequence[str]) -> str:
585589
if extra:
586590
extra_str = "; ".join(extra)
587591
extra_str = f"[{extra_str}]"
588-
if HAS_RICH:
592+
rich_markup_mode = None
593+
if hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
594+
rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None)
595+
if HAS_RICH and rich_markup_mode == "rich":
589596
# This is needed for when we want to export to HTML
590597
from . import rich_utils
591598

@@ -729,6 +736,10 @@ def main(
729736

730737
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
731738
if not HAS_RICH or self.rich_markup_mode is None:
739+
if not hasattr(ctx, "obj") or ctx.obj is None:
740+
ctx.ensure_object(dict)
741+
if isinstance(ctx.obj, dict):
742+
ctx.obj[MARKUP_MODE_KEY] = self.rich_markup_mode
732743
return super().format_help(ctx, formatter)
733744
from . import rich_utils
734745

0 commit comments

Comments
 (0)