From 178106180078834740569bee781b74ad3b15829c Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Jul 2023 15:37:42 +0200 Subject: [PATCH 01/11] Fix types, introduce type tests --- .github/workflows/tests.yaml | 1 + CHANGES.rst | 3 ++ setup.cfg | 2 +- src/click/decorators.py | 12 +++---- tests/typing/typing_aliased_group.py | 45 +++++++++++++++++++++++++++ tests/typing/typing_simple_example.py | 16 ++++++++++ tox.ini | 13 ++++++++ 7 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 tests/typing/typing_aliased_group.py create mode 100644 tests/typing/typing_simple_example.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0a839517d..0009b78d9 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -34,6 +34,7 @@ jobs: - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - {name: 'PyPy', python: 'pypy-3.10', os: ubuntu-latest, tox: pypy310} - {name: Typing, python: '3.11', os: ubuntu-latest, tox: typing} + - {name: Pyright, python: '3.11', os: ubuntu-latest, tox: pyright} steps: - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 diff --git a/CHANGES.rst b/CHANGES.rst index 83d959125..dc406e1bb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 ------------- diff --git a/setup.cfg b/setup.cfg index 7446f699b..d5fcdcf01 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/src/click/decorators.py b/src/click/decorators.py index 797449d63..c29ead8d1 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -150,16 +150,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]: ... @@ -359,7 +355,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``). diff --git a/tests/typing/typing_aliased_group.py b/tests/typing/typing_aliased_group.py new file mode 100644 index 000000000..ccc706bc5 --- /dev/null +++ b/tests/typing/typing_aliased_group.py @@ -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 diff --git a/tests/typing/typing_simple_example.py b/tests/typing/typing_simple_example.py new file mode 100644 index 000000000..0a94b8cb7 --- /dev/null +++ b/tests/typing/typing_simple_example.py @@ -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) diff --git a/tox.ini b/tox.ini index 1b163ff5a..13eeefd36 100644 --- a/tox.ini +++ b/tox.ini @@ -25,3 +25,16 @@ commands = mypy [testenv:docs] deps = -r requirements/docs.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html + +[testenv:pyright] +# Install and configure node and pyright +# This *could* be folded into a custom install_command +# Use nodeenv to configure node in the running tox virtual environment +# Seeing errors using "nodeenv -p" +# Use npm install -g to install "globally" into the virtual environment +deps = nodeenv +commands = + nodeenv --prebuilt --node=lts --force {envdir} + npm install -g --no-package-lock --no-save pyright + pyright --version + pyright tests/typing From 6833ebc31d29221046d5a9def527b1f4b3e1ff29 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Jul 2023 15:51:59 +0200 Subject: [PATCH 02/11] Tidy up tox.ini --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 13eeefd36..9ef500f76 100644 --- a/tox.ini +++ b/tox.ini @@ -33,8 +33,9 @@ commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html # Seeing errors using "nodeenv -p" # Use npm install -g to install "globally" into the virtual environment deps = nodeenv -commands = +commands_pre = nodeenv --prebuilt --node=lts --force {envdir} npm install -g --no-package-lock --no-save pyright pyright --version +commands= pyright tests/typing From b5fa4b9ed6d3390fef3fd41be8dd7a9c8f89f30b Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Jul 2023 15:59:15 +0200 Subject: [PATCH 03/11] More tests, more fixes --- src/click/decorators.py | 2 +- tests/typing/typing_options.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/typing/typing_options.py diff --git a/src/click/decorators.py b/src/click/decorators.py index c29ead8d1..00b03136c 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -327,7 +327,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``). diff --git a/tests/typing/typing_options.py b/tests/typing/typing_options.py new file mode 100644 index 000000000..6a6611928 --- /dev/null +++ b/tests/typing/typing_options.py @@ -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) From 06bead45c452c49786acfd9998f47c8609baf140 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Jul 2023 16:04:34 +0200 Subject: [PATCH 04/11] Tests for `password_option` --- src/click/decorators.py | 9 ++------- tests/typing/typing_password_option.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 tests/typing/typing_password_option.py diff --git a/src/click/decorators.py b/src/click/decorators.py index 00b03136c..349cbec79 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -4,12 +4,7 @@ from functools import update_wrapper from gettext import gettext as _ -from .core import Argument -from .core import Command -from .core import Context -from .core import Group -from .core import Option -from .core import Parameter +from .core import Argument, Command, Context, Group, Option, Parameter from .globals import get_current_context from .utils import echo @@ -405,7 +400,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. diff --git a/tests/typing/typing_password_option.py b/tests/typing/typing_password_option.py new file mode 100644 index 000000000..8537e6ff5 --- /dev/null +++ b/tests/typing/typing_password_option.py @@ -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) From eac3e75beacd91d33aa4acb9054cd5acfc9da0ab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Jul 2023 14:05:22 +0000 Subject: [PATCH 05/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/click/decorators.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/click/decorators.py b/src/click/decorators.py index 349cbec79..5124d9a2c 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -4,7 +4,12 @@ from functools import update_wrapper from gettext import gettext as _ -from .core import Argument, Command, Context, Group, Option, Parameter +from .core import Argument +from .core import Command +from .core import Context +from .core import Group +from .core import Option +from .core import Parameter from .globals import get_current_context from .utils import echo From 876b6cf93ad2c8fe87630da4850fccd247ae943e Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Jul 2023 16:10:20 +0200 Subject: [PATCH 06/11] Test for `confirmation_option` --- src/click/decorators.py | 2 +- tests/typing/typing_confirmation_option.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/typing/typing_confirmation_option.py diff --git a/src/click/decorators.py b/src/click/decorators.py index 5124d9a2c..5e2dd563b 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -381,7 +381,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. diff --git a/tests/typing/typing_confirmation_option.py b/tests/typing/typing_confirmation_option.py new file mode 100644 index 000000000..09ecaa96d --- /dev/null +++ b/tests/typing/typing_confirmation_option.py @@ -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) From 694fd1954f4531d7304fe9656e1c2a30cedc004b Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Jul 2023 16:15:03 +0200 Subject: [PATCH 07/11] Test `version_option` --- src/click/decorators.py | 2 +- tests/typing/typing_version_option.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/typing/typing_version_option.py diff --git a/src/click/decorators.py b/src/click/decorators.py index 5e2dd563b..74430312b 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -429,7 +429,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. diff --git a/tests/typing/typing_version_option.py b/tests/typing/typing_version_option.py new file mode 100644 index 000000000..e9a3ef324 --- /dev/null +++ b/tests/typing/typing_version_option.py @@ -0,0 +1,13 @@ +"""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) From 1b258ceb0d69c567a5f601a7b90c7cf9e1d146ce Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Jul 2023 23:42:30 +0200 Subject: [PATCH 08/11] Test `help_option` --- src/click/decorators.py | 3 +-- tests/typing/typing_help_option.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 tests/typing/typing_help_option.py diff --git a/src/click/decorators.py b/src/click/decorators.py index 74430312b..674aee53b 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -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]) @@ -535,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. diff --git a/tests/typing/typing_help_option.py b/tests/typing/typing_help_option.py new file mode 100644 index 000000000..6b822c131 --- /dev/null +++ b/tests/typing/typing_help_option.py @@ -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) From 2b3f24ed452c3dc877d7dc26e475fadc52372039 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Jul 2023 23:47:07 +0200 Subject: [PATCH 09/11] Add `pyright --verifytypes` --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 9ef500f76..cdcc63ba9 100644 --- a/tox.ini +++ b/tox.ini @@ -38,4 +38,5 @@ commands_pre = npm install -g --no-package-lock --no-save pyright pyright --version commands= + pyright --verifytypes click pyright tests/typing From d3fc283036a13f7094742c334b9a37cffae3650e Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Jul 2023 23:51:27 +0200 Subject: [PATCH 10/11] Tweak line length --- tests/typing/typing_version_option.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/typing/typing_version_option.py b/tests/typing/typing_version_option.py index e9a3ef324..fd473a001 100644 --- a/tests/typing/typing_version_option.py +++ b/tests/typing/typing_version_option.py @@ -1,4 +1,6 @@ -"""From https://click.palletsprojects.com/en/8.1.x/options/#callbacks-and-eager-options""" +""" +From https://click.palletsprojects.com/en/8.1.x/options/#callbacks-and-eager-options. +""" from typing_extensions import assert_type import click From 2eccd6a09933672b1cc6776f73e20dac06873d6c Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 13 Jul 2023 07:49:46 -0700 Subject: [PATCH 11/11] pin pyright --- .github/workflows/tests.yaml | 1 - requirements/dev.txt | 2 -- requirements/typing.in | 1 + requirements/typing.txt | 9 ++++++++- tox.ini | 20 ++++---------------- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0009b78d9..0a839517d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -34,7 +34,6 @@ jobs: - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - {name: 'PyPy', python: 'pypy-3.10', os: ubuntu-latest, tox: pypy310} - {name: Typing, python: '3.11', os: ubuntu-latest, tox: typing} - - {name: Pyright, python: '3.11', os: ubuntu-latest, tox: pyright} steps: - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 diff --git a/requirements/dev.txt b/requirements/dev.txt index ed462080a..1a4ecc7b5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -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 diff --git a/requirements/typing.in b/requirements/typing.in index f0aa93ac8..a20f06c42 100644 --- a/requirements/typing.in +++ b/requirements/typing.in @@ -1 +1,2 @@ mypy +pyright diff --git a/requirements/typing.txt b/requirements/typing.txt index 13afd660e..f04fde5dd 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -1,4 +1,4 @@ -# SHA1:7983aaa01d64547827c20395d77e248c41b2572f +# SHA1:0d25c235a98f3c8c55aefb59b91c82834e185f0a # # This file is autogenerated by pip-compile-multi # To update, run: @@ -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 diff --git a/tox.ini b/tox.ini index cdcc63ba9..df5360228 100644 --- a/tox.ini +++ b/tox.ini @@ -20,23 +20,11 @@ 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 commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html - -[testenv:pyright] -# Install and configure node and pyright -# This *could* be folded into a custom install_command -# Use nodeenv to configure node in the running tox virtual environment -# Seeing errors using "nodeenv -p" -# Use npm install -g to install "globally" into the virtual environment -deps = nodeenv -commands_pre = - nodeenv --prebuilt --node=lts --force {envdir} - npm install -g --no-package-lock --no-save pyright - pyright --version -commands= - pyright --verifytypes click - pyright tests/typing