python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.53k stars 2.83k forks source link

mypy failed to return correct overloaded func type when decorate func accepts more than one parameter #18167

Open uriyyo opened 3 days ago

uriyyo commented 3 days ago

Bug Report

mypy failed to return the correct overloaded func type when decorate func accepts more than one parameter.

To Reproduce

from functools import wraps
from typing import overload, Any, TypeVar, Callable, Type

from typing_extensions import ParamSpec, reveal_type

@overload
def foo(a: str, /) -> str:
    pass

@overload
def foo(a: int, /) -> int:
    pass

def foo(a: Any) -> Any:
    return a

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

def forbid_return_type(
    func: Callable[P, T],
    tp: Type[Any],
    /,
) -> Callable[P, T]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        result = func(*args, **kwargs)

        if isinstance(result, tp):
            raise TypeError(f"Return type {tp} is forbidden")

        return result

    return wrapper

foo_as_int = forbid_return_type(foo, str)

reveal_type(foo_as_int(1))
reveal_type(foo_as_int("1"))

Expected Behavior

note: Revealed type is "builtins.int"
note: Revealed type is "builtins.str"

Actual Behavior

note: Revealed type is "builtins.str"
error: Argument 1 has incompatible type "int"; expected "str"  [arg-type]
note: Revealed type is "builtins.str"

Your Environment

Both codes bellow work as expected:

from functools import wraps
from typing import overload, Any, TypeVar, Callable

from typing_extensions import ParamSpec, reveal_type

@overload
def foo(a: str, /) -> str:
    pass

@overload
def foo(a: int, /) -> int:
    pass

def foo(a: Any) -> Any:
    return a

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

def forbid_return_type(
    func: Callable[P, T],
) -> Callable[P, T]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        result = func(*args, **kwargs)
        return result

    return wrapper

foo_as_int = forbid_return_type(foo)

reveal_type(foo_as_int(1))
reveal_type(foo_as_int("1"))
from functools import wraps
from typing import Any, TypeVar, Callable, Type, Union

from typing_extensions import ParamSpec, reveal_type

def foo(a: Union[int, str]) -> Union[int, str]:
    return a

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

def forbid_return_type(
    func: Callable[P, T],
    tp: Type[Any],
    /,
) -> Callable[P, T]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        result = func(*args, **kwargs)

        if isinstance(result, tp):
            raise TypeError(f"Return type {tp} is forbidden")

        return result

    return wrapper

foo_as_int = forbid_return_type(foo, str)

reveal_type(foo_as_int(1))
reveal_type(foo_as_int("1"))

So it's not working only when the function accepts an overloaded function and another argument.

I checked with pyright and it correctly handles code.

  /Users/yuriikarabas/projects/mypy/temp.py:45:13 - information: Type of "foo_as_int(1)" is "int"
  /Users/yuriikarabas/projects/mypy/temp.py:46:13 - information: Type of "foo_as_int("1")" is "str"
0 errors, 0 warnings, 2 informations 
uriyyo commented 3 days ago

I guess issue is here: https://github.com/python/mypy/blob/6759dbd5bbcca4501748dc52d93d5a50060a825a/mypy/checkexpr.py#L1399-L1401

brianschubert commented 3 days ago

13540 looks related