Skip to content

Commit 449d38f

Browse files
authored
Fix types, introduce type tests (#2562)
1 parent d17dbc2 commit 449d38f

14 files changed

Lines changed: 157 additions & 19 deletions

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Version 8.1.5
55

66
Unreleased
77

8+
- Fix type hints for ``@click.command()`` and ``@click.option()``. Introduce typing
9+
tests. :issue:`2558`
10+
811

912
Version 8.1.4
1013
-------------

requirements/dev.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ filelock==3.12.2
3030
# virtualenv
3131
identify==2.5.24
3232
# via pre-commit
33-
nodeenv==1.8.0
34-
# via pre-commit
3533
pip-compile-multi==2.6.3
3634
# via -r requirements/dev.in
3735
pip-tools==6.13.0

requirements/typing.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
mypy
2+
pyright

requirements/typing.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SHA1:7983aaa01d64547827c20395d77e248c41b2572f
1+
# SHA1:0d25c235a98f3c8c55aefb59b91c82834e185f0a
22
#
33
# This file is autogenerated by pip-compile-multi
44
# To update, run:
@@ -9,5 +9,12 @@ mypy==1.4.1
99
# via -r requirements/typing.in
1010
mypy-extensions==1.0.0
1111
# via mypy
12+
nodeenv==1.8.0
13+
# via pyright
14+
pyright==1.1.317
15+
# via -r requirements/typing.in
1216
typing-extensions==4.6.3
1317
# via mypy
18+
19+
# The following packages are considered to be unsafe in a requirements file:
20+
# setuptools

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ per-file-ignores =
7878
src/click/__init__.py: F401
7979

8080
[mypy]
81-
files = src/click
81+
files = src/click, tests/typing
8282
python_version = 3.7
8383
show_error_codes = True
8484
disallow_subclassing_any = True

src/click/decorators.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
R = t.TypeVar("R")
2222
T = t.TypeVar("T")
2323
_AnyCallable = t.Callable[..., t.Any]
24-
_Decorator: "te.TypeAlias" = t.Callable[[T], T]
2524
FC = t.TypeVar("FC", bound=t.Union[_AnyCallable, Command])
2625

2726

@@ -150,16 +149,12 @@ def command(
150149
...
151150

152151

153-
# variant: name omitted, cls _must_ be a keyword argument, @command(cmd=CommandCls, ...)
154-
# The correct way to spell this overload is to use keyword-only argument syntax:
155-
# def command(*, cls: t.Type[CmdType], **attrs: t.Any) -> ...
156-
# However, mypy thinks this doesn't fit the overloaded function. Pyright does
157-
# accept that spelling, and the following work-around makes pyright issue a
158-
# warning that CmdType could be left unsolved, but mypy sees it as fine. *shrug*
152+
# variant: name omitted, cls _must_ be a keyword argument, @command(cls=CommandCls, ...)
159153
@t.overload
160154
def command(
161155
name: None = None,
162-
cls: t.Type[CmdType] = ...,
156+
*,
157+
cls: t.Type[CmdType],
163158
**attrs: t.Any,
164159
) -> t.Callable[[_AnyCallable], CmdType]:
165160
...
@@ -331,7 +326,7 @@ def _param_memo(f: t.Callable[..., t.Any], param: Parameter) -> None:
331326

332327
def argument(
333328
*param_decls: str, cls: t.Optional[t.Type[Argument]] = None, **attrs: t.Any
334-
) -> _Decorator[FC]:
329+
) -> t.Callable[[FC], FC]:
335330
"""Attaches an argument to the command. All positional arguments are
336331
passed as parameter declarations to :class:`Argument`; all keyword
337332
arguments are forwarded unchanged (except ``cls``).
@@ -359,7 +354,7 @@ def decorator(f: FC) -> FC:
359354

360355
def option(
361356
*param_decls: str, cls: t.Optional[t.Type[Option]] = None, **attrs: t.Any
362-
) -> _Decorator[FC]:
357+
) -> t.Callable[[FC], FC]:
363358
"""Attaches an option to the command. All positional arguments are
364359
passed as parameter declarations to :class:`Option`; all keyword
365360
arguments are forwarded unchanged (except ``cls``).
@@ -385,7 +380,7 @@ def decorator(f: FC) -> FC:
385380
return decorator
386381

387382

388-
def confirmation_option(*param_decls: str, **kwargs: t.Any) -> _Decorator[FC]:
383+
def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
389384
"""Add a ``--yes`` option which shows a prompt before continuing if
390385
not passed. If the prompt is declined, the program will exit.
391386
@@ -409,7 +404,7 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None:
409404
return option(*param_decls, **kwargs)
410405

411406

412-
def password_option(*param_decls: str, **kwargs: t.Any) -> _Decorator[FC]:
407+
def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
413408
"""Add a ``--password`` option which prompts for a password, hiding
414409
input and asking to enter the value again for confirmation.
415410
@@ -433,7 +428,7 @@ def version_option(
433428
prog_name: t.Optional[str] = None,
434429
message: t.Optional[str] = None,
435430
**kwargs: t.Any,
436-
) -> _Decorator[FC]:
431+
) -> t.Callable[[FC], FC]:
437432
"""Add a ``--version`` option which immediately prints the version
438433
number and exits the program.
439434
@@ -539,7 +534,7 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None:
539534
return option(*param_decls, **kwargs)
540535

541536

542-
def help_option(*param_decls: str, **kwargs: t.Any) -> _Decorator[FC]:
537+
def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
543538
"""Add a ``--help`` option which immediately prints the help page
544539
and exits the program.
545540
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Example from https://click.palletsprojects.com/en/8.1.x/advanced/#command-aliases"""
2+
from __future__ import annotations
3+
4+
from typing_extensions import assert_type
5+
6+
import click
7+
8+
9+
class AliasedGroup(click.Group):
10+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
11+
rv = click.Group.get_command(self, ctx, cmd_name)
12+
if rv is not None:
13+
return rv
14+
matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)]
15+
if not matches:
16+
return None
17+
elif len(matches) == 1:
18+
return click.Group.get_command(self, ctx, matches[0])
19+
ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
20+
21+
def resolve_command(
22+
self, ctx: click.Context, args: list[str]
23+
) -> tuple[str | None, click.Command, list[str]]:
24+
# always return the full command name
25+
_, cmd, args = super().resolve_command(ctx, args)
26+
assert cmd is not None
27+
return cmd.name, cmd, args
28+
29+
30+
@click.command(cls=AliasedGroup)
31+
def cli() -> None:
32+
pass
33+
34+
35+
assert_type(cli, AliasedGroup)
36+
37+
38+
@cli.command()
39+
def push() -> None:
40+
pass
41+
42+
43+
@cli.command()
44+
def pop() -> None:
45+
pass
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""From https://click.palletsprojects.com/en/8.1.x/options/#yes-parameters"""
2+
from typing_extensions import assert_type
3+
4+
import click
5+
6+
7+
@click.command()
8+
@click.confirmation_option(prompt="Are you sure you want to drop the db?")
9+
def dropdb() -> None:
10+
click.echo("Dropped all tables!")
11+
12+
13+
assert_type(dropdb, click.Command)

tests/typing/typing_help_option.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing_extensions import assert_type
2+
3+
import click
4+
5+
6+
@click.command()
7+
@click.help_option("-h", "--help")
8+
def hello() -> None:
9+
"""Simple program that greets NAME for a total of COUNT times."""
10+
click.echo("Hello!")
11+
12+
13+
assert_type(hello, click.Command)

tests/typing/typing_options.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""From https://click.palletsprojects.com/en/8.1.x/quickstart/#adding-parameters"""
2+
from typing_extensions import assert_type
3+
4+
import click
5+
6+
7+
@click.command()
8+
@click.option("--count", default=1, help="number of greetings")
9+
@click.argument("name")
10+
def hello(count: int, name: str) -> None:
11+
for _ in range(count):
12+
click.echo(f"Hello {name}!")
13+
14+
15+
assert_type(hello, click.Command)

0 commit comments

Comments
 (0)