microsoft / pyright

Static Type Checker for Python
Other
13.12k stars 1.4k forks source link

@lru_cache interferes with @overload'ed definitions #8692

Closed bukzor closed 1 month ago

bukzor commented 1 month ago

This is a simplified version of what's seen in stdlib urllib.parse.

Code sample in pyright playground

# pyright:strict
import functools
from typing import AnyStr, NamedTuple, assert_type, overload

class SplitResult(NamedTuple):
    pass

class SplitResultBytes(NamedTuple):
    pass

@overload
def urlsplit(url: str) -> SplitResult: ...
@overload
def urlsplit(url: bytes) -> SplitResultBytes: ...
@functools.lru_cache(typed=True)
def urlsplit(url: AnyStr) -> SplitResult | SplitResultBytes:
    if isinstance(url, str):
        return SplitResult()
    else:
        return SplitResultBytes()

x = urlsplit("ohai")
assert_type(x, SplitResult)

As written, currently, this assert_type fails, but removing the lru_cache removes the error.

erictraut commented 1 month ago

Thanks for the bug report. This will be addressed in the next release.

bukzor commented 1 month ago

I dug into this and found that it seems to be a bug in typeshed, rather -- lru_cache wrapper uses ... as the paramspec which means pyright loses the overload information. pyright and mypy give exactly matching results for the below program.

What did you find?

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

P = ParamSpec("P")
T = TypeVar("T")
def cached_broken(f: Callable[..., T]) -> Callable[..., T]: return f
def cached_worken(f: Callable[P, T]) -> Callable[P, T]: return f

@overload
def g(x: float) -> complex: ...
@overload
def g(x: str) -> None: ...
def g(x: float | str) -> complex | None:
    del x
    return None

cg = cached_broken(g)
assert_type(cg(1.2), complex)
assert_type(cg("1.2"), complex)

cg = cached_worken(g)
assert_type(cg(1.2), complex)
assert_type(cg("1.2"), None)
erictraut commented 1 month ago

This is addressed in pyright 1.1.376.