From d9f091e683292475432c1d5200c6d1b929b755e9 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 3 Nov 2024 15:36:01 -0800 Subject: [PATCH 1/2] Fixed bug that results in false positive under certain circumstances involving classes parameterized with a contravariant type variable. This addresses #9054. --- .../src/analyzer/typeEvaluator.ts | 2 +- .../src/tests/samples/overloadImpl1.py | 336 ++++-------------- .../src/tests/samples/overloadImpl2.py | 145 ++++++++ .../src/tests/typeEvaluator6.test.ts | 7 +- 4 files changed, 223 insertions(+), 267 deletions(-) create mode 100644 packages/pyright-internal/src/tests/samples/overloadImpl2.py diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index a1467b287402..71653a3fe9a2 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -23876,7 +23876,7 @@ export function createTypeEvaluator( effectiveFlags = flags | AssignTypeFlags.RetainLiteralsForTypeVar; errorSource = LocAddendum.typeVarIsCovariant; } else if (variance === Variance.Contravariant) { - effectiveFlags = (flags ^ AssignTypeFlags.Contravariant) | AssignTypeFlags.RetainLiteralsForTypeVar; + effectiveFlags = flags | AssignTypeFlags.Contravariant | AssignTypeFlags.RetainLiteralsForTypeVar; errorSource = LocAddendum.typeVarIsContravariant; } else { effectiveFlags = flags | AssignTypeFlags.Invariant | AssignTypeFlags.RetainLiteralsForTypeVar; diff --git a/packages/pyright-internal/src/tests/samples/overloadImpl1.py b/packages/pyright-internal/src/tests/samples/overloadImpl1.py index c054ecb0c007..13842efd3ed1 100644 --- a/packages/pyright-internal/src/tests/samples/overloadImpl1.py +++ b/packages/pyright-internal/src/tests/samples/overloadImpl1.py @@ -9,22 +9,19 @@ Iterable, Literal, NoReturn, - ParamSpec, - Protocol, TypeVar, - TypeVarTuple, overload, ) +T = TypeVar("T") + @overload -def func1(a: int) -> str: - ... +def func1(a: int) -> str: ... @overload -def func1(a: str) -> int: - ... +def func1(a: str) -> int: ... # This should generate two errors: @@ -37,420 +34,229 @@ def func1(a: str) -> str: @overload -def func2(a: int, b: str = ...) -> str: - ... +def func2(a: int, b: str = ...) -> str: ... @overload -def func2(a: None) -> str: - ... +def func2(a: None) -> str: ... # This should generate an error because the parameter "b" is missing # from the implementation but is required by overload 1. -def func2(a: int | None) -> str: - ... +def func2(a: int | None) -> str: ... @overload -def func3(a: int, *, b: Literal["r"]) -> str: - ... +def func3(a: int, *, b: Literal["r"]) -> str: ... @overload -def func3(a: int, *, b: Literal["b"]) -> bytes: - ... - - -def func3(*args: Any, **kwargs: Any) -> Any: - ... +def func3(a: int, *, b: Literal["b"]) -> bytes: ... -_T = TypeVar("_T") +def func3(*args: Any, **kwargs: Any) -> Any: ... @overload -def func4(a: None) -> None: - ... +def func4(a: None) -> None: ... @overload -def func4(a: list[_T]) -> _T: - ... +def func4(a: list[T]) -> T: ... -def func4(a: list[_T] | None) -> _T | None: - ... +def func4(a: list[T] | None) -> T | None: ... -class A: +class ClassA: @overload - def method4(self, a: None) -> None: - ... + def method4(self, a: None) -> None: ... @overload - def method4(self, a: list[_T]) -> _T: - ... + def method4(self, a: list[T]) -> T: ... - def method4(self, a: list[_T] | None) -> _T | None: - ... + def method4(self, a: list[T] | None) -> T | None: ... @overload -def func5(a: list[_T]) -> _T: - ... +def func5(a: list[T]) -> T: ... @overload -def func5(a: None) -> None: - ... +def func5(a: None) -> None: ... # This should generate an error because list is not compatible with dict. -def func5(a: dict[Any, Any] | None) -> Any | None: - ... +def func5(a: dict[Any, Any] | None) -> Any | None: ... @overload -def func6(foo: int, /) -> int: - ... +def func6(foo: int, /) -> int: ... @overload -def func6(bar: str, /) -> int: - ... +def func6(bar: str, /) -> int: ... def func6(p0: int | str, /) -> int: return 3 -_T1 = TypeVar("_T1") - - -class ClassA(Generic[_T1]): +class ClassB(Generic[T]): @overload - def method1(self: "ClassA[None]") -> None: - ... + def method1(self: "ClassB[None]") -> None: ... @overload - def method1(self, value: _T1) -> None: - ... + def method1(self, value: T) -> None: ... - def method1(self, value: Any = None) -> None: - ... + def method1(self, value: Any = None) -> None: ... -class ClassB: - ... +class ClassC: ... -class ClassC: - ... +class ClassD: ... -_T2 = TypeVar("_T2", ClassB, ClassC) +T_CD = TypeVar("T_CD", ClassC, ClassD) @overload -def func7(cls: type[ClassB], var: int) -> ClassB: - ... +def func7(cls: type[ClassC], var: int) -> ClassC: ... @overload -def func7(cls: type[ClassC], var: str) -> ClassC: - ... +def func7(cls: type[ClassD], var: str) -> ClassD: ... -def func7(cls: type[_T2], var: int | str) -> _T2: +def func7(cls: type[T_CD], var: int | str) -> T_CD: return cls() -_T3 = TypeVar("_T3", bound=str) +T_str = TypeVar("T_str", bound=str) @overload -def func8(foo: int) -> int: - ... +def func8(foo: int) -> int: ... @overload -def func8(foo: _T3) -> tuple[_T3]: - ... +def func8(foo: T_str) -> tuple[T_str]: ... -def func8(foo: _T3 | int) -> tuple[_T3] | int: - ... +def func8(foo: T_str | int) -> tuple[T_str] | int: ... -class Foo: - ... +class ClassE: ... -_T4 = TypeVar("_T4", bound=Foo) +T_E = TypeVar("T_E", bound=ClassE) @overload -def func9() -> None: - ... +def func9() -> None: ... @overload -def func9(bar: _T4) -> _T4: - ... +def func9(bar: T_E) -> T_E: ... -def func9(bar: _T4 | None = None) -> _T4 | None: +def func9(bar: T_E | None = None) -> T_E | None: raise NotImplementedError -_T5 = TypeVar("_T5", int, str) +T_int_str = TypeVar("T_int_str", int, str) @overload -def func10(option: Literal["a"], var: str) -> str: - ... +def func10(option: Literal["a"], var: str) -> str: ... @overload -def func10(option: Literal["b"], var: int) -> str: - ... +def func10(option: Literal["b"], var: int) -> str: ... # This should generate an error. -def func10(option: Literal["a", "b"], var: _T5) -> _T5: - ... +def func10(option: Literal["a", "b"], var: T_int_str) -> T_int_str: ... -class X: - ... +class ClassF: ... -_T6 = TypeVar("_T6", bound=type[X]) +T_F = TypeVar("T_F", bound=type[ClassF]) @overload -def func11(var: _T6) -> _T6: - ... +def func11(var: T_F) -> T_F: ... @overload -def func11(var: int) -> int: - ... +def func11(var: int) -> int: ... -def func11(var: _T6 | int) -> _T6 | int: - ... +def func11(var: T_F | int) -> T_F | int: ... -_T7 = TypeVar("_T7") -_T8 = TypeVar("_T8") -_T9 = TypeVar("_T9") +T7 = TypeVar("T7") +T8 = TypeVar("T8") +T9 = TypeVar("T9") @overload def func12( - func: Callable[[_T7], _T8], iterable: Iterable[_T7], default_value: None = None, / -) -> Iterable[_T8 | None]: - ... + func: Callable[[T7], T8], iterable: Iterable[T7], default_value: None = None, / +) -> Iterable[T8 | None]: ... @overload def func12( - func: Callable[[_T7], _T8], iterable: Iterable[_T7], /, default_value: _T9 -) -> Iterable[_T8 | _T9]: - ... + func: Callable[[T7], T8], iterable: Iterable[T7], /, default_value: T9 +) -> Iterable[T8 | T9]: ... def func12( - func: Callable[[_T7], _T8], - iterable: Iterable[_T7], + func: Callable[[T7], T8], + iterable: Iterable[T7], /, - default_value: _T9 = None, -) -> Iterable[_T8 | _T9]: - ... + default_value: T9 = None, +) -> Iterable[T8 | T9]: ... @overload -def func13(x: int) -> NoReturn: - ... +def func13(x: int) -> NoReturn: ... @overload -def func13(x: str) -> str | NoReturn: - ... +def func13(x: str) -> str | NoReturn: ... -def func13(x: int | str) -> str: - ... +def func13(x: int | str) -> str: ... -_T14 = TypeVar("_T14") - - -class Wrapper1(Generic[_T14]): - ... +class ClassG(Generic[T]): ... @overload -def func14(target: Callable[..., Awaitable[_T14]]) -> Wrapper1[_T14]: - ... +def func14(target: Callable[..., Awaitable[T]]) -> ClassG[T]: ... @overload -def func14(target: Callable[..., _T14]) -> Wrapper1[_T14]: - ... +def func14(target: Callable[..., T]) -> ClassG[T]: ... def func14( - target: Callable[..., Awaitable[_T14]] | Callable[..., _T14], -) -> Wrapper1[_T14]: - ... + target: Callable[..., Awaitable[T]] | Callable[..., T], +) -> ClassG[T]: ... @overload -def func15(client_id: str, client_secret: str, /) -> None: - ... +def func15(client_id: str, client_secret: str, /) -> None: ... @overload -def func15(client_id: str, client_secret: str) -> None: - ... +def func15(client_id: str, client_secret: str) -> None: ... # This should generate an error because some of the keyword arguments are not present. def func15(*creds: str) -> None: pass - - -T1 = TypeVar("T1", covariant=True) -T2 = TypeVar("T2", bound=Callable[..., Any]) -R = TypeVar("R") -P = ParamSpec("P") - - -class Builds(Protocol[T1]): - _target_: str - - -class BuildsWithSig(Builds[T1], Protocol[T1, P]): - def __init__(self, *args: P.args, **kwds: P.kwargs): - ... - - -class ClassD(Protocol): - @overload - def __call__( - self, - x: Callable[P, R], - *, - sig: Literal[True] = ..., - ) -> BuildsWithSig[type[R], P]: - ... - - @overload - def __call__(self, x: T2, *, sig: Literal[False] = ...) -> Builds[type[T2]]: - ... - - @overload - def __call__( - self, x: T2 | Callable[P, R], *, sig: bool - ) -> Builds[type[T2]] | BuildsWithSig[type[R], P]: - ... - - def __call__( - self, x: T2 | Callable[P, R], *, sig: bool = False - ) -> Builds[type[T2]] | BuildsWithSig[type[R], P]: - ... - - -Ts = TypeVarTuple("Ts") - -Func = Callable[[*Ts], None] - - -@overload -def func16(function: Func[*Ts]) -> Func[*Ts]: - ... - - -@overload -def func16() -> Callable[[Func[*Ts]], Func[*Ts]]: - ... - - -def func16( - function: Func[*Ts] | None = None, -) -> Func[*Ts] | Callable[[Func[*Ts]], Func[*Ts]]: - ... - - -@overload -def func17(d: dict[str, float], /) -> None: - ... - - -@overload -def func17(**kwargs: float) -> None: - ... - - -def func17(d: dict[str, float] | None = None, /, **kwargs: float) -> None: - pass - - -@overload -def func18(a: int) -> int: - ... - - -@overload -def func18(*args: int) -> int: - ... - - -# This should generate an error because the keyword parameter "a" is missing. -def func18(*args: int) -> int: - ... - - -@overload -def func19(a: int) -> int: - ... - - -@overload -def func19(*args: int) -> int: - ... - - -def func19(*args: int, a: int = 1) -> int: - ... - - -@overload -def func20(a: int) -> int: - ... - - -@overload -def func20(*args: int) -> int: - ... - - -def func20(*args: int, **kwargs: int) -> int: - ... - - -@overload -def func21(x: tuple[()], /) -> None: - ... - - -@overload -def func21(x: tuple[object], /) -> None: - ... - - -def func21(x: tuple[object, ...], /) -> None: - ... diff --git a/packages/pyright-internal/src/tests/samples/overloadImpl2.py b/packages/pyright-internal/src/tests/samples/overloadImpl2.py new file mode 100644 index 000000000000..47936c912ec2 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/overloadImpl2.py @@ -0,0 +1,145 @@ +# This sample tests the verification that overload implementation signatures +# are a superset of their associated overload signatures. + +from typing import ( + Any, + Callable, + Generic, + Literal, + ParamSpec, + Protocol, + TypeVar, + TypeVarTuple, + overload, +) + + +T = TypeVar("T") +T_contra = TypeVar("T_contra", contravariant=True) +T_co = TypeVar("T_co", covariant=True) +TCall = TypeVar("TCall", bound=Callable[..., Any]) +R = TypeVar("R") +P = ParamSpec("P") +Ts = TypeVarTuple("Ts") + + +class ClassA(Protocol[T_co]): + _target_: str + + +class ClassB(ClassA[T_co], Protocol[T_co, P]): + def __init__(self, *args: P.args, **kwds: P.kwargs): ... + + +class ClassC(Protocol): + @overload + def __call__( + self, + x: Callable[P, R], + *, + sig: Literal[True] = ..., + ) -> ClassB[type[R], P]: ... + + @overload + def __call__( + self, x: TCall, *, sig: Literal[False] = ... + ) -> ClassA[type[TCall]]: ... + + @overload + def __call__( + self, x: TCall | Callable[P, R], *, sig: bool + ) -> ClassA[type[TCall]] | ClassB[type[R], P]: ... + + def __call__( + self, x: TCall | Callable[P, R], *, sig: bool = False + ) -> ClassA[type[TCall]] | ClassB[type[R], P]: ... + + +Func = Callable[[*Ts], None] + + +@overload +def func1(function: Func[*Ts]) -> Func[*Ts]: ... + + +@overload +def func1() -> Callable[[Func[*Ts]], Func[*Ts]]: ... + + +def func1( + function: Func[*Ts] | None = None, +) -> Func[*Ts] | Callable[[Func[*Ts]], Func[*Ts]]: ... + + +@overload +def func2(d: dict[str, float], /) -> None: ... + + +@overload +def func2(**kwargs: float) -> None: ... + + +def func2(d: dict[str, float] | None = None, /, **kwargs: float) -> None: + pass + + +@overload +def func3(a: int) -> int: ... + + +@overload +def func3(*args: int) -> int: ... + + +# This should generate an error because the keyword parameter "a" is missing. +def func3(*args: int) -> int: ... + + +@overload +def func4(a: int) -> int: ... + + +@overload +def func4(*args: int) -> int: ... + + +def func4(*args: int, a: int = 1) -> int: ... + + +@overload +def func5(a: int) -> int: ... + + +@overload +def func5(*args: int) -> int: ... + + +def func5(*args: int, **kwargs: int) -> int: ... + + +@overload +def func6(x: tuple[()], /) -> None: ... + + +@overload +def func6(x: tuple[object], /) -> None: ... + + +def func6(x: tuple[object, ...], /) -> None: ... + + +class ClassD(Generic[T_contra]): + def method(self, x: T_contra) -> int: + assert False + + +@overload +def func7(x: None) -> int: ... + + +@overload +def func7(x: ClassD[T]) -> int: ... + + +def func7(x: ClassD[T] | None) -> int: + assert False diff --git a/packages/pyright-internal/src/tests/typeEvaluator6.test.ts b/packages/pyright-internal/src/tests/typeEvaluator6.test.ts index 5551158ef425..1e89799dd666 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator6.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator6.test.ts @@ -96,7 +96,12 @@ test('OverloadOverride1', () => { test('OverloadImpl1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['overloadImpl1.py']); - TestUtils.validateResults(analysisResults, 7); + TestUtils.validateResults(analysisResults, 6); +}); + +test('OverloadImpl2', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['overloadImpl2.py']); + TestUtils.validateResults(analysisResults, 1); }); test('OverloadOverlap1', () => { From 9bfb4081e23d86a7c077acbe526c9c05a44d3699 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 4 Nov 2024 08:43:28 -0800 Subject: [PATCH 2/2] Fixed bug that results in incorrect evaluation of class variables assigned in an `__init_subclass__` or `__class_getitem__` method. These methods are implicitly class methods even though they are not decorated with `@classmethod`. This addresses https://github.com/microsoft/pylance-release/discussions/2781. --- packages/pyright-internal/src/analyzer/binder.ts | 7 ++++--- .../src/tests/samples/initsubclass1.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/binder.ts b/packages/pyright-internal/src/analyzer/binder.ts index 58a04999da69..49eb10d7c899 100644 --- a/packages/pyright-internal/src/analyzer/binder.ts +++ b/packages/pyright-internal/src/analyzer/binder.ts @@ -4033,9 +4033,10 @@ export class Binder extends ParseTreeWalker { // To determine whether the first parameter of the method // refers to the class or the instance, we need to apply // some heuristics. - if (methodNode.d.name.d.value === '__new__') { - // The __new__ method is special. It acts as a classmethod even - // though it doesn't have a @classmethod decorator. + const implicitClassMethods = ['__new__', '__init_subclass__', '__class_getitem__']; + if (implicitClassMethods.includes(methodNode.d.name.d.value)) { + // Several methods are special. They act as class methods even + // though they don't have a @classmethod decorator. isInstanceMember = false; } else { // Assume that it's an instance member unless we find diff --git a/packages/pyright-internal/src/tests/samples/initsubclass1.py b/packages/pyright-internal/src/tests/samples/initsubclass1.py index fa6b5673cfc9..f39eb94cb1ec 100644 --- a/packages/pyright-internal/src/tests/samples/initsubclass1.py +++ b/packages/pyright-internal/src/tests/samples/initsubclass1.py @@ -62,3 +62,14 @@ class ClassH(ClassG): # in the object.__init_subclass__ method. class ClassI(a=3): a: int + + +class ClassJ: + def __init_subclass__(cls, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + cls.custom_attribute = 9 + + +class ClassJChild(ClassJ): + def __init__(self): + reveal_type(self.custom_attribute, expected_text="int")