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

Wrapped overloaded function drops all but the first overload #15737

Closed
maffoo opened this issue Jul 22, 2023 · 3 comments · Fixed by #15898
Closed

Wrapped overloaded function drops all but the first overload #15737

maffoo opened this issue Jul 22, 2023 · 3 comments · Fixed by #15898
Labels
bug mypy got something wrong topic-paramspec PEP 612, ParamSpec, Concatenate

Comments

@maffoo
Copy link

maffoo commented Jul 22, 2023

Bug Report

When wrapping an overloaded function and using ParamSpec to preserve the call signature, only the first overload is preserved on the resulting function.

To Reproduce

See:

from typing import Callable, overload, TypeVar
from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")

def transform(func: Callable[P, list[T]]) -> Callable[P, T]:
    def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
        return func(*args, **kwargs)[0]

    return wrapped

@overload
def foo(x: int) -> list[float]:
    pass

@overload
def foo(x: str) -> list[str]:
    pass

def foo(x: int | str) -> list[float] | list[str]:
    if isinstance(x, int):
        return [1 / x]
    return [x[::-1]]

bar = transform(foo)

assert bar(2) == 0.5
assert bar("abc") == "cba"

reveal_type(foo)
reveal_type(bar)

Expected Behavior

I would expect the overloads to be preserved on the decorated function, e.g.

main.py:31: note: Revealed type is "Overload(def (x: builtins.int) -> builtins.list[builtins.float], def (x: builtins.str) -> builtins.list[builtins.str])"
main.py:32: note: Revealed type is Overload(def (x: builtins.int) -> builtins.float, def (x: builtins.str) -> builtins.str)"

Actual Behavior

main.py:29: error: Argument 1 has incompatible type "str"; expected "int"  [arg-type]
main.py:31: note: Revealed type is "Overload(def (x: builtins.int) -> builtins.list[builtins.float], def (x: builtins.str) -> builtins.list[builtins.str])"
main.py:32: note: Revealed type is "def (x: builtins.int) -> builtins.float"

Your Environment

  • Mypy version used: 1.4.1
  • Python version used: 3.10.10
@maffoo maffoo added the bug mypy got something wrong label Jul 22, 2023
@erictraut
Copy link

Supporting overloads for ParamSpec is difficult. PEP 612 (which introduced ParamSpec) didn't contemplate this case or discuss the behaviors and limitations thereof. If the ParamSpec is used to parameterize a class or a generic type alias, it's not clear how to handle an overload. However, in the case where the function returns a Callable[P, T] (as the example above does), the meaning and behavior is well defined. I've implemented support for this in pyright, so the above type checks as you would expect. If someone is interested in implementing this in mypy and would like some tips or suggestions based on my experience, let me know.

@hauntsaninja hauntsaninja added the topic-paramspec PEP 612, ParamSpec, Concatenate label Jul 22, 2023
ilevkivskyi added a commit that referenced this issue Aug 18, 2023
Fixes #15737
Fixes #12844
Fixes #12716

My goal was to fix the `ParamSpec` issues, but it turns out decorated
overloads were not supported at all. Namely:
* Decorators on overload items were ignored, caller would see original
undecorated item types
* Overload item overlap checks were performed for original types, while
arguably we should use decorated types
* Overload items completeness w.r.t. to implementation was checked with
decorated implementation, and undecorated items

Here I add basic support using same logic as for regular decorated
functions: initially set type to `None` and defer callers until
definition is type-checked. Note this results in few more `Cannot
determine type` in case of other errors, but I think it is fine.

Note I also add special-casing for "inline" applications of generic
functions to overload arguments. This use case was mentioned few times
alongside overloads. The general fix would be tricky, and my
special-casing should cover typical use cases.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
@hauntsaninja
Copy link
Collaborator

@maffoo is the project you were running into this on open source?

@maffoo
Copy link
Author

maffoo commented Aug 21, 2023

@maffoo is the project you were running into this on open source?

Hi @hauntsaninja, yes, the project where I ran into this is in "duet", a small library for doing concurrency with futures: https://github.com/google/duet. In particular, there's a function duet.sync that wraps an async function into a synchronous version and we've been using a mypy plugin to get the typing right (https://github.com/google/duet/blob/main/duet/typing.py), but really what we'd like is to use ParamSpec and define duet.sync like:

def sync(f: Callable[P, Awaitable[T]]) -> Callable[P, T]:
    ...

Unfortunately, we use duet.sync internally with overloaded functions in many places, which is how I encountered this issue. I have a branch where I've switched to using ParamSpec (https://github.com/google/duet/tree/u/maffoo/param-spec). I tried out the latest version of mypy with that branch and it seems to work as expected (in particular see the file https://github.com/google/duet/blob/u/maffoo/param-spec/check.py). Thanks for fixing this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong topic-paramspec PEP 612, ParamSpec, Concatenate
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants