Skip to content

Commit c1bbfe5

Browse files
committed
Merge branch 'master' into fix/escape
2 parents 9a502ea + a5e41c5 commit c1bbfe5

11 files changed

Lines changed: 147 additions & 24 deletions

docs/release-notes.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@
22

33
## Latest Changes
44

5+
## 0.17.3
6+
7+
### Features
8+
9+
* ✨ Allow annotated parsing with a subclass of `Path`. PR [#1183](https://github.com/fastapi/typer/pull/1183) by [@emfdavid](https://github.com/emfdavid).
10+
11+
## 0.17.2
12+
13+
### Fixes
14+
15+
* 🐛 Avoid printing `default: None` in the help section when using Rich. PR [#1120](https://github.com/fastapi/typer/pull/1120) by [@mattmess1221](https://github.com/mattmess1221).
16+
17+
## 0.17.1
18+
19+
### Fixes
20+
21+
* 🐛 Fix markdown formatting in `--help` output. PR [#815](https://github.com/fastapi/typer/pull/815) by [@gar1t](https://github.com/gar1t).
22+
23+
## 0.17.0
24+
25+
### Features
26+
27+
* ⚡️ Lazy-load `rich_utils` to reduce startup time. PR [#1128](https://github.com/fastapi/typer/pull/1128) by [@oefe](https://github.com/oefe).
28+
529
### Internal
630

731
* ⬆ Bump ruff from 0.12.9 to 0.12.10. PR [#1280](https://github.com/fastapi/typer/pull/1280) by [@dependabot[bot]](https://github.com/apps/dependabot).

tests/assets/print_modules.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import sys
2+
3+
import typer
4+
5+
app = typer.Typer()
6+
7+
8+
@app.command()
9+
def main():
10+
for m in sys.modules:
11+
print(m)
12+
13+
14+
if __name__ == "__main__":
15+
app()

tests/test_annotated.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import sys
2+
from pathlib import Path
3+
14
import typer
25
from typer.testing import CliRunner
36
from typing_extensions import Annotated
@@ -76,3 +79,22 @@ def cmd(force: Annotated[bool, typer.Option("--force")] = False):
7679
result = runner.invoke(app, ["--force"])
7780
assert result.exit_code == 0, result.output
7881
assert "Forcing operation" in result.output
82+
83+
84+
def test_annotated_custom_path():
85+
app = typer.Typer()
86+
87+
class CustomPath(Path):
88+
# Subclassing Path was not fully supported before 3.12
89+
# https://docs.python.org/3.12/whatsnew/3.12.html
90+
if sys.version_info < (3, 12):
91+
_flavour = type(Path())._flavour
92+
93+
@app.command()
94+
def custom_parser(
95+
my_path: Annotated[CustomPath, typer.Argument(parser=CustomPath)],
96+
):
97+
assert isinstance(my_path, CustomPath)
98+
99+
result = runner.invoke(app, "/some/quirky/path/implementation")
100+
assert result.exit_code == 0

tests/test_rich_import.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import subprocess
2+
import sys
3+
from pathlib import Path
4+
5+
ACCEPTED_MODULES = {"rich._extension", "rich"}
6+
7+
8+
def test_rich_not_imported_unnecessary():
9+
file_path = Path(__file__).parent / "assets/print_modules.py"
10+
result = subprocess.run(
11+
[sys.executable, "-m", "coverage", "run", str(file_path)],
12+
capture_output=True,
13+
encoding="utf-8",
14+
)
15+
modules = result.stdout.splitlines()
16+
modules = [
17+
module
18+
for module in modules
19+
if module not in ACCEPTED_MODULES and module.startswith("rich")
20+
]
21+
assert not modules

tests/test_rich_markup_mode.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def main(arg: str):
5050
pytest.param(
5151
"markdown",
5252
["First line", "", "Line 1", "", "Line 2", "", "Line 3", ""],
53-
marks=pytest.mark.xfail,
5453
),
5554
pytest.param(
5655
"rich", ["First line", "", "Line 1", "", "Line 2", "", "Line 3", ""]
@@ -141,7 +140,6 @@ def main(arg: str):
141140
"Line 3",
142141
"",
143142
],
144-
marks=pytest.mark.xfail,
145143
),
146144
pytest.param(
147145
"rich",

tests/test_rich_utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,32 @@ def main() -> None:
5050

5151
assert result.exit_code == 0
5252
assert "Show this message" in result.stdout
53+
54+
55+
def test_rich_doesnt_print_None_default():
56+
app = typer.Typer(rich_markup_mode="rich")
57+
58+
@app.command()
59+
def main(
60+
name: str,
61+
option_1: str = typer.Option(
62+
"option_1_default",
63+
),
64+
option_2: str = typer.Option(
65+
...,
66+
),
67+
):
68+
print(f"Hello {name}")
69+
print(f"First: {option_1}")
70+
print(f"Second: {option_2}")
71+
72+
result = runner.invoke(app, ["--help"])
73+
assert "Usage" in result.stdout
74+
assert "name" in result.stdout
75+
assert "option-1" in result.stdout
76+
assert "option-2" in result.stdout
77+
assert result.stdout.count("[default: None]") == 0
78+
result = runner.invoke(app, ["Rick", "--option-2=Morty"])
79+
assert "Hello Rick" in result.stdout
80+
assert "First: option_1_default" in result.stdout
81+
assert "Second: Morty" in result.stdout

typer/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Typer, build great CLIs. Easy to code. Based on Python type hints."""
22

3-
__version__ = "0.16.1"
3+
__version__ = "0.17.3"
44

55
from shutil import get_terminal_size as get_terminal_size
66

typer/cli.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import rich
1818

1919
has_rich = True
20-
from . import rich_utils
2120

2221
except ImportError: # pragma: no cover
2322
has_rich = False
@@ -280,8 +279,16 @@ def get_docs_for_click(
280279

281280
def _parse_html(to_parse: bool, input_text: str) -> str:
282281
if to_parse:
282+
from . import rich_utils
283+
283284
return rich_utils.rich_to_html(input_text)
284285
return input_text
286+
def _parse_html(input_text: str) -> str:
287+
if not has_rich: # pragma: no cover
288+
return input_text
289+
from . import rich_utils
290+
291+
return rich_utils.rich_to_html(input_text)
285292

286293

287294
@utils_app.command()

typer/core.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@
3535
try:
3636
import rich
3737

38-
from . import rich_utils
39-
4038
DEFAULT_MARKUP_MODE: MarkupMode = "rich"
4139

4240
except ImportError: # pragma: no cover
@@ -215,6 +213,8 @@ def _main(
215213
raise
216214
# Typer override
217215
if rich and rich_markup_mode is not None:
216+
from . import rich_utils
217+
218218
rich_utils.rich_format_error(e)
219219
else:
220220
e.show()
@@ -245,6 +245,8 @@ def _main(
245245
raise
246246
# Typer override
247247
if rich and rich_markup_mode is not None:
248+
from . import rich_utils
249+
248250
rich_utils.rich_abort_error()
249251
else:
250252
click.echo(_("Aborted!"), file=sys.stderr)
@@ -721,6 +723,8 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non
721723
else:
722724
ctx.obj[MARKUP_MODE_KEY] = self.rich_markup_mode
723725
return super().format_help(ctx, formatter)
726+
from . import rich_utils
727+
724728
return rich_utils.rich_format_help(
725729
obj=self,
726730
ctx=ctx,
@@ -784,6 +788,8 @@ def main(
784788
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
785789
if not rich or self.rich_markup_mode is None:
786790
return super().format_help(ctx, formatter)
791+
from . import rich_utils
792+
787793
return rich_utils.rich_format_help(
788794
obj=self,
789795
ctx=ctx,

typer/main.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,6 @@
5151

5252
try:
5353
import rich
54-
from rich.traceback import Traceback
55-
56-
from . import rich_utils
57-
58-
console_stderr = rich_utils._get_rich_console(stderr=True)
5954

6055
except ImportError: # pragma: no cover
6156
rich = None # type: ignore
@@ -83,16 +78,19 @@ def except_hook(
8378
supress_internal_dir_names = [typer_path, click_path]
8479
exc = exc_value
8580
if rich:
86-
from .rich_utils import MAX_WIDTH
81+
from rich.traceback import Traceback
82+
83+
from . import rich_utils
8784

8885
rich_tb = Traceback.from_exception(
8986
type(exc),
9087
exc,
9188
exc.__traceback__,
9289
show_locals=exception_config.pretty_exceptions_show_locals,
9390
suppress=supress_internal_dir_names,
94-
width=MAX_WIDTH,
91+
width=rich_utils.MAX_WIDTH,
9592
)
93+
console_stderr = rich_utils._get_rich_console(stderr=True)
9694
console_stderr.print(rich_tb)
9795
return
9896
tb_exc = traceback.TracebackException.from_exception(exc)
@@ -622,7 +620,9 @@ def determine_type_convertor(type_: Any) -> Optional[Callable[[Any], Any]]:
622620

623621
def param_path_convertor(value: Optional[str] = None) -> Optional[Path]:
624622
if value is not None:
625-
return Path(value)
623+
# allow returning any subclass of Path created by an annotated parser without converting
624+
# it back to a Path
625+
return value if isinstance(value, Path) else Path(value)
626626
return None
627627

628628

0 commit comments

Comments
 (0)