Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(types): decorator typing fails #2233

Merged
merged 1 commit into from Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Expand Up @@ -5,6 +5,9 @@ Version 8.1.1

Unreleased

- Fix an issue with decorator typing that caused type checking to
report that a command was not callable. :issue:`2227`


Version 8.1.0
-------------
Expand Down
34 changes: 30 additions & 4 deletions src/click/core.py
Expand Up @@ -1809,6 +1809,16 @@ def add_command(self, cmd: Command, name: t.Optional[str] = None) -> None:
_check_multicommand(self, name, cmd, register=True)
self.commands[name] = cmd

@t.overload
def command(self, __func: t.Callable[..., t.Any]) -> Command:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

You cannot pass other arguments if you use it as a decorator without parenthesis. I've added assert statements requiring this be true. If you want to add other arguments, it should be used in the traditional way as a factory. And mypy certainly should not encourage that even if was supported at runtime!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was not possible before 8.1, and it should not be (I think). Plus it messes up mypy to do it that way; this causes it to overlap with the generator signature and break.

...

@t.overload
def command(
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], Command]:
...

def command(
self, *args: t.Any, **kwargs: t.Any
) -> t.Union[t.Callable[[t.Callable[..., t.Any]], Command], Command]:
Expand All @@ -1834,8 +1844,11 @@ def command(
func: t.Optional[t.Callable] = None

if args and callable(args[0]):
func = args[0]
args = args[1:]
assert (
len(args) == 1 and not kwargs
), "Use 'command(**kwargs)(callable)' to provide arguments."
(func,) = args
args = ()

def decorator(f: t.Callable[..., t.Any]) -> Command:
cmd: Command = command(*args, **kwargs)(f)
Expand All @@ -1847,6 +1860,16 @@ def decorator(f: t.Callable[..., t.Any]) -> Command:

return decorator

@t.overload
def group(self, __func: t.Callable[..., t.Any]) -> "Group":
...

@t.overload
def group(
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], "Group"]:
...

def group(
self, *args: t.Any, **kwargs: t.Any
) -> t.Union[t.Callable[[t.Callable[..., t.Any]], "Group"], "Group"]:
Expand All @@ -1869,8 +1892,11 @@ def group(
func: t.Optional[t.Callable] = None

if args and callable(args[0]):
func = args[0]
args = args[1:]
assert (
len(args) == 1 and not kwargs
), "Use 'group(**kwargs)(callable)' to provide arguments."
(func,) = args
args = ()

if self.group_class is not None and kwargs.get("cls") is None:
if self.group_class is type:
Expand Down
34 changes: 13 additions & 21 deletions src/click/decorators.py
Expand Up @@ -126,10 +126,8 @@ def new_func(*args, **kwargs): # type: ignore

@t.overload
def command(
name: t.Optional[str] = None,
cls: t.Type[CmdType] = ...,
**attrs: t.Any,
) -> t.Callable[..., CmdType]:
__func: t.Callable[..., t.Any],
) -> Command:
...


Expand All @@ -143,18 +141,10 @@ def command(

@t.overload
def command(
name: t.Callable,
name: t.Optional[str] = None,
cls: t.Type[CmdType] = ...,
**attrs: t.Any,
) -> CmdType:
...


@t.overload
def command(
name: t.Callable,
**attrs: t.Any,
) -> Command:
) -> t.Callable[..., CmdType]:
...


Expand Down Expand Up @@ -191,14 +181,17 @@ def command(
The ``params`` argument can be used. Decorated params are
appended to the end of the list.
"""
if cls is None:
cls = Command

func: t.Optional[t.Callable] = None

if callable(name):
func = name
name = None
assert cls is None, "Use 'command(cls=cls)(callable)' to specify a class."
assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments."

if cls is None:
cls = Command

def decorator(f: t.Callable[..., t.Any]) -> Command:
if isinstance(f, Command):
Expand Down Expand Up @@ -235,17 +228,16 @@ def decorator(f: t.Callable[..., t.Any]) -> Command:

@t.overload
def group(
name: t.Optional[str] = None,
**attrs: t.Any,
) -> t.Callable[[F], Group]:
__func: t.Callable,
) -> Group:
...


@t.overload
def group(
name: t.Callable,
name: t.Optional[str] = None,
**attrs: t.Any,
) -> Group:
) -> t.Callable[[F], Group]:
...


Expand Down