Skip to content
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ github-actions = [
tests = [
"coverage[toml]>=6.2,<8.0",
"mypy==1.19.1",
"ty==0.0.9",
"pytest>=4.4.0,<9.0.0",
"pytest-cov>=2.10.0,<8.0.0",
"pytest-sugar>=0.9.4,<1.2.0",
Expand Down
1 change: 1 addition & 0 deletions scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ set -e
set -x

mypy typer
ty check typer
ruff check typer tests docs_src scripts
ruff format typer tests docs_src scripts --check
11 changes: 7 additions & 4 deletions typer/_completion_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import re
import sys
from typing import Any
from typing import Any, Callable, cast

import click
import click.parser
Expand All @@ -17,13 +17,16 @@
)

try:
from click.shell_completion import split_arg_string as click_split_arg_string
from click.shell_completion import split_arg_string as _split_arg_string
except ImportError: # pragma: no cover
# TODO: when removing support for Click < 8.2, remove this import
# TODO: when removing support for Click < 8.2, remove this import & the cast
from click.parser import ( # type: ignore[no-redef]
split_arg_string as click_split_arg_string,
split_arg_string as _split_arg_string,
)

# We need this cast to make ty happy
click_split_arg_string = cast(Callable[[str], list[str]], _split_arg_string)


def _sanitize_help_text(text: str) -> str:
"""Sanitizes the help text by removing rich tags"""
Expand Down
5 changes: 3 additions & 2 deletions typer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ def list_commands(self, ctx: click.Context) -> list[str]:
self.maybe_add_run(ctx)
return super().list_commands(ctx)

def get_command(self, ctx: click.Context, name: str) -> Optional[Command]:
def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[Command]:
self.maybe_add_run(ctx)
return super().get_command(ctx, name)
return super().get_command(ctx, cmd_name)

def invoke(self, ctx: click.Context) -> Any:
self.maybe_add_run(ctx)
Expand Down Expand Up @@ -131,6 +131,7 @@ def get_typer_from_state() -> Optional[typer.Typer]:
else:
typer.echo(f"Could not import as Python module: {state.module}", err=True)
sys.exit(1)
assert spec is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
obj = get_typer_from_module(module)
Expand Down
17 changes: 5 additions & 12 deletions typer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def except_hook(
_original_except_hook(exc_type, exc_value, tb)
return
typer_path = os.path.dirname(__file__)
assert click.__file__ is not None
click_path = os.path.dirname(click.__file__)
internal_dir_names = [typer_path, click_path]
exc = exc_value
Expand Down Expand Up @@ -395,42 +396,34 @@ def solve_typer_info_help(typer_info: TyperInfo) -> str:
if not isinstance(typer_info.help, DefaultPlaceholder):
return inspect.cleandoc(typer_info.help or "")
# Priority 2: Explicit value was set in sub_app.callback()
try:
if typer_info.typer_instance and typer_info.typer_instance.registered_callback:
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 edit and the lines below is not strictly necessary: they are "possibly-missing-attribute" warnings from ty that won't fail the CI. Still, IMO it's nice to test for this explicitely up-front instead of having the try-catch. This is a bit of personal preference though, so will leave it to Tiangolo to make the final decision.

Copy link
Member Author

Choose a reason for hiding this comment

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

Instead of making these edits, we have two other options:

  • add ty: ignore statements
  • just leave the warnings as is (though this is somewhat annoying when running the tool locally and getting all the warnings output)

callback_help = typer_info.typer_instance.registered_callback.help
if not isinstance(callback_help, DefaultPlaceholder):
return inspect.cleandoc(callback_help or "")
except AttributeError:
pass
# Priority 3: Explicit value was set in sub_app = typer.Typer()
try:
if typer_info.typer_instance and typer_info.typer_instance.info:
instance_help = typer_info.typer_instance.info.help
if not isinstance(instance_help, DefaultPlaceholder):
return inspect.cleandoc(instance_help or "")
except AttributeError:
pass
# Priority 4: Implicit inference from callback docstring in app.add_typer()
if typer_info.callback:
doc = inspect.getdoc(typer_info.callback)
if doc:
return doc
# Priority 5: Implicit inference from callback docstring in @app.callback()
try:
if typer_info.typer_instance and typer_info.typer_instance.registered_callback:
callback = typer_info.typer_instance.registered_callback.callback
if not isinstance(callback, DefaultPlaceholder):
doc = inspect.getdoc(callback or "")
if doc:
return doc
except AttributeError:
pass
# Priority 6: Implicit inference from callback docstring in typer.Typer()
try:
if typer_info.typer_instance and typer_info.typer_instance.info:
instance_callback = typer_info.typer_instance.info.callback
if not isinstance(instance_callback, DefaultPlaceholder):
doc = inspect.getdoc(instance_callback)
if doc:
return doc
except AttributeError:
pass
# Value not set, use the default
return typer_info.help.value

Expand Down
29 changes: 29 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.