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

Generic, higher kinded function not sharing Callable’s limitations #13107

Closed
flying-sheep opened this issue Jul 12, 2022 · 2 comments · Fixed by #15913
Closed

Generic, higher kinded function not sharing Callable’s limitations #13107

flying-sheep opened this issue Jul 12, 2022 · 2 comments · Fixed by #15913
Labels
documentation topic-calls Function calls, *args, **kwargs, defaults

Comments

@flying-sheep
Copy link

flying-sheep commented Jul 12, 2022

Documentation

I can define the following decorator:

P = ParamSpec('P')
R = TypeVar('R', covariant=True)

def file_cache(fn: Callable[Concatenate[Path, P], R]) -> Callable[P, R]:
    path: Path = ...
    def wrapper(*args: P.args, **kw: P.kwargs) -> R:
        if path.is_file():
            return read(path)
        return fn(path, *args, **kw)
    return wrapper

But Callable is limited to kwargs-less functions, so mypy complains when I apply the decorator to a function, and try to specify the keyword argument:

@file_cache
def get_thing(path: Path, *, some_arg: int) -> X:
    x = calculate_x()
    path.write_bytes(x.serialize())
    return x

if __name__ == '__main__':
    get_thing(some_arg=1)
error: Unexpected keyword argument "some_arg" for "get_thing"

In an attempt to circumvent the problem, I tried to define the following, but mypy doesn’t seem to understand it:

P = ParamSpec('P')
R = TypeVar('R', covariant=True)


class Function(Protocol[P, R]):
    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
        ...

And will complain that the decorated function was a Callable[[Path, DefaultNamedArg(int, 'some_arg')], X, not a Function[<nothing>, <nothing>].

If I made a mistake in defining Function, please help.

If it looks correct: a) why doesn’t it exist / why is Callable so limited? and b) mypy should understand my definition.

@AlexWaygood AlexWaygood added the topic-calls Function calls, *args, **kwargs, defaults label Jul 12, 2022
@erictraut
Copy link

You are correct that Callable is limited to describing positional-only parameters, so you are correct to use a callback protocol here.

Here's my attempt to create a complete stand-alone sample from your description. Is this correct?

from typing import Callable, Concatenate, ParamSpec, Protocol, TypeVar

P = ParamSpec("P")
R = TypeVar("R", covariant=True)

class Path:
    ...

class Function(Protocol[P, R]):
    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
        ...

def file_cache(fn: Function[Concatenate[Path, P], R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kw: P.kwargs) -> R:
        return fn(Path(), *args, **kw)

    return wrapper

@file_cache
def get_thing(path: Path, *, some_arg: int) -> int:
    return 1

For this sample, mypy emits two errors:
error: Argument 1 to "__call__" of "Function" has incompatible type "Path"; expected "[Path, **P.args]"
error: Argument 1 to "file_cache" has incompatible type "Callable[[Path, NamedArg(int, 'some_arg')], int]"; expected "Function[<nothing>, <nothing>]"

Both of these errors look like false positives — probably two separate bugs. For comparison, pyright doesn't emit any errors for this snippet.

@flying-sheep
Copy link
Author

Here's my attempt to create a complete stand-alone sample from your description. Is this correct?

Almost, thank you! I should have made a working example from the start. I think your consolidation has one little mistake, but that actually doesn’t change anything in the errors encountered.

-def file_cache(fn: Function[Concatenate[Path, P], R]) -> Callable[P, R]:
+def file_cache(fn: Function[Concatenate[Path, P], R]) -> Function[P, R]:

ParamSpec support just seems to be really buggy and needs a lot more tests, see e.g. #12595

Also very weird that mypy chooses to express the type as Callable[[Path, NamedArg(int, 'some_arg')], int]: Seems like mypy itself is trying to get around the arbitrary limitation of Callable and trips over its own feet.

hauntsaninja pushed a commit that referenced this issue Sep 14, 2023
Fixes #15734
Fixes #15188
Fixes #14321
Fixes #13107 (plain Callable was
already working, this fixes the protocol example)
Fixes #16058

It looks like treating trivial suffixes (especially for erased
callables) as "whatever works" is a right thing, because it reflects the
whole idea of why we normally check subtyping with respect to an e.g.
erased type. As you can see this fixes a bunch of issues. Note it was
necessary to make couple more tweaks to make everything work smoothly:
* Adjust self-type erasure level in `checker.py` to match other places.
* Explicitly allow `Callable` as a `self`/`cls` annotation (actually I
am not sure we need to keep this check at all, since we now have good
inference for self-types, and we check they are safe either at
definition site or at call site).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation topic-calls Function calls, *args, **kwargs, defaults
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants