Skip to content

Commit

Permalink
Fix types, introduce type tests (#2562)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche authored Jul 13, 2023
1 parent d17dbc2 commit 449d38f
Show file tree
Hide file tree
Showing 14 changed files with 157 additions and 19 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Version 8.1.5

Unreleased

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


Version 8.1.4
-------------
Expand Down
2 changes: 0 additions & 2 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ filelock==3.12.2
# virtualenv
identify==2.5.24
# via pre-commit
nodeenv==1.8.0
# via pre-commit
pip-compile-multi==2.6.3
# via -r requirements/dev.in
pip-tools==6.13.0
Expand Down
1 change: 1 addition & 0 deletions requirements/typing.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mypy
pyright
9 changes: 8 additions & 1 deletion requirements/typing.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SHA1:7983aaa01d64547827c20395d77e248c41b2572f
# SHA1:0d25c235a98f3c8c55aefb59b91c82834e185f0a
#
# This file is autogenerated by pip-compile-multi
# To update, run:
Expand All @@ -9,5 +9,12 @@ mypy==1.4.1
# via -r requirements/typing.in
mypy-extensions==1.0.0
# via mypy
nodeenv==1.8.0
# via pyright
pyright==1.1.317
# via -r requirements/typing.in
typing-extensions==4.6.3
# via mypy

# The following packages are considered to be unsafe in a requirements file:
# setuptools
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ per-file-ignores =
src/click/__init__.py: F401

[mypy]
files = src/click
files = src/click, tests/typing
python_version = 3.7
show_error_codes = True
disallow_subclassing_any = True
Expand Down
23 changes: 9 additions & 14 deletions src/click/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
R = t.TypeVar("R")
T = t.TypeVar("T")
_AnyCallable = t.Callable[..., t.Any]
_Decorator: "te.TypeAlias" = t.Callable[[T], T]
FC = t.TypeVar("FC", bound=t.Union[_AnyCallable, Command])


Expand Down Expand Up @@ -150,16 +149,12 @@ def command(
...


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

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

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


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


def password_option(*param_decls: str, **kwargs: t.Any) -> _Decorator[FC]:
def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
"""Add a ``--password`` option which prompts for a password, hiding
input and asking to enter the value again for confirmation.
Expand All @@ -433,7 +428,7 @@ def version_option(
prog_name: t.Optional[str] = None,
message: t.Optional[str] = None,
**kwargs: t.Any,
) -> _Decorator[FC]:
) -> t.Callable[[FC], FC]:
"""Add a ``--version`` option which immediately prints the version
number and exits the program.
Expand Down Expand Up @@ -539,7 +534,7 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None:
return option(*param_decls, **kwargs)


def help_option(*param_decls: str, **kwargs: t.Any) -> _Decorator[FC]:
def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
"""Add a ``--help`` option which immediately prints the help page
and exits the program.
Expand Down
45 changes: 45 additions & 0 deletions tests/typing/typing_aliased_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Example from https://click.palletsprojects.com/en/8.1.x/advanced/#command-aliases"""
from __future__ import annotations

from typing_extensions import assert_type

import click


class AliasedGroup(click.Group):
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
rv = click.Group.get_command(self, ctx, cmd_name)
if rv is not None:
return rv
matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)]
if not matches:
return None
elif len(matches) == 1:
return click.Group.get_command(self, ctx, matches[0])
ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")

def resolve_command(
self, ctx: click.Context, args: list[str]
) -> tuple[str | None, click.Command, list[str]]:
# always return the full command name
_, cmd, args = super().resolve_command(ctx, args)
assert cmd is not None
return cmd.name, cmd, args


@click.command(cls=AliasedGroup)
def cli() -> None:
pass


assert_type(cli, AliasedGroup)


@cli.command()
def push() -> None:
pass


@cli.command()
def pop() -> None:
pass
13 changes: 13 additions & 0 deletions tests/typing/typing_confirmation_option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""From https://click.palletsprojects.com/en/8.1.x/options/#yes-parameters"""
from typing_extensions import assert_type

import click


@click.command()
@click.confirmation_option(prompt="Are you sure you want to drop the db?")
def dropdb() -> None:
click.echo("Dropped all tables!")


assert_type(dropdb, click.Command)
13 changes: 13 additions & 0 deletions tests/typing/typing_help_option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing_extensions import assert_type

import click


@click.command()
@click.help_option("-h", "--help")
def hello() -> None:
"""Simple program that greets NAME for a total of COUNT times."""
click.echo("Hello!")


assert_type(hello, click.Command)
15 changes: 15 additions & 0 deletions tests/typing/typing_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""From https://click.palletsprojects.com/en/8.1.x/quickstart/#adding-parameters"""
from typing_extensions import assert_type

import click


@click.command()
@click.option("--count", default=1, help="number of greetings")
@click.argument("name")
def hello(count: int, name: str) -> None:
for _ in range(count):
click.echo(f"Hello {name}!")


assert_type(hello, click.Command)
14 changes: 14 additions & 0 deletions tests/typing/typing_password_option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import codecs

from typing_extensions import assert_type

import click


@click.command()
@click.password_option()
def encrypt(password: str) -> None:
click.echo(f"encoded: to {codecs.encode(password, 'rot13')}")


assert_type(encrypt, click.Command)
16 changes: 16 additions & 0 deletions tests/typing/typing_simple_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""The simple example from https://github.com/pallets/click#a-simple-example."""
from typing_extensions import assert_type

import click


@click.command()
@click.option("--count", default=1, help="Number of greetings.")
@click.option("--name", prompt="Your name", help="The person to greet.")
def hello(count: int, name: str) -> None:
"""Simple program that greets NAME for a total of COUNT times."""
for _ in range(count):
click.echo(f"Hello, {name}!")


assert_type(hello, click.Command)
15 changes: 15 additions & 0 deletions tests/typing/typing_version_option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
From https://click.palletsprojects.com/en/8.1.x/options/#callbacks-and-eager-options.
"""
from typing_extensions import assert_type

import click


@click.command()
@click.version_option("0.1")
def hello() -> None:
click.echo("Hello World!")


assert_type(hello, click.Command)
5 changes: 4 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ commands = pre-commit run --all-files

[testenv:typing]
deps = -r requirements/typing.txt
commands = mypy
commands =
mypy
pyright --verifytypes click
pyright tests/typing

[testenv:docs]
deps = -r requirements/docs.txt
Expand Down

0 comments on commit 449d38f

Please sign in to comment.