click: Typing changes in 8.1.4 cause failure of mypy check

Click’s simple example https://github.com/pallets/click#a-simple-example doesn’t pass mypy 1.4.1 since 8.1.4. With click 8.1.3 the example did pass. This affects not only the simple example but also existing projects that use click. I ran mypy on one of my projects and everything passed, ran it 30 minutes later and saw failures out of the blue. Click 8.1.4 was published to PyPI between my checks.

To reproduce:

  • Save the simple example as click.py
  • pip install mypy==1.4.1 click==8.1.3
  • mypy click.py

This is the output:

click.py:3: error: Argument 1 has incompatible type "Callable[[Any, Any], Any]"; expected <nothing>  [arg-type]
click.py:12: error: <nothing> not callable  [misc]

Environment:

  • Python version: 3.11
  • Click version: 8.1.4

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 85
  • Comments: 28 (20 by maintainers)

Commits related to this issue

Most upvoted comments

Click 8.1.6 is available on PyPI.

I can put together a PR over the weekend. How about I set up some typing tests too (by stealing them from @hynek in attrs)?

Click 8.1.5 is available on PyPI.

Thanks @Tinche and @sirosen for helping diagnose and fix the issue.

That was what I used originally. It was apparently lacking, because we continue to get reports from users saying that their type checker doesn’t like something, then we add more complexity to the type, then something else isn’t correct, etc. Python’s and mypy’s implementation of typing can be a huge mess sometimes.

Whoever is going to change this, be sure to follow git blame (or GitHub’s view of it) back in time until you get to the original addition of types. Make sure to take into account why the various commits were made along the way.

Locking to avoid “me too” comments.

I’m still experiencing this issue, the examples in https://click.palletsprojects.com/en/8.1.x/quickstart/#nesting-commands fail to typecheck under MyPy 1.4.1. I imagine whatever type adjustments were done to click.command for 8.1.5 also needs to be done to click.group.

import click

@click.group()
def cli():
    pass

@cli.command()
def initdb():
    click.echo('Initialized the database')

@cli.command()
def dropdb():
    click.echo('Dropped the database')
❯ python3 -m pip list
Package           Version
----------------- -------
click             8.1.5
mypy              1.4.1
mypy-extensions   1.0.0
pip               23.1.2
setuptools        68.0.0
typing_extensions 4.7.1

❯ mypy --pretty example.py
example.py:7: error: <nothing> has no attribute "command"  [attr-defined]
    @cli.command()
     ^~~~~~~~~~~
example.py:11: error: <nothing> has no attribute "command"  [attr-defined]
    @cli.command()
     ^~~~~~~~~~~
Found 2 errors in 1 file (checked 1 source file)

PR up at https://github.com/pallets/click/pull/2562. I’ve also added simple Mypy and Pyright tests that should be fleshed out over time.

Here’s a very minimal reproduction of this issue using just version_option (which returns _Decorator[FC]) that might help narrow things down:

@click.group()
@click.version_option()
def foo():
    pass
cli.py:XX: error: Argument 1 has incompatible type "Callable[[], Any]"; expected <nothing>  [arg-type]

Taking a quick look at this.

Would you consider changing the signature of option (and maybe others) to:

def option(
    *param_decls: str, cls: t.Optional[t.Type[Option]] = None, **attrs: t.Any
) -> t.Callable[[FC], FC]:

? This syntax is equivalent to what we have today.

The current implementation with a generic type alias might be a little too much for Mypy at this time (I’m getting note: Revealed type is "<nothing>"). This by itself won’t solve this issue but it might be a necessary step.

After that, I think this overload is the issue:

# 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*
@t.overload
def command(
    name: None = None,
    cls: t.Type[CmdType] = ...,
    **attrs: t.Any,
) -> t.Callable[[_AnyCallable], CmdType]:
    ...

If I comment it out, the example type-checks. So we need to figure out how to fix it. Given the comment, it might not be super easy.

A minor note: I’m now fairly certain that my “solution” above is incorrect.

If FC2 is defined as an alias for _AnyCallable | Command, then it won’t allow subtypes of _AnyCallable, which is not acceptable. (It would mean that a command whose callback is defined with a return type other than Any gets rejected.)

It might be okay to use Callable (with no parameters) in lieu of _AnyCallable, but I haven’t yet learned why _AnyCallable is being used rather than Callable.