diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md index 1bc97ad4c4..096ab2e0f8 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md @@ -9,15 +9,19 @@ ```py from typing_extensions import Annotated + def _(x: Annotated[int, "foo"]): reveal_type(x) # revealed: int + def _(x: Annotated[int, lambda: 0 + 1 * 2 // 3, _(4)]): reveal_type(x) # revealed: int + def _(x: Annotated[int, "arbitrary", "metadata", "elements", "are", "fine"]): reveal_type(x) # revealed: int + def _(x: Annotated[tuple[str, int], bytes]): reveal_type(x) # revealed: tuple[str, int] ``` @@ -29,10 +33,12 @@ It is invalid to parameterize `Annotated` with less than two arguments. ```py from typing_extensions import Annotated + # error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression" def _(x: Annotated): reveal_type(x) # revealed: Unknown + def _(flag: bool): if flag: X = Annotated @@ -43,14 +49,17 @@ def _(flag: bool): def f(y: X): reveal_type(y) # revealed: Unknown | bool + # error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression" def _(x: Annotated | bool): reveal_type(x) # revealed: Unknown | bool + # error: [invalid-type-form] "Special form `typing.Annotated` expected at least 2 arguments (one type and at least one metadata element)" def _(x: Annotated[()]): reveal_type(x) # revealed: Unknown + # error: [invalid-type-form] def _(x: Annotated[int]): # `Annotated[T]` is invalid and will raise an error at runtime, @@ -59,6 +68,7 @@ def _(x: Annotated[int]): # Same for the `(int,)` form below. reveal_type(x) # revealed: int + # error: [invalid-type-form] def _(x: Annotated[(int,)]): reveal_type(x) # revealed: int @@ -74,18 +84,24 @@ Inheriting from `Annotated[T, ...]` is equivalent to inheriting from `T` itself. from typing_extensions import Annotated from ty_extensions import reveal_mro + class C(Annotated[int, "foo"]): ... + # revealed: (, , ) reveal_mro(C) + class D(Annotated[list[str], "foo"]): ... + # revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(D) + class E(Annotated[list["E"], "metadata"]): ... + # error: [revealed-type] "Revealed MRO: (, , , , , , , , typing.Protocol, typing.Generic, )" reveal_mro(E) ``` @@ -96,9 +112,11 @@ reveal_mro(E) from typing_extensions import Annotated from ty_extensions import reveal_mro + # At runtime, this is an error. # error: [invalid-base] class C(Annotated): ... + reveal_mro(C) # revealed: (, Unknown, ) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/any.md b/crates/ty_python_semantic/resources/mdtest/annotations/any.md index eef66d6b74..7df1fdd255 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/any.md @@ -10,6 +10,7 @@ from typing import Any x: Any = 1 x = "foo" + def f(): reveal_type(x) # revealed: Any ``` @@ -24,6 +25,7 @@ from typing import Any as RenamedAny x: RenamedAny = 1 x = "foo" + def f(): reveal_type(x) # revealed: Any ``` @@ -36,11 +38,14 @@ isn't a spelling of the Any type. ```py class Any: ... + x: Any + def f(): reveal_type(x) # revealed: Any + # This verifies that we're not accidentally seeing typing.Any, since str is assignable # to that but not to our locally defined class. y: Any = "not an Any" # error: [invalid-assignment] @@ -58,8 +63,10 @@ allowed, even when the unknown superclass is `int`. The assignment to `y` should from typing import Any from ty_extensions import reveal_mro + class SubclassOfAny(Any): ... + reveal_mro(SubclassOfAny) # revealed: (, Any, ) x: SubclassOfAny = 1 # error: [invalid-assignment] @@ -72,14 +79,18 @@ possibly be a subclass of `FinalClass`: ```py from typing import final + @final class FinalClass: ... + f: FinalClass = SubclassOfAny() # error: [invalid-assignment] + @final class OtherFinalClass: ... + f: FinalClass | OtherFinalClass = SubclassOfAny() # error: [invalid-assignment] ``` @@ -88,33 +99,44 @@ A subclass of `Any` can also be assigned to arbitrary `Callable` and `Protocol` ```py from typing import Callable, Any, Protocol + def takes_callable1(f: Callable): f() + takes_callable1(SubclassOfAny()) + def takes_callable2(f: Callable[[int], None]): f(1) + takes_callable2(SubclassOfAny()) + class CallbackProtocol(Protocol): def __call__(self, x: int, /) -> None: ... + def takes_callback_proto(f: CallbackProtocol): f(1) + takes_callback_proto(SubclassOfAny()) + class OtherProtocol(Protocol): x: int + @property def foo(self) -> bytes: ... @foo.setter def foo(self, x: str) -> None: ... + def takes_other_protocol(f: OtherProtocol): ... + takes_other_protocol(SubclassOfAny()) ``` @@ -123,9 +145,11 @@ A subclass of `Any` cannot be assigned to literal types, since those cannot be s ```py from typing import Any, Literal + class MockAny(Any): pass + x: Literal[1] = MockAny() # error: [invalid-assignment] ``` @@ -161,6 +185,7 @@ static_assert(is_assignable_to(TypeOf[Any], type)) ```py from typing import Any + # error: [invalid-type-form] "Special form `typing.Any` expected no type parameter" def f(x: Any[int]): reveal_type(x) # revealed: Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index 34dd6e7d5d..eed0c84c12 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -22,6 +22,7 @@ A bare `Callable` without any type arguments: ```py from typing import Callable + def _(c: Callable): reveal_type(c) # revealed: (...) -> Unknown ``` @@ -33,6 +34,7 @@ When it's not a list: ```py from typing import Callable + # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" def _(c: Callable[int, str]): reveal_type(c) # revealed: (...) -> Unknown @@ -63,6 +65,7 @@ Using a parameter list: ```py from typing import Callable + # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" def _(c: Callable[[int, str]]): reveal_type(c) # revealed: (...) -> Unknown @@ -111,6 +114,7 @@ which argument corresponds to either the parameters or the return type. ```py from typing import Callable + # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" def _(c: Callable[[int], str, str]): reveal_type(c) # revealed: (...) -> Unknown @@ -151,6 +155,7 @@ def _(c: Callable[ ```py from typing import Callable + # error: [invalid-type-form] "List literals are not allowed in this context in a type expression" def _(c: Callable[[int], [str]]): reveal_type(c) # revealed: (int, /) -> Unknown @@ -180,6 +185,7 @@ A simple `Callable` with multiple parameters and a return type: ```py from typing import Callable + def _(c: Callable[[int, str], int]): reveal_type(c) # revealed: (int, str, /) -> int ``` @@ -189,6 +195,7 @@ def _(c: Callable[[int, str], int]): ```py from typing import Callable, Union + def _( c: Callable[[Union[int, str]], int] | None, d: None | Callable[[Union[int, str]], int], @@ -205,8 +212,10 @@ def _( from typing import Callable, Union from ty_extensions import Intersection, Not + class Foo: ... + def _( c: Intersection[Callable[[Union[int, str]], int], int], d: Intersection[int, Callable[[Union[int, str]], int]], @@ -226,6 +235,7 @@ A nested `Callable` as one of the parameter types: ```py from typing import Callable + def _(c: Callable[[Callable[[int], str]], int]): reveal_type(c) # revealed: ((int, /) -> str, /) -> int ``` @@ -245,6 +255,7 @@ is a [gradual form] indicating that the type is consistent with any input signat ```py from typing import Callable + def gradual_form(c: Callable[..., str]): reveal_type(c) # revealed: (...) -> str ``` @@ -256,6 +267,7 @@ Using `Concatenate` as the first argument to `Callable`: ```py from typing_extensions import Callable, Concatenate + def _(c: Callable[Concatenate[int, str, ...], int]): # TODO: Should reveal the correct signature reveal_type(c) # revealed: (...) -> int @@ -306,6 +318,7 @@ Using a `ParamSpec` in a `Callable` annotation: ```py from typing_extensions import Callable + def _[**P1](c: Callable[P1, int]): reveal_type(P1.args) # revealed: P1@_.args reveal_type(P1.kwargs) # revealed: P1@_.kwargs @@ -320,6 +333,7 @@ from typing_extensions import ParamSpec P2 = ParamSpec("P2") + def _(c: Callable[P2, int]): reveal_type(c) # revealed: (**P2@_) -> int ``` @@ -333,6 +347,7 @@ from typing_extensions import Callable, TypeVarTuple Ts = TypeVarTuple("Ts") + def _(c: Callable[[int, *Ts], int]): # TODO: Should reveal the correct signature reveal_type(c) # revealed: (...) -> int @@ -343,6 +358,7 @@ And, using the legacy syntax using `Unpack`: ```py from typing_extensions import Unpack + def _(c: Callable[[int, Unpack[Ts]], int]): # TODO: Should reveal the correct signature reveal_type(c) # revealed: (...) -> int @@ -353,6 +369,7 @@ def _(c: Callable[[int, Unpack[Ts]], int]): ```py from typing import Callable + def _(c: Callable[[int], int]): reveal_type(c.__init__) # revealed: bound method object.__init__() -> None reveal_type(c.__class__) # revealed: type @@ -379,6 +396,7 @@ class MyCallable: def __call__(self) -> None: pass + f_wrong(MyCallable()) # raises `AttributeError` at runtime ``` @@ -388,6 +406,7 @@ of the attribute first: ```py from inspect import getattr_static + def f_okay(c: Callable[[], None]): if hasattr(c, "__qualname__"): reveal_type(c.__qualname__) # revealed: object @@ -414,13 +433,16 @@ def f_okay(c: Callable[[], None]): ```py from ty_extensions import into_callable + class Base: def __init__(self) -> None: pass + class A(Base): pass + # revealed: () -> A reveal_type(into_callable(A)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md b/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md index 0a1f8ea912..1510aaaa03 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md @@ -23,11 +23,14 @@ In (regular) source files, annotations are *not* deferred. This also tests that ```py from __future__ import with_statement as annotations + # error: [unresolved-reference] def get_foo() -> Foo: ... + class Foo: ... + reveal_type(get_foo()) # revealed: Unknown ``` @@ -38,11 +41,14 @@ If `__future__.annotations` is imported, annotations *are* deferred. ```py from __future__ import annotations + def get_foo() -> Foo: return Foo() + class Foo: ... + reveal_type(get_foo()) # revealed: Foo ``` @@ -56,6 +62,7 @@ python-version = "3.12" ```py from __future__ import annotations + class Foo: this: Foo # error: [unresolved-reference] @@ -77,9 +84,11 @@ class Foo: def f(self, x: Foo): return self + # error: [unresolved-reference] def g(self) -> Bar: return self + # error: [unresolved-reference] def h[T: Bar](self): pass @@ -108,6 +117,7 @@ class Foo: def h[T: Bar](): # error: [unresolved-reference] return Bar() + type Baz = Foo ``` @@ -132,6 +142,7 @@ class Foo: # error: [unresolved-reference] def f(self, x: Foo): reveal_type(x) # revealed: Unknown + # error: [unresolved-reference] def g(self) -> Foo: _: Foo = self @@ -145,9 +156,11 @@ class Foo: # error: [unresolved-reference] def f(self, x: Foo): return self + # error: [unresolved-reference] def g(self) -> Bar: return self + # error: [unresolved-reference] def h[T: Bar](self): pass @@ -176,8 +189,10 @@ class Foo: def h[T: Bar](): # error: [unresolved-reference] return Bar() + type Qux = Foo + def _(): class C: # error: [unresolved-reference] @@ -192,9 +207,11 @@ def _(): ```py from __future__ import annotations + class A(B): # error: [unresolved-reference] pass + class B: pass ``` @@ -215,6 +232,7 @@ class B: ... def f(mode: int = ParseMode.test): pass + class ParseMode: test = 1 ``` @@ -246,6 +264,7 @@ def f(mode: int = NeverDefined.test): ... class Foo(metaclass=SomeMeta): pass + class SomeMeta(type): pass ``` @@ -275,6 +294,7 @@ class Foo(metaclass=NeverDefined): ... # error: [unresolved-reference] f = lambda x=Foo(): x + class Foo: pass ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/generic_alias.md b/crates/ty_python_semantic/resources/mdtest/annotations/generic_alias.md index 86a115d90e..4a622bedbb 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/generic_alias.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/generic_alias.md @@ -11,6 +11,7 @@ Numbers = list[int] # this as `list[int]` is more helpful, though: reveal_type(Numbers) # revealed: + def _(numbers: Numbers) -> None: reveal_type(numbers) # revealed: list[int] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md index fada88bd9b..fab019d5e1 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md @@ -12,6 +12,7 @@ An annotation of `float` means `int | float`, so `int` is assignable to it: def takes_float(x: float): pass + def passes_int_to_float(x: int): # no error! takes_float(x) @@ -31,10 +32,12 @@ It doesn't work the other way around: def takes_int(x: int): pass + def passes_float_to_int(x: float): # error: [invalid-argument-type] takes_int(x) + def assigns_float_to_int(x: float): # error: [invalid-assignment] y: int = x @@ -57,34 +60,41 @@ to it (but not the other way around): def takes_complex(x: complex): pass + def passes_to_complex(x: float, y: int): # no errors! takes_complex(x) takes_complex(y) + def assigns_to_complex(x: float, y: int): # no errors! a: complex = x b: complex = y + def takes_int(x: int): pass + def takes_float(x: float): pass + def passes_complex(x: complex): # error: [invalid-argument-type] takes_int(x) # error: [invalid-argument-type] takes_float(x) + def assigns_complex(x: complex): # error: [invalid-assignment] y: int = x # error: [invalid-assignment] z: float = x + def f(x: complex): reveal_type(x) # revealed: int | float | complex ``` @@ -98,6 +108,7 @@ be narrowed to `int` or `float`: from typing_extensions import assert_type from ty_extensions import JustFloat + def f(x: complex): reveal_type(x) # revealed: int | float | complex diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index 32626dc519..19a3a7172a 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -9,8 +9,10 @@ import typing from ty_extensions import AlwaysTruthy, AlwaysFalsy from typing_extensions import Literal, Never + class A: ... + def _( a: type[int], b: AlwaysTruthy, @@ -49,6 +51,7 @@ def _( reveal_type(i_) # revealed: Unknown reveal_type(j_) # revealed: Unknown + # Inspired by the conformance test suite at # https://github.com/python/typing/blob/d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc/conformance/tests/aliases_implicit.py#L88-L122 B = [x for x in range(42)] @@ -56,6 +59,7 @@ C = {x for x in range(42)} D = {x: y for x, y in enumerate(range(42))} E = (x for x in range(42)) + def _( b: B, # error: [invalid-type-form] c: C, # error: [invalid-type-form] @@ -74,11 +78,13 @@ def _( def bar() -> None: return None + def outer_sync(): # `yield` from is only valid syntax inside a synchronous function def _( a: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions" ): ... + async def baz(): ... async def outer_async(): # avoid unrelated syntax errors on `yield` and `await` def _( @@ -120,6 +126,7 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await` reveal_type(p) # revealed: int | Unknown reveal_type(q) # revealed: Unknown + class Mat: def __init__(self, value: int): self.value = value @@ -127,6 +134,7 @@ class Mat: def __matmul__(self, other) -> int: return 42 + def invalid_binary_operators( a: "1" + "2", # error: [invalid-type-form] "Invalid binary operator `+` in type annotation" b: 3 - 5.0, # error: [invalid-type-form] "Invalid binary operator `-` in type annotation" @@ -186,10 +194,12 @@ def _( reveal_type(h) # revealed: Unknown reveal_type(i) # revealed: Unknown + # error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `list[int]`?" class name_0[name_2: [int]]: pass + # error: [invalid-type-form] "List literals are not allowed in this context in a type expression" # error: [invalid-type-form] "Dict literals are not allowed in type expressions" class name_4[name_1: [{}]]: @@ -211,6 +221,7 @@ for this case: ```py import datetime + def f(x: datetime): ... # error: [invalid-type-form] ``` @@ -225,6 +236,7 @@ class Image: ... ```py from PIL import Image + def g(x: Image): ... # error: [invalid-type-form] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/literal.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal.md index 8544ddd782..23eada896d 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/literal.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/literal.md @@ -18,16 +18,19 @@ a6: Literal[True] a7: Literal[None] a8: Literal[Literal[1]] + class Color(Enum): RED = 0 GREEN = 1 BLUE = 2 + b1: Literal[Color.RED] MissingT = Enum("MissingT", {"MISSING": "MISSING"}) b2: Literal[MissingT.MISSING] + def f(): reveal_type(mode) # revealed: Literal["w", "r"] reveal_type(a1) # revealed: Literal[26] @@ -42,6 +45,7 @@ def f(): # TODO should be `Literal[MissingT.MISSING]` reveal_type(b2) # revealed: @Todo(functional `Enum` syntax) + # error: [invalid-type-form] invalid1: Literal[3 + 4] # error: [invalid-type-form] @@ -57,9 +61,11 @@ invalid4: Literal[ (1, 2, 3), # error: [invalid-type-form] ] + class NotAnEnum: x: int = 1 + # error: [invalid-type-form] invalid5: Literal[NotAnEnum.x] @@ -86,10 +92,12 @@ from enum import Enum import mod + class E(Enum): A = 1 B = 2 + type SingleInt = Literal[1] type SingleStr = Literal["foo"] type SingleBytes = Literal[b"bar"] @@ -105,6 +113,7 @@ type AnEnum2 = Literal[E.A, E.B] type Bool1 = bool type Bool2 = Literal[True, False] + def _( single_int: Literal[SingleInt], single_str: Literal[SingleStr], @@ -149,10 +158,12 @@ type SingleInt = Literal[2] from typing import Literal, TypeAlias from enum import Enum + class E(Enum): A = 1 B = 2 + SingleInt: TypeAlias = Literal[1] SingleStr: TypeAlias = Literal["foo"] SingleBytes: TypeAlias = Literal[b"bar"] @@ -165,6 +176,7 @@ AnEnum2: TypeAlias = Literal[E.A, E.B] Bool1: TypeAlias = bool Bool2: TypeAlias = Literal[True, False] + def _( single_int: Literal[SingleInt], single_str: Literal[SingleStr], @@ -203,10 +215,12 @@ def _( from typing import Literal from enum import Enum + class E(Enum): A = 1 B = 2 + SingleInt = Literal[1] SingleStr = Literal["foo"] SingleBytes = Literal[b"bar"] @@ -222,6 +236,7 @@ AnEnum2 = Literal[E.A, E.B] Bool1 = bool Bool2 = Literal[True, False] + def _( single_int: Literal[SingleInt], single_str: Literal[SingleStr], @@ -258,6 +273,7 @@ the union of those types. ```py from typing import Literal + def x( a1: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None], a2: Literal["w"] | Literal["r"], @@ -275,15 +291,21 @@ def x( ```py from typing import Literal, Union + def foo(x: int) -> int: return x + 1 + def bar(s: str) -> str: return s + class A: ... + + class B: ... + def union_example( x: Union[ # unknown type @@ -333,6 +355,7 @@ from other import Literal # error: [invalid-type-form] "Invalid subscript of object of type `_SpecialForm` in type expression" a1: Literal[26] + def f(): reveal_type(a1) # revealed: Unknown ``` @@ -344,6 +367,7 @@ from typing_extensions import Literal a1: Literal[26] + def f(): reveal_type(a1) # revealed: Literal[26] ``` @@ -353,6 +377,7 @@ def f(): ```py from typing import Literal + # error: [invalid-type-form] "`typing.Literal` requires at least one argument when used in a type expression" def _(x: Literal): reveal_type(x) # revealed: Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md index 3b0aa2d26c..e58cfc85b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md @@ -16,6 +16,7 @@ from typing_extensions import LiteralString x: LiteralString + def f(): reveal_type(x) # revealed: LiteralString ``` @@ -54,6 +55,7 @@ Subclassing `LiteralString` leads to a runtime error. ```py from typing_extensions import LiteralString + class C(LiteralString): ... # error: [invalid-base] ``` @@ -92,6 +94,7 @@ vice versa. ```py from typing_extensions import Literal, LiteralString + def _(flag: bool): foo_1: Literal["foo"] = "foo" bar_1: LiteralString = foo_1 # fine @@ -146,6 +149,7 @@ from typing import LiteralString x: LiteralString = "foo" + def f(): reveal_type(x) # revealed: LiteralString ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/never.md b/crates/ty_python_semantic/resources/mdtest/annotations/never.md index 62f0968d29..c1545b9d90 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/never.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/never.md @@ -9,9 +9,11 @@ interchangeably. ```py from typing import NoReturn + def stop() -> NoReturn: raise RuntimeError("no way") + # revealed: Never reveal_type(stop()) ``` @@ -28,6 +30,7 @@ a2: Never b1: Any b2: int + def f(): # revealed: Never reveal_type(a1) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index c6b4af5f52..cde31420f1 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -9,6 +9,7 @@ from typing_extensions import NewType UserId = NewType("UserId", int) + def _(user_id: UserId): reveal_type(user_id) # revealed: UserId ``` @@ -38,10 +39,12 @@ Foo(Bar(Foo(42))) # allowed: `Bar` is a subtype of `int`. Foo(True) # allowed: `bool` is a subtype of `int`. Foo("forty-two") # error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `Literal["forty-two"]`" + def f(_: int): ... def g(_: Foo): ... def h(_: Bar): ... + f(42) f(Foo(42)) f(Bar(Foo(42))) @@ -60,11 +63,14 @@ h(Bar(Foo(42))) ```py from typing_extensions import NewType + class Foo: foo_member: str = "hello" + def foo_method(self) -> int: return 42 + Bar = NewType("Bar", Foo) Baz = NewType("Baz", Bar) baz = Baz(Bar(Foo())) @@ -88,16 +94,21 @@ from ty_extensions import CallableTypeOf Foo = NewType("Foo", int) + def _(obj: CallableTypeOf[Foo]): reveal_type(obj) # revealed: (int, /) -> Foo + def f(_: Callable[[int], Foo]): ... + f(Foo) map(Foo, [1, 2, 3]) + def g(_: Callable[[str], Foo]): ... + g(Foo) # error: [invalid-argument-type] ``` @@ -112,19 +123,23 @@ i = N(42) y: Callable[..., Any] = i # error: [invalid-assignment] "Object of type `N` is not assignable to `(...) -> Any`" + # error: [invalid-type-form] "Expected the first argument to `ty_extensions.CallableTypeOf` to be a callable object, but got an object of type `N`" def f(x: CallableTypeOf[i]): reveal_type(x) # revealed: Unknown + class SomethingCallable: def __call__(self, a: str) -> bytes: raise NotImplementedError + N2 = NewType("N2", SomethingCallable) j = N2(SomethingCallable()) z: Callable[[str], bytes] = j # fine + def g(x: CallableTypeOf[j]): reveal_type(x) # revealed: (a: str) -> bytes ``` @@ -134,6 +149,7 @@ def g(x: CallableTypeOf[j]): ```py from typing_extensions import NewType + def _(name: str) -> None: _ = NewType(name, int) # error: [invalid-newtype] "The first argument to `NewType` must be a string literal" ``` @@ -205,6 +221,7 @@ class Bar: def __contains__(self, key: Foo) -> bool: return True + reveal_type(Foo(42) + Bar()) # revealed: Foo reveal_type(Bar() + Foo(42)) # revealed: Foo reveal_type(Foo(42) < Bar()) # revealed: bool @@ -279,12 +296,16 @@ type: ```py from collections.abc import Callable + def f(_: Callable[[int | float], Foo]): ... + f(Foo) + def g(_: Callable[[int | float | complex], Bar]): ... + g(Bar) ``` @@ -321,6 +342,7 @@ class Bing: def __contains__(self, key: Foo) -> bool: return True + reveal_type(Foo(3.14) + Bing()) # revealed: Foo reveal_type(Bing() + Foo(42)) # revealed: Foo reveal_type(Foo(3.14) < Bing()) # revealed: bool @@ -439,6 +461,7 @@ from typing import NewType N = NewType("N", int) + def f(x: N): reveal_type(isinstance(x, int)) # revealed: Literal[True] ``` @@ -470,6 +493,7 @@ from typing import NewType X = NewType("X", int) + class Foo(X): ... # error: [invalid-base] ``` @@ -481,12 +505,15 @@ class Foo(X): ... # error: [invalid-base] from enum import Enum from typing import NewType + class Foo(Enum): X = 0 Y = 1 + N = NewType("N", Foo) + def f(x: N): match x: case Foo.X: @@ -530,14 +557,18 @@ reveal_type(Bar(42)) # revealed: Bar ```py from typing import NewType, Protocol, TypedDict + class Id(Protocol): code: int + UserId = NewType("UserId", Id) # error: [invalid-newtype] + class Foo(TypedDict): a: int + Bar = NewType("Bar", Foo) # error: [invalid-newtype] ``` @@ -578,11 +609,15 @@ from stub import N, A n = N(A()) # fine + def f(x: A): ... + f(n) # fine + class Invalid: ... + bad = N(Invalid()) # error: [invalid-argument-type] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/optional.md b/crates/ty_python_semantic/resources/mdtest/annotations/optional.md index 654c88b199..7a094d101e 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/optional.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/optional.md @@ -12,6 +12,7 @@ a1: Optional[bool] a2: Optional[Optional[bool]] a3: Optional[None] + def f(): # revealed: int | None reveal_type(a) @@ -41,6 +42,7 @@ from typing_extensions import Optional a: Optional[int] + def f(): # revealed: int | None reveal_type(a) @@ -51,6 +53,7 @@ def f(): ```py from typing import Optional + # error: [invalid-type-form] "`typing.Optional` requires exactly one argument when used in a type expression" def f(x: Optional) -> None: reveal_type(x) # revealed: Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 32fd3930cf..eaf5431e4c 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -14,6 +14,7 @@ python-version = "3.13" ```py from typing import Self + class Shape: def set_scale(self: Self, scale: float) -> Self: reveal_type(self) # revealed: Self@set_scale @@ -26,21 +27,26 @@ class Shape: def inner() -> Self: reveal_type(self) # revealed: Self@nested_func return self + return inner() def nested_func_without_enclosing_binding(self): def inner(x: Self): reveal_type(x) # revealed: Self@nested_func_without_enclosing_binding + inner(self) + reveal_type(Shape().nested_type()) # revealed: list[Shape] reveal_type(Shape().nested_func()) # revealed: Shape + class Circle(Shape): def set_scale(self: Self, scale: float) -> Self: reveal_type(self) # revealed: Self@set_scale return self + class Outer: class Inner: def foo(self: Self) -> Self: @@ -62,6 +68,7 @@ python-version = "3.12" ```py from typing import Self + class A: def __init__(self): reveal_type(self) # revealed: Self@__init__ @@ -103,6 +110,7 @@ class A: @staticmethod def a_staticmethod(x: int): ... + a = A() reveal_type(a.implicit_self()) # revealed: A @@ -123,9 +131,11 @@ Passing `self` implicitly also verifies the type: ```py from typing import Never, Callable + class Strange: def can_not_be_called(self: Never) -> None: ... + # error: [invalid-argument-type] "Argument to bound method `can_not_be_called` is incorrect: Expected `Never`, found `Strange`" Strange().can_not_be_called() ``` @@ -147,6 +157,7 @@ The name `self` is not special in any way. def some_decorator[**P, R](f: Callable[P, R]) -> Callable[P, R]: return f + class B: def name_does_not_matter(this) -> Self: reveal_type(this) # revealed: Self@name_does_not_matter @@ -184,12 +195,14 @@ class B: @some_decorator def decorated_static_method(self): reveal_type(self) # revealed: Unknown + # TODO: On Python <3.10, this should ideally be rejected, because `staticmethod` objects were not callable. @some_decorator @staticmethod def decorated_static_method_2(self): reveal_type(self) # revealed: Unknown + reveal_type(B().name_does_not_matter()) # revealed: B reveal_type(B().positional_only(1)) # revealed: B reveal_type(B().keyword_only(x=1)) # revealed: B @@ -197,6 +210,7 @@ reveal_type(B().decorated_method()) # revealed: B reveal_type(B().a_property) # revealed: B + async def _(): reveal_type(await B().async_method()) # revealed: B ``` @@ -208,12 +222,14 @@ from typing import Self, Generic, TypeVar T = TypeVar("T") + class G(Generic[T]): def id(self) -> Self: reveal_type(self) # revealed: Self@id return self + reveal_type(G[int]().id()) # revealed: G[int] reveal_type(G[str]().id()) # revealed: G[str] ``` @@ -224,15 +240,18 @@ Free functions and nested functions do not use implicit `Self`: def not_a_method(self): reveal_type(self) # revealed: Unknown + # error: [invalid-type-form] def does_not_return_self(self) -> Self: return self + class C: def outer(self) -> None: def inner(self): reveal_type(self) # revealed: Unknown + reveal_type(not_a_method) # revealed: def not_a_method(self) -> Unknown ``` @@ -248,15 +267,18 @@ affect that subtitution. If we blindly substitute all occurrences of `Self`, we ```py from typing import Self + class Foo[T]: def foo(self: Self) -> T: raise NotImplementedError + class Bar: def bar(self: Self, x: Foo[Self]): # revealed: bound method Foo[Self@bar].foo() -> Self@bar reveal_type(x.foo) + def f[U: Bar](x: Foo[U]): # revealed: bound method Foo[U@f].foo() -> U@f reveal_type(x.foo) @@ -272,10 +294,12 @@ python-version = "3.10" ```py from typing_extensions import Self + class C: def method(self: Self) -> Self: return self + reveal_type(C().method()) # revealed: C ``` @@ -286,6 +310,7 @@ reveal_type(C().method()) # revealed: C ```py from typing import Self + class Shape: def foo(self: Self) -> Self: return self @@ -295,8 +320,10 @@ class Shape: reveal_type(cls) # revealed: type[Self@bar] return cls() + class Circle(Shape): ... + reveal_type(Shape().foo()) # revealed: Shape reveal_type(Shape.bar()) # revealed: Shape @@ -309,6 +336,7 @@ reveal_type(Circle.bar()) # revealed: Circle ```py from typing import Self + class Shape: def foo(self) -> Self: return self @@ -318,8 +346,10 @@ class Shape: reveal_type(cls) # revealed: type[Self@bar] return cls() + class Circle(Shape): ... + reveal_type(Shape().foo()) # revealed: Shape reveal_type(Shape.bar()) # revealed: Shape @@ -332,6 +362,7 @@ reveal_type(Circle.bar()) # revealed: Circle ```py from typing import Self + class GenericShape[T]: def foo(self) -> Self: return self @@ -346,8 +377,10 @@ class GenericShape[T]: reveal_type(cls) # revealed: type[Self@baz] return cls() + class GenericCircle[T](GenericShape[T]): ... + reveal_type(GenericShape().foo()) # revealed: GenericShape[Unknown] reveal_type(GenericShape.bar()) # revealed: GenericShape[Unknown] reveal_type(GenericShape[int].bar()) # revealed: GenericShape[int] @@ -369,16 +402,19 @@ the return type should be the child's `Self` type variable, not the concrete chi ```py from typing import Self + class Parent: def copy(self) -> Self: return self + class Child(Parent): def copy(self) -> Self: result = super().copy() reveal_type(result) # revealed: Self@copy return result + # When called on concrete types, Self is substituted correctly. reveal_type(Child().copy()) # revealed: Child ``` @@ -388,11 +424,13 @@ The same applies to classmethods with `Self` return types: ```py from typing import Self + class Parent: @classmethod def create(cls) -> Self: return cls() + class Child(Parent): @classmethod def create(cls) -> Self: @@ -400,6 +438,7 @@ class Child(Parent): reveal_type(result) # revealed: Self@create return result + # When called on concrete types, Self is substituted correctly. reveal_type(Child.create()) # revealed: Child ``` @@ -412,6 +451,7 @@ TODO: The use of `Self` to annotate the `next_node` attribute should be ```py from typing import Self + class LinkedList: value: int next_node: Self @@ -422,6 +462,7 @@ class LinkedList: # error: [invalid-return-type] return self.next_node + reveal_type(LinkedList().next()) # revealed: LinkedList ``` @@ -432,8 +473,10 @@ from typing import Generic, TypeVar T = TypeVar("T") + class C(Generic[T]): foo: T + def method(self) -> None: reveal_type(self) # revealed: Self@method reveal_type(self.foo) # revealed: T@C @@ -446,11 +489,14 @@ from typing import Self, Generic, TypeVar T = TypeVar("T") + class Container(Generic[T]): value: T + def set_value(self: Self, value: T) -> Self: return self + int_container: Container[int] = Container[int]() reveal_type(int_container) # revealed: Container[int] reveal_type(int_container.set_value(1)) # revealed: Container[int] @@ -466,26 +512,31 @@ a type that satisfies a bound. ```py from typing import NewType + class Base: ... + class C[T: Base]: x: T def g(self) -> None: pass + # Calling a method on a specialized instance should not produce an error C[Base]().g() # Test with a NewType bound K = NewType("K", int) + class D[T: K]: x: T def h(self) -> None: pass + # Calling a method on a specialized instance should not produce an error D[K]().h() ``` @@ -499,6 +550,7 @@ TODO: ```py from typing import Self + class Shape: def union(self: Self, other: Self | None): reveal_type(other) # revealed: Self@union | None @@ -512,10 +564,12 @@ This is a regression test for . ```py from typing import Self + class Container[T = bytes]: def __init__(self: Self, data: T | None = None) -> None: self.data = data + reveal_type(Container()) # revealed: Container[bytes] reveal_type(Container(1)) # revealed: Container[int] reveal_type(Container("a")) # revealed: Container[str] @@ -527,20 +581,25 @@ reveal_type(Container(b"a")) # revealed: Container[bytes] ```py from typing import Self, TypeVar, Generic + class Container[T = bytes]: def method(self) -> Self: return self + def _(c: Container[str], d: Container): reveal_type(c.method()) # revealed: Container[str] reveal_type(d.method()) # revealed: Container[bytes] + T = TypeVar("T", default=bytes) + class LegacyContainer(Generic[T]): def method(self) -> Self: return self + def _(c: LegacyContainer[str], d: LegacyContainer): reveal_type(c.method()) # revealed: LegacyContainer[str] reveal_type(d.method()) # revealed: LegacyContainer[bytes] @@ -555,12 +614,15 @@ from typing import Self, Generic, TypeVar T = TypeVar("T") + # error: [invalid-type-form] def x(s: Self): ... + # error: [invalid-type-form] b: Self + # TODO: "Self" cannot be used in a function with a `self` or `cls` parameter that has a type annotation other than "Self" class Foo: # TODO: This `self: T` annotation should be rejected because `T` is not `Self` @@ -578,11 +640,14 @@ class Foo: # error: [invalid-return-type] return Foo() + class Bar(Generic[T]): ... + # error: [invalid-type-form] class Baz(Bar[Self]): ... + class MyMetaclass(type): # TODO: reject the Self usage. because self cannot be used within a metaclass. def __new__(cls) -> Self: @@ -604,9 +669,11 @@ from __future__ import annotations from typing import final + @final class Disjoint: ... + class Explicit: # TODO: We could emit a warning if the annotated type of `self` is disjoint from `Explicit` def bad(self: Disjoint) -> None: @@ -615,15 +682,18 @@ class Explicit: def forward(self: Explicit) -> None: reveal_type(self) # revealed: Explicit + # error: [invalid-argument-type] "Argument to bound method `bad` is incorrect: Expected `Disjoint`, found `Explicit`" Explicit().bad() Explicit().forward() + class ExplicitGeneric[T]: def special(self: ExplicitGeneric[int]) -> None: reveal_type(self) # revealed: ExplicitGeneric[int] + ExplicitGeneric[int]().special() # TODO: this should be an `invalid-argument-type` error @@ -638,6 +708,7 @@ specific type of the bound parameter. ```py from typing import Self + class C: def instance_method(self, other: Self) -> Self: return self @@ -646,13 +717,16 @@ class C: def class_method(cls) -> Self: return cls() + # revealed: bound method C.instance_method(other: C) -> C reveal_type(C().instance_method) # revealed: bound method .class_method() -> C reveal_type(C.class_method) + class D(C): ... + # revealed: bound method D.instance_method(other: D) -> D reveal_type(D().instance_method) # revealed: bound method .class_method() -> D @@ -666,13 +740,16 @@ bound at `C.f`. from typing import Self from ty_extensions import generic_context + class C[T](): def f(self: Self): def b(x: Self): reveal_type(x) # revealed: Self@f + # revealed: None reveal_type(generic_context(b)) + # revealed: ty_extensions.GenericContext[Self@f] reveal_type(generic_context(C.f)) ``` @@ -684,13 +761,16 @@ Even if the `Self` annotation appears first in the nested function, it is the me from typing import Self from ty_extensions import generic_context + class C: def f(self: "C"): def b(x: Self): reveal_type(x) # revealed: Self@f + # revealed: None reveal_type(generic_context(b)) + # revealed: None reveal_type(generic_context(C.f)) ``` @@ -702,9 +782,11 @@ This makes sure that we don't bind `self` if it's not a positional parameter: ```py from ty_extensions import CallableTypeOf + class C: def method(*args, **kwargs) -> None: ... + def _(c: CallableTypeOf[C().method]): reveal_type(c) # revealed: (...) -> None ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md index e7eb565cca..5828e22f52 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md @@ -12,11 +12,13 @@ from typing_extensions import TypeVarTuple Ts = TypeVarTuple("Ts") + def append_int(*args: *Ts) -> tuple[*Ts, int]: reveal_type(args) # revealed: @Todo(PEP 646) return (*args, 1) + # TODO should be tuple[Literal[True], Literal["a"], int] reveal_type(append_int(True, "a")) # revealed: tuple[@Todo(PEP 646), ...] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md index f96be3951b..673d088254 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -10,6 +10,7 @@ All of the following symbols can be mapped one-to-one with the actual type: ```py import typing + def f( list_bare: typing.List, list_parametrized: typing.List[int], @@ -65,6 +66,7 @@ In case the incorrect number of type arguments is passed, a diagnostic is given. ```py import typing + def f( # error: [invalid-type-form] "Legacy alias `typing.List` expected exactly 1 argument, got 2" incorrect_list: typing.List[int, int], @@ -120,23 +122,31 @@ from ty_extensions import reveal_mro ### Built-ins #################### + class ListSubclass(typing.List): ... + # revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(ListSubclass) + class DictSubclass(typing.Dict): ... + # revealed: (, , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(DictSubclass) + class SetSubclass(typing.Set): ... + # revealed: (, , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(SetSubclass) + class FrozenSetSubclass(typing.FrozenSet): ... + # revealed: (, , , , , , typing.Protocol, typing.Generic, ) reveal_mro(FrozenSetSubclass) @@ -144,28 +154,38 @@ reveal_mro(FrozenSetSubclass) ### `collections` #################### + class ChainMapSubclass(typing.ChainMap): ... + # revealed: (, , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(ChainMapSubclass) + class CounterSubclass(typing.Counter): ... + # revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(CounterSubclass) + class DefaultDictSubclass(typing.DefaultDict): ... + # revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(DefaultDictSubclass) + class DequeSubclass(typing.Deque): ... + # revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(DequeSubclass) + class OrderedDictSubclass(typing.OrderedDict): ... + # revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(OrderedDictSubclass) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md index 810927d7a3..80e1812f1e 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -35,6 +35,7 @@ def f(v: tuple[int, "str"]): def f(v: "Foo"): reveal_type(v) # revealed: Foo + class Foo: ... ``` @@ -52,6 +53,7 @@ def f(v: "Foo"): def f(v: int | "Foo"): reveal_type(v) # revealed: int | Foo + class Foo: ... ``` @@ -60,10 +62,12 @@ class Foo: ... ```py from typing import Literal + def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'): reveal_type(v) # revealed: Literal["Foo", "Bar"] reveal_type(w) # revealed: Literal["Foo", "Bar"] + class Foo: ... ``` @@ -104,6 +108,7 @@ def f1( ```py from typing import Literal + def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]): reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h", b"c"] ``` @@ -113,12 +118,14 @@ def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", " ```py MyType = int + class Aliases: MyType = str forward: "MyType" = "value" not_forward: MyType = "value" + reveal_type(Aliases.forward) # revealed: str reveal_type(Aliases.not_forward) # revealed: str ``` @@ -132,8 +139,10 @@ c: "Foo" # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`" d: "Foo" = 1 + class Foo: ... + c = Foo() reveal_type(a) # revealed: Literal[1] @@ -211,6 +220,7 @@ def valid( reveal_type(a1) # revealed: int | str reveal_type(a2) # revealed: int | str + def invalid( # error: [invalid-syntax-in-forward-annotation] a1: """ diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/union.md b/crates/ty_python_semantic/resources/mdtest/annotations/union.md index 8313d7142a..a2e27bd1fe 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/union.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/union.md @@ -15,6 +15,7 @@ a4: Union[Union[bytes, str]] a5: Union[int] a6: Union[()] + def f(): # revealed: int | str reveal_type(a) @@ -55,6 +56,7 @@ from typing_extensions import Union a: Union[int, str] + def f(): # revealed: int | str reveal_type(a) @@ -65,6 +67,7 @@ def f(): ```py from typing import Union + # error: [invalid-type-form] "`typing.Union` requires at least one argument when used in a type expression" def f(x: Union) -> None: reveal_type(x) # revealed: Unknown @@ -80,6 +83,7 @@ python-version = "3.10" ```py X = int | str + def f(y: X): reveal_type(y) # revealed: int | str ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 50df54b556..a383929fe0 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -12,34 +12,44 @@ P = ParamSpec("P") Ts = TypeVarTuple("Ts") R_co = TypeVar("R_co", covariant=True) + def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...] return args + def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co: reveal_type(args) # revealed: P@i.args reveal_type(kwargs) # revealed: P@i.kwargs return callback(42, *args, **kwargs) + class Foo: def method(self, x: Self): reveal_type(x) # revealed: Self@method + def ex2(msg: str): def wrapper(fn: Callable[P, R_co]) -> Callable[P, R_co]: def wrapped(*args: P.args, **kwargs: P.kwargs) -> R_co: print(msg) return fn(*args, **kwargs) + return wrapped + return wrapper + def ex3(msg: str): P = ParamSpec("P") + def wrapper(fn: Callable[P, R_co]) -> Callable[P, R_co]: def wrapped(*args: P.args, **kwargs: P.kwargs) -> R_co: print(msg) return fn(*args, **kwargs) + return wrapped + return wrapper ``` @@ -50,6 +60,7 @@ One thing that is supported is error messages for using special forms in type ex ```py from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, Generic + def _( a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression" b: TypeGuard, # error: [invalid-type-form] "`typing.TypeGuard` requires exactly one argument when used in a type expression" @@ -77,14 +88,28 @@ from typing import Callable from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate, Generic from ty_extensions import reveal_mro + class A(Self): ... # error: [invalid-base] + + class B(Unpack): ... # error: [invalid-base] + + class C(TypeGuard): ... # error: [invalid-base] + + class D(TypeIs): ... # error: [invalid-base] + + class E(Concatenate): ... # error: [invalid-base] + + class F(Callable): ... + + class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`" + reveal_mro(F) # revealed: (, @Todo(Support for Callable as a base class), ) ``` @@ -105,6 +130,7 @@ T = TypeVar("T") # error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter" X: TypeAlias[T] = int + class Foo[T]: # error: [invalid-type-form] "Special form `typing.Self` expected no type parameter" # error: [invalid-type-form] "Special form `typing.Self` expected no type parameter" diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md index dc22ac2539..908ee0d561 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md @@ -14,5 +14,6 @@ MyTypedDict = typing.TypedDict("MyTypedDict", {"foo": int}) MyNamedTuple1 = typing.NamedTuple("MyNamedTuple1", [("foo", int)]) MyNamedTuple2 = collections.namedtuple("MyNamedTuple2", ["foo"]) + def f(a: MyEnum, b: MyTypedDict, c: MyNamedTuple1, d: MyNamedTuple2): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md index 1c02eac9f0..78efbd1310 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md @@ -11,6 +11,7 @@ from typing_extensions import Final, ReadOnly, TypedDict X: Final = 42 Y: Final[int] = 42 + class Bar(TypedDict): z: ReadOnly[bytes] ``` @@ -22,6 +23,7 @@ One thing that is supported is error messages for using type qualifiers in type ```py from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly + def _( # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)" a: Final | int, @@ -42,7 +44,12 @@ You can't inherit from a type qualifier. ```py from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly + class A(Final): ... # error: [invalid-base] + + class B(ClassVar): ... # error: [invalid-base] + + class C(ReadOnly): ... # error: [invalid-base] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index c0f42e204b..9da1c47839 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -239,10 +239,12 @@ python-version = "3.12" ```py from typing import Any + class X[T]: def __init__(self, value: T): self.value = value + x1: X[int] = X(1) reveal_type(x1) # revealed: X[int] @@ -252,13 +254,16 @@ reveal_type(x2) # revealed: X[int | None] x3: X[int | None] | None = X(1) reveal_type(x3) # revealed: X[int | None] + def _[T](x1: X[T]): x2: X[T | int] = X(x1.value) reveal_type(x2) # revealed: X[T@_ | int] + x4: X[Any] = X(1) reveal_type(x4) # revealed: X[Any] + def _(flag: bool): x5: X[int | None] = X(1) if flag else X(2) reveal_type(x5) # revealed: X[int | None] @@ -267,10 +272,12 @@ def _(flag: bool): ```py from dataclasses import dataclass + @dataclass class Y[T]: value: T + y1 = Y(value=1) reveal_type(y1) # revealed: Y[int] @@ -285,6 +292,7 @@ class Z[T]: def __new__(cls, value: T): return super().__new__(cls) + z1 = Z(1) reveal_type(z1) # revealed: Z[int] @@ -352,8 +360,10 @@ from __future__ import annotations x: Foo + class Foo: ... + x = Foo() reveal_type(x) # revealed: Foo ``` @@ -379,8 +389,10 @@ python-version = "3.14" ```py x: Foo + class Foo: ... + x = Foo() reveal_type(x) # revealed: Foo ``` @@ -404,9 +416,11 @@ python-version = "3.12" ```py from typing import Literal, Sequence + def f[T](x: T) -> list[T]: return [x] + x1 = f("a") reveal_type(x1) # revealed: list[str] @@ -428,9 +442,11 @@ x6: list[int] = f("a") # error: [invalid-assignment] "Object of type `list[str]` is not assignable to `tuple[int]`" x7: tuple[int] = f("a") + def f2[T: int](x: T) -> T: return x + x8: int = f2(True) reveal_type(x8) # revealed: Literal[True] @@ -453,12 +469,15 @@ A function's arguments are also inferred using the type context: ```py from typing import TypedDict + class TD(TypedDict): x: int + def f[T](x: list[T]) -> T: return x[0] + a: TD = f([{"x": 0}, {"x": 1}]) reveal_type(a) # revealed: TD @@ -483,12 +502,15 @@ But not in a way that leads to assignability errors: ```py from typing import TypedDict, Any + class TD(TypedDict, total=False): x: str + class TD2(TypedDict): x: str + def f(self, dt: dict[str, Any], key: str): x1: TD = dt.get(key, {}) reveal_type(x1) # revealed: Any @@ -525,15 +547,19 @@ python-version = "3.14" ```py from typing import Any + def f[T](x: T) -> list[T]: return [x] + def f2[T](x: T) -> list[T] | None: return [x] + def f3[T](x: T) -> list[T] | dict[T, T]: return [x] + a = f(1) reveal_type(a) # revealed: list[int] @@ -564,29 +590,37 @@ We only prefer the declared type if it is in non-covariant position. class Bivariant[T]: pass + class Covariant[T]: def pop(self) -> T: raise NotImplementedError + class Contravariant[T]: def push(self, value: T) -> None: pass + class Invariant[T]: x: T + def bivariant[T](x: T) -> Bivariant[T]: return Bivariant() + def covariant[T](x: T) -> Covariant[T]: return Covariant() + def contravariant[T](x: T) -> Contravariant[T]: return Contravariant() + def invariant[T](x: T) -> Invariant[T]: return Invariant() + x1 = bivariant(1) x2 = covariant(1) x3 = contravariant(1) @@ -614,6 +648,7 @@ class X[T]: def pop(self) -> T: raise NotImplementedError + x1: X[int | None] = X() reveal_type(x1) # revealed: X[None] ``` @@ -650,16 +685,20 @@ reveal_type(x5) # revealed: list[Iterable[Any]] x6: Iterable[list[Any]] = [[1, 2, 3]] reveal_type(x6) # revealed: list[list[Any]] + class X[T]: value: T def __init__(self, value: T): ... + class A[T](X[T]): ... + def a[T](value: T) -> A[T]: return A(value) + x7: A[object] = A(1) reveal_type(x7) # revealed: A[object] @@ -672,9 +711,11 @@ reveal_type(x9) # revealed: A[object] x10: X[object] | None = a(1) reveal_type(x10) # revealed: A[object] + def f[T](x: T) -> list[list[T]]: return [[x]] + x11: Sequence[Sequence[Any]] = f(1) reveal_type(x11) # revealed: list[list[int]] @@ -695,28 +736,35 @@ python-version = "3.12" ```py from typing import reveal_type, TypedDict + def identity[T](x: T) -> T: return x + def _(narrow: dict[str, str], target: list[str] | dict[str, str] | None): target = identity(narrow) reveal_type(target) # revealed: dict[str, str] + def _(narrow: list[str], target: list[str] | dict[str, str] | None): target = identity(narrow) reveal_type(target) # revealed: list[str] + def _(narrow: list[str] | dict[str, str], target: list[str] | dict[str, str] | None): target = identity(narrow) reveal_type(target) # revealed: list[str] | dict[str, str] + class TD(TypedDict): x: int + def _(target: list[TD] | dict[str, TD] | None): target = identity([{"x": 1}]) reveal_type(target) # revealed: list[TD] + def _(target: list[TD] | dict[str, TD] | None): target = identity({"x": {"x": 1}}) reveal_type(target) # revealed: dict[str, TD] @@ -733,9 +781,11 @@ python-version = "3.12" def identity[T](x: T) -> T: return x + def lst[T](x: T) -> list[T]: return [x] + def _(i: int): a: int | None = i b: int | None = identity(i) @@ -765,9 +815,11 @@ def _(i: int): reveal_type(b) # revealed: list[Unknown] reveal_type(c) # revealed: list[Unknown] + def f[T](x: list[T]) -> T: return x[0] + def _(a: int, b: str, c: int | str): x1: int = f(lst(a)) reveal_type(x1) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md index fc4af5a52e..2121e5cda5 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md @@ -23,14 +23,17 @@ class C: def __isub__(self, other: int) -> str: return "Hello, world!" + x = C() x -= 1 reveal_type(x) # revealed: str + class C: def __iadd__(self, other: str) -> int: return 1 + x = C() x += "Hello" reveal_type(x) # revealed: int @@ -45,6 +48,7 @@ class C: def __isub__(self, other: str) -> int: return 42 + x = C() # error: [unsupported-operator] "Operator `-=` is not supported between objects of type `C` and `Literal[1]`" x -= 1 @@ -58,9 +62,12 @@ reveal_type(x) # revealed: int def _(flag: bool): class Foo: if flag: + def __iadd__(self, other: int) -> str: return "Hello, world!" + else: + def __iadd__(self, other: int) -> int: return 42 @@ -76,6 +83,7 @@ def _(flag: bool): def _(flag: bool): class Foo: if flag: + def __iadd__(self, other: str) -> int: return 42 @@ -94,7 +102,9 @@ def _(flag: bool): class Foo: def __add__(self, other: str) -> str: return "Hello, world!" + if flag: + def __iadd__(self, other: str) -> int: return 42 @@ -111,7 +121,9 @@ def _(flag1: bool, flag2: bool): class Foo: def __add__(self, other: int) -> str: return "Hello, world!" + if flag1: + def __iadd__(self, other: int) -> int: return 42 @@ -148,7 +160,9 @@ def f(flag: bool, flag2: bool): class Foo: def __add__(self, other: int) -> str: return "Hello, world!" + if flag: + def __iadd__(self, other: int) -> int: return 42 @@ -175,8 +189,10 @@ class Meta(type): def __iadd__(cls, other: int) -> str: return "" + class C(metaclass=Meta): ... + cls = C cls += 1 diff --git a/crates/ty_python_semantic/resources/mdtest/async.md b/crates/ty_python_semantic/resources/mdtest/async.md index d0b9b4b464..e2fcdedc41 100644 --- a/crates/ty_python_semantic/resources/mdtest/async.md +++ b/crates/ty_python_semantic/resources/mdtest/async.md @@ -6,6 +6,7 @@ async def retrieve() -> int: return 42 + async def main(): result = await retrieve() @@ -19,9 +20,11 @@ from typing import TypeVar T = TypeVar("T") + async def persist(x: T) -> T: return x + async def f(x: int): result = await persist(x) @@ -36,9 +39,11 @@ async def f(x: int): import asyncio import concurrent.futures + def blocking_function() -> int: return 42 + async def main(): loop = asyncio.get_event_loop() with concurrent.futures.ThreadPoolExecutor() as pool: @@ -51,9 +56,11 @@ async def main(): ```py import asyncio + async def f() -> int: return 1 + async def main(): task = asyncio.create_task(f()) @@ -67,9 +74,11 @@ async def main(): ```py import asyncio + async def task(name: str) -> int: return len(name) + async def main(): (a, b) = await asyncio.gather( task("A"), @@ -113,6 +122,7 @@ final type of the `await` expression, we retrieve that third argument of the `Ge ```py from typing import Generator + def _(): result = yield from retrieve().__await__() reveal_type(result) # revealed: int @@ -127,5 +137,6 @@ not just `Unknown`: async def f(): pass + reveal_type(f()) # revealed: CoroutineType[Any, Any, Unknown] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index d12d243aef..990ef30767 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -23,6 +23,7 @@ class C: if flag: self.possibly_undeclared_unbound: str = "possibly set in __init__" + c_instance = C(1) reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"] @@ -82,6 +83,7 @@ class C: def __init__(self) -> None: self.declared_and_bound = "value set in __init__" + c_instance = C() reveal_type(c_instance.declared_and_bound) # revealed: str | None @@ -106,6 +108,7 @@ the Python ecosystem: class C: only_declared: str + c_instance = C() reveal_type(c_instance.only_declared) # revealed: str @@ -139,6 +142,7 @@ class C: if flag: self.bound_in_body_and_init = "a" + c_instance = C(True) reveal_type(c_instance.only_declared_in_body) # revealed: str | None @@ -171,6 +175,7 @@ class C: self.declared_only: bytes self.declared_and_bound: bool = True + c_instance = C(1) reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"] @@ -200,9 +205,11 @@ attribute, that should be an error. def get_int() -> int: return 0 + def get_str() -> str: return "a" + class C: z: int @@ -219,6 +226,7 @@ class C: # TODO: this redeclaration should be an error self.z: str = "a" + c_instance = C() reveal_type(c_instance.x) # revealed: Unknown | int | str @@ -233,6 +241,7 @@ class C: def __init__(self) -> None: self.a = self.b = 1 + c_instance = C() reveal_type(c_instance.a) # revealed: Unknown | Literal[1] @@ -246,11 +255,13 @@ class Weird: def __iadd__(self, other: None) -> str: return "a" + class C: def __init__(self) -> None: self.w = Weird() self.w += None + # TODO: Mypy and pyright do not support this, but it would be great if we could # infer `Unknown | str` here (`Weird` is not a possible type for the `w` attribute). reveal_type(C().w) # revealed: Unknown | Weird @@ -262,6 +273,7 @@ reveal_type(C().w) # revealed: Unknown | Weird def returns_tuple() -> tuple[int, str]: return (1, "a") + class C: a1, b1 = (1, "a") c1, d1 = returns_tuple() @@ -270,6 +282,7 @@ class C: self.a2, self.b2 = (1, "a") self.c2, self.d2 = returns_tuple() + c_instance = C() reveal_type(c_instance.a1) # revealed: Unknown | Literal[1] @@ -292,6 +305,7 @@ class C: def __init__(self) -> None: self.a, *self.b = (1, 2, 3) + c_instance = C() reveal_type(c_instance.a) # revealed: Unknown | Literal[1] reveal_type(c_instance.b) # revealed: Unknown | list[Literal[2, 3]] @@ -304,12 +318,15 @@ class TupleIterator: def __next__(self) -> tuple[int, str]: return (1, "a") + class TupleIterable: def __iter__(self) -> TupleIterator: return TupleIterator() + class NonIterable: ... + class C: def __init__(self): for self.x in range(3): @@ -322,6 +339,7 @@ class C: for self.z in NonIterable(): pass + reveal_type(C().x) # revealed: Unknown | int reveal_type(C().y) # revealed: Unknown | str ``` @@ -336,11 +354,13 @@ class ContextManager: def __exit__(self, exc_type, exc_value, traceback) -> None: pass + class C: def __init__(self) -> None: with ContextManager() as self.x: pass + c_instance = C() reveal_type(c_instance.x) # revealed: Unknown | int | None @@ -356,11 +376,13 @@ class ContextManager: def __exit__(self, exc_type, exc_value, traceback) -> None: pass + class C: def __init__(self) -> None: with ContextManager() as (self.x, self.y): pass + c_instance = C() reveal_type(c_instance.x) # revealed: Unknown | int | None @@ -379,10 +401,12 @@ class TupleIterator: def __next__(self) -> tuple[int, str]: return (1, "a") + class TupleIterable: def __iter__(self) -> TupleIterator: return TupleIterator() + class C: def __init__(self) -> None: [... for self.a in range(3)] @@ -391,9 +415,11 @@ class C: [[... for self.f in range(3)] for _ in range(3)] [[... for self.g in range(3)] for self in [D()]] + class D: g: int + c_instance = C() reveal_type(c_instance.a) # revealed: Unknown | int @@ -423,6 +449,7 @@ class C: [... for self.a in [1]] [[... for self.b in [1]] for _ in [1]] + c_instance = C() reveal_type(c_instance.a) # revealed: Unknown | int @@ -441,8 +468,10 @@ class C: def g(): # error: [unresolved-attribute] [... for self.b in [1]] + g() + c_instance = C() # This attribute is in the function f and is not reachable @@ -461,6 +490,7 @@ class C: class D: [[... for self.a in [1]] for _ in [1]] + reveal_type(C().a) # revealed: Unknown | int ``` @@ -473,16 +503,20 @@ defined: def flag() -> bool: return True + class C: def f(self) -> None: if flag(): self.a1: str | None = "a" self.b1 = 1 + if flag(): + def f(self) -> None: self.a2: str | None = "a" self.b2 = 1 + c_instance = C() reveal_type(c_instance.a1) # revealed: str | None @@ -500,6 +534,7 @@ class C: def __init__(this) -> None: this.declared_and_bound: str | None = "a" + reveal_type(C().declared_and_bound) # revealed: str | None ``` @@ -511,6 +546,7 @@ class C: this = self this.declared_and_bound: str | None = "a" + # This would ideally be `str | None`, but mypy/pyright don't support this either, # so `Unknown` + a diagnostic is also fine. # error: [unresolved-attribute] @@ -523,11 +559,13 @@ reveal_type(C().declared_and_bound) # revealed: Unknown class Other: x: int + class C: @staticmethod def f(other: Other) -> None: other.x = 1 + # error: [unresolved-attribute] reveal_type(C.x) # revealed: Unknown @@ -538,11 +576,13 @@ reveal_type(C().x) # revealed: Unknown my_staticmethod = staticmethod + class D: @my_staticmethod def f(other: Other) -> None: other.x = 1 + # error: [unresolved-attribute] reveal_type(D.x) # revealed: Unknown @@ -556,11 +596,13 @@ If `staticmethod` is something else, that should not influence the behavior: def staticmethod(f): return f + class C: @staticmethod def f(self) -> None: self.x = 1 + reveal_type(C().x) # revealed: Unknown | Literal[1] ``` @@ -569,14 +611,17 @@ And if `staticmethod` is fully qualified, that should also be recognized: ```py import builtins + class Other: x: int + class C: @builtins.staticmethod def f(other: Other) -> None: other.x = 1 + # error: [unresolved-attribute] reveal_type(C.x) # revealed: Unknown @@ -596,6 +641,7 @@ class C: if (2 + 3) < 4: self.x: str = "a" + # TODO: this would ideally raise an `unresolved-attribute` error reveal_type(C().x) # revealed: str ``` @@ -621,12 +667,15 @@ class C: def set_c(self, c: str) -> None: self.c = c + if False: + def set_e(self, e: str) -> None: # TODO: Should not emit this diagnostic # error: [unresolved-attribute] self.e = e + # TODO: this would ideally be `Unknown | Literal[1]` reveal_type(C(True).a) # revealed: Unknown | Literal[1, "a"] # TODO: this would ideally raise an `unresolved-attribute` error @@ -653,9 +702,11 @@ class C: # This is because, it is not possible to access a partially-initialized object by normal means. self.y = 2 + reveal_type(C(False).x) # revealed: Unknown | Literal[1] reveal_type(C(False).y) # revealed: Unknown | Literal[2] + class C: def __init__(self, b: bytes) -> None: self.b = b @@ -667,9 +718,11 @@ class C: self.s = s + reveal_type(C(b"abc").b) # revealed: Unknown | bytes reveal_type(C(b"abc").s) # revealed: Unknown | str + class C: def __init__(self, iter) -> None: self.x = 1 @@ -681,6 +734,7 @@ class C: # but we consider the subsequent attributes to be definitely-bound. self.y = 2 + reveal_type(C([]).x) # revealed: Unknown | Literal[1] reveal_type(C([]).y) # revealed: Unknown | Literal[2] ``` @@ -707,6 +761,7 @@ For more details, see the [typing spec on `ClassVar`]. ```py from typing import ClassVar + class C: pure_class_variable1: ClassVar[str] = "value in class body" pure_class_variable2: ClassVar = 1 @@ -715,6 +770,7 @@ class C: # error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `Self@method`" self.pure_class_variable1 = "value set through instance" + reveal_type(C.pure_class_variable1) # revealed: str reveal_type(C.pure_class_variable2) # revealed: Unknown | Literal[1] @@ -734,9 +790,11 @@ C.pure_class_variable1 = "overwritten on class" # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `pure_class_variable1` of type `str`" C.pure_class_variable1 = 1 + class Subclass(C): pure_class_variable1: ClassVar[str] = "overwritten on subclass" + reveal_type(Subclass.pure_class_variable1) # revealed: str ``` @@ -746,12 +804,14 @@ If a class variable is additionally qualified as `Final`, we do not union with ` ```py from typing import Final + class D: final1: Final[ClassVar] = 1 final2: ClassVar[Final] = 1 final3: ClassVar[Final[int]] = 1 final4: Final[ClassVar[int]] = 1 + reveal_type(D.final1) # revealed: Literal[1] reveal_type(D.final2) # revealed: Literal[1] reveal_type(D.final3) # revealed: int @@ -769,6 +829,7 @@ class C: def class_method(cls): cls.pure_class_variable = "value set in class method" + # for a more realistic example, let's actually call the method C.class_method() @@ -801,6 +862,7 @@ class C: def instance_method(self): self.variable_with_class_default1 = "value set in instance method" + reveal_type(C.variable_with_class_default1) # revealed: str reveal_type(C.variable_with_class_default2) # revealed: Unknown | Literal[1] @@ -831,15 +893,18 @@ called (the descriptor protocol is not invoked for instance variables). ```py from typing import ClassVar + class Descriptor: def __get__(self, instance, owner) -> int: return 42 + class C: a: ClassVar[Descriptor] b: Descriptor = Descriptor() c: ClassVar[Descriptor] = Descriptor() + reveal_type(C().a) # revealed: int reveal_type(C().b) # revealed: int reveal_type(C().c) # revealed: int @@ -872,6 +937,7 @@ class Base: self.pure_undeclared = "base" + class Intermediate(Base): # Redeclaring base class attributes with the *same *type is fine: redeclared_with_same_type: str | None = None @@ -918,8 +984,10 @@ class Intermediate(Base): self.pure_undeclared = "intermediate" + class Derived(Intermediate): ... + reveal_type(Derived.attribute) # revealed: int | None reveal_type(Derived().attribute) # revealed: int | None @@ -978,11 +1046,14 @@ object first, i.e. on the metaclass: ```py from typing import Literal + class Meta1: attr: Literal["metaclass value"] = "metaclass value" + class C1(metaclass=Meta1): ... + reveal_type(C1.attr) # revealed: Literal["metaclass value"] ``` @@ -994,9 +1065,11 @@ instead (see the [descriptor protocol tests] for data/non-data descriptor attrib class Meta2: attr: str = "metaclass value" + class C2(metaclass=Meta2): attr: Literal["class value"] = "class value" + reveal_type(C2.attr) # revealed: Literal["class value"] ``` @@ -1029,6 +1102,7 @@ def _(flag: bool): attr1: str = "metaclass value" class C4(metaclass=Meta4): ... + # error: [possibly-missing-attribute] reveal_type(C4.attr1) # revealed: str ``` @@ -1104,6 +1178,7 @@ In a classmethod, if the name matches a class attribute, we suggest `cls.`. ```py from typing import ClassVar + class Foo: x: ClassVar[int] = 42 @@ -1159,6 +1234,7 @@ in the sub-diagnostic. ```py from typing import ClassVar + class Foo: x: ClassVar[int] = 42 @@ -1174,6 +1250,7 @@ the first parameter is keyword-only: ```py from typing import ClassVar + class Foo: x: ClassVar[int] = 42 @@ -1195,11 +1272,13 @@ infer those union types accordingly: ```py def _(flag: bool): if flag: + class C1: x = 1 y: int = 1 else: + class C1: x = 2 y: int | str = "b" @@ -1229,16 +1308,19 @@ def _(flag: bool): C2.y = "problematic" if flag: + class Meta3(type): x = 5 y: int = 5 else: + class Meta3(type): x = 6 y: int | str = "f" class C3(metaclass=Meta3): ... + reveal_type(C3.x) # revealed: Unknown | Literal[5, 6] reveal_type(C3.y) # revealed: int | str @@ -1257,6 +1339,7 @@ def _(flag: bool): y: int | str = "h" class C4(metaclass=Meta4): ... + reveal_type(C4.x) # revealed: Unknown | Literal[7, 8] reveal_type(C4.y) # revealed: int | str @@ -1337,6 +1420,7 @@ def _(flag: bool, flag1: bool, flag2: bool): ```py from typing import Any + def _(flag: bool): class Base: x: Any @@ -1455,7 +1539,9 @@ If the symbol is unbound in all elements of the union, we detect that: ```py def _(flag: bool): class C1: ... + class C2: ... + C = C1 if flag else C2 # error: [unresolved-attribute] "Object of type ` | ` has no attribute `x`" @@ -1475,9 +1561,13 @@ def _(flag: bool): class A: X = "foo" + class B(A): ... + + class C(B): ... + reveal_type(C.X) # revealed: Unknown | Literal["foo"] C.X = "bar" @@ -1488,19 +1578,30 @@ C.X = "bar" ```py from ty_extensions import reveal_mro + class O: ... + class F(O): X = 56 + class E(O): X = 42 + class D(O): ... + + class C(D, F): ... + + class B(E, D): ... + + class A(B, C): ... + # revealed: (, , , , , , , ) reveal_mro(A) @@ -1517,16 +1618,20 @@ A.X = 100 ```py from ty_extensions import Intersection + class A: x: int = 1 + class B: ... + def _(a_and_b: Intersection[A, B]): reveal_type(a_and_b.x) # revealed: int a_and_b.x = 2 + # Same for class objects def _(a_and_b: Intersection[type[A], type[B]]): reveal_type(a_and_b.x) # revealed: int @@ -1539,20 +1644,29 @@ def _(a_and_b: Intersection[type[A], type[B]]): ```py from ty_extensions import Intersection + class P: ... + + class Q: ... + + class R(P, Q): ... + class A: x: P = P() + class B: x: Q = Q() + def _(a_and_b: Intersection[A, B]): reveal_type(a_and_b.x) # revealed: P & Q a_and_b.x = R() + # Same for class objects def _(a_and_b: Intersection[type[A], type[B]]): reveal_type(a_and_b.x) # revealed: P & Q @@ -1567,6 +1681,7 @@ which is equivalent to `object & ~P`: ```py class P: ... + def _(obj: object): if not isinstance(obj, P): reveal_type(obj) # revealed: ~P @@ -1579,10 +1694,16 @@ def _(obj: object): ```py from ty_extensions import Intersection + class P: ... + + class Q: ... + + class R(P, Q): ... + def _(flag: bool): class A1: if flag: @@ -1596,6 +1717,7 @@ def _(flag: bool): # error: [possibly-missing-attribute] a_and_b.x = R() + # Same for class objects def inner1_class(a_and_b: Intersection[type[A1], type[B1]]): # error: [possibly-missing-attribute] @@ -1618,6 +1740,7 @@ def _(flag: bool): # handling in `validate_attribute_assignment` for this # error: [possibly-missing-attribute] a_and_b.x = R() + # Same for class objects def inner2_class(a_and_b: Intersection[type[A2], type[B1]]): reveal_type(a_and_b.x) # revealed: P & Q @@ -1636,6 +1759,7 @@ def _(flag: bool): # error: [possibly-missing-attribute] a_and_b.x = R() + # Same for class objects def inner3_class(a_and_b: Intersection[type[A3], type[B3]]): # error: [possibly-missing-attribute] @@ -1645,6 +1769,7 @@ def _(flag: bool): a_and_b.x = R() class A4: ... + class B4: ... def inner4(a_and_b: Intersection[A4, B4]): @@ -1653,6 +1778,7 @@ def _(flag: bool): # error: [invalid-assignment] a_and_b.x = R() + # Same for class objects def inner4_class(a_and_b: Intersection[type[A4], type[B4]]): # error: [unresolved-attribute] @@ -1667,17 +1793,23 @@ def _(flag: bool): ```py from ty_extensions import Intersection + class P: ... + + class Q: ... + class A: def __init__(self): self.x: P = P() + class B: def __init__(self): self.x: Q = Q() + def _(a_and_b: Intersection[A, B]): reveal_type(a_and_b.x) # revealed: P & Q ``` @@ -1692,8 +1824,10 @@ from this that attribute access on `Any` resolves to `Any` if the attribute does ```py from typing import Any + class Foo(Any): ... + reveal_type(Foo.bar) # revealed: Any reveal_type(Foo.__repr__) # revealed: (def __repr__(self) -> str) & Any ``` @@ -1704,12 +1838,17 @@ Similar principles apply if `Any` appears in the middle of an inheritance hierar from typing import ClassVar, Literal from ty_extensions import reveal_mro + class A: x: ClassVar[Literal[1]] = 1 + class B(Any): ... + + class C(B, A): ... + reveal_mro(C) # revealed: (, , Any, , ) reveal_type(C.x) # revealed: Literal[1] & Any ``` @@ -1724,11 +1863,14 @@ for unknown attributes. Consider the following `CustomGetAttr` class: ```py from typing import Literal + def flag() -> bool: return True + class GetAttrReturnType: ... + class CustomGetAttr: class_attr: int = 1 @@ -1789,10 +1931,12 @@ we only consider the attribute access to be valid if the accessed attribute is o ```py from typing import Literal + class Date: def __getattr__(self, name: Literal["day", "month", "year"]) -> int: return 0 + date = Date() reveal_type(date.day) # revealed: int @@ -1810,6 +1954,7 @@ A standard library example of a class with a custom `__getattr__` method is `arg ```py import argparse + def _(ns: argparse.Namespace): reveal_type(ns.whatever) # revealed: Any ``` @@ -1825,11 +1970,14 @@ behavior matches other type checkers such as mypy and pyright. ```py from typing import Any + class Foo: x: str + def __getattribute__(self, attr: str) -> Any: return 42 + reveal_type(Foo().x) # revealed: str reveal_type(Foo().y) # revealed: Any ``` @@ -1854,6 +2002,7 @@ class C: def __getattr__(self, name: str) -> str: return "a" + c = C() reveal_type(c.x) # revealed: int @@ -1865,11 +2014,13 @@ Like all dunder methods, `__getattribute__` is not looked up on instances: def external_getattribute(name) -> int: return 1 + class ThisFails: def __init__(self): # error: [invalid-assignment] self.__getattribute__ = external_getattribute + # error: [unresolved-attribute] ThisFails().x ``` @@ -1903,11 +2054,13 @@ we only consider the attribute assignment to be valid if the assigned attribute ```py from typing import Literal + class Date: # error: [invalid-method-override] def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: pass + date = Date() date.day = 8 date.month = 4 @@ -1925,12 +2078,14 @@ on instances of that class: ```py from typing_extensions import Never + class Frozen: existing: int = 1 def __setattr__(self, name, value) -> Never: raise AttributeError("Attributes cannot be modified") + instance = Frozen() instance.non_existing = 2 # error: [invalid-assignment] "Cannot assign to unresolved attribute `non_existing` on type `Frozen`" instance.existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`" @@ -1954,6 +2109,7 @@ of type `Never`): ```py from typing_extensions import Never, Any + def _(n: Never): reveal_type(n.__setattr__) # revealed: Never @@ -1978,14 +2134,18 @@ If a `__setattr__` method is only partially bound, the behavior is still the sam ```py from typing_extensions import Never + def flag() -> bool: return True + class Frozen: if flag(): + def __setattr__(self, name, value) -> Never: raise AttributeError("Attributes cannot be modified") + instance = Frozen() instance.non_existing = 2 # error: [invalid-assignment] instance.existing = 2 # error: [invalid-assignment] @@ -1998,6 +2158,7 @@ A standard library example of a class with a custom `__setattr__` method is `arg ```py import argparse + def _(ns: argparse.Namespace): ns.whatever = 42 ``` @@ -2015,15 +2176,19 @@ PyTorch, where `__setattr__` may have a narrow type signature but forwards to ```py from typing import Union + class Tensor: ... + class Module: def __setattr__(self, name: str, value: Union[Tensor, "Module"]) -> None: super().__setattr__(name, value) + class MyModule(Module): some_param: int # Explicit attribute with type `int` + def use_module(m: MyModule, param: int) -> None: # This is allowed because `some_param` is explicitly defined with type `int`, # even though `__setattr__` only accepts `Union[Tensor, Module]`. @@ -2043,12 +2208,14 @@ blocked, even if the value type doesn't match `__setattr__`'s parameter type. ```py from typing import NoReturn + class Immutable: x: float def __setattr__(self, name: str, value: int) -> NoReturn: raise AttributeError("Immutable") + def _(obj: Immutable) -> None: # Even though `"foo"` doesn't match `__setattr__`'s `value: int` parameter, # we still detect that `__setattr__` returns `Never` and block the assignment. @@ -2087,6 +2254,7 @@ reveal_type(d.__class__) # revealed: e = (42, 42) reveal_type(e.__class__) # revealed: type[tuple[Literal[42], Literal[42]]] + def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]): reveal_type(a.__class__) # revealed: type[int] reveal_type(type(a)) # revealed: type[int] @@ -2103,10 +2271,13 @@ def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]): # All we know is that the metaclass must be a (non-strict) subclass of `type`. reveal_type(d.__class__) # revealed: type[type] + reveal_type(f.__class__) # revealed: + class Foo: ... + reveal_type(Foo.__class__) # revealed: ``` @@ -2188,6 +2359,7 @@ global_symbol: str = "a" import mod1 import mod2 + def _(flag: bool): if flag: mod = mod1 @@ -2210,6 +2382,7 @@ functions are instances of that class: ```py def f(): ... + reveal_type(f.__defaults__) # revealed: tuple[Any, ...] | None reveal_type(f.__kwdefaults__) # revealed: dict[str, Any] | None ``` @@ -2281,10 +2454,12 @@ reveal_type(b"foo".endswith) class Other: x: int = 1 + class C: def __init__(self, other: Other) -> None: other.x = 1 + def f(c: C): # error: [unresolved-attribute] reveal_type(c.x) # revealed: Unknown @@ -2304,6 +2479,7 @@ class Outer: def __init__(self): self.x: str = "a" + reveal_type(Outer().x) # revealed: int # error: [unresolved-attribute] @@ -2318,12 +2494,14 @@ reveal_type(Outer.Middle.Inner().x) # revealed: str class Other: x: int = 1 + class C: def __init__(self) -> None: # Redeclaration of self. `self` does not refer to the instance anymore. self: Other = Other() self.x: int = 1 + # TODO: this should be an error C().x ``` @@ -2334,12 +2512,15 @@ C().x class Other: x: str = "a" + class C: def __init__(self) -> None: def nested_function(self: Other): self.x = "b" + self.x: int = 1 + reveal_type(C().x) # revealed: int ``` @@ -2350,8 +2531,10 @@ class C: def __init__(self) -> None: def set_attribute(value: str): self.x: str = value + set_attribute("a") + # TODO: ideally, this would be `str`. Mypy supports this, pyright does not. # error: [unresolved-attribute] reveal_type(C().x) # revealed: Unknown @@ -2364,6 +2547,7 @@ Arbitrary attributes can be accessed on `Never` without emitting any errors: ```py from typing_extensions import Never + def f(never: Never): reveal_type(never.arbitrary_attribute) # revealed: Never @@ -2383,6 +2567,7 @@ class C: def copy(self, other: "C"): self.x = other.x + reveal_type(C().x) # revealed: Unknown | Literal[1] ``` @@ -2393,6 +2578,7 @@ class D: def copy(self, other: "D"): self.x = other.x + reveal_type(D().x) # revealed: Unknown ``` @@ -2407,8 +2593,10 @@ class E: def copy(self, other: "E"): self.x = other.x + reveal_type(E().x) # revealed: int + class F: def __init__(self): self.x = 1 @@ -2416,12 +2604,15 @@ class F: def copy(self, other: "F"): self.x: int = other.x + reveal_type(F().x) # revealed: int + class G: def copy(self, other: "G"): self.x: int = other.x + reveal_type(G().x) # revealed: int ``` @@ -2435,22 +2626,27 @@ class A: def copy(self, other: "B"): self.x = other.x + class B: def copy(self, other: "A"): self.x = other.x + reveal_type(B().x) # revealed: Unknown | Literal[1] reveal_type(A().x) # revealed: Unknown | Literal[1] + class Base: def flip(self) -> "Sub": return Sub() + class Sub(Base): # error: [invalid-method-override] def flip(self) -> "Base": return Base() + class C2: def __init__(self, x: Sub): self.x = x @@ -2458,8 +2654,10 @@ class C2: def replace_with(self, other: "C2"): self.x = other.x.flip() + reveal_type(C2(Sub()).x) # revealed: Unknown | Base + class C3: def __init__(self, x: Sub): self.x = [x] @@ -2467,6 +2665,7 @@ class C3: def replace_with(self, other: "C3"): self.x = [self.x[0].flip()] + # TODO: should be `Unknown | list[Unknown | Sub] | list[Unknown | Base]` reveal_type(C3(Sub()).x) # revealed: Unknown | list[Unknown | Sub] | list[Divergent] ``` @@ -2519,6 +2718,7 @@ class ManyCycles: reveal_type(self.x6) # revealed: Unknown | int reveal_type(self.x7) # revealed: Unknown | int + class ManyCycles2: def __init__(self: "ManyCycles2"): self.x1 = [0] @@ -2561,9 +2761,11 @@ test for : ```py from typing import Literal + def check(x) -> Literal[False]: return False + class Toggle: def __init__(self: "Toggle"): if not self.x: @@ -2571,6 +2773,7 @@ class Toggle: if check(self.y): self.y = True + reveal_type(Toggle().x) # revealed: Literal[True] reveal_type(Toggle().y) # revealed: Unknown | Literal[True] ``` @@ -2586,6 +2789,7 @@ class Counter: def increment(self: "Counter"): self.count = self.count + 1 + reveal_type(Counter().count) # revealed: Unknown | int ``` @@ -2599,8 +2803,10 @@ class NestedLists: def f(self: "NestedLists"): self.x = [self.x] + reveal_type(NestedLists().x) # revealed: Unknown | Literal[1] | list[Divergent] + class NestedMixed: def f(self: "NestedMixed"): self.x = [self.x] @@ -2608,6 +2814,7 @@ class NestedMixed: def g(self: "NestedMixed"): self.x = {self.x} + reveal_type(NestedMixed().x) # revealed: Unknown | list[Divergent] | set[Divergent] ``` @@ -2618,13 +2825,16 @@ from typing import TypeVar T = TypeVar("T") + def make_list(value: T) -> list[T]: return [value] + class NestedLists2: def f(self: "NestedLists2"): self.x = make_list(self.x) + reveal_type(NestedLists2().x) # revealed: Unknown | list[Divergent] ``` @@ -2649,6 +2859,7 @@ class C: a_type: type = int a_none: None = None + reveal_type(C.a_int) # revealed: int reveal_type(C.a_str) # revealed: str reveal_type(C.a_bytes) # revealed: bytes @@ -2681,6 +2892,7 @@ class C: self.x: int = 1 return t + reveal_type(C().x) # revealed: int ``` @@ -2694,11 +2906,13 @@ gradual guarantee, because the unknown decorator *could* be an alias for `builti # error: [unresolved-import] from unknown_library import unknown_decorator + class C: @unknown_decorator def f(self): self.x: int = 1 + reveal_type(C.x) # revealed: int reveal_type(C().x) # revealed: int ``` @@ -2710,10 +2924,12 @@ import enum reveal_type(enum.Enum.__members__) # revealed: MappingProxyType[str, Enum] + class Answer(enum.Enum): NO = 0 YES = 1 + reveal_type(Answer.NO) # revealed: Literal[Answer.NO] reveal_type(Answer.NO.value) # revealed: Literal[0] reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Answer] @@ -2731,6 +2947,7 @@ class C: def f(self, other: "C"): self.x = (other.x, 1) + reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]] reveal_type(C().x[0]) # revealed: Unknown | Divergent ``` @@ -2742,13 +2959,16 @@ from typing import TypeVar, Literal T = TypeVar("T") + def make_tuple(x: T) -> tuple[T, Literal[1]]: return (x, 1) + class D: def f(self, other: "D"): self.x = make_tuple(other.x) + reveal_type(D().x) # revealed: Unknown | tuple[Divergent, Literal[1]] ``` @@ -2758,10 +2978,12 @@ The tuple type may also expand exponentially "in breadth": def duplicate(x: T) -> tuple[T, T]: return (x, x) + class E: def f(self: "E"): self.x = duplicate(self.x) + reveal_type(E().x) # revealed: Unknown | tuple[Divergent, Divergent] ``` @@ -2771,10 +2993,12 @@ And it also works for homogeneous tuples: def make_homogeneous_tuple(x: T) -> tuple[T, ...]: return (x, x) + class F: def f(self, other: "F"): self.x = make_homogeneous_tuple(other.x) + reveal_type(F().x) # revealed: Unknown | tuple[Divergent, ...] ``` @@ -2845,6 +3069,7 @@ python-version = "3.14" ```py from typing import Callable + def f(x: Callable): x.__name__ # error: [unresolved-attribute] x.__annotate__ # error: [unresolved-attribute] diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index c9c6a5c065..c3057c5f0a 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -18,9 +18,11 @@ python-version = "3.12" ```py from typing import Literal + def list1[T](x: T) -> list[T]: return [x] + l1: list[Literal[1]] = list1(1) reveal_type(l1) # revealed: list[Literal[1]] @@ -30,6 +32,7 @@ reveal_type(l2) # revealed: list[int] l3: list[int | str] | None = list1(1) reveal_type(l3) # revealed: list[int | str] + def _(l: list[int] | None = None): l1 = l or list() reveal_type(l1) # revealed: (list[int] & ~AlwaysFalsy) | list[Unknown] @@ -38,9 +41,11 @@ def _(l: list[int] | None = None): # it would be better if this were `list[int]`? (https://github.com/astral-sh/ty/issues/136) reveal_type(l2) # revealed: (list[int] & ~AlwaysFalsy) | list[Unknown] + def f[T](x: T, cond: bool) -> T | list[T]: return x if cond else [x] + l5: int | list[int] = f(1, True) a: list[int] = [1, 2, *(3, 4, 5)] @@ -55,9 +60,11 @@ reveal_type(b) # revealed: list[list[int]] ```py from typing import TypedDict + class TD(TypedDict): x: int + d1 = {"x": 1} d2: TD = {"x": 1} d3: dict[str, int] = {"x": 1} @@ -69,9 +76,11 @@ reveal_type(d2) # revealed: TD reveal_type(d3) # revealed: dict[str, int] reveal_type(d4) # revealed: TD + def _() -> TD: return {"x": 1} + def _() -> TD: # error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor" # error: [invalid-return-type] @@ -88,12 +97,15 @@ python-version = "3.12" ```py from typing import overload, Callable + def list1[T](x: T) -> list[T]: return [x] + def get_data() -> dict | None: return {} + def wrap_data() -> list[dict]: if not (res := get_data()): return list1({}) @@ -103,15 +115,18 @@ def wrap_data() -> list[dict]: # by bidirectional type inference using the annotated return type, and the type of `res` is not used. return list1(res) + def wrap_data2() -> list[dict] | None: if not (res := get_data()): return None reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown] & ~AlwaysFalsy] return list1(res) + def deco[T](func: Callable[[], T]) -> Callable[[], T]: return func + def outer() -> Callable[[], list[dict]]: @deco def inner() -> list[dict]: @@ -119,8 +134,10 @@ def outer() -> Callable[[], list[dict]]: return list1({}) reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown] & ~AlwaysFalsy] return list1(res) + return inner + @overload def f(x: int) -> list[int]: ... @overload @@ -132,15 +149,19 @@ def f(x: int | str) -> list[int] | list[str]: else: return list1(x) + reveal_type(f(1)) # revealed: list[int] reveal_type(f("a")) # revealed: list[str] + async def g() -> list[int | str]: return list1(1) + def h[T](x: T, cond: bool) -> T | list[T]: return i(x, cond) + def i[T](x: T, cond: bool) -> T | list[T]: return x if cond else [x] ``` @@ -160,6 +181,7 @@ Function parameter annotations: ```py def b(x: list[Literal[1]]): ... + b([1]) ``` @@ -170,6 +192,7 @@ class C: def __init__(self, x: list[Literal[1]]): ... def foo(self, x: list[Literal[1]]): ... + C([1]).foo([1]) ``` @@ -187,6 +210,7 @@ class E: a: list[Literal[1]] b: list[Literal[1]] + def _(e: E): e.a = [1] E.b = [1] @@ -211,6 +235,7 @@ Both meta and class/instance attribute annotations are used as type context: ```py from typing import Literal, Any + class DataDescriptor: def __get__(self, instance: object, owner: type | None = None) -> list[Literal[1]]: return [] @@ -218,9 +243,11 @@ class DataDescriptor: def __set__(self, instance: object, value: list[Literal[1]]) -> None: pass + def lst[T](x: T) -> list[T]: return [x] + def _(flag: bool): class Meta(type): if flag: @@ -242,15 +269,19 @@ For union targets, each element of the union is considered as a separate type co ```py from typing import Literal + class X: x: list[int | str] + class Y: x: list[int | None] + def lst[T](x: T) -> list[T]: return [x] + def _(xy: X | Y): xy.x = lst(1) ``` @@ -269,12 +300,14 @@ calls: def f[T](x: T) -> list[T]: return [x] + class A: def __new__(cls, value: list[int | str]): return super().__new__(cls, value) def __init__(self, value: list[int | None]): ... + A(f(1)) # error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `list[int | str]`, found `list[list[Unknown]]`" @@ -295,6 +328,7 @@ The type context is propagated through both branches of conditional expressions: def f[T](x: T) -> list[T]: raise NotImplementedError + def _(flag: bool): x1 = f(1) if flag else f(2) reveal_type(x1) # revealed: list[int] @@ -310,19 +344,24 @@ The key and value parameters types are used as type context for `__setitem__` du ```py from typing import TypedDict + class Bar(TypedDict): baz: float + def _(x: dict[str, Bar]): x["foo"] = reveal_type({"baz": 2}) # revealed: Bar + class X: def __setitem__(self, key: Bar, value: Bar): ... + def _(x: X): # revealed: Bar x[reveal_type({"baz": 1})] = reveal_type({"baz": 2}) # revealed: Bar + # TODO: Support type context with union subscripting. def _(x: X | dict[Bar, Bar]): # error: [invalid-assignment] @@ -345,6 +384,7 @@ Diagnostics unrelated to the type-context are only reported once: def f[T](x: T) -> list[T]: return [x] + def a(x: list[bool], y: list[bool]): ... def b(x: list[int], y: list[int]): ... def c(x: list[int], y: list[int]): ... @@ -385,12 +425,15 @@ def _(a: object, b: object, flag: bool): ```py from typing import TypedDict + class TD(TypedDict): y: int + class X: td: TD + def _(x: X, flag: bool): if flag: y = 1 diff --git a/crates/ty_python_semantic/resources/mdtest/binary/booleans.md b/crates/ty_python_semantic/resources/mdtest/binary/booleans.md index e0bcca56e9..dc722ac1d1 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/booleans.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/booleans.md @@ -109,6 +109,7 @@ def _(a: bool): ```py import random + def _(a: bool): def lhs_is_int(x: int): reveal_type(x | a) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/binary/classes.md b/crates/ty_python_semantic/resources/mdtest/binary/classes.md index 26a7f664b2..26771fee48 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/classes.md @@ -11,8 +11,11 @@ python-version = "3.10" ```py class A: ... + + class B: ... + reveal_type(A | B) # revealed: ``` @@ -25,8 +28,11 @@ python-version = "3.9" ```py class A: ... + + class B: ... + # error: "Operator `|` is not supported between objects of type `` and ``" reveal_type(A | B) # revealed: Unknown ``` @@ -40,16 +46,23 @@ python-version = "3.12" ```py class A: ... + + class B: ... + def _(sub_a: type[A], sub_b: type[B]): reveal_type(A | sub_b) # revealed: reveal_type(sub_a | B) # revealed: reveal_type(sub_a | sub_b) # revealed: + class C[T]: ... + + class D[T]: ... + reveal_type(C | D) # revealed: reveal_type(C[int] | D[str]) # revealed: diff --git a/crates/ty_python_semantic/resources/mdtest/binary/custom.md b/crates/ty_python_semantic/resources/mdtest/binary/custom.md index ad2b837195..958350b03b 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/custom.md @@ -5,6 +5,7 @@ ```py from typing import Literal + class Yes: def __add__(self, other) -> Literal["+"]: return "+" @@ -45,9 +46,13 @@ class Yes: def __floordiv__(self, other) -> Literal["//"]: return "//" + class Sub(Yes): ... + + class No: ... + # Yes implements all of the dunder methods. reveal_type(Yes() + Yes()) # revealed: Literal["+"] reveal_type(Yes() - Yes()) # revealed: Literal["-"] @@ -140,6 +145,7 @@ reveal_type(No() // Yes()) # revealed: Unknown ```py from typing import Literal + class Yes: def __add__(self, other) -> Literal["+"]: return "+" @@ -180,6 +186,7 @@ class Yes: def __floordiv__(self, other) -> Literal["//"]: return "//" + class Sub(Yes): def __radd__(self, other) -> Literal["r+"]: return "r+" @@ -220,6 +227,7 @@ class Sub(Yes): def __rfloordiv__(self, other) -> Literal["r//"]: return "r//" + class No: def __radd__(self, other) -> Literal["r+"]: return "r+" @@ -260,6 +268,7 @@ class No: def __rfloordiv__(self, other) -> Literal["r//"]: return "r//" + # Subclass reflected dunder methods take precedence over the superclass's regular dunders. reveal_type(Yes() + Sub()) # revealed: Literal["r+"] reveal_type(Yes() - Sub()) # revealed: Literal["r-"] @@ -302,13 +311,18 @@ class's type, i.e. `type`.) ```py from typing import Literal + class Yes: def __add__(self, other) -> Literal["+"]: return "+" + class Sub(Yes): ... + + class No: ... + # error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" reveal_type(Yes + Yes) # revealed: Unknown # error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" @@ -322,22 +336,30 @@ reveal_type(No + No) # revealed: Unknown ```py from typing import Literal + class Yes: def __add__(self, other) -> Literal["+"]: return "+" + class Sub(Yes): ... + + class No: ... + def yes() -> type[Yes]: return Yes + def sub() -> type[Sub]: return Sub + def no() -> type[No]: return No + # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `type[Yes]`" reveal_type(yes() + yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `type[Sub]`" @@ -352,6 +374,7 @@ reveal_type(no() + no()) # revealed: Unknown def f(): pass + # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f + f) # revealed: Unknown # error: [unsupported-operator] "Operator `-` is not supported between two objects of type `def f() -> Unknown`" @@ -398,8 +421,10 @@ class A: ... ```py import mod1 + class A: ... + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `mod2.A` and `mod1.A`" A() + mod1.A() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/instances.md b/crates/ty_python_semantic/resources/mdtest/binary/instances.md index 1106bfbb74..7dfacbd858 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/instances.md @@ -53,8 +53,10 @@ class A: def __or__(self, other) -> "A": return self + class B: ... + reveal_type(A() + B()) # revealed: A reveal_type(A() - B()) # revealed: A reveal_type(A() * B()) # revealed: A @@ -115,8 +117,10 @@ class A: def __ror__(self, other) -> "A": return self + class B: ... + reveal_type(B() + A()) # revealed: A reveal_type(B() - A()) # revealed: A reveal_type(B() * A()) # revealed: A @@ -144,8 +148,10 @@ class A: def __rsub__(self, other) -> int: return 1 + class B: ... + reveal_type(A() + B()) # revealed: int reveal_type(B() - A()) # revealed: int ``` @@ -160,12 +166,15 @@ class A: def __add__(self, other: "B") -> int: return 42 + class B: def __radd__(self, other: "A") -> str: return "foo" + reveal_type(A() + B()) # revealed: int + # Edge case: C is a subtype of C, *but* if the two sides are of *equal* types, # the lhs *still* takes precedence class C: @@ -175,6 +184,7 @@ class C: def __radd__(self, other: "C") -> str: return "foo" + reveal_type(C() + C()) # revealed: int ``` @@ -191,17 +201,22 @@ class A: def __radd__(self, other) -> str: return "foo" + class MyString(str): ... + class B(A): def __radd__(self, other) -> MyString: return MyString() + reveal_type(A() + B()) # revealed: MyString + # N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__` class C(B): ... + reveal_type(A() + C()) # revealed: MyString ``` @@ -218,8 +233,10 @@ class A: def __radd__(self, other) -> int: return 42 + class B(A): ... + reveal_type(A() + B()) # revealed: str ``` @@ -240,10 +257,12 @@ class A: def __sub__(self, other: "A") -> "A": return A() + class B: def __rsub__(self, other: A) -> "B": return B() + reveal_type(A() - B()) # revealed: B ``` @@ -256,9 +275,11 @@ class A: def __call__(self, other) -> int: return 42 + class B: __add__ = A() + reveal_type(B() + B()) # revealed: Unknown | int ``` @@ -269,6 +290,7 @@ the callable is declared: class B2: __add__: A = A() + reveal_type(B2() + B2()) # revealed: int ``` @@ -287,6 +309,7 @@ reveal_type(3.14 + 3j) # revealed: int | float | complex reveal_type(42 + 4.2) # revealed: int | float reveal_type(3 + 3j) # revealed: int | float | complex + def _(x: bool, y: int): reveal_type(x + y) # revealed: int reveal_type(4.2 + x) # revealed: int | float @@ -306,6 +329,7 @@ class A: def __radd__(self, other) -> "A": return self + reveal_type(A() + 1) # revealed: A reveal_type(1 + A()) # revealed: A @@ -341,12 +365,15 @@ from does_not_exist import Foo # error: [unresolved-import] reveal_type(Foo) # revealed: Unknown + class X: def __add__(self, other: object) -> int: return 42 + class Y(Foo): ... + # TODO: Should be `int | Unknown`; see above discussion. reveal_type(X() + Y()) # revealed: int ``` @@ -359,6 +386,7 @@ reveal_type(X() + Y()) # revealed: int class NotBoolable: __bool__: int = 3 + a = NotBoolable() # error: [unsupported-bool-conversion] @@ -372,6 +400,7 @@ When operating on class objects, the corresponding dunder methods are looked up ```py from __future__ import annotations + class Meta(type): def __add__(self, other: Meta) -> int: return 1 @@ -382,9 +411,13 @@ class Meta(type): def __getitem__(self, key: int) -> str: return "a" + class A(metaclass=Meta): ... + + class B(metaclass=Meta): ... + reveal_type(A + B) # revealed: int # error: [unsupported-operator] "Operator `-` is not supported between objects of type `` and ``" reveal_type(A - B) # revealed: Unknown @@ -408,10 +441,12 @@ The magic method must exist on the class, not just on the instance: def add_impl(self, other) -> int: return 1 + class A: def __init__(self): self.__add__ = add_impl + # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `A`" # revealed: Unknown reveal_type(A() + A()) @@ -422,6 +457,7 @@ reveal_type(A() + A()) ```py class A: ... + # error: [unsupported-operator] # revealed: Unknown reveal_type(A() + A()) @@ -436,12 +472,15 @@ class A: def __add__(self, other) -> int: return 1 + class B: def __radd__(self, other) -> int: return 1 + class C: ... + # error: [unsupported-operator] # revealed: Unknown reveal_type(C() + A()) @@ -463,6 +502,7 @@ class Foo: def __radd__(self, other: "Foo") -> "Foo": return self + # error: [unsupported-operator] # revealed: Unknown reveal_type(Foo() + Foo()) diff --git a/crates/ty_python_semantic/resources/mdtest/binary/integers.md b/crates/ty_python_semantic/resources/mdtest/binary/integers.md index 401c09d756..7ae0c91b89 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/integers.md @@ -16,6 +16,7 @@ reveal_type(7 ^ 2) # revealed: Literal[5] # error: [unsupported-operator] "Operator `+` is not supported between objects of type `Literal[2]` and `Literal["f"]`" reveal_type(2 + "f") # revealed: Unknown + def lhs(x: int): reveal_type(x + 1) # revealed: int reveal_type(x - 4) # revealed: int @@ -24,6 +25,7 @@ def lhs(x: int): reveal_type(x / 3) # revealed: int | float reveal_type(x % 3) # revealed: int + def rhs(x: int): reveal_type(2 + x) # revealed: int reveal_type(3 - x) # revealed: int @@ -32,6 +34,7 @@ def rhs(x: int): reveal_type(-3 / x) # revealed: int | float reveal_type(5 % x) # revealed: int + def both(x: int): reveal_type(x + x) # revealed: int reveal_type(x - x) # revealed: int @@ -52,6 +55,7 @@ reveal_type(2**2) # revealed: Literal[4] reveal_type(1 ** (largest_u32 + 1)) # revealed: int reveal_type(2**largest_u32) # revealed: int + def variable(x: int): reveal_type(x**2) # revealed: int reveal_type(2**x) # revealed: Any @@ -134,8 +138,10 @@ bool(1) / False # error: "Cannot divide object of type `float` by zero" reveal_type(1.0 / 0) # revealed: int | float + class MyInt(int): ... + # No error for a subclass of int reveal_type(MyInt(3) / 0) # revealed: int | float ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/tuples.md b/crates/ty_python_semantic/resources/mdtest/binary/tuples.md index c6a2142044..cff19a2946 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/tuples.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/tuples.md @@ -8,6 +8,7 @@ reveal_type(() + (1, 2)) # revealed: tuple[Literal[1, 2], ...] reveal_type((1, 2) + ()) # revealed: tuple[Literal[1, 2], ...] reveal_type(() + ()) # revealed: tuple[()] + def _(x: tuple[int, str], y: tuple[None, tuple[int]]): reveal_type(x + y) # revealed: tuple[int | str | None | tuple[int], ...] reveal_type(y + x) # revealed: tuple[None | tuple[int] | int | str, ...] @@ -38,6 +39,7 @@ ThreeFour = tuple[Literal[3], Literal[4]] IntTuple = tuple[int, ...] StrTuple = tuple[str, ...] + def _(one_two: OneTwo, x: IntTuple, y: StrTuple, three_four: ThreeFour): reveal_type(x + x) # revealed: tuple[int, ...] reveal_type(x + y) # revealed: tuple[int | str, ...] diff --git a/crates/ty_python_semantic/resources/mdtest/binary/unions.md b/crates/ty_python_semantic/resources/mdtest/binary/unions.md index c450d8d8de..1f47cdf44e 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/unions.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/unions.md @@ -28,6 +28,7 @@ the possible outcomes: ```py from typing import Literal + def f3(two_or_three: Literal[2, 3], a_or_b: Literal["a", "b"]): reveal_type(two_or_three + two_or_three) # revealed: Literal[4, 5, 6] reveal_type(two_or_three**two_or_three) # revealed: Literal[4, 8, 9, 27] diff --git a/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md b/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md index 8eeb52079e..ce47ec7939 100644 --- a/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md +++ b/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md @@ -39,14 +39,17 @@ If a symbol has a declared type (`int`), we use that even if there is a more pre ```py from typing import Any + def any() -> Any: ... + class Public: a: int = 1 b: str = 2 # error: [invalid-assignment] c: Any = 3 d: int = any() + reveal_type(Public.a) # revealed: int reveal_type(Public.b) # revealed: str reveal_type(Public.c) # revealed: Any @@ -60,10 +63,12 @@ If a symbol is declared and *possibly* unbound, we trust the declared type witho ```py from typing import Any + def any() -> Any: ... def flag() -> bool: return True + class Public: a: int b: str @@ -76,6 +81,7 @@ class Public: c = 3 d = any() + reveal_type(Public.a) # revealed: int reveal_type(Public.b) # revealed: str reveal_type(Public.c) # revealed: Any @@ -90,10 +96,12 @@ is available somehow and simply use the declared type. ```py from typing import Any + class Public: a: int b: Any + reveal_type(Public.a) # revealed: int reveal_type(Public.b) # revealed: Any ``` @@ -108,10 +116,12 @@ inferred types: ```py from typing import Any + def any() -> Any: ... def flag() -> bool: return True + class Public: a = 1 b = 2 @@ -123,6 +133,7 @@ class Public: c: str # error: [invalid-declaration] d: int + reveal_type(Public.a) # revealed: int reveal_type(Public.b) # revealed: Literal[2] | Any reveal_type(Public.c) # revealed: Literal[3] | Unknown @@ -143,9 +154,11 @@ error for both `a` and `b`: ```py from typing import Any + def flag() -> bool: return True + class Public: if flag(): a: Any = 1 @@ -153,6 +166,7 @@ class Public: else: b: str + # error: [possibly-missing-attribute] reveal_type(Public.a) # revealed: Literal[1] | Any # error: [possibly-missing-attribute] @@ -173,10 +187,12 @@ seems inconsistent when compared to the case just above. def flag() -> bool: return True + class Public: if flag(): a: int + # TODO: this should raise an error. Once we fix this, update the section description and the table # on top of this document. reveal_type(Public.a) # revealed: int @@ -202,6 +218,7 @@ class Public: # Implicitly declared with `Unknown`, due to the usage of an unknown name in the annotation: b: SomeUnknownName = 1 # error: [unresolved-reference] + reveal_type(Public.a) # revealed: Unknown | Literal[1] reveal_type(Public.b) # revealed: Unknown @@ -218,11 +235,13 @@ inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" cas def flag() -> bool: return True + class Public: if flag: a = 1 b: SomeUnknownName = 1 # error: [unresolved-reference] + # TODO: these should raise an error. Once we fix this, update the section description and the table # on top of this document. reveal_type(Public.a) # revealed: Unknown | Literal[1] @@ -241,6 +260,7 @@ class Public: if False: a: int = 1 + # error: [unresolved-attribute] reveal_type(Public.a) # revealed: Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/call/annotation.md b/crates/ty_python_semantic/resources/mdtest/call/annotation.md index 937d4226fc..c71020ee55 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/annotation.md +++ b/crates/ty_python_semantic/resources/mdtest/call/annotation.md @@ -3,9 +3,11 @@ ```py from typing import Callable + def _(c: Callable[[], int]): reveal_type(c()) # revealed: int + def _(c: Callable[[int, str], int]): reveal_type(c(1, "a")) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index 5d783a93d3..bbe8a28535 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -6,6 +6,7 @@ class NotBool: __bool__ = None + # error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2" bool(1, 2) @@ -68,10 +69,12 @@ from enum import Enum from types import FunctionType from typing import TypeVar + class Answer(Enum): NO = 0 YES = 1 + reveal_type(isinstance(True, bool)) # revealed: Literal[True] reveal_type(isinstance(True, int)) # revealed: Literal[True] reveal_type(isinstance(True, object)) # revealed: Literal[True] @@ -82,17 +85,27 @@ reveal_type(isinstance(Answer.NO, Answer)) # revealed: Literal[True] reveal_type(isinstance((1, 2), tuple)) # revealed: Literal[True] + def f(): ... + reveal_type(isinstance(f, FunctionType)) # revealed: Literal[True] reveal_type(isinstance("", int)) # revealed: bool + class A: ... + + class SubclassOfA(A): ... + + class OtherSubclassOfA(A): ... + + class B: ... + reveal_type(isinstance(A, type)) # revealed: Literal[True] a = A() @@ -106,6 +119,7 @@ s = SubclassOfA() reveal_type(isinstance(s, SubclassOfA)) # revealed: Literal[True] reveal_type(isinstance(s, A)) # revealed: Literal[True] + def _(x: A | B, y: list[int]): reveal_type(isinstance(y, list)) # revealed: Literal[True] reveal_type(isinstance(x, A)) # revealed: bool @@ -116,10 +130,12 @@ def _(x: A | B, y: list[int]): reveal_type(x) # revealed: B & ~A reveal_type(isinstance(x, B)) # revealed: Literal[True] + T = TypeVar("T") T_bound_A = TypeVar("T_bound_A", bound=A) T_constrained = TypeVar("T_constrained", SubclassOfA, OtherSubclassOfA) + def _( x: T, x_bound_a: T_bound_A, diff --git a/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md b/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md index 52f61bb5ed..1ccd214837 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md +++ b/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md @@ -10,11 +10,14 @@ class Multiplier: def __call__(self, number: int) -> int: return number * self.factor + a = Multiplier(2)(3) reveal_type(a) # revealed: int + class Unit: ... + b = Unit()(3.0) # error: "Object of type `Unit` is not callable" reveal_type(b) # revealed: Unknown ``` @@ -25,6 +28,7 @@ reveal_type(b) # revealed: Unknown def _(flag: bool): class PossiblyNotCallable: if flag: + def __call__(self) -> int: return 1 @@ -38,6 +42,7 @@ def _(flag: bool): ```py def _(flag: bool): if flag: + class PossiblyUnbound: def __call__(self) -> int: return 1 @@ -53,6 +58,7 @@ def _(flag: bool): class NonCallable: __call__ = 1 + a = NonCallable() # error: [call-non-callable] "Object of type `Literal[1]` is not callable" reveal_type(a()) # revealed: Unknown @@ -66,6 +72,7 @@ def _(flag: bool): if flag: __call__ = 1 else: + def __call__(self) -> int: return 1 @@ -83,6 +90,7 @@ class C: def __call__(self, x: int) -> int: return 1 + c = C() # error: 15 [invalid-argument-type] "Argument to bound method `__call__` is incorrect: Expected `int`, found `Literal["foo"]`" @@ -97,6 +105,7 @@ class C: def __call__(self: int) -> int: return 1 + c = C() # error: 13 [invalid-argument-type] "Argument to bound method `__call__` is incorrect: Expected `int`, found `C`" @@ -111,6 +120,7 @@ reveal_type(c()) # revealed: int def outer(cond1: bool): class Test: if cond1: + def __call__(self): ... class Other: diff --git a/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md b/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md index d194cc87c1..283ff483e8 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md +++ b/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md @@ -16,10 +16,12 @@ the first argument: from ty_extensions import CallableTypeOf from typing import Callable + class C1: def method(self: C1, x: int) -> str: return str(x) + def _( accessed_on_class: CallableTypeOf[C1.method], accessed_on_instance: CallableTypeOf[C1().method], @@ -37,9 +39,11 @@ class NonDescriptorCallable2: def __call__(self, c2: C2, x: int) -> str: return str(x) + class C2: non_descriptor_callable: NonDescriptorCallable2 = NonDescriptorCallable2() + def _( accessed_on_class: CallableTypeOf[C2.non_descriptor_callable], accessed_on_instance: CallableTypeOf[C2().non_descriptor_callable], @@ -55,9 +59,11 @@ class NonDescriptorCallable3: def __call__(self, c3: C3, x: int) -> str: return str(x) + class C3: def method(self: C3, x: int) -> str: return str(x) + non_descriptor_callable: NonDescriptorCallable3 = NonDescriptorCallable3() callable_m: Callable[[C3, int], str] = method @@ -113,9 +119,11 @@ intention that it shouldn't influence the method's descriptor behavior. For exam ```py from typing import Callable + def memoize[**P, R](f: Callable[P, R]) -> Callable[P, R]: raise NotImplementedError + class C1: def method(self, x: int) -> str: return str(x) @@ -124,6 +132,7 @@ class C1: def method_decorated(self, x: int) -> str: return str(x) + C1().method(1) C1().method_decorated(1) @@ -135,11 +144,13 @@ This also works with an argumentless `Callable` annotation: def memoize2(f: Callable) -> Callable: raise NotImplementedError + class C2: @memoize2 def method_decorated(self, x: int) -> str: return str(x) + C2().method_decorated(1) ``` @@ -148,14 +159,17 @@ And with unions of `Callable` types: ```py from typing import Callable + def expand(f: Callable[[C3, int], int]) -> Callable[[C3, int], int] | Callable[[C3, int], str]: raise NotImplementedError + class C3: @expand def method_decorated(self, x: int) -> int: return x + reveal_type(C3().method_decorated(1)) # revealed: int | str ``` @@ -167,11 +181,14 @@ but here we emit errors: def memoize3(f: Callable[[C4, int], str]) -> Callable[[C4, int], str]: raise NotImplementedError + class C4: def method(self, x: int) -> str: return str(x) + method_decorated = memoize3(method) + # error: [missing-argument] # error: [invalid-argument-type] C4().method_decorated(1) @@ -194,12 +211,15 @@ class SquareCalculator: def __call__(self, x: float) -> int: return self.post_process(x * x) + def square_then(c: Callable[[float], int]) -> Callable[[float], int]: return SquareCalculator(c) + class Calculator: square_then_round = square_then(round) + reveal_type(Calculator().square_then_round(3.14)) # revealed: Unknown | int ``` @@ -212,12 +232,15 @@ example. We generally treat dunder attributes as bound-method descriptors since ```py from typing import Callable + def pow_impl(tensor: Tensor, exponent: int) -> Tensor: raise NotImplementedError + class Tensor: __pow__: Callable[[Tensor, int], Tensor] = pow_impl + Tensor() ** 2 ``` @@ -229,9 +252,11 @@ treat it as a bound-method descriptor: def make_comparison_operator(name: str) -> Callable[[Matrix, Matrix], bool]: raise NotImplementedError + class Matrix: __lt__ = make_comparison_operator("lt") + Matrix() < Matrix() ``` @@ -245,14 +270,17 @@ function-like: ```py from typing import Callable + def my_lossy_decorator(fn: Callable[..., int]) -> Callable[..., int]: return fn + class MyClass: @my_lossy_decorator def method(self) -> int: return 42 + reveal_type(MyClass().method) # revealed: (...) -> int reveal_type(MyClass().method.__name__) # revealed: str ``` @@ -267,9 +295,11 @@ behavior. ```py from typing import Callable + def callable_identity[**P, R](func: Callable[P, R]) -> Callable[P, R]: return func + class C: @callable_identity @classmethod @@ -281,6 +311,7 @@ class C: def f2(cls, x: int) -> str: return "a" + # error: [too-many-positional-arguments] # error: [invalid-argument-type] C.f1(C, 1) @@ -303,18 +334,22 @@ The callable type of a type object is not function-like. from typing import ClassVar from ty_extensions import CallableTypeOf + class WithNew: def __new__(self, x: int) -> WithNew: return super().__new__(WithNew) + class WithInit: def __init__(self, x: int) -> None: pass + class C: with_new: ClassVar[CallableTypeOf[WithNew]] with_init: ClassVar[CallableTypeOf[WithInit]] + C.with_new(1) C().with_new(1) C.with_init(1) diff --git a/crates/ty_python_semantic/resources/mdtest/call/constructor.md b/crates/ty_python_semantic/resources/mdtest/call/constructor.md index b6e65fcdf2..ee03c4470d 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/ty_python_semantic/resources/mdtest/call/constructor.md @@ -45,6 +45,7 @@ reveal_type(object(1)) # revealed: object ```py class Foo: ... + reveal_type(Foo()) # revealed: Foo # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" @@ -58,6 +59,7 @@ class Foo: def __new__(cls, x: int) -> "Foo": return object.__new__(cls) + reveal_type(Foo(1)) # revealed: Foo # error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" @@ -74,12 +76,15 @@ constructor from it. ```py from typing_extensions import Self + class Base: def __new__(cls, x: int) -> Self: return cls() + class Foo(Base): ... + reveal_type(Foo(1)) # revealed: Foo # error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" @@ -94,8 +99,11 @@ reveal_type(Foo(1, 2)) # revealed: Foo def _(flag: bool) -> None: class Foo: if flag: + def __new__(cls, x: int): ... + else: + def __new__(cls, x: int, y: int = 1): ... reveal_type(Foo(1)) # revealed: Foo @@ -118,13 +126,16 @@ class SomeCallable: obj.x = x return obj + class Descriptor: def __get__(self, instance, owner) -> SomeCallable: return SomeCallable() + class Foo: __new__: Descriptor = Descriptor() + reveal_type(Foo(1)) # revealed: Foo # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" reveal_type(Foo()) # revealed: Foo @@ -139,9 +150,11 @@ class Callable: def __call__(self, cls, x: int) -> "Foo": return object.__new__(cls) + class Foo: __new__ = Callable() + reveal_type(Foo(1)) # revealed: Foo # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" reveal_type(Foo()) # revealed: Foo @@ -155,6 +168,7 @@ reveal_type(Foo()) # revealed: Foo def _(flag: bool) -> None: class Foo: if flag: + def __new__(cls): return object.__new__(cls) @@ -172,6 +186,7 @@ def _(flag: bool) -> None: def _(flag: bool) -> None: class Callable: if flag: + def __call__(self, cls, x: int) -> "Foo": return object.__new__(cls) @@ -194,6 +209,7 @@ If the class has an `__init__` method, we can infer the signature of the constru class Foo: def __init__(self, x: int): ... + reveal_type(Foo(1)) # revealed: Foo # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" @@ -211,8 +227,10 @@ constructor from it. class Base: def __init__(self, x: int): ... + class Foo(Base): ... + reveal_type(Foo(1)) # revealed: Foo # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" @@ -227,8 +245,11 @@ reveal_type(Foo(1, 2)) # revealed: Foo def _(flag: bool) -> None: class Foo: if flag: + def __init__(self, x: int): ... + else: + def __init__(self, x: int, y: int = 1): ... reveal_type(Foo(1)) # revealed: Foo @@ -253,13 +274,16 @@ class SomeCallable: def __call__(self, x: int) -> str: return "a" + class Descriptor: def __get__(self, instance, owner) -> SomeCallable: return SomeCallable() + class Foo: __init__: Descriptor = Descriptor() + reveal_type(Foo(1)) # revealed: Foo # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" reveal_type(Foo()) # revealed: Foo @@ -274,9 +298,11 @@ class Callable: def __call__(self, x: int) -> None: pass + class Foo: __init__ = Callable() + reveal_type(Foo(1)) # revealed: Foo # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" reveal_type(Foo()) # revealed: Foo @@ -288,6 +314,7 @@ reveal_type(Foo()) # revealed: Foo def _(flag: bool) -> None: class Callable: if flag: + def __call__(self, x: int) -> None: pass @@ -320,6 +347,7 @@ class Foo: def __init__(self, x: int): ... + # error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" reveal_type(Foo()) # revealed: Foo @@ -340,6 +368,7 @@ class Foo: def __init__(self, x: int) -> None: self.x = x + # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" reveal_type(Foo()) # revealed: Foo reveal_type(Foo(1)) # revealed: Foo @@ -353,6 +382,7 @@ reveal_type(Foo(1, 2)) # revealed: Foo ```py import abc + class Foo: def __new__(cls) -> "Foo": return object.__new__(cls) @@ -360,12 +390,14 @@ class Foo: def __init__(self, x): self.x = 42 + # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" reveal_type(Foo()) # revealed: Foo # error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2" reveal_type(Foo(42)) # revealed: Foo + class Foo2: def __new__(cls, x) -> "Foo2": return object.__new__(cls) @@ -373,12 +405,14 @@ class Foo2: def __init__(self): pass + # error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" reveal_type(Foo2()) # revealed: Foo2 # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" reveal_type(Foo2(42)) # revealed: Foo2 + class Foo3(metaclass=abc.ABCMeta): def __new__(cls) -> "Foo3": return object.__new__(cls) @@ -386,12 +420,14 @@ class Foo3(metaclass=abc.ABCMeta): def __init__(self, x): self.x = 42 + # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" reveal_type(Foo3()) # revealed: Foo3 # error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2" reveal_type(Foo3(42)) # revealed: Foo3 + class Foo4(metaclass=abc.ABCMeta): def __new__(cls, x) -> "Foo4": return object.__new__(cls) @@ -399,6 +435,7 @@ class Foo4(metaclass=abc.ABCMeta): def __init__(self): pass + # error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" reveal_type(Foo4()) # revealed: Foo4 @@ -415,6 +452,7 @@ meta-type, never on the type itself). ```py from typing_extensions import Literal + class Meta(type): def __new__(mcls, name, bases, namespace, /, **kwargs): return super().__new__(mcls, name, bases, namespace) @@ -422,8 +460,10 @@ class Meta(type): def __lt__(cls, other) -> Literal[True]: return True + class C(metaclass=Meta): ... + # No error is raised here, since we don't implicitly call `Meta.__new__` reveal_type(C()) # revealed: C diff --git a/crates/ty_python_semantic/resources/mdtest/call/dunder.md b/crates/ty_python_semantic/resources/mdtest/call/dunder.md index 258702b6b6..61732b338b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/dunder.md +++ b/crates/ty_python_semantic/resources/mdtest/call/dunder.md @@ -16,10 +16,12 @@ as the `instance` argument to `__get__`. A desugared version of `obj[key]` is ro ```py from typing import Any + def find_name_in_mro(typ: type, name: str) -> Any: # See implementation in https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance pass + def getitem_desugared(obj: object, key: object) -> object: getitem_callable = find_name_in_mro(type(obj), "__getitem__") if hasattr(getitem_callable, "__get__"): @@ -40,9 +42,11 @@ class Meta(type): def __getitem__(cls, key: int) -> str: return str(key) + class DunderOnMetaclass(metaclass=Meta): pass + reveal_type(DunderOnMetaclass[0]) # revealed: str ``` @@ -53,6 +57,7 @@ class ClassWithNormalDunder: def __getitem__(self, key: int) -> str: return str(key) + # error: [not-subscriptable] ClassWithNormalDunder[0] ``` @@ -68,6 +73,7 @@ class ClassWithNormalDunder: def __getitem__(self, key: int) -> str: return str(key) + class_with_normal_dunder = ClassWithNormalDunder() reveal_type(class_with_normal_dunder[0]) # revealed: str @@ -79,10 +85,12 @@ Which can be demonstrated by trying to attach a dunder method to an instance, wh def external_getitem(instance, key: int) -> str: return str(key) + class ThisFails: def __init__(self): self.__getitem__ = external_getitem + this_fails = ThisFails() # error: [not-subscriptable] "Cannot subscript object of type `ThisFails` with no `__getitem__` method" @@ -101,9 +109,11 @@ The instance-level method is also not called when the class-level method is pres def external_getitem1(instance, key) -> str: return "a" + def external_getitem2(key) -> int: return 1 + def _(flag: bool): class ThisFails: if flag: @@ -129,9 +139,11 @@ Class-level annotations with no value assigned are considered to be accessible o ```py from typing import Callable + class C: __call__: Callable[..., None] + C()() _: Callable[..., None] = C() @@ -142,10 +154,12 @@ And of course the same is true if we have only an implicit assignment inside a m ```py from typing import Callable + class C: def __init__(self): self.__call__ = lambda *a, **kw: None + # error: [call-non-callable] C()() @@ -162,9 +176,11 @@ class SomeCallable: def __call__(self, key: int) -> str: return str(key) + class ClassWithNonMethodDunder: __getitem__: SomeCallable = SomeCallable() + class_with_callable_dunder = ClassWithNonMethodDunder() reveal_type(class_with_callable_dunder[0]) # revealed: str @@ -178,17 +194,21 @@ that the `instance` argument is on object of type `ClassWithDescriptorDunder`: ```py from __future__ import annotations + class SomeCallable: def __call__(self, key: int) -> str: return str(key) + class Descriptor: def __get__(self, instance: ClassWithDescriptorDunder, owner: type[ClassWithDescriptorDunder]) -> SomeCallable: return SomeCallable() + class ClassWithDescriptorDunder: __getitem__: Descriptor = Descriptor() + class_with_descriptor_dunder = ClassWithDescriptorDunder() reveal_type(class_with_descriptor_dunder[0]) # revealed: str @@ -208,6 +228,7 @@ class C: # error: [invalid-assignment] self.__getitem__ = None + # This is still fine, and simply calls the `__getitem__` method on the class reveal_type(C()[0]) # revealed: str ``` @@ -218,9 +239,12 @@ reveal_type(C()[0]) # revealed: str def _(flag: bool): class C: if flag: + def __getitem__(self, key: int) -> str: return str(key) + else: + def __getitem__(self, key: int) -> bytes: return bytes() @@ -228,11 +252,13 @@ def _(flag: bool): reveal_type(c[0]) # revealed: str | bytes if flag: + class D: def __getitem__(self, key: int) -> str: return str(key) else: + class D: def __getitem__(self, key: int) -> bytes: return bytes() @@ -250,14 +276,17 @@ regular method calls. def external_getitem(instance, key: int) -> str: return str(key) + class NotSubscriptable1: def __init__(self, value: int): self.__getitem__ = external_getitem + class NotSubscriptable2: def __init__(self, value: int): self.__getitem__ = external_getitem + def _(union: NotSubscriptable1 | NotSubscriptable2): # error: [not-subscriptable] "Cannot subscript object of type `NotSubscriptable2` with no `__getitem__` method" # error: [not-subscriptable] "Cannot subscript object of type `NotSubscriptable1` with no `__getitem__` method" @@ -270,6 +299,7 @@ def _(union: NotSubscriptable1 | NotSubscriptable2): def _(flag: bool): class C: if flag: + def __getitem__(self, key: int) -> str: return str(key) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 550d7920b2..8100aee9ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -6,6 +6,7 @@ def get_int() -> int: return 42 + reveal_type(get_int()) # revealed: int ``` @@ -15,6 +16,7 @@ reveal_type(get_int()) # revealed: int async def get_int_async() -> int: return 42 + reveal_type(get_int_async()) # revealed: CoroutineType[Any, Any, int] ``` @@ -29,6 +31,7 @@ python-version = "3.12" def get_int[T]() -> int: return 42 + reveal_type(get_int()) # revealed: int ``` @@ -37,16 +40,20 @@ reveal_type(get_int()) # revealed: int ```py from typing import Callable + def foo() -> int: return 42 + def decorator(func) -> Callable[[], int]: return foo + @decorator def bar() -> str: return "bar" + reveal_type(bar()) # revealed: int ``` @@ -62,8 +69,10 @@ x = nonsense() # error: "Object of type `Literal[123]` is not callable" ```py def _(flag: bool): if flag: + def foo() -> int: return 42 + # error: [possibly-unresolved-reference] reveal_type(foo()) # revealed: int ``` @@ -89,6 +98,7 @@ still continue to use the old convention, so it is supported by ty as well. ```py def f(__x: int): ... + f(1) # error: [positional-only-parameter-as-kwarg] f(__x=1) @@ -99,6 +109,7 @@ But not if they follow a non-positional-only parameter: ```py def g(x: int, __y: str): ... + g(x=1, __y="foo") ``` @@ -107,6 +118,7 @@ And also not if they both start and end with `__`: ```py def h(__x__: str): ... + h(__x__="foo") ``` @@ -115,6 +127,7 @@ And if *any* parameters use the new PEP-570 convention, the old convention does ```py def i(x: str, /, __y: int): ... + i("foo", __y=42) # fine ``` @@ -125,11 +138,13 @@ class C: def method(self, __x: int): ... @classmethod def class_method(cls, __x: str): ... + # (the name of the first parameter is irrelevant; # a staticmethod works the same as a free function in the global scope) @staticmethod def static_method(self, __x: int): ... + # error: [positional-only-parameter-as-kwarg] C().method(__x=1) # error: [positional-only-parameter-as-kwarg] @@ -153,8 +168,10 @@ def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... + # Test all of the above with a number of different splatted argument types + def _(args: list[int]) -> None: takes_zero(*args) takes_one(*args) @@ -169,6 +186,7 @@ def _(args: list[int]) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + def _(args: tuple[int, ...]) -> None: takes_zero(*args) takes_one(*args) @@ -196,8 +214,10 @@ def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... + # Test all of the above with a number of different splatted argument types + def _(args: tuple[int]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) @@ -210,6 +230,7 @@ def _(args: tuple[int]) -> None: takes_at_least_two(*args) # error: [missing-argument] takes_at_least_two_positional_only(*args) # error: [missing-argument] + def _(args: tuple[int, int]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -222,6 +243,7 @@ def _(args: tuple[int, int]) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + def _(args: tuple[int, str]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -249,10 +271,13 @@ def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... + # Test all of the above with a number of different splatted argument types + class SingleElementTuple(tuple[int]): ... + def _(args: SingleElementTuple) -> None: takes_zero(*args) # error: [too-many-positional-arguments] @@ -270,8 +295,10 @@ def _(args: SingleElementTuple) -> None: takes_at_least_two(*args) # error: [missing-argument] takes_at_least_two_positional_only(*args) # error: [missing-argument] + class TwoElementIntTuple(tuple[int, int]): ... + def _(args: TwoElementIntTuple) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -284,8 +311,10 @@ def _(args: TwoElementIntTuple) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + class IntStrTuple(tuple[int, str]): ... + def _(args: IntStrTuple) -> None: takes_zero(*args) # error: [too-many-positional-arguments] @@ -326,8 +355,10 @@ def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... + # Test all of the above with a number of different splatted argument types + def _(args: tuple[int, *tuple[int, ...]]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) @@ -340,6 +371,7 @@ def _(args: tuple[int, *tuple[int, ...]]) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + def _(args: tuple[int, *tuple[str, ...]]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) @@ -352,6 +384,7 @@ def _(args: tuple[int, *tuple[str, ...]]) -> None: takes_at_least_two(*args) # error: [invalid-argument-type] takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] + def _(args: tuple[int, int, *tuple[int, ...]]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -364,6 +397,7 @@ def _(args: tuple[int, int, *tuple[int, ...]]) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + def _(args: tuple[int, int, *tuple[str, ...]]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -376,6 +410,7 @@ def _(args: tuple[int, int, *tuple[str, ...]]) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + def _(args: tuple[int, *tuple[int, ...], int]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -388,6 +423,7 @@ def _(args: tuple[int, *tuple[int, ...], int]) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + def _(args: tuple[int, *tuple[str, ...], int]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -420,10 +456,13 @@ def takes_at_least_one(x: int, *args) -> None: ... def takes_at_least_two(x: int, y: int, *args) -> None: ... def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... + # Test all of the above with a number of different splatted argument types + class IntStarInt(tuple[int, *tuple[int, ...]]): ... + def _(args: IntStarInt) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) @@ -436,8 +475,10 @@ def _(args: IntStarInt) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + class IntStarStr(tuple[int, *tuple[str, ...]]): ... + def _(args: IntStarStr) -> None: takes_zero(*args) # error: [too-many-positional-arguments] @@ -460,8 +501,10 @@ def _(args: IntStarStr) -> None: # error: [invalid-argument-type] takes_at_least_two_positional_only(*args) + class IntIntStarInt(tuple[int, int, *tuple[int, ...]]): ... + def _(args: IntIntStarInt) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -474,8 +517,10 @@ def _(args: IntIntStarInt) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + class IntIntStarStr(tuple[int, int, *tuple[str, ...]]): ... + def _(args: IntIntStarStr) -> None: takes_zero(*args) # error: [too-many-positional-arguments] @@ -497,8 +542,10 @@ def _(args: IntIntStarStr) -> None: takes_at_least_two_positional_only(*args) + class IntStarIntInt(tuple[int, *tuple[int, ...], int]): ... + def _(args: IntStarIntInt) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -511,8 +558,10 @@ def _(args: IntStarIntInt) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + class IntStarStrInt(tuple[int, *tuple[str, ...], int]): ... + def _(args: IntStarStrInt) -> None: takes_zero(*args) # error: [too-many-positional-arguments] @@ -542,6 +591,7 @@ def _(args: IntStarStrInt) -> None: ```py from typing import Literal + def takes_zero() -> None: ... def takes_one(x: str) -> None: ... def takes_two(x: str, y: str) -> None: ... @@ -553,8 +603,10 @@ def takes_at_least_one(x: str, *args) -> None: ... def takes_at_least_two(x: str, y: str, *args) -> None: ... def takes_at_least_two_positional_only(x: str, y: str, /, *args) -> None: ... + # Test all of the above with a number of different splatted argument types + def _(args: Literal["a"]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) @@ -571,6 +623,7 @@ def _(args: Literal["a"]) -> None: takes_at_least_two(*args) # error: [missing-argument] takes_at_least_two_positional_only(*args) # error: [missing-argument] + def _(args: Literal["ab"]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -583,6 +636,7 @@ def _(args: Literal["ab"]) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + def _(args: Literal["abc"]) -> None: takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) # error: [too-many-positional-arguments] @@ -599,6 +653,7 @@ def _(args: Literal["abc"]) -> None: takes_at_least_two(*args) takes_at_least_two_positional_only(*args) + def _(args: str) -> None: takes_zero(*args) takes_one(*args) @@ -620,6 +675,7 @@ def _(args: str) -> None: def f(x: int) -> int: return 1 + # error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" reveal_type(f("foo")) # revealed: int ``` @@ -630,6 +686,7 @@ reveal_type(f("foo")) # revealed: int def f(x: int, /) -> int: return 1 + # error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" reveal_type(f("foo")) # revealed: int ``` @@ -640,6 +697,7 @@ reveal_type(f("foo")) # revealed: int def f(*args: int) -> int: return 1 + # error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" reveal_type(f("foo")) # revealed: int ``` @@ -655,6 +713,7 @@ python-version = "3.11" def f(*args: int) -> int: return 1 + def _(args: list[str]) -> None: # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`" reveal_type(f(*args)) # revealed: int @@ -701,6 +760,7 @@ A union of heterogeneous tuples provided to a variadic parameter: # - # - + def f2(a: str, b: bool): ... def f3(coinflip: bool): if coinflip: @@ -722,8 +782,10 @@ def f3(coinflip: bool): # error: [invalid-argument-type] "Argument to function `f2` is incorrect: Expected `bool`, found `Literal[True] | tuple[Literal[True]]`" f2(*other_args) + def f4(a=None, b=None, c=None, d=None, e=None): ... + my_args = ((1, 2), (3, 4), (5, 6)) for tup in my_args: @@ -750,6 +812,7 @@ python-version = "3.11" def f(x: int, *args: str) -> int: return 1 + def _( args1: list[int], args2: tuple[int], @@ -785,6 +848,7 @@ def _( def f(x: int) -> int: return 1 + # error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" reveal_type(f(x="foo")) # revealed: int ``` @@ -795,6 +859,7 @@ reveal_type(f(x="foo")) # revealed: int def f(*, x: int) -> int: return 1 + # error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" reveal_type(f(x="foo")) # revealed: int ``` @@ -805,6 +870,7 @@ reveal_type(f(x="foo")) # revealed: int def f(**kwargs: int) -> int: return 1 + # error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" reveal_type(f(x="foo")) # revealed: int ``` @@ -815,6 +881,7 @@ reveal_type(f(x="foo")) # revealed: int def f(x: int = 1, y: str = "foo") -> int: return 1 + # error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `Literal[2]`" # error: 20 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["bar"]`" reveal_type(f(y=2, x="bar")) # revealed: int @@ -827,10 +894,16 @@ reveal_type(f(y=2, x="bar")) # revealed: int ```py from typing import Sized + class Foo: ... + + class Bar: ... + + class Baz: ... + def f(x: Sized): ... def g( a: str | Foo, @@ -852,6 +925,7 @@ def g( def f() -> int: return 1 + # error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1" reveal_type(f("foo")) # revealed: int ``` @@ -862,6 +936,7 @@ reveal_type(f("foo")) # revealed: int def f() -> int: return 1 + # error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2" reveal_type(f("foo", "bar")) # revealed: int ``` @@ -872,6 +947,7 @@ reveal_type(f("foo", "bar")) # revealed: int def f(*args: int) -> int: return 1 + reveal_type(f(1, 2, 3)) # revealed: int ``` @@ -881,6 +957,7 @@ reveal_type(f(1, 2, 3)) # revealed: int def f(**kwargs: int) -> int: return 1 + reveal_type(f(foo=1, bar=2)) # revealed: int ``` @@ -892,6 +969,7 @@ reveal_type(f(foo=1, bar=2)) # revealed: int def f(x: int) -> int: return 1 + # error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" reveal_type(f()) # revealed: int ``` @@ -902,6 +980,7 @@ reveal_type(f()) # revealed: int def f(x: int, y: str = "foo") -> int: return 1 + # error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" reveal_type(f()) # revealed: int ``` @@ -912,6 +991,7 @@ reveal_type(f()) # revealed: int def f(x: int = 1) -> int: return 1 + reveal_type(f()) # revealed: int ``` @@ -921,6 +1001,7 @@ reveal_type(f()) # revealed: int def f(x: int, *y: str) -> int: return 1 + # error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" reveal_type(f()) # revealed: int ``` @@ -931,6 +1012,7 @@ reveal_type(f()) # revealed: int def f(*args: int) -> int: return 1 + reveal_type(f()) # revealed: int ``` @@ -940,6 +1022,7 @@ reveal_type(f()) # revealed: int def f(**kwargs: int) -> int: return 1 + reveal_type(f()) # revealed: int ``` @@ -949,6 +1032,7 @@ reveal_type(f()) # revealed: int def f(x: int, y: int) -> int: return 1 + # error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`" reveal_type(f()) # revealed: int ``` @@ -959,6 +1043,7 @@ reveal_type(f()) # revealed: int def f(x: int) -> int: return 1 + # error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`" reveal_type(f(x=1, y=2)) # revealed: int ``` @@ -969,6 +1054,7 @@ reveal_type(f(x=1, y=2)) # revealed: int def f(x: int) -> int: return 1 + # error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`" reveal_type(f(1, x=2)) # revealed: int ``` @@ -1044,6 +1130,7 @@ def empty() -> None: ... def _(kwargs: dict[str, int]) -> None: empty(**kwargs) + empty(**{}) empty(**dict()) ``` @@ -1053,15 +1140,19 @@ empty(**dict()) ```py from typing_extensions import TypedDict + def f(**kwargs: int) -> None: ... + class Foo(TypedDict): a: int b: int + def _(kwargs: dict[str, int]) -> None: f(**kwargs) + f(**{"foo": 1}) f(**dict(foo=1)) f(**Foo(a=1, b=2)) @@ -1085,14 +1176,17 @@ def _(kwargs: dict[str, int]) -> None: ```py from typing_extensions import TypedDict + class Foo(TypedDict): a: int b: int + def f(a: int, b: int) -> None: ... def _(kwargs: dict[str, int]) -> None: f(**kwargs) + f(**{"a": 1, "b": 2}) f(**dict(a=1, b=2)) f(**Foo(a=1, b=2)) @@ -1103,14 +1197,17 @@ f(**Foo(a=1, b=2)) ```py from typing_extensions import TypedDict + class Foo(TypedDict): a: int b: int + def f(*, a: int, b: int) -> None: ... def _(kwargs: dict[str, int]) -> None: f(**kwargs) + f(**{"a": 1, "b": 2}) f(**dict(a=1, b=2)) f(**Foo(a=1, b=2)) @@ -1135,6 +1232,7 @@ def _(kwargs1: dict[str, int], kwargs2: dict[str, int], kwargs3: dict[str, str], ```py class B: ... + def f(*, a: int, b: B, **kwargs: int) -> None: ... def _(kwargs: dict[str, int]): # Make sure that the `b` argument is not being matched against `kwargs` by passing an integer @@ -1160,16 +1258,20 @@ def _(kwargs1: dict[str, int], kwargs2: dict[str, str]): ```py from typing_extensions import NotRequired, TypedDict + class Foo1(TypedDict): a: int b: str + class Foo2(TypedDict): a: int b: NotRequired[str] + def f(**kwargs: int) -> None: ... + # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`" f(**Foo1(a=1, b="b")) # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`" @@ -1183,11 +1285,16 @@ The keys of the mapping passed to a double-starred argument must be strings. ```py from collections.abc import Mapping + def f(**kwargs: int) -> None: ... + class DictSubclass(dict[int, int]): ... + + class MappingSubclass(Mapping[int, int]): ... + class MappingProtocol: def keys(self) -> list[int]: return [1] @@ -1195,10 +1302,12 @@ class MappingProtocol: def __getitem__(self, key: int) -> int: return 1 + def _(kwargs: dict[int, int]) -> None: # error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`" f(**kwargs) + # error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`" f(**DictSubclass()) # error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`" @@ -1211,8 +1320,11 @@ The key can also be a custom type that inherits from `str`. ```py class SubStr(str): ... + + class SubInt(int): ... + def _(kwargs1: dict[SubStr, int], kwargs2: dict[SubInt, int]) -> None: f(**kwargs1) # error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `SubInt`" @@ -1225,6 +1337,7 @@ Or, it can be a type that is assignable to `str`. from typing import Any from ty_extensions import Unknown + def _(kwargs1: dict[Any, int], kwargs2: dict[Unknown, int]) -> None: f(**kwargs1) f(**kwargs2) @@ -1235,11 +1348,16 @@ def _(kwargs1: dict[Any, int], kwargs2: dict[Unknown, int]) -> None: ```py from collections.abc import Mapping + def f(**kwargs: str) -> None: ... + class DictSubclass(dict[str, int]): ... + + class MappingSubclass(Mapping[str, int]): ... + class MappingProtocol: def keys(self) -> list[str]: return ["foo"] @@ -1247,6 +1365,7 @@ class MappingProtocol: def __getitem__(self, key: str) -> int: return 1 + def _(kwargs: dict[str, int]) -> None: # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`" f(**kwargs) @@ -1263,6 +1382,7 @@ def _(kwargs: dict[str, int]) -> None: ```py from ty_extensions import Unknown + def f(**kwargs: int) -> None: ... def _(kwargs: Unknown): f(**kwargs) @@ -1273,8 +1393,10 @@ def _(kwargs: Unknown): ```py def f(**kwargs: int) -> None: ... + class A: ... + class InvalidMapping: def keys(self) -> A: return A() @@ -1282,6 +1404,7 @@ class InvalidMapping: def __getitem__(self, key: str) -> int: return 1 + def _(kwargs: dict[str, int] | int): # error: [invalid-argument-type] "Argument expression after ** must be a mapping type: Found `dict[str, int] | int`" f(**kwargs) @@ -1299,9 +1422,11 @@ from typing import TypeVar _T = TypeVar("_T") + def f(**kwargs: _T) -> _T: return kwargs["a"] + def _(kwargs: dict[str, int]) -> None: reveal_type(f(**kwargs)) # revealed: int ``` @@ -1314,12 +1439,15 @@ from typing_extensions import TypedDict _T = TypeVar("_T") + class Foo(TypedDict): a: int b: str + def f(**kwargs: _T) -> _T: return kwargs["a"] + reveal_type(f(**Foo(a=1, b="b"))) # revealed: int | str ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md b/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md index 04e57dea02..2708256001 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md +++ b/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md @@ -10,10 +10,12 @@ Consider the following example: ```py import inspect + class Descriptor: def __get__(self, instance, owner) -> str: return "a" + class C: normal: int = 1 descriptor: Descriptor = Descriptor() @@ -70,6 +72,7 @@ class D: def __init__(self) -> None: self.instance_attr: int = 1 + reveal_type(inspect.getattr_static(D(), "instance_attr")) # revealed: int ``` @@ -79,8 +82,10 @@ And attributes on metaclasses can be accessed when probing the class: class Meta(type): attr: int = 1 + class E(metaclass=Meta): ... + reveal_type(inspect.getattr_static(E, "attr")) # revealed: int ``` @@ -98,9 +103,11 @@ back to `Any`: ```py import inspect + class C: x: int = 1 + def _(attr_name: str): reveal_type(inspect.getattr_static(C(), attr_name)) # revealed: Any reveal_type(inspect.getattr_static(C(), attr_name, 1)) # revealed: Any @@ -127,6 +134,7 @@ inspect.getattr_static(C(), "x", "default-arg", "one too many") ```py import inspect + def _(flag: bool): class C: if flag: @@ -141,6 +149,7 @@ def _(flag: bool): import inspect from typing import Any + def _(a: Any, tuple_of_any: tuple[Any]): reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"] diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index fea7c85a57..e348c9f4d1 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -83,6 +83,7 @@ When we access methods from derived classes, they will be bound to instances of class D(C): pass + reveal_type(D().f) # revealed: bound method D.f(x: int) -> str ``` @@ -107,10 +108,12 @@ class Base: def method_on_base(self, x: int | None) -> str: return "a" + class Derived(Base): def method_on_derived(self, x: bytes) -> tuple[int, str]: return (1, "a") + reveal_type(Base().method_on_base(1)) # revealed: str reveal_type(Base.method_on_base(Base(), 1)) # revealed: str @@ -159,6 +162,7 @@ reveal_type(b"abcde".startswith(b"abc")) # revealed: bool ```py from typing_extensions import LiteralString + def f(s: LiteralString) -> None: reveal_type(s.find("a")) # revealed: int ``` @@ -175,14 +179,17 @@ def f(t: tuple[int, str]) -> None: ```py from typing import Any + class A: def f(self) -> int: return 1 + class B: def f(self) -> str: return "a" + def f(a_or_b: A | B, any_or_a: Any | A): reveal_type(a_or_b.f) # revealed: (bound method A.f() -> int) | (bound method B.f() -> str) reveal_type(a_or_b.f()) # revealed: int | str @@ -216,14 +223,18 @@ class: from typing import Protocol, Literal from ty_extensions import AlwaysFalsy + class Foo: ... + class SupportsStr(Protocol): def __str__(self) -> str: ... + class Falsy(Protocol): def __bool__(self) -> Literal[False]: ... + def _(a: object, b: SupportsStr, c: Falsy, d: AlwaysFalsy, e: None, f: Foo | None): a.__str__() b.__str__() @@ -253,10 +264,12 @@ Here, we test that this signature is enforced correctly: ```py from inspect import getattr_static + class C: def f(self, x: int) -> str: return "a" + method_wrapper = getattr_static(C, "f").__get__ reveal_type(method_wrapper) # revealed: @@ -298,13 +311,16 @@ the class itself. This also creates a bound method that is bound to the class ob ```py from __future__ import annotations + class Meta(type): def f(cls, arg: int) -> str: return "a" + class C(metaclass=Meta): pass + reveal_type(C.f) # revealed: bound method .f(arg: int) -> str reveal_type(C.f(1)) # revealed: str ``` @@ -321,10 +337,12 @@ A metaclass function can be shadowed by a method on the class: ```py from typing import Any, Literal + class D(metaclass=Meta): def f(arg: int) -> Literal["a"]: return "a" + reveal_type(D.f(1)) # revealed: Literal["a"] ``` @@ -334,11 +352,14 @@ If the class method is possibly missing, we union the return types: def flag() -> bool: return True + class E(metaclass=Meta): if flag(): + def f(arg: int) -> Any: return "a" + reveal_type(E.f(1)) # revealed: str | Any ``` @@ -352,11 +373,13 @@ the class object itself: ```py from __future__ import annotations + class C: @classmethod def f(cls: type[C], x: int) -> str: return "a" + reveal_type(C.f) # revealed: bound method .f(x: int) -> str reveal_type(C().f) # revealed: bound method type[C].f(x: int) -> str ``` @@ -385,6 +408,7 @@ class D: # This function is wrongly annotated, it should be `type[D]` instead of `D` pass + # error: [invalid-argument-type] "Argument to bound method `f` is incorrect: Expected `D`, found ``" D.f() ``` @@ -395,6 +419,7 @@ When a class method is accessed on a derived class, it is bound to that derived class Derived(C): pass + reveal_type(Derived.f) # revealed: bound method .f(x: int) -> str reveal_type(Derived().f) # revealed: bound method type[Derived].f(x: int) -> str @@ -410,10 +435,12 @@ currently don't model this explicitly: ```py from inspect import getattr_static + class C: @classmethod def f(cls): ... + reveal_type(getattr_static(C, "f")) # revealed: def f(cls) -> Unknown # revealed: reveal_type(getattr_static(C, "f").__get__) @@ -447,6 +474,7 @@ class method: def does_nothing[T](f: T) -> T: return f + class C: @classmethod @does_nothing @@ -458,6 +486,7 @@ class C: def f2(cls, x: int) -> str: return "a" + reveal_type(C.f1(1)) # revealed: str reveal_type(C().f1(1)) # revealed: str reveal_type(C.f2(1)) # revealed: str @@ -475,14 +504,17 @@ from contextlib import contextmanager from typing import Iterator from typing_extensions import Self + class Base: @classmethod @contextmanager def create(cls) -> Iterator[Self]: yield cls() + class Child(Base): ... + reveal_type(Base.create()) # revealed: _GeneratorContextManager[Base, None, None] with Base.create() as base: reveal_type(base) # revealed: Base @@ -515,9 +547,11 @@ class Base: super().__init_subclass__(**kwargs) cls.custom_attribute: int = 0 + class Derived(Base): pass + reveal_type(Derived.custom_attribute) # revealed: int ``` @@ -527,17 +561,25 @@ Subclasses must be constructed with arguments matching the required arguments of ```py class Empty: ... + class RequiresArg: def __init_subclass__(cls, arg: int): ... + class NoArg: def __init_subclass__(cls): ... + # Single-base definitions class MissingArg(RequiresArg): ... # error: [missing-argument] + + class InvalidType(RequiresArg, arg="foo"): ... # error: [invalid-argument-type] + + class Valid(RequiresArg, arg=1): ... + # error: [missing-argument] # error: [unknown-argument] class IncorrectArg(RequiresArg, not_arg="foo"): ... @@ -548,28 +590,53 @@ For multiple inheritance, the first resolved `__init_subclass__` method is used. ```py class Empty: ... + class RequiresArg: def __init_subclass__(cls, arg: int): ... + class NoArg: def __init_subclass__(cls): ... + class Valid(NoArg, RequiresArg): ... + + class MissingArg(RequiresArg, NoArg): ... # error: [missing-argument] + + class InvalidType(RequiresArg, NoArg, arg="foo"): ... # error: [invalid-argument-type] + + class Valid(RequiresArg, NoArg, arg=1): ... + # Ensure base class without __init_subclass__ is ignored class Valid(Empty, NoArg): ... + + class Valid(Empty, RequiresArg, NoArg, arg=1): ... + + class MissingArg(Empty, RequiresArg): ... # error: [missing-argument] + + class MissingArg(Empty, RequiresArg, NoArg): ... # error: [missing-argument] + + class InvalidType(Empty, RequiresArg, NoArg, arg="foo"): ... # error: [invalid-argument-type] + # Multiple inheritance with args class Base(Empty, RequiresArg, NoArg, arg=1): ... + + class Valid(Base, arg=1): ... + + class MissingArg(Base): ... # error: [missing-argument] + + class InvalidType(Base, arg="foo"): ... # error: [invalid-argument-type] ``` @@ -578,23 +645,30 @@ Keyword splats are allowed if their type can be determined: ```py from typing import TypedDict + class RequiresKwarg: def __init_subclass__(cls, arg: int): ... + class WrongArg(TypedDict): kwarg: int + class InvalidType(TypedDict): arg: str + wrong_arg: WrongArg = {"kwarg": 5} + # error: [missing-argument] # error: [unknown-argument] class MissingArg(RequiresKwarg, **wrong_arg): ... + invalid_type: InvalidType = {"arg": "foo"} + # error: [invalid-argument-type] class InvalidType(RequiresKwarg, **invalid_type): ... ``` @@ -604,19 +678,28 @@ So are generics: ```py from typing import Generic, TypeVar, Literal, overload + class Base[T]: def __init_subclass__(cls, arg: T): ... + class Valid(Base[int], arg=1): ... + + class InvalidType(Base[int], arg="x"): ... # error: [invalid-argument-type] + # Old generic syntax T = TypeVar("T") + class Base(Generic[T]): def __init_subclass__(cls, arg: T) -> None: ... + class Valid(Base[int], arg=1): ... + + class InvalidType(Base[int], arg="x"): ... # error: [invalid-argument-type] ``` @@ -630,8 +713,13 @@ class Base: def __init_subclass__(cls, mode: Literal["b"], arg: str) -> None: ... def __init_subclass__(cls, mode: str, arg: int | str) -> None: ... + class Valid(Base, mode="a", arg=5): ... + + class Valid(Base, mode="b", arg="foo"): ... + + class InvalidType(Base, mode="b", arg=5): ... # error: [no-matching-overload] ``` @@ -642,6 +730,7 @@ The `metaclass` keyword is ignored, as it has special meaning and is not passed class Base: def __init_subclass__(cls, arg: int): ... + class Valid(Base, arg=5, metaclass=object): ... ``` @@ -655,11 +744,13 @@ true whether it's accessed on the class or on an instance of the class. ```py from __future__ import annotations + class C: @staticmethod def f(x: int) -> str: return "a" + reveal_type(C.f) # revealed: def f(x: int) -> str reveal_type(C().f) # revealed: def f(x: int) -> str ``` @@ -686,6 +777,7 @@ When a static method is accessed on a derived class, it behaves identically: class Derived(C): pass + reveal_type(Derived.f) # revealed: def f(x: int) -> str reveal_type(Derived().f) # revealed: def f(x: int) -> str @@ -698,6 +790,7 @@ reveal_type(Derived().f(1)) # revealed: str ```py from inspect import getattr_static + class C: @staticmethod def f(): ... @@ -733,9 +826,11 @@ static method: ```py from __future__ import annotations + def does_nothing[T](f: T) -> T: return f + class C: @staticmethod @does_nothing @@ -747,6 +842,7 @@ class C: def f2(x: int) -> str: return "a" + reveal_type(C.f1(1)) # revealed: str reveal_type(C().f1(1)) # revealed: str reveal_type(C.f2(1)) # revealed: str @@ -760,6 +856,7 @@ bind `self`: from contextlib import contextmanager from collections.abc import Iterator + class D: @staticmethod @contextmanager @@ -771,6 +868,7 @@ class D: with self.ctx(10) as x: reveal_type(x) # revealed: int + # Accessing via class works reveal_type(D.ctx(5)) # revealed: _GeneratorContextManager[int, None, None] @@ -793,6 +891,7 @@ reveal_type(int.__new__) # revealed: Overload[[Self](cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self, [Self](cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self] reveal_type((42).__new__) + class X: def __init__(self, val: int): ... def make_another(self) -> Self: @@ -810,8 +909,10 @@ import types from typing import Callable from ty_extensions import static_assert, CallableTypeOf, is_assignable_to, TypeOf + def f(obj: type) -> None: ... + class MyClass: @property def my_property(self) -> int: @@ -820,6 +921,7 @@ class MyClass: @my_property.setter def my_property(self, value: int | str) -> None: ... + static_assert(is_assignable_to(types.FunctionType, Callable)) # revealed: @@ -871,6 +973,7 @@ static_assert(is_assignable_to(TypeOf[str.startswith], Callable)) reveal_type("foo".startswith) static_assert(is_assignable_to(TypeOf["foo".startswith], Callable)) + def _( a: CallableTypeOf[types.FunctionType.__get__], b: CallableTypeOf[f], diff --git a/crates/ty_python_semantic/resources/mdtest/call/never.md b/crates/ty_python_semantic/resources/mdtest/call/never.md index a8a149c48e..f3ebaf6f59 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/never.md +++ b/crates/ty_python_semantic/resources/mdtest/call/never.md @@ -5,6 +5,7 @@ The type `Never` is callable with an arbitrary set of arguments. The result is a ```py from typing_extensions import Never + def f(never: Never): reveal_type(never()) # revealed: Never reveal_type(never(1)) # revealed: Never diff --git a/crates/ty_python_semantic/resources/mdtest/call/open.md b/crates/ty_python_semantic/resources/mdtest/call/open.md index b6cdc125ab..fe2d7a93c5 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/open.md +++ b/crates/ty_python_semantic/resources/mdtest/call/open.md @@ -15,6 +15,7 @@ reveal_type(open("", "rb")) # revealed: BufferedReader[_BufferedReaderStream] with open("foo.pickle", "rb") as f: x = pickle.load(f) # fine + def _(mode: str): reveal_type(open("", mode)) # revealed: IO[Any] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index 72672b31ed..e1652ba94d 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -174,6 +174,7 @@ def f(x: C) -> C: ... ```py from overloaded import A, B, C, f + def _(ab: A | B, ac: A | C, bc: B | C): reveal_type(f(ab)) # revealed: A | B reveal_type(f(*(ab,))) # revealed: A | B @@ -213,6 +214,7 @@ def f(x: B, y: D) -> D: ... ```py from overloaded import A, B, C, D, f + def _(a_b: A | B): reveal_type(f(a_b, C())) # revealed: A | C reveal_type(f(*(a_b, C()))) # revealed: A | C @@ -220,6 +222,7 @@ def _(a_b: A | B): reveal_type(f(a_b, D())) # revealed: B | D reveal_type(f(*(a_b, D()))) # revealed: B | D + # But, if it doesn't, it should expand the second argument and try again: def _(a_b: A | B, c_d: C | D): reveal_type(f(a_b, c_d)) # revealed: A | B | C | D @@ -252,6 +255,7 @@ def f(x: B, y: D) -> D: ... ```py from overloaded import A, B, C, D, f + def _(a: A, bc: B | C, cd: C | D): # This also tests that partial matching works correctly as the argument type expansion results # in matching the first and second overloads, but not the third one. @@ -285,6 +289,7 @@ def f(x: _T) -> _T: ... ```py from overloaded import A, f + def _(x: int, y: A | int): reveal_type(f(x)) # revealed: int reveal_type(f(*(x,))) # revealed: int @@ -317,6 +322,7 @@ def f[T](x: T) -> T: ... ```py from overloaded import B, f + def _(x: int, y: B | int): reveal_type(f(x)) # revealed: int reveal_type(f(*(x,))) # revealed: int @@ -344,6 +350,7 @@ def f(x: Literal[False]) -> F: ... ```py from overloaded import f + def _(flag: bool): reveal_type(f(True)) # revealed: T reveal_type(f(*(True,))) # revealed: T @@ -380,6 +387,7 @@ def f(x: tuple[B, int], y: tuple[int, Literal[False]]) -> D: ... ```py from overloaded import A, B, f + def _(x: tuple[A | B, int], y: tuple[int, bool]): reveal_type(f(x, y)) # revealed: A | B | C | D reveal_type(f(*(x, y))) # revealed: A | B | C | D @@ -407,6 +415,7 @@ def f(x: type[B]) -> B: ... ```py from overloaded import A, B, f + def _(x: type[A | B]): reveal_type(x) # revealed: type[A] | type[B] reveal_type(f(x)) # revealed: A | B @@ -445,6 +454,7 @@ def f(x: Literal[SomeEnum.C]) -> C: ... from typing import Literal from overloaded import SomeEnum, A, B, C, f + def _(x: SomeEnum, y: Literal[SomeEnum.A, SomeEnum.C]): reveal_type(f(SomeEnum.A)) # revealed: A reveal_type(f(*(SomeEnum.A,))) # revealed: A @@ -498,6 +508,7 @@ reveal_type(f(b=0)) # revealed: OnlyBSpecified f(a=0, b=0) # error: [no-matching-overload] + def _(missing: Literal[Missing.Value], missing_or_present: Literal[Missing.Value] | int): reveal_type(f(a=missing, b=missing)) # revealed: BothMissing reveal_type(f(a=missing)) # revealed: BothMissing @@ -546,6 +557,7 @@ def f(x: MyEnumSubclass) -> MyEnumSubclass: ... ```py from overloaded import MyEnumSubclass, ActualEnum, f + def _(actual_enum: ActualEnum, my_enum_instance: MyEnumSubclass): reveal_type(f(actual_enum)) # revealed: Both reveal_type(f(*(actual_enum,))) # revealed: Both @@ -584,6 +596,7 @@ def f(x: B) -> B: ... ```py from overloaded import A, B, C, D, f + def _(ab: A | B, ac: A | C, cd: C | D): reveal_type(f(ab)) # revealed: A | B reveal_type(f(*(ab,))) # revealed: A | B @@ -644,6 +657,7 @@ class Foo: from overloaded import A, B, C, Foo, f from typing_extensions import Any, reveal_type + def _(ab: A | B, a: int | Any): reveal_type(f(a1=a, a2=a, a3=a)) # revealed: C reveal_type(f(A(), a1=a, a2=a, a3=a)) # revealed: A @@ -732,6 +746,7 @@ def _(ab: A | B, a: int | Any): ) ) + def _(foo: Foo, ab: A | B, a: int | Any): reveal_type(foo.f(a1=a, a2=a, a3=a)) # revealed: C reveal_type(foo.f(A(), a1=a, a2=a, a3=a)) # revealed: A @@ -843,6 +858,7 @@ def f(x: B, /, **kwargs: int) -> B: ... from overloaded import A, B, f from typing_extensions import reveal_type + def _(a: int | None): reveal_type( # error: [no-matching-overload] @@ -906,17 +922,20 @@ from overloaded import f # Test all of the above with a number of different splatted argument types + def _(t: tuple[int, str]) -> None: # This correctly produces an error because the first element of the union has a precise arity of # 2, which matches the first overload, but the second element of the tuple doesn't match the # second parameter type, yielding an `invalid-argument-type` error. f(*t) # error: [invalid-argument-type] + def _(t: tuple[int, str, int]) -> None: # This correctly produces no error because the first element of the union has a precise arity of # 3, which matches the second overload. f(*t) + def _(t: tuple[int, str] | tuple[int, str, int]) -> None: # This produces an error because the expansion produces two argument lists: `[*tuple[int, str]]` # and `[*tuple[int, str, int]]`. The first list produces produces a type checking error as @@ -954,6 +973,7 @@ def f(*args: int) -> int: ... ```py from overloaded import f + def _(x1: int, x2: int, args: list[int]): reveal_type(f(x1)) # revealed: tuple[int] reveal_type(f(x1, x2)) # revealed: tuple[int, int] @@ -986,6 +1006,7 @@ def f(x1: int, *args: int) -> tuple[int, ...]: ... ```py from overloaded import f + def _(x1: int, x2: int, args1: list[int], args2: tuple[int, *tuple[int, ...]]): reveal_type(f(x1, x2)) # revealed: tuple[int, int] reveal_type(f(*(x1, x2))) # revealed: tuple[int, int] @@ -1013,6 +1034,7 @@ def f(**kwargs: int) -> int: ... ```py from overloaded import f + def _(x1: int, x2: int, kwargs: dict[str, int]): reveal_type(f(x1=x1)) # revealed: int reveal_type(f(x1=x1, x2=x2)) # revealed: tuple[int, int] @@ -1044,10 +1066,12 @@ def f(**kwargs: int) -> tuple[int, ...]: ... from typing import TypedDict from overloaded import f + class Foo(TypedDict): x: int y: int + def _(foo: Foo, kwargs: dict[str, int]): reveal_type(f(**foo)) # revealed: tuple[int, int] reveal_type(f(**kwargs)) # revealed: tuple[int, ...] @@ -1089,6 +1113,7 @@ from overloaded import f reveal_type(f(1)) # revealed: str reveal_type(f(*(1,))) # revealed: str + def _(list_int: list[int], list_any: list[Any]): reveal_type(f(list_int)) # revealed: int reveal_type(f(*(list_int,))) # revealed: int @@ -1124,6 +1149,7 @@ from overloaded import f reveal_type(f(1)) # revealed: str reveal_type(f(*(1,))) # revealed: str + def _(list_int: list[int], list_any: list[Any]): # All materializations of `list[int]` are assignable to `list[int]`, so it matches the first # overload. @@ -1166,6 +1192,7 @@ reveal_type(f(*((1, "b"),))) # revealed: int reveal_type(f((1, 2))) # revealed: int reveal_type(f(*((1, 2),))) # revealed: int + def _(int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, Any]): # All materializations are assignable to first overload, so second and third overloads are # eliminated @@ -1206,6 +1233,7 @@ class Foo: from module import Foo from typing_extensions import LiteralString + def f(a: Foo, b: list[str], c: list[LiteralString], e): reveal_type(e) # revealed: Unknown reveal_type(a.join(b)) # revealed: str @@ -1245,6 +1273,7 @@ from typing import Any from overloaded import A, f + def _(list_int: list[int], list_any: list[Any], int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, Any]): # All materializations of both argument types are assignable to the first overload, so the # second and third overloads are filtered out @@ -1293,6 +1322,7 @@ from typing_extensions import LiteralString from overloaded import f + def _(literal: LiteralString, string: str, any: Any): reveal_type(f(literal)) # revealed: LiteralString reveal_type(f(*(literal,))) # revealed: LiteralString @@ -1331,6 +1361,7 @@ from typing import Any from overloaded import f + def _(list_int: list[int], list_str: list[str], list_any: list[Any], any: Any): reveal_type(f(list_int)) # revealed: A reveal_type(f(*(list_int,))) # revealed: A @@ -1365,6 +1396,7 @@ from typing import Any from overloaded import f + def _(integer: int, string: str, any: Any, list_any: list[Any]): reveal_type(f(integer, string)) # revealed: int reveal_type(f(*(integer, string))) # revealed: int @@ -1407,11 +1439,13 @@ from typing import Any from overloaded import A, B + def _(a_int: A[int], a_str: A[str], a_any: A[Any]): reveal_type(a_int.method()) # revealed: int reveal_type(a_str.method()) # revealed: int reveal_type(a_any.method()) # revealed: int + def _(b_int: B[int], b_str: B[str], b_any: B[Any]): reveal_type(b_int.method()) # revealed: int reveal_type(b_str.method()) # revealed: str @@ -1458,6 +1492,7 @@ from typing import Any from overloaded import f1, f2, f3, f4 + def _(arg: list[Any]): # Matches both overload and the return types are equivalent reveal_type(f1(*arg)) # revealed: A @@ -1520,10 +1555,12 @@ reveal_type(f1(1)) # revealed: tuple[Literal[1]] reveal_type(f1(1, 2)) # revealed: tuple[Literal[1], Literal[2]] reveal_type(f1(1, 2, 3)) # revealed: tuple[Literal[1], Literal[2], Literal[3]] + def _(args1: list[int], args2: list[Any]): reveal_type(f1(*args1)) # revealed: tuple[Any, ...] reveal_type(f1(*args2)) # revealed: tuple[Any, ...] + reveal_type(f2()) # revealed: tuple[Any, ...] reveal_type(f2(1, 2)) # revealed: tuple[Literal[1], Literal[2]] # TODO: Should be `tuple[Literal[1], Literal[2]]` @@ -1574,6 +1611,7 @@ from typing import Any from overloaded import f + def _(any: Any): reveal_type(f(any, flag=True)) # revealed: int reveal_type(f(*(any,), flag=True)) # revealed: int @@ -1602,6 +1640,7 @@ from typing import Any, Literal from overloaded import f + def _(any: Any): reveal_type(f(any, flag=True)) # revealed: int reveal_type(f(*(any,), flag=True)) # revealed: int @@ -1609,9 +1648,11 @@ def _(any: Any): reveal_type(f(any, flag=False)) # revealed: str reveal_type(f(*(any,), flag=False)) # revealed: str + def _(args: tuple[Any, Literal[True]]): reveal_type(f(*args)) # revealed: int + def _(args: tuple[Any, Literal[False]]): reveal_type(f(*args)) # revealed: str ``` @@ -1656,6 +1697,7 @@ from typing import Any from overloaded import A, B, f + def _(arg: tuple[A | B, Any]): reveal_type(f(arg)) # revealed: A | B reveal_type(f(*(arg,))) # revealed: A | B @@ -1691,6 +1733,7 @@ from typing import Any from overloaded import A, B, C, f + def _(arg: tuple[A | B, Any]): reveal_type(f(arg)) # revealed: A | Unknown reveal_type(f(*(arg,))) # revealed: A | Unknown @@ -1725,6 +1768,7 @@ from typing import Any from overloaded import A, B, C, f + def _(arg: tuple[A | B, Any]): reveal_type(f(arg)) # revealed: Unknown reveal_type(f(*(arg,))) # revealed: Unknown @@ -1742,9 +1786,11 @@ Type inference accounts for parameter type annotations across all overloads. ```py from typing import TypedDict, overload + class T(TypedDict): x: int + @overload def f(a: list[T], b: int) -> int: ... @overload @@ -1752,9 +1798,11 @@ def f(a: list[dict[str, int]], b: str) -> str: ... def f(a: list[dict[str, int]] | list[T], b: int | str) -> int | str: return 1 + def int_or_str() -> int | str: return 1 + x = f([{"x": 1}], int_or_str()) reveal_type(x) # revealed: int | str @@ -1767,9 +1815,11 @@ Non-matching overloads do not produce diagnostics: ```py from typing import TypedDict, overload + class T(TypedDict): x: int + @overload def f(a: T, b: int) -> int: ... @overload @@ -1777,6 +1827,7 @@ def f(a: dict[str, int], b: str) -> str: ... def f(a: T | dict[str, int], b: int | str) -> int | str: return 1 + x = f({"y": 1}, "a") reveal_type(x) # revealed: str ``` @@ -1784,11 +1835,13 @@ reveal_type(x) # revealed: str ```py from typing import SupportsRound, overload + @overload def takes_str_or_float(x: str): ... @overload def takes_str_or_float(x: float): ... def takes_str_or_float(x: float | str): ... + takes_str_or_float(round(1.0)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/replace.md b/crates/ty_python_semantic/resources/mdtest/call/replace.md index 43e59bdf79..e78d0c38f1 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/replace.md +++ b/crates/ty_python_semantic/resources/mdtest/call/replace.md @@ -31,11 +31,13 @@ Dataclasses support the `__replace__` protocol: from dataclasses import dataclass from copy import replace + @dataclass class Point: x: int y: int + reveal_type(Point.__replace__) # revealed: (self: Point, *, x: int = ..., y: int = ...) -> Point ``` @@ -80,10 +82,12 @@ NamedTuples also support the `__replace__` protocol: from typing import NamedTuple from copy import replace + class Point(NamedTuple): x: int y: int + reveal_type(Point.__replace__) # revealed: (self: Self, *, x: int = ..., y: int = ...) -> Self ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/str_startswith.md b/crates/ty_python_semantic/resources/mdtest/call/str_startswith.md index 581c5598d6..db405a34ed 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/str_startswith.md +++ b/crates/ty_python_semantic/resources/mdtest/call/str_startswith.md @@ -39,6 +39,7 @@ And similiarly, we should still infer `bool` if the instance or the prefix are n ```py from typing_extensions import LiteralString + def _(string_instance: str, literalstring: LiteralString): reveal_type(string_instance.startswith("a")) # revealed: bool reveal_type(literalstring.startswith("a")) # revealed: bool diff --git a/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md b/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md index 544c4c7c90..264d53a811 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md +++ b/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md @@ -7,6 +7,7 @@ ```py class C: ... + def _(subclass_of_c: type[C]): reveal_type(subclass_of_c()) # revealed: C ``` @@ -17,6 +18,7 @@ def _(subclass_of_c: type[C]): class C: def __init__(self, x: int): ... + def _(subclass_of_c: type[C]): reveal_type(subclass_of_c(1)) # revealed: C @@ -34,6 +36,7 @@ def _(subclass_of_c: type[C]): from typing import Any from ty_extensions import Unknown + def _(subclass_of_any: type[Any], subclass_of_unknown: type[Unknown]): reveal_type(subclass_of_any()) # revealed: Any reveal_type(subclass_of_any("any", "args", 1, 2)) # revealed: Any @@ -45,8 +48,11 @@ def _(subclass_of_any: type[Any], subclass_of_unknown: type[Unknown]): ```py class A: ... + + class B: ... + def _(subclass_of_ab: type[A | B]): reveal_type(subclass_of_ab()) # revealed: A | B ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 609fff6c4d..a74539e52d 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -17,8 +17,11 @@ from the first argument: ```py class Base: ... + + class Mixin: ... + # We synthesize a class type using the name argument Foo = type("Foo", (), {}) reveal_type(Foo) # revealed: @@ -44,9 +47,11 @@ name = "IndirectClass" IndirectClass = type(name, (), {}) reveal_type(IndirectClass) # revealed: + # Works with base classes too class Base: ... + base_name = "DerivedClass" DerivedClass = type(base_name, (Base,), {}) reveal_type(DerivedClass) # revealed: @@ -59,8 +64,10 @@ Each `type()` call produces a distinct class type, even if they have the same na ```py from ty_extensions import static_assert, is_equivalent_to + class Base: ... + Foo1 = type("Foo", (Base,), {}) Foo2 = type("Foo", (Base,), {}) @@ -71,9 +78,11 @@ static_assert(not is_equivalent_to(Foo1, Foo2)) foo1 = Foo1() foo2 = Foo2() + def takes_foo1(x: Foo1) -> None: ... def takes_foo2(x: Foo2) -> None: ... + takes_foo1(foo1) # OK takes_foo2(foo2) # OK @@ -95,9 +104,11 @@ class Base: def base_method(self) -> str: return "hello" + class Mixin: mixin_attr: str = "mixin" + Foo = type("Foo", (Base,), {}) foo = Foo() @@ -120,6 +131,7 @@ Attributes from the namespace dict (third argument) are tracked: ```py class Base: ... + Foo = type("Foo", (Base,), {"custom_attr": 42}) # Class attribute access @@ -136,8 +148,10 @@ When the namespace dict is not a literal (e.g., passed as a parameter), attribut ```py from typing import Any + class DynamicBase: ... + def f(attributes: dict[str, Any]): X = type("X", (DynamicBase,), attributes) @@ -156,6 +170,7 @@ keys), static attributes have precise types while unknown attributes return `Unk ```py from typing import Any + def f(extra_attrs: dict[str, Any], y: str): X = type("X", (), {"a": 42, **extra_attrs}) @@ -177,9 +192,11 @@ arbitrary additional string keys), unknown attributes return `Unknown`: ```py from typing import TypedDict + class Namespace(TypedDict): z: int + def g(attributes: Namespace): Y = type("Y", (), attributes) @@ -205,10 +222,12 @@ emit an error instead of returning `Unknown`. ```py from typing import TypedDict + class ClosedNamespace(TypedDict, closed=True): x: int y: str + def h(ns: ClosedNamespace): X = type("X", (), ns) @@ -235,11 +254,14 @@ Regular classes can inherit from dynamic classes: class Base: base_attr: int = 1 + DynamicClass = type("DynamicClass", (Base,), {}) + class Child(DynamicClass): child_attr: str = "child" + child = Child() # Attributes from the dynamic class's base are accessible @@ -248,19 +270,24 @@ reveal_type(child.base_attr) # revealed: int # The child class's own attributes are accessible reveal_type(child.child_attr) # revealed: str + # Child instances are subtypes of DynamicClass instances def takes_dynamic(x: DynamicClass) -> None: ... + takes_dynamic(child) # No error - Child is a subtype of DynamicClass + # isinstance narrows to the dynamic class instance type def check_isinstance(x: object) -> None: if isinstance(x, DynamicClass): reveal_type(x) # revealed: DynamicClass + # Dynamic class inheriting from int narrows correctly with isinstance IntSubclass = type("IntSubclass", (int,), {}) + def check_int_subclass(x: IntSubclass | str) -> None: if isinstance(x, int): # IntSubclass inherits from int, so it's included in the narrowed type @@ -277,8 +304,10 @@ from both): ```py class Base: ... + Foo = type("Foo", (Base,), {}) + def check_disjointness(x: Foo | int) -> None: if isinstance(x, int): reveal_type(x) # revealed: int @@ -299,9 +328,11 @@ StrClass = type("StrClass", (str,), {}) static_assert(is_disjoint_from(type[IntClass], type[StrClass])) static_assert(is_disjoint_from(type[StrClass], type[IntClass])) + # Dynamic classes that share a common base are not disjoint. class Base: ... + Foo = type("Foo", (Base,), {}) Bar = type("Bar", (Base,), {}) @@ -317,6 +348,7 @@ class Base: def method(self) -> int: return 42 + DynamicChild = type("DynamicChild", (Base,), {}) # Using dynamic class as pivot with dynamic class instance owner @@ -324,10 +356,12 @@ fc = DynamicChild() reveal_type(super(DynamicChild, fc)) # revealed: , DynamicChild> reveal_type(super(DynamicChild, fc).method()) # revealed: int + # Regular class inheriting from dynamic class class RegularChild(DynamicChild): pass + rc = RegularChild() reveal_type(super(RegularChild, rc)) # revealed: , RegularChild> reveal_type(super(RegularChild, rc).method()) # revealed: int @@ -345,6 +379,7 @@ Dynamic classes can inherit from other dynamic classes: class Base: base_attr: int = 1 + # Create a dynamic class that inherits from a regular class. Parent = type("Parent", (Base,), {}) reveal_type(Parent) # revealed: @@ -358,9 +393,11 @@ child = ChildCls() reveal_type(child) # revealed: ChildCls reveal_type(child.base_attr) # revealed: int + # Child instances are subtypes of `Parent` instances. def takes_parent(x: Parent) -> None: ... + takes_parent(child) # No error - `ChildCls` is a subtype of `Parent` ``` @@ -373,12 +410,14 @@ dataclass-like and have the synthesized `__dataclass_fields__` attribute: from dataclasses import Field from typing_extensions import dataclass_transform + @dataclass_transform() class DataclassBase: """Base class decorated with @dataclass_transform().""" pass + # A dynamic class inheriting from a dataclass_transform base DynamicModel = type("DynamicModel", (DataclassBase,), {}) @@ -408,9 +447,11 @@ from typing import Generic, TypeVar T = TypeVar("T") + class Container(Generic[T]): value: T + # Dynamic class inheriting from a generic class specialization IntContainer = type("IntContainer", (Container[int],), {}) reveal_type(IntContainer) # revealed: @@ -427,6 +468,7 @@ reveal_type(container.value) # revealed: int ```py class Base: ... + Foo = type("Foo", (Base,), {}) foo = Foo() @@ -439,6 +481,7 @@ reveal_type(type(foo)) # revealed: type[Foo] ```py class Base: ... + Foo = type("Foo", (Base,), {}) foo = Foo() @@ -451,6 +494,7 @@ reveal_type(foo.__class__) # revealed: type[Foo] ```py class StaticClass: ... + DynamicClass = type("DynamicClass", (), {}) # Both static and dynamic classes have `type` as their metaclass @@ -465,12 +509,15 @@ Dynamic instances are subtypes of `object`: ```py class Base: ... + Foo = type("Foo", (Base,), {}) foo = Foo() + # All dynamic instances are subtypes of object def takes_object(x: object) -> None: ... + takes_object(foo) # No error - Foo is a subtype of object # Even dynamic classes with no explicit bases are subtypes of object @@ -522,6 +569,7 @@ produces slightly different error messages than assigned dynamic class creation: ```py class Base: ... + # error: 6 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`" type(b"Foo", (), {}) @@ -544,9 +592,11 @@ diagnostic about the unsupported base, rather than cascading errors: ```py from ty_extensions import reveal_mro + class Base: base_attr: int = 1 + def f(x: type[Base]): # error: [unsupported-dynamic-base] "Unsupported class base" Child = type("Child", (x,), {}) @@ -568,6 +618,7 @@ MRO errors are detected and reported: ```py class A: ... + # Duplicate bases are detected # error: [duplicate-base] "Duplicate base class in class `Dup`" Dup = type("Dup", (A, A), {}) @@ -586,14 +637,22 @@ X = type("X", (Bar, Baz), {}) ```py class A: ... + + class B(A): ... + + class C(A): ... + # This creates an inconsistent MRO because D would need B before C (from first base) # but also C before B (from second base inheritance through A) class X(B, C): ... + + class Y(C, B): ... + # error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Conflict` with bases `[, ]`" Conflict = type("Conflict", (X, Y), {}) ``` @@ -604,10 +663,17 @@ Metaclass conflicts are detected and reported: ```py class Meta1(type): ... + + class Meta2(type): ... + + class A(metaclass=Meta1): ... + + class B(metaclass=Meta2): ... + # error: [conflicting-metaclass] "The metaclass of a derived class (`Bad`) must be a subclass of the metaclasses of all its bases, but `Meta1` (metaclass of base class ``) and `Meta2` (metaclass of base class ``) have no subclass relationship" Bad = type("Bad", (A, B), {}) ``` @@ -640,8 +706,10 @@ Dynamic classes with non-empty `__slots__` cannot coexist with other disjoint ba class RegularSlotted: __slots__ = ("a",) + DynSlotted = type("DynSlotted", (), {"__slots__": ("b",)}) + # error: [instance-layout-conflict] class Conflict( RegularSlotted, @@ -655,6 +723,7 @@ Two dynamic classes with non-empty `__slots__` also conflict: A = type("A", (), {"__slots__": ("x",)}) B = type("B", (), {"__slots__": ("y",)}) + # error: [instance-layout-conflict] class Conflict( A, @@ -668,12 +737,15 @@ with disjoint bases: ```py from typing import Any + class DisjointBase1: __slots__ = ("a",) + class DisjointBase2: __slots__ = ("b",) + def f(ns: dict[str, Any]): cls1 = type("cls1", (DisjointBase1,), ns) cls2 = type("cls2", (DisjointBase2,), ns) @@ -691,9 +763,11 @@ defined, so no diagnostic is emitted: ```py from typing import Any + class SlottedBase: __slots__ = ("a",) + def f(ns: dict[str, Any]): # The namespace might or might not contain __slots__, so no error is emitted Dynamic = type("Dynamic", (), ns) @@ -712,9 +786,11 @@ When the bases are a tuple literal, the diagnostic includes annotations for each class A: __slots__ = ("x",) + class B: __slots__ = ("y",) + # error: [instance-layout-conflict] X = type("X", (A, B), {}) ``` @@ -726,9 +802,11 @@ per-base annotations: class C: __slots__ = ("x",) + class D: __slots__ = ("y",) + bases: tuple[type[C], type[D]] = (C, D) # error: [instance-layout-conflict] Y = type("Y", bases, {}) @@ -756,6 +834,7 @@ def make_class(name: str): reveal_type(cls) # revealed: '> return cls + def make_classes(name1: str, name2: str): cls1 = type(name1, (), {}) cls2 = type(name2, (), {}) @@ -788,9 +867,13 @@ any attribute access returns `Unknown`: ```py from ty_extensions import reveal_mro + class Base1: ... + + class Base2: ... + def make_class(bases: tuple[type, ...]): # Class literal is created with Unknown base in MRO cls = type("Cls", bases, {}) @@ -813,6 +896,7 @@ classes: class Base: attr: int = 1 + bases = (Base,) Cls = type("Cls", bases, {}) reveal_type(Cls) # revealed: @@ -828,8 +912,10 @@ Unpacking arguments with `*args` or `**kwargs`: ```py from ty_extensions import reveal_mro + class Base: ... + # Unpacking a tuple for bases bases_tuple = (Base,) Cls1 = type("Cls1", (*bases_tuple,), {}) @@ -883,6 +969,7 @@ This will be fixed when we support all `type()` calls (including inline) via gen ```py class Base: ... + # TODO: Should infer `` instead of `type` T: type = type("T", (), {}) reveal_type(T) # revealed: type @@ -905,9 +992,11 @@ synthesized: from typing import Protocol from ty_extensions import reveal_mro + class MyProtocol(Protocol): def method(self) -> int: ... + ProtoImpl = type("ProtoImpl", (MyProtocol,), {}) reveal_type(ProtoImpl) # revealed: reveal_mro(ProtoImpl) # revealed: (, , typing.Protocol, typing.Generic, ) @@ -923,10 +1012,12 @@ reveal_type(instance) # revealed: ProtoImpl from typing_extensions import TypedDict from ty_extensions import reveal_mro + class MyDict(TypedDict): name: str age: int + DictSubclass = type("DictSubclass", (MyDict,), {}) reveal_type(DictSubclass) # revealed: reveal_mro(DictSubclass) # revealed: (, , typing.TypedDict, ) @@ -939,10 +1030,12 @@ reveal_mro(DictSubclass) # revealed: (, , from typing import NamedTuple from ty_extensions import reveal_mro + class Point(NamedTuple): x: int y: int + Point3D = type("Point3D", (Point,), {}) reveal_type(Point3D) # revealed: # fmt: off @@ -957,13 +1050,16 @@ reveal_mro(Point3D) # revealed: (, , @@ -985,10 +1081,12 @@ class Base: super().__init_subclass__(**kwargs) cls.config = required_arg + # Regular class definition - this works and passes the argument class Child(Base, required_arg="value"): pass + # The dynamically assigned attribute has Unknown in its type reveal_type(Child.config) # revealed: Unknown | str @@ -1025,8 +1123,10 @@ When a base class has a custom metaclass, the dynamic class inherits that metacl class MyMeta(type): custom_attr: str = "meta" + class Base(metaclass=MyMeta): ... + # Dynamic class inherits the metaclass from Base Dynamic = type("Dynamic", (Base,), {}) reveal_type(Dynamic) # revealed: diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index c944a49cb7..5a4207fc5a 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -5,11 +5,15 @@ ```py def _(flag: bool): if flag: + def f() -> int: return 1 + else: + def f() -> str: return "foo" + reveal_type(f()) # revealed: int | str ``` @@ -18,13 +22,17 @@ def _(flag: bool): ```py from nonexistent import f # error: [unresolved-import] "Cannot resolve imported module `nonexistent`" + def coinflip() -> bool: return True + if coinflip(): + def f() -> int: return 1 + reveal_type(f()) # revealed: Unknown | int ``` @@ -37,8 +45,10 @@ def _(flag: bool): if flag: f = 1 else: + def f() -> int: return 1 + x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable" reveal_type(x) # revealed: Unknown | int ``` @@ -54,8 +64,10 @@ def _(flag: bool, flag2: bool): elif flag2: f = "foo" else: + def f() -> int: return 1 + # error: [call-non-callable] "Object of type `Literal[1]` is not callable" # error: [call-non-callable] "Object of type `Literal["foo"]` is not callable" # revealed: Unknown | int @@ -85,9 +97,11 @@ Calling a union where the arguments don't match the signature of all variants. def f1(a: int) -> int: return a + def f2(a: str) -> str: return a + def _(flag: bool): if flag: f = f1 @@ -136,6 +150,7 @@ def _(flag: bool): ```py class C: ... + def f1(): ... def _(flag: bool): if flag: @@ -178,6 +193,7 @@ static_assert(is_subtype_of(Not[Literal[""]], Literal["a", ""] | Not[Literal[""] static_assert(is_subtype_of(Literal["a", ""], Not[Literal[""]] | Literal["a", ""])) static_assert(is_subtype_of(Not[Literal[""]], Not[Literal[""]] | Literal["a", ""])) + def _( a: Literal["a", ""] | Not[AlwaysFalsy], b: Literal["a", ""] | Not[Literal[""]], @@ -205,6 +221,7 @@ def _( ```py from ty_extensions import is_singleton + def _(flag: bool): if flag: f = repr @@ -222,6 +239,7 @@ Beyond a certain size, large unions of literal types collapse to their nearest s ```py from typing import Literal + def _(literals_2: Literal[0, 1], b: bool, flag: bool): literals_4 = 2 * literals_2 + literals_2 # Literal[0, 1, 2, 3] literals_16 = 4 * literals_4 + literals_4 # Literal[0, 1, .., 15] @@ -255,8 +273,10 @@ class RecursiveAttr: def update(self): self.i = self.i + 1 + reveal_type(RecursiveAttr().i) # revealed: Unknown | int + # Here are some recursive but saturating examples. Because it's difficult to statically determine whether literal unions saturate or diverge, # we widen them early, even though they may actually be convergent. class RecursiveAttr2: @@ -266,8 +286,10 @@ class RecursiveAttr2: def update(self): self.i = (self.i + 1) % 9 + reveal_type(RecursiveAttr2().i) # revealed: Unknown | Literal[0, 1, 2, 3, 4, 5, 6, 7, 8] + class RecursiveAttr3: def __init__(self): self.i = 0 @@ -275,6 +297,7 @@ class RecursiveAttr3: def update(self): self.i = (self.i + 1) % 10 + # Going beyond the MAX_RECURSIVE_UNION_LITERALS limit: reveal_type(RecursiveAttr3().i) # revealed: Unknown | int ``` @@ -287,6 +310,7 @@ enums: from enum import Enum from ty_extensions import Intersection, Not + class Huge(Enum): OPTION0 = "0" OPTION1 = "1" @@ -789,6 +813,7 @@ class Huge(Enum): OPTION498 = "498" OPTION499 = "499" + def f(x: Intersection[Huge, Not[Literal[Huge.OPTION499]]]): # revealed: Literal[Huge.OPTION0, Huge.OPTION1, Huge.OPTION2, Huge.OPTION3, Huge.OPTION4, Huge.OPTION5, Huge.OPTION6, Huge.OPTION7, Huge.OPTION8, Huge.OPTION9, Huge.OPTION10, Huge.OPTION11, Huge.OPTION12, Huge.OPTION13, Huge.OPTION14, Huge.OPTION15, Huge.OPTION16, Huge.OPTION17, Huge.OPTION18, Huge.OPTION19, Huge.OPTION20, Huge.OPTION21, Huge.OPTION22, Huge.OPTION23, Huge.OPTION24, Huge.OPTION25, Huge.OPTION26, Huge.OPTION27, Huge.OPTION28, Huge.OPTION29, Huge.OPTION30, Huge.OPTION31, Huge.OPTION32, Huge.OPTION33, Huge.OPTION34, Huge.OPTION35, Huge.OPTION36, Huge.OPTION37, Huge.OPTION38, Huge.OPTION39, Huge.OPTION40, Huge.OPTION41, Huge.OPTION42, Huge.OPTION43, Huge.OPTION44, Huge.OPTION45, Huge.OPTION46, Huge.OPTION47, Huge.OPTION48, Huge.OPTION49, Huge.OPTION50, Huge.OPTION51, Huge.OPTION52, Huge.OPTION53, Huge.OPTION54, Huge.OPTION55, Huge.OPTION56, Huge.OPTION57, Huge.OPTION58, Huge.OPTION59, Huge.OPTION60, Huge.OPTION61, Huge.OPTION62, Huge.OPTION63, Huge.OPTION64, Huge.OPTION65, Huge.OPTION66, Huge.OPTION67, Huge.OPTION68, Huge.OPTION69, Huge.OPTION70, Huge.OPTION71, Huge.OPTION72, Huge.OPTION73, Huge.OPTION74, Huge.OPTION75, Huge.OPTION76, Huge.OPTION77, Huge.OPTION78, Huge.OPTION79, Huge.OPTION80, Huge.OPTION81, Huge.OPTION82, Huge.OPTION83, Huge.OPTION84, Huge.OPTION85, Huge.OPTION86, Huge.OPTION87, Huge.OPTION88, Huge.OPTION89, Huge.OPTION90, Huge.OPTION91, Huge.OPTION92, Huge.OPTION93, Huge.OPTION94, Huge.OPTION95, Huge.OPTION96, Huge.OPTION97, Huge.OPTION98, Huge.OPTION99, Huge.OPTION100, Huge.OPTION101, Huge.OPTION102, Huge.OPTION103, Huge.OPTION104, Huge.OPTION105, Huge.OPTION106, Huge.OPTION107, Huge.OPTION108, Huge.OPTION109, Huge.OPTION110, Huge.OPTION111, Huge.OPTION112, Huge.OPTION113, Huge.OPTION114, Huge.OPTION115, Huge.OPTION116, Huge.OPTION117, Huge.OPTION118, Huge.OPTION119, Huge.OPTION120, Huge.OPTION121, Huge.OPTION122, Huge.OPTION123, Huge.OPTION124, Huge.OPTION125, Huge.OPTION126, Huge.OPTION127, Huge.OPTION128, Huge.OPTION129, Huge.OPTION130, Huge.OPTION131, Huge.OPTION132, Huge.OPTION133, Huge.OPTION134, Huge.OPTION135, Huge.OPTION136, Huge.OPTION137, Huge.OPTION138, Huge.OPTION139, Huge.OPTION140, Huge.OPTION141, Huge.OPTION142, Huge.OPTION143, Huge.OPTION144, Huge.OPTION145, Huge.OPTION146, Huge.OPTION147, Huge.OPTION148, Huge.OPTION149, Huge.OPTION150, Huge.OPTION151, Huge.OPTION152, Huge.OPTION153, Huge.OPTION154, Huge.OPTION155, Huge.OPTION156, Huge.OPTION157, Huge.OPTION158, Huge.OPTION159, Huge.OPTION160, Huge.OPTION161, Huge.OPTION162, Huge.OPTION163, Huge.OPTION164, Huge.OPTION165, Huge.OPTION166, Huge.OPTION167, Huge.OPTION168, Huge.OPTION169, Huge.OPTION170, Huge.OPTION171, Huge.OPTION172, Huge.OPTION173, Huge.OPTION174, Huge.OPTION175, Huge.OPTION176, Huge.OPTION177, Huge.OPTION178, Huge.OPTION179, Huge.OPTION180, Huge.OPTION181, Huge.OPTION182, Huge.OPTION183, Huge.OPTION184, Huge.OPTION185, Huge.OPTION186, Huge.OPTION187, Huge.OPTION188, Huge.OPTION189, Huge.OPTION190, Huge.OPTION191, Huge.OPTION192, Huge.OPTION193, Huge.OPTION194, Huge.OPTION195, Huge.OPTION196, Huge.OPTION197, Huge.OPTION198, Huge.OPTION199, Huge.OPTION200, Huge.OPTION201, Huge.OPTION202, Huge.OPTION203, Huge.OPTION204, Huge.OPTION205, Huge.OPTION206, Huge.OPTION207, Huge.OPTION208, Huge.OPTION209, Huge.OPTION210, Huge.OPTION211, Huge.OPTION212, Huge.OPTION213, Huge.OPTION214, Huge.OPTION215, Huge.OPTION216, Huge.OPTION217, Huge.OPTION218, Huge.OPTION219, Huge.OPTION220, Huge.OPTION221, Huge.OPTION222, Huge.OPTION223, Huge.OPTION224, Huge.OPTION225, Huge.OPTION226, Huge.OPTION227, Huge.OPTION228, Huge.OPTION229, Huge.OPTION230, Huge.OPTION231, Huge.OPTION232, Huge.OPTION233, Huge.OPTION234, Huge.OPTION235, Huge.OPTION236, Huge.OPTION237, Huge.OPTION238, Huge.OPTION239, Huge.OPTION240, Huge.OPTION241, Huge.OPTION242, Huge.OPTION243, Huge.OPTION244, Huge.OPTION245, Huge.OPTION246, Huge.OPTION247, Huge.OPTION248, Huge.OPTION249, Huge.OPTION250, Huge.OPTION251, Huge.OPTION252, Huge.OPTION253, Huge.OPTION254, Huge.OPTION255, Huge.OPTION256, Huge.OPTION257, Huge.OPTION258, Huge.OPTION259, Huge.OPTION260, Huge.OPTION261, Huge.OPTION262, Huge.OPTION263, Huge.OPTION264, Huge.OPTION265, Huge.OPTION266, Huge.OPTION267, Huge.OPTION268, Huge.OPTION269, Huge.OPTION270, Huge.OPTION271, Huge.OPTION272, Huge.OPTION273, Huge.OPTION274, Huge.OPTION275, Huge.OPTION276, Huge.OPTION277, Huge.OPTION278, Huge.OPTION279, Huge.OPTION280, Huge.OPTION281, Huge.OPTION282, Huge.OPTION283, Huge.OPTION284, Huge.OPTION285, Huge.OPTION286, Huge.OPTION287, Huge.OPTION288, Huge.OPTION289, Huge.OPTION290, Huge.OPTION291, Huge.OPTION292, Huge.OPTION293, Huge.OPTION294, Huge.OPTION295, Huge.OPTION296, Huge.OPTION297, Huge.OPTION298, Huge.OPTION299, Huge.OPTION300, Huge.OPTION301, Huge.OPTION302, Huge.OPTION303, Huge.OPTION304, Huge.OPTION305, Huge.OPTION306, Huge.OPTION307, Huge.OPTION308, Huge.OPTION309, Huge.OPTION310, Huge.OPTION311, Huge.OPTION312, Huge.OPTION313, Huge.OPTION314, Huge.OPTION315, Huge.OPTION316, Huge.OPTION317, Huge.OPTION318, Huge.OPTION319, Huge.OPTION320, Huge.OPTION321, Huge.OPTION322, Huge.OPTION323, Huge.OPTION324, Huge.OPTION325, Huge.OPTION326, Huge.OPTION327, Huge.OPTION328, Huge.OPTION329, Huge.OPTION330, Huge.OPTION331, Huge.OPTION332, Huge.OPTION333, Huge.OPTION334, Huge.OPTION335, Huge.OPTION336, Huge.OPTION337, Huge.OPTION338, Huge.OPTION339, Huge.OPTION340, Huge.OPTION341, Huge.OPTION342, Huge.OPTION343, Huge.OPTION344, Huge.OPTION345, Huge.OPTION346, Huge.OPTION347, Huge.OPTION348, Huge.OPTION349, Huge.OPTION350, Huge.OPTION351, Huge.OPTION352, Huge.OPTION353, Huge.OPTION354, Huge.OPTION355, Huge.OPTION356, Huge.OPTION357, Huge.OPTION358, Huge.OPTION359, Huge.OPTION360, Huge.OPTION361, Huge.OPTION362, Huge.OPTION363, Huge.OPTION364, Huge.OPTION365, Huge.OPTION366, Huge.OPTION367, Huge.OPTION368, Huge.OPTION369, Huge.OPTION370, Huge.OPTION371, Huge.OPTION372, Huge.OPTION373, Huge.OPTION374, Huge.OPTION375, Huge.OPTION376, Huge.OPTION377, Huge.OPTION378, Huge.OPTION379, Huge.OPTION380, Huge.OPTION381, Huge.OPTION382, Huge.OPTION383, Huge.OPTION384, Huge.OPTION385, Huge.OPTION386, Huge.OPTION387, Huge.OPTION388, Huge.OPTION389, Huge.OPTION390, Huge.OPTION391, Huge.OPTION392, Huge.OPTION393, Huge.OPTION394, Huge.OPTION395, Huge.OPTION396, Huge.OPTION397, Huge.OPTION398, Huge.OPTION399, Huge.OPTION400, Huge.OPTION401, Huge.OPTION402, Huge.OPTION403, Huge.OPTION404, Huge.OPTION405, Huge.OPTION406, Huge.OPTION407, Huge.OPTION408, Huge.OPTION409, Huge.OPTION410, Huge.OPTION411, Huge.OPTION412, Huge.OPTION413, Huge.OPTION414, Huge.OPTION415, Huge.OPTION416, Huge.OPTION417, Huge.OPTION418, Huge.OPTION419, Huge.OPTION420, Huge.OPTION421, Huge.OPTION422, Huge.OPTION423, Huge.OPTION424, Huge.OPTION425, Huge.OPTION426, Huge.OPTION427, Huge.OPTION428, Huge.OPTION429, Huge.OPTION430, Huge.OPTION431, Huge.OPTION432, Huge.OPTION433, Huge.OPTION434, Huge.OPTION435, Huge.OPTION436, Huge.OPTION437, Huge.OPTION438, Huge.OPTION439, Huge.OPTION440, Huge.OPTION441, Huge.OPTION442, Huge.OPTION443, Huge.OPTION444, Huge.OPTION445, Huge.OPTION446, Huge.OPTION447, Huge.OPTION448, Huge.OPTION449, Huge.OPTION450, Huge.OPTION451, Huge.OPTION452, Huge.OPTION453, Huge.OPTION454, Huge.OPTION455, Huge.OPTION456, Huge.OPTION457, Huge.OPTION458, Huge.OPTION459, Huge.OPTION460, Huge.OPTION461, Huge.OPTION462, Huge.OPTION463, Huge.OPTION464, Huge.OPTION465, Huge.OPTION466, Huge.OPTION467, Huge.OPTION468, Huge.OPTION469, Huge.OPTION470, Huge.OPTION471, Huge.OPTION472, Huge.OPTION473, Huge.OPTION474, Huge.OPTION475, Huge.OPTION476, Huge.OPTION477, Huge.OPTION478, Huge.OPTION479, Huge.OPTION480, Huge.OPTION481, Huge.OPTION482, Huge.OPTION483, Huge.OPTION484, Huge.OPTION485, Huge.OPTION486, Huge.OPTION487, Huge.OPTION488, Huge.OPTION489, Huge.OPTION490, Huge.OPTION491, Huge.OPTION492, Huge.OPTION493, Huge.OPTION494, Huge.OPTION495, Huge.OPTION496, Huge.OPTION497, Huge.OPTION498] reveal_type(x) @@ -802,6 +827,7 @@ If two types are gradually equivalent, we can keep just one of them in a union: from typing import Any, Union from ty_extensions import Intersection, Not + def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]): reveal_type(x) # revealed: Any & ~int ``` @@ -818,16 +844,22 @@ Type inference accounts for parameter type annotations across all signatures in ```py from typing import TypedDict, overload + class T(TypedDict): x: int + def _(flag: bool): if flag: + def f(x: T) -> int: return 1 + else: + def f(x: dict[str, int]) -> int: return 1 + x = f({"x": 1}) reveal_type(x) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index 24676024b9..cb9ab5037c 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -28,18 +28,25 @@ python-version = "3.12" from __future__ import annotations from ty_extensions import reveal_mro + class A: def a(self): ... + aa: int = 1 + class B(A): def b(self): ... + bb: int = 2 + class C(B): def c(self): ... + cc: int = 3 + reveal_mro(C) # revealed: (, , , ) super(C, C()).a @@ -68,19 +75,24 @@ synthesized `Protocol`s that cannot be upcast to, or interpreted as, a non-`obje import types from typing_extensions import Callable, TypeIs, Literal, NewType, TypedDict + def f(): ... + class Foo[T]: def method(self): ... @property def some_property(self): ... + type Alias = int + class SomeTypedDict(TypedDict): x: int y: bytes + N = NewType("N", int) # revealed: , FunctionType> @@ -100,9 +112,11 @@ reveal_type(super(object, Foo.some_property)) # revealed: , int> reveal_type(super(object, N(42))) + def g(x: object) -> TypeIs[list[object]]: return isinstance(x, list) + def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]): if hasattr(x, "bar"): # revealed: @@ -124,6 +138,7 @@ def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]): # revealed: , dict[Literal["x", "y"], int | bytes]> reveal_type(super(object, y)) + # The first argument to `super()` must be an actual class object; # instances of `GenericAlias` are not accepted at runtime: # @@ -139,6 +154,7 @@ class Super: def method(self) -> int: return 42 + class Sub(Super): def method(self: Sub) -> int: # revealed: , Sub> @@ -161,11 +177,13 @@ python-version = "3.12" ```py from __future__ import annotations + class A: def __init__(self, a: int): ... @classmethod def f(cls): ... + class B(A): def __init__(self, a: int): reveal_type(super()) # revealed: , Self@__init__> @@ -177,6 +195,7 @@ class B(A): reveal_type(super()) # revealed: , type[Self@f]> super().f() + super(B, B(42)).__init__(42) super(B, B).f() ``` @@ -188,6 +207,7 @@ import enum from typing import Any, Self, Never, Protocol, Callable from ty_extensions import Intersection + class BuilderMeta(type): def __new__( cls: type[Any], @@ -200,6 +220,7 @@ class BuilderMeta(type): # revealed: Any return reveal_type(s.__new__(cls, name, bases, dct)) + class BuilderMeta2(type): def __new__( cls: type[BuilderMeta2], @@ -211,6 +232,7 @@ class BuilderMeta2(type): s = reveal_type(super()) return reveal_type(s.__new__(cls, name, bases, dct)) # revealed: BuilderMeta2 + class Foo[T]: x: T @@ -265,12 +287,14 @@ class Foo[T]: # revealed: Unknown reveal_type(super()) return self + # TypeVar bounded by `type[Foo]` rather than `Foo` # TODO: Should error on signature - `self` is annotated as a class type, not an instance type def method11[S: type[Foo[int]]](self: S, other: S) -> S: # Delegates to the bound to resolve the super type reveal_type(super()) # revealed: , > return self + # TypeVar bounded by `type[Foo]`, used in `type[T]` position # TODO: Should error on signature - `cls` would be `type[type[Foo[int]]]`, a metaclass # Delegates to `type[Unknown]` since `type[type[Foo[int]]]` can't be constructed @@ -279,8 +303,10 @@ class Foo[T]: reveal_type(super()) # revealed: , Unknown> raise NotImplementedError + type Alias = Bar + class Bar: def method(self: Alias): # revealed: , Bar> @@ -294,11 +320,13 @@ class Bar: # revealed: , Bar> reveal_type(super()) + class P(Protocol): def method(self: P): # revealed: , P> reveal_type(super()) + class E(enum.Enum): X = 1 @@ -318,8 +346,10 @@ a plain `super` instance and does not support name lookup via the MRO. class A: a: int = 42 + class B(A): ... + reveal_type(super(B)) # revealed: super # error: [unresolved-attribute] "Object of type `super` has no attribute `a`" @@ -335,8 +365,10 @@ successfully. class A: a: int = 3 + class B(A): ... + reveal_type(super(B, B()).a) # revealed: int # error: [invalid-assignment] "Cannot assign to attribute `a` on type `, B>`" super(B, B()).a = 3 @@ -353,6 +385,7 @@ member, it should effectively behave like a dynamic type. class A: a: int = 1 + def f(x): reveal_type(x) # revealed: Unknown @@ -370,6 +403,7 @@ def f(x): ```py from __future__ import annotations + class A: def test(self): reveal_type(super()) # revealed: , Self@test> @@ -384,6 +418,7 @@ class A: def inner(t: C): reveal_type(super()) # revealed: , C> + lambda x: reveal_type(super()) # revealed: , Unknown> ``` @@ -399,9 +434,11 @@ reveal_type(super(int, 3)) # revealed: , int> reveal_type(super(str, "")) # revealed: , str> reveal_type(super(bytes, b"")) # revealed: , bytes> + class E(Enum): X = 42 + reveal_type(super(E, E.X)) # revealed: , E> ``` @@ -424,8 +461,10 @@ class A: @classmethod def a2(cls): ... + class B(A): ... + # A.__dict__["a1"].__get__(B(), B) reveal_type(super(B, B()).a1) # revealed: bound method B.a1() -> Unknown # A.__dict__["a2"].__get__(B(), B) @@ -445,14 +484,20 @@ super objects are combined into a union. ```py from ty_extensions import reveal_mro + class A: ... + class B: b: int = 42 + class C(A, B): ... + + class D(B, A): ... + def f(x: C | D): reveal_mro(C) # revealed: (, , , ) reveal_mro(D) # revealed: (, , , ) @@ -463,11 +508,13 @@ def f(x: C | D): # error: [possibly-missing-attribute] "Attribute `b` may be missing on object of type `, C> | , D>`" s.b + def f(flag: bool): x = str() if flag else str("hello") reveal_type(x) # revealed: Literal["", "hello"] reveal_type(super(str, x)) # revealed: , str> + def f(x: int | str): # error: [invalid-super-argument] "`str` is not an instance or subclass of `` in `super(, str)` call" super(int, x) @@ -479,6 +526,7 @@ in all cases. ```py def f(flag: bool): if flag: + class A: x = 1 y: int = 1 @@ -486,13 +534,16 @@ def f(flag: bool): a: str = "hello" class B(A): ... + s = super(B, B()) else: + class C: x = 2 y: int | str = "test" class D(C): ... + s = super(D, D()) reveal_type(s) # revealed: , B> | , D> @@ -514,10 +565,12 @@ python-version = "3.12" ```py from ty_extensions import TypeOf, static_assert, is_subtype_of + class A[T]: def f(self, a: T) -> T: return a + class B[T](A[T]): def f(self, a: T) -> T: return super().f(a) @@ -535,10 +588,12 @@ from __future__ import annotations # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" reveal_type(super()) # revealed: Unknown + def f(): # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" super() + # No first argument in its scope class A: # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" @@ -548,6 +603,7 @@ class A: def g(): # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" super() + # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" lambda: super() @@ -575,6 +631,7 @@ runtime. import typing import collections + def f(x: int): # error: [invalid-super-argument] "`int` is not a valid class" super(x, x) @@ -583,6 +640,7 @@ def f(x: int): # error: [invalid-super-argument] "`TypeAliasType` is not a valid class" super(IntAlias, 0) + # error: [invalid-super-argument] "`str` is not an instance or subclass of `` in `super(, str)` call" # revealed: Unknown reveal_type(super(int, str())) @@ -591,9 +649,13 @@ reveal_type(super(int, str())) # revealed: Unknown reveal_type(super(int, str)) + class A: ... + + class B(A): ... + # error: [invalid-super-argument] "`A` is not an instance or subclass of `` in `super(, A)` call" # revealed: Unknown reveal_type(super(B, A())) @@ -624,6 +686,7 @@ reveal_type(super(typing.ChainMap, collections.ChainMap())) # revealed: Unknown # revealed: , > reveal_type(super(typing.Generic, typing.SupportsInt)) + def _(x: type[typing.Any], y: typing.Any): reveal_type(super(x, y)) # revealed: ``` @@ -636,11 +699,16 @@ def _(x: type[typing.Any], y: typing.Any): def coinflip() -> bool: return False + def f(): if coinflip(): + class A: ... + else: + class A: ... + super(A, A()) # error: [invalid-super-argument] ``` @@ -651,16 +719,19 @@ Accessing instance members through `super()` is not allowed. ```py from __future__ import annotations + class A: def __init__(self, a: int): self.a = a + class B(A): def __init__(self, a: int): super().__init__(a) # error: [unresolved-attribute] "Object of type `, Self@__init__>` has no attribute `a`" super().a + # error: [unresolved-attribute] "Object of type `, B>` has no attribute `a`" super(B, B(42)).a ``` @@ -676,8 +747,10 @@ class A: def __getitem__(self, key: int) -> int: return 42 + class B(A): ... + reveal_type(A()[0]) # revealed: int reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(key: int) -> int # error: [not-subscriptable] "Cannot subscript object of type `, B>` with no `__getitem__` method" @@ -700,21 +773,26 @@ from __future__ import annotations from collections.abc import Mapping from typing import Self + class Parent: def __init__(self, children: Mapping[str, Self] | None = None) -> None: self.children = children + class Child(Parent): def __init__(self, children: Mapping[str, Child] | None = None) -> None: # error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `Mapping[str, Self@__init__] | None`, found `Mapping[str, Child] | None`" super().__init__(children) + # The fix is to use `Self` consistently in the subclass: + class Parent2: def __init__(self, children: Mapping[str, Self] | None = None) -> None: self.children = children + class Child2(Parent2): def __init__(self, children: Mapping[str, Self] | None = None) -> None: super().__init__(children) # OK @@ -730,6 +808,7 @@ from typing import Protocol, Generic, TypeVar _T_co = TypeVar("_T_co", covariant=True) + class MyProtocol(Protocol, Generic[_T_co]): def __class_getitem__(cls, item): # Accessing parent's __class_getitem__ through super() diff --git a/crates/ty_python_semantic/resources/mdtest/classes.md b/crates/ty_python_semantic/resources/mdtest/classes.md index 0a137a85fa..6b757c778e 100644 --- a/crates/ty_python_semantic/resources/mdtest/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/classes.md @@ -17,9 +17,13 @@ from ty_extensions import reveal_mro A = int + class G[T]: ... + + class C(A, G["B"]): ... + A = str B = bytes @@ -33,14 +37,22 @@ These are currently not supported, but ideally we would support them in some lim ```py from ty_extensions import reveal_mro + class A: ... + + class B: ... + + class C: ... + bases = (A, B, C) + class Foo(*bases): ... + # revealed: (, @Todo(StarredExpression), ) reveal_mro(Foo) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/enums.md b/crates/ty_python_semantic/resources/mdtest/comparison/enums.md index e4034adf59..bfcd30caa6 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/enums.md @@ -3,10 +3,12 @@ ```py from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + reveal_type(Answer.NO == Answer.NO) # revealed: Literal[True] reveal_type(Answer.NO == Answer.YES) # revealed: Literal[False] diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md index a636307a1d..8069b3a59c 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md @@ -3,6 +3,7 @@ ```py class A: ... + def _(a1: A, a2: A, o: object): n1 = None n2 = None diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md index a53aaef3bb..7443cddf05 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md @@ -19,6 +19,7 @@ class A: def __contains__(self, item: str) -> bool: return True + reveal_type("hello" in A()) # revealed: bool reveal_type("hello" not in A()) # revealed: bool # error: [unsupported-operator] "Operator `in` is not supported between objects of type `Literal[42]` and `A`" @@ -37,10 +38,12 @@ class StringIterator: def __next__(self) -> str: return "foo" + class A: def __iter__(self) -> StringIterator: return StringIterator() + reveal_type("hello" in A()) # revealed: bool reveal_type("hello" not in A()) # revealed: bool reveal_type(42 in A()) # revealed: bool @@ -59,6 +62,7 @@ class A: def __getitem__(self, key: int) -> str: return "foo" + reveal_type("hello" in A()) # revealed: bool reveal_type("hello" not in A()) # revealed: bool reveal_type(42 in A()) # revealed: bool @@ -75,6 +79,7 @@ class A: def __contains__(self, item: str) -> str: return "foo" + reveal_type("hello" in A()) # revealed: bool reveal_type("hello" not in A()) # revealed: bool ``` @@ -86,14 +91,17 @@ reveal_type("hello" not in A()) # revealed: bool ```py from typing import Literal + class AlwaysTrue: def __contains__(self, item: int) -> Literal[1]: return 1 + class AlwaysFalse: def __contains__(self, item: int) -> Literal[""]: return "" + reveal_type(42 in AlwaysTrue()) # revealed: Literal[True] reveal_type(42 not in AlwaysTrue()) # revealed: Literal[False] @@ -108,13 +116,19 @@ doesn't result in a fallback to `__iter__` or `__getitem__`: ```py class CheckContains: ... + + class CheckIter: ... + + class CheckGetItem: ... + class CheckIterIterator: def __next__(self) -> CheckIter: return CheckIter() + class A: def __contains__(self, item: CheckContains) -> bool: return True @@ -125,6 +139,7 @@ class A: def __getitem__(self, key: int) -> CheckGetItem: return CheckGetItem() + reveal_type(CheckContains() in A()) # revealed: bool # error: [unsupported-operator] "Operator `in` is not supported between objects of type `CheckIter` and `A`" @@ -132,6 +147,7 @@ reveal_type(CheckIter() in A()) # revealed: bool # error: [unsupported-operator] "Operator `in` is not supported between objects of type `CheckGetItem` and `A`" reveal_type(CheckGetItem() in A()) # revealed: bool + class B: def __iter__(self) -> CheckIterIterator: return CheckIterIterator() @@ -139,6 +155,7 @@ class B: def __getitem__(self, key: int) -> CheckGetItem: return CheckGetItem() + reveal_type(CheckIter() in B()) # revealed: bool # Always use `__iter__`, regardless of iterated type; there's no NotImplemented # in this case, so there's no fallback to `__getitem__` @@ -155,6 +172,7 @@ class A: def __getitem__(self, key: str) -> str: return "foo" + # error: [unsupported-operator] "Operator `in` is not supported between objects of type `Literal[42]` and `A`" reveal_type(42 in A()) # revealed: bool # error: [unsupported-operator] "Operator `in` is not supported between objects of type `Literal["hello"]` and `A`" @@ -193,10 +211,12 @@ It may also be more appropriate to use `unsupported-operator` as the error code. class NotBoolable: __bool__: int = 3 + class WithContains: def __contains__(self, item) -> NotBoolable: return NotBoolable() + # error: [unsupported-bool-conversion] 10 in WithContains() # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md index 85f88ab181..7f243d15e8 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md @@ -16,13 +16,25 @@ most common case involves implementing these methods for the same type: ```py from __future__ import annotations + class EqReturnType: ... + + class NeReturnType: ... + + class LtReturnType: ... + + class LeReturnType: ... + + class GtReturnType: ... + + class GeReturnType: ... + class A: def __eq__(self, other: A) -> EqReturnType: # error: [invalid-method-override] return EqReturnType() @@ -42,6 +54,7 @@ class A: def __ge__(self, other: A) -> GeReturnType: return GeReturnType() + reveal_type(A() == A()) # revealed: EqReturnType reveal_type(A() != A()) # revealed: NeReturnType reveal_type(A() < A()) # revealed: LtReturnType @@ -58,13 +71,25 @@ type: ```py from __future__ import annotations + class EqReturnType: ... + + class NeReturnType: ... + + class LtReturnType: ... + + class LeReturnType: ... + + class GtReturnType: ... + + class GeReturnType: ... + class A: def __eq__(self, other: B) -> EqReturnType: # error: [invalid-method-override] return EqReturnType() @@ -84,8 +109,10 @@ class A: def __ge__(self, other: B) -> GeReturnType: return GeReturnType() + class B: ... + reveal_type(A() == B()) # revealed: EqReturnType reveal_type(A() != B()) # revealed: NeReturnType reveal_type(A() < B()) # revealed: LtReturnType @@ -103,13 +130,25 @@ these methods will be ignored here because they require a mismatched operand typ ```py from __future__ import annotations + class EqReturnType: ... + + class NeReturnType: ... + + class LtReturnType: ... + + class LeReturnType: ... + + class GtReturnType: ... + + class GeReturnType: ... + class A: def __eq__(self, other: B) -> EqReturnType: # error: [invalid-method-override] return EqReturnType() @@ -129,8 +168,10 @@ class A: def __ge__(self, other: B) -> GeReturnType: return GeReturnType() + class Unrelated: ... + class B: def __eq__(self, other: Unrelated) -> B: # error: [invalid-method-override] return B() @@ -138,6 +179,7 @@ class B: def __ne__(self, other: Unrelated) -> B: # error: [invalid-method-override] return B() + # Because `object.__eq__` and `object.__ne__` accept `object` in typeshed, # this can only happen with an invalid override of these methods, # but we still support it. @@ -150,6 +192,7 @@ reveal_type(B() <= A()) # revealed: GeReturnType reveal_type(B() > A()) # revealed: LtReturnType reveal_type(B() >= A()) # revealed: LeReturnType + class C: def __gt__(self, other: C) -> EqReturnType: return EqReturnType() @@ -157,6 +200,7 @@ class C: def __ge__(self, other: C) -> NeReturnType: return NeReturnType() + reveal_type(C() < C()) # revealed: EqReturnType reveal_type(C() <= C()) # revealed: NeReturnType ``` @@ -170,13 +214,25 @@ than `A`. ```py from __future__ import annotations + class EqReturnType: ... + + class NeReturnType: ... + + class LtReturnType: ... + + class LeReturnType: ... + + class GtReturnType: ... + + class GeReturnType: ... + class A: def __eq__(self, other: A) -> A: # error: [invalid-method-override] return A() @@ -196,6 +252,7 @@ class A: def __ge__(self, other: A) -> A: return A() + class B(A): def __eq__(self, other: A) -> EqReturnType: # error: [invalid-method-override] return EqReturnType() @@ -215,6 +272,7 @@ class B(A): def __ge__(self, other: A) -> GeReturnType: # error: [invalid-method-override] return GeReturnType() + reveal_type(A() == B()) # revealed: EqReturnType reveal_type(A() != B()) # revealed: NeReturnType @@ -233,6 +291,7 @@ method has an mismatched type to operand, the comparison will fall back to the l ```py from __future__ import annotations + class A: def __lt__(self, other: A) -> A: return A() @@ -240,6 +299,7 @@ class A: def __gt__(self, other: A) -> A: return A() + class B(A): def __lt__(self, other: int) -> B: # error: [invalid-method-override] return B() @@ -247,6 +307,7 @@ class B(A): def __gt__(self, other: int) -> B: # error: [invalid-method-override] return B() + reveal_type(A() < B()) # revealed: A reveal_type(A() > B()) # revealed: A ``` @@ -268,12 +329,15 @@ from does_not_exist import Foo # error: [unresolved-import] reveal_type(Foo) # revealed: Unknown + class X: def __lt__(self, other: object) -> int: return 42 + class Y(Foo): ... + # TODO: Should be `int | Unknown`; see above discussion. reveal_type(X() < Y()) # revealed: int ``` @@ -288,6 +352,7 @@ Please refer to the [docs](https://docs.python.org/3/reference/datamodel.html#ob ```py from __future__ import annotations + class A: def __eq__(self, other: int) -> A: # error: [invalid-method-override] return A() @@ -295,6 +360,7 @@ class A: def __ne__(self, other: int) -> A: # error: [invalid-method-override] return A() + reveal_type(A() == A()) # revealed: bool reveal_type(A() != A()) # revealed: bool ``` @@ -304,6 +370,7 @@ reveal_type(A() != A()) # revealed: bool ```py class A: ... + reveal_type(A() == object()) # revealed: bool reveal_type(A() != object()) # revealed: bool reveal_type(object() == A()) # revealed: bool @@ -336,6 +403,7 @@ reveal_type(1 > 2j) # revealed: Unknown # error: [unsupported-operator] "Operator `>=` is not supported between objects of type `Literal[1]` and `complex`" reveal_type(1 >= 2j) # revealed: Unknown + def f(x: bool, y: int): reveal_type(x < y) # revealed: bool reveal_type(y < x) # revealed: bool @@ -354,6 +422,7 @@ element) of a chained comparison. class NotBoolable: __bool__: int = 3 + class Comparable: def __lt__(self, item) -> NotBoolable: return NotBoolable() @@ -361,6 +430,7 @@ class Comparable: def __gt__(self, item) -> NotBoolable: return NotBoolable() + # error: [unsupported-bool-conversion] 10 < Comparable() < 20 # error: [unsupported-bool-conversion] @@ -374,14 +444,17 @@ Comparable() < Comparable() # fine ```py from typing import Literal + class AlwaysTrue: def __call__(self, other: object) -> Literal[True]: return True + class A: __eq__: AlwaysTrue = AlwaysTrue() __lt__: AlwaysTrue = AlwaysTrue() + reveal_type(A() == A()) # revealed: Literal[True] reveal_type(A() < A()) # revealed: Literal[True] reveal_type(A() > A()) # revealed: Literal[True] diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md index 6c6d7bc827..0c453be254 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md @@ -8,16 +8,20 @@ types, we can infer that the result for the intersection type is also true/false ```py from typing import Literal + class Base: def __gt__(self, other) -> bool: return False + class Child1(Base): def __eq__(self, other) -> Literal[True]: return True + class Child2(Base): ... + def _(x: Base): c1 = Child1() @@ -95,6 +99,7 @@ def _(x: int): ```py class A: ... + def _(o: object): a = A() n = None @@ -116,8 +121,11 @@ intersection type: ```py class NonContainer1: ... + + class NonContainer2: ... + def _(x: object): if isinstance(x, NonContainer1): if isinstance(x, NonContainer2): @@ -135,6 +143,7 @@ class Container: def __contains__(self, x) -> bool: return False + def _(x: object): if isinstance(x, NonContainer1): if isinstance(x, Container): @@ -167,8 +176,10 @@ class Container: def __contains__(self, x) -> bool: return False + class NonContainer: ... + def _(x: object): if isinstance(x, Container): if not isinstance(x, NonContainer): diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/non_bool_returns.md b/crates/ty_python_semantic/resources/mdtest/comparison/non_bool_returns.md index 2da190aa5d..4405414511 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/non_bool_returns.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/non_bool_returns.md @@ -21,6 +21,7 @@ Walking through examples: ```py from __future__ import annotations + class A: def __lt__(self, other) -> A: return self @@ -28,14 +29,17 @@ class A: def __gt__(self, other) -> bool: return False + class B: def __lt__(self, other) -> B: return self + class C: def __lt__(self, other) -> C: return self + x = A() < B() < C() reveal_type(x) # revealed: (A & ~AlwaysTruthy) | B diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md index 97c712735a..ad34085cd0 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md @@ -152,13 +152,25 @@ of the dunder methods.) ```py from __future__ import annotations + class EqReturnType: ... + + class NeReturnType: ... + + class LtReturnType: ... + + class LeReturnType: ... + + class GtReturnType: ... + + class GeReturnType: ... + class A: def __eq__(self, o: object) -> EqReturnType: # error: [invalid-method-override] return EqReturnType() @@ -178,6 +190,7 @@ class A: def __ge__(self, o: A) -> GeReturnType: return GeReturnType() + a = (A(), A()) reveal_type(a == a) # revealed: bool @@ -198,12 +211,15 @@ reveal_type(b <= c) # revealed: Literal[True] reveal_type(b > c) # revealed: Literal[False] reveal_type(b >= c) # revealed: Literal[False] + class LtReturnTypeOnB: ... + class B: def __lt__(self, o: B) -> LtReturnTypeOnB: return LtReturnTypeOnB() + reveal_type((A(), B()) < (A(), B())) # revealed: LtReturnType | LtReturnTypeOnB | Literal[False] ``` @@ -262,6 +278,7 @@ comparison can clearly conclude before encountering an error, the error should n ```py def _(n: int, s: str): class A: ... + # error: [unsupported-operator] "Operator `<` is not supported between two objects of type `A`" A() < A() # error: [unsupported-operator] "Operator `<=` is not supported between two objects of type `A`" @@ -466,6 +483,7 @@ def compute_chained_comparison(): class NotBoolable: __bool__: int = 5 + class Comparable: def __lt__(self, other) -> NotBoolable: return NotBoolable() @@ -473,6 +491,7 @@ class Comparable: def __gt__(self, other) -> NotBoolable: return NotBoolable() + a = (1, Comparable()) b = (1, Comparable()) @@ -494,11 +513,13 @@ pair of elements at equivalent positions cannot be converted to a `bool`: class NotBoolable: __bool__: None = None + class A: # error: [invalid-method-override] def __eq__(self, other) -> NotBoolable: return NotBoolable() + # error: [unsupported-bool-conversion] (A(),) == (A(),) ``` @@ -509,9 +530,11 @@ class A: from __future__ import annotations from typing import NamedTuple + class Node(NamedTuple): parent: Node | None + def _(n: Node): reveal_type(n.parent is n) # revealed: bool ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/unions.md b/crates/ty_python_semantic/resources/mdtest/comparison/unions.md index 6a7feea646..44969a7b32 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/unions.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/unions.md @@ -75,6 +75,7 @@ back to `bool` for the result type instead of trying to infer something more pre ```py from typing import Literal + def _( x: list[int] | Literal[1], y: list[int] | Literal[1], diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md b/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md index c74fd57928..603267516c 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md @@ -5,6 +5,7 @@ ```py def _(flag: bool, flag1: bool, flag2: bool): class A: ... + a = 1 in 7 # error: "Operator `in` is not supported between objects of type `Literal[1]` and `Literal[7]`" reveal_type(a) # revealed: bool diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md index 0f2d1e4b0c..b751d89720 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md @@ -6,14 +6,17 @@ # revealed: int [reveal_type(x) for x in range(3)] + class Row: def __next__(self) -> range: return range(3) + class Table: def __iter__(self) -> Row: return Row() + # revealed: tuple[int, range] [reveal_type((cell, row)) for row in Table() for cell in row] @@ -38,10 +41,12 @@ class Row: def __next__(self) -> range: return range(3) + class Table: def __iter__(self) -> Row: return Row() + # revealed: tuple[int, range] [[reveal_type((cell, row)) for cell in row] for row in Table()] ``` @@ -85,6 +90,7 @@ Starred expressions must be iterable ```py class NotIterable: ... + # This is fine: x = [*range(3)] @@ -101,10 +107,12 @@ class AsyncIterator: async def __anext__(self) -> int: return 42 + class AsyncIterable: def __aiter__(self) -> AsyncIterator: return AsyncIterator() + async def _(): # revealed: int [reveal_type(x) async for x in AsyncIterable()] @@ -180,6 +188,7 @@ The type context is propagated down into the comprehension: class Person(TypedDict): name: str + # TODO: This should not error. # error: [invalid-assignment] persons: list[Person] = [{"name": n} for n in ["Alice", "Bob"]] diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md b/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md index 082e0d43db..86b3241cd6 100644 --- a/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md @@ -42,6 +42,7 @@ def _(flag: bool): class NotBoolable: __bool__: int = 3 + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" 3 if NotBoolable() else 4 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md b/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md index f55dc41160..53245212e6 100644 --- a/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md @@ -122,6 +122,7 @@ def _(flag: bool, flag2: bool): def check(x: int) -> bool: return bool(x) + if check(x := 1): x = 2 elif check(x := 3): @@ -136,6 +137,7 @@ reveal_type(x) # revealed: Literal[2, 3, 4] def check(x) -> bool: return bool(x) + def _(flag: bool): x = 1 if flag else None y = 0 @@ -154,6 +156,7 @@ def _(flag: bool): class NotBoolable: __bool__: int = 3 + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" if NotBoolable(): ... diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/match.md b/crates/ty_python_semantic/resources/mdtest/conditional/match.md index 729483fcf5..cd8d07a778 100644 --- a/crates/ty_python_semantic/resources/mdtest/conditional/match.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/match.md @@ -63,10 +63,12 @@ This leads us to infer `Literal[1, 3]` as the type of `y` after the `match` stat ```py from typing import final + @final class C: pass + def _(subject: C): y = 1 match subject: @@ -85,19 +87,24 @@ all subpatterns in the class pattern match. ```py from typing import final + class Foo: pass + class FooSub(Foo): pass + class Bar: pass + @final class Baz: pass + def _(target: FooSub): y = 1 @@ -111,6 +118,7 @@ def _(target: FooSub): reveal_type(y) # revealed: Literal[3] + def _(target: FooSub): y = 1 @@ -124,6 +132,7 @@ def _(target: FooSub): reveal_type(y) # revealed: Literal[3, 4] + def _(target: FooSub | str): y = 1 @@ -144,13 +153,16 @@ def _(target: FooSub | str): from typing_extensions import assert_never from dataclasses import dataclass + @dataclass class Point: x: int y: int + class Other: ... + def _(target: Point): y = 1 @@ -164,6 +176,7 @@ def _(target: Point): reveal_type(y) # revealed: Literal[1, 2, 3, 4] + def _(target: Point): match target: case Point(x, y): # irrefutable sub-patterns @@ -171,6 +184,7 @@ def _(target: Point): case _: assert_never(target) + def _(target: Point | Other): match target: case Point(0, 0): @@ -190,6 +204,7 @@ Singleton patterns are matched based on identity, not equality comparisons or `i ```py from typing import Literal + def _(target: Literal[True, False]): y = 1 @@ -203,6 +218,7 @@ def _(target: Literal[True, False]): reveal_type(y) # revealed: Literal[2, 3] + def _(target: bool): y = 1 @@ -216,6 +232,7 @@ def _(target: bool): reveal_type(y) # revealed: Literal[2, 3] + def _(target: None): y = 1 @@ -229,6 +246,7 @@ def _(target: None): reveal_type(y) # revealed: Literal[4] + def _(target: None | Literal[True]): y = 1 @@ -242,6 +260,7 @@ def _(target: None | Literal[True]): reveal_type(y) # revealed: Literal[2, 4] + # bool is an int subclass def _(target: int): y = 1 @@ -256,6 +275,7 @@ def _(target: int): reveal_type(y) # revealed: Literal[1, 2, 3] + def _(target: str): y = 1 @@ -275,10 +295,12 @@ def _(target: str): ```py from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + def _(answer: Answer): y = 0 match answer: @@ -299,6 +321,7 @@ A `|` pattern matches if any of the subpatterns match. ```py from typing import Literal, final + def _(target: Literal["foo", "baz"]): y = 1 @@ -310,6 +333,7 @@ def _(target: Literal["foo", "baz"]): reveal_type(y) # revealed: Literal[2, 3] + def _(target: None): y = 1 @@ -321,10 +345,12 @@ def _(target: None): reveal_type(y) # revealed: Literal[2] + @final class Baz: pass + def _(target: int | None | float): y = 1 @@ -336,8 +362,10 @@ def _(target: int | None | float): reveal_type(y) # revealed: Literal[1, 2] + class Foo: ... + def _(target: None | Foo): y = 1 @@ -375,6 +403,7 @@ def _(target: int | str): class NotBoolable: __bool__: int = 3 + def _(target: int, flag: NotBoolable): y = 1 match target: @@ -395,10 +424,12 @@ is not covered by any case, even when all enum members are covered. ```py from enum import Enum + class Answer(Enum): YES = 1 NO = 2 + def _(answer: Answer | None): y = 0 match answer: @@ -411,6 +442,7 @@ def _(answer: Answer | None): # so y could still be 0 reveal_type(y) # revealed: Literal[0, 1, 2] + def _(answer: Answer | None): match answer: case Answer.YES: @@ -422,8 +454,10 @@ def _(answer: Answer | None): reveal_type(answer) # revealed: None return 3 + class Foo: ... + def _(answer: Answer | None): match answer: case Answer.YES: diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index 775b4e8ac6..0f158299e0 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -7,10 +7,12 @@ Deferred annotations can result in cycles in resolving a function signature: ```py from __future__ import annotations + # error: [invalid-type-form] def f(x: f): pass + reveal_type(f) # revealed: def f(x: Unknown) -> Unknown ``` @@ -27,6 +29,7 @@ class Point: def replace_with(self, other: "Point") -> None: self.x, self.y = other.x, other.y + p = Point() reveal_type(p.x) # revealed: Unknown | int reveal_type(p.y) # revealed: Unknown | int @@ -44,15 +47,18 @@ from typing import Union, TypeAliasType, Sequence, Mapping A = list["A" | None] + def f(x: A): # TODO: should be `list[A | None]`? reveal_type(x) # revealed: list[Divergent] # TODO: should be `A | None`? reveal_type(x[0]) # revealed: Divergent + JSONPrimitive = Union[str, int, float, bool, None] JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]') + def _(x: JSONValue): # TODO: should be `JSONValue` reveal_type(x) # revealed: Divergent @@ -65,6 +71,7 @@ from typing import Generic, TypeVar B = TypeVar("B", bound="Base") + class Base(Generic[B]): pass ``` @@ -86,24 +93,28 @@ class C: def f(self: "C"): def inner_a(positional=self.a): return + self.a = inner_a # revealed: def inner_a(positional=...) -> Unknown reveal_type(inner_a) def inner_b(*, kw_only=self.b): return + self.b = inner_b # revealed: def inner_b(*, kw_only=...) -> Unknown reveal_type(inner_b) def inner_c(positional_only=self.c, /): return + self.c = inner_c # revealed: def inner_c(positional_only=..., /) -> Unknown reveal_type(inner_c) def inner_d(*, kw_only=self.d): return + self.d = inner_d # revealed: def inner_d(*, kw_only=...) -> Unknown reveal_type(inner_d) @@ -116,6 +127,7 @@ class D: def f(self: "D"): # error: [invalid-parameter-default] "Default value of type `Unknown | (def inner_a(a: int = ...) -> Unknown)` is not assignable to annotated parameter type `int`" def inner_a(a: int = self.a): ... + self.a = inner_a ``` @@ -153,6 +165,7 @@ class Cyclic: if isinstance(self.data, str): self.data = {"url": self.data} + # revealed: Unknown | str | dict[Unknown, Unknown] | dict[Unknown | str, Unknown | str] reveal_type(Cyclic("").data) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md index 78db11169c..081820cbea 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md @@ -13,16 +13,19 @@ class, or metaclass is a `dataclass`-like construct. ```py from typing_extensions import dataclass_transform + @dataclass_transform() def my_dataclass[T](cls: type[T]) -> type[T]: # modify cls return cls + @my_dataclass class Person: name: str age: int | None = None + Person("Alice", 20) Person("Bob", None) Person("Bob") @@ -38,18 +41,22 @@ If we want our `dataclass`-like decorator to also take parameters, that is also ```py from typing_extensions import dataclass_transform, Callable + @dataclass_transform() def versioned_class[T](*, version: int = 1): def decorator(cls): # modify cls return cls + return decorator + @versioned_class(version=2) class Person: name: str age: int | None = None + Person("Alice", 20) # error: [missing-argument] @@ -61,6 +68,7 @@ We properly type-check the arguments to the decorator: ```py from typing_extensions import dataclass_transform, Callable + # error: [invalid-argument-type] @versioned_class(version="a string") class C: @@ -77,16 +85,19 @@ The examples from this section are straight from the Python documentation on ```py from typing_extensions import dataclass_transform + @dataclass_transform() def create_model[T](cls: type[T]) -> type[T]: ... return cls + @create_model class CustomerModel: id: int name: str + CustomerModel(id=1, name="Test") ``` @@ -95,15 +106,19 @@ CustomerModel(id=1, name="Test") ```py from typing_extensions import dataclass_transform + @dataclass_transform() class ModelMeta(type): ... + class ModelBase(metaclass=ModelMeta): ... + class CustomerModel(ModelBase): id: int name: str + CustomerModel(id=1, name="Test") # error: [missing-argument] @@ -115,13 +130,16 @@ CustomerModel() ```py from typing_extensions import dataclass_transform + @dataclass_transform() class ModelBase: ... + class CustomerModel(ModelBase): id: int name: str + CustomerModel(id=1, name="Test") ``` @@ -140,52 +158,67 @@ This can be overwritten using the `order` argument to the custom decorator: ```py from typing_extensions import dataclass_transform + @dataclass_transform() def normal(*, order: bool = False): raise NotImplementedError + @dataclass_transform(order_default=False) def order_default_false(*, order: bool = False): raise NotImplementedError + @dataclass_transform(order_default=True) def order_default_true(*, order: bool = True): raise NotImplementedError + @normal class Normal: inner: int + Normal(1) < Normal(2) # error: [unsupported-operator] + @normal(order=True) class NormalOverwritten: inner: int + reveal_type(NormalOverwritten(1) < NormalOverwritten(2)) # revealed: bool + @order_default_false class OrderFalse: inner: int + OrderFalse(1) < OrderFalse(2) # error: [unsupported-operator] + @order_default_false(order=True) class OrderFalseOverwritten: inner: int + reveal_type(OrderFalseOverwritten(1) < OrderFalseOverwritten(2)) # revealed: bool + @order_default_true class OrderTrue: inner: int + reveal_type(OrderTrue(1) < OrderTrue(2)) # revealed: bool + @order_default_true(order=False) class OrderTrueOverwritten: inner: int + # error: [unsupported-operator] OrderTrueOverwritten(1) < OrderTrueOverwritten(2) ``` @@ -196,11 +229,14 @@ This also works for metaclass-based transformers: @dataclass_transform(order_default=True) class OrderedModelMeta(type): ... + class OrderedModel(metaclass=OrderedModelMeta): ... + class TestWithMeta(OrderedModel): inner: int + reveal_type(TestWithMeta(1) < TestWithMeta(2)) # revealed: bool ``` @@ -210,9 +246,11 @@ And for base-class-based transformers: @dataclass_transform(order_default=True) class OrderedModelBase: ... + class TestWithBase(OrderedModelBase): inner: int + reveal_type(TestWithBase(1) < TestWithBase(2)) # revealed: bool ``` @@ -224,12 +262,14 @@ When provided, sets the default value for the `kw_only` parameter of `field()`. from typing import dataclass_transform from dataclasses import field + @dataclass_transform(kw_only_default=True) def create_model(*, kw_only: bool = True): ... @create_model() class A: name: str + a = A(name="Harry") # error: [missing-argument] # error: [too-many-positional-arguments] @@ -244,6 +284,7 @@ class CustomerModel: id: int name: str + c = CustomerModel(1, "Harry") ``` @@ -253,11 +294,14 @@ This also works for metaclass-based transformers: @dataclass_transform(kw_only_default=True) class ModelMeta(type): ... + class ModelBase(metaclass=ModelMeta): ... + class TestMeta(ModelBase): name: str + reveal_type(TestMeta.__init__) # revealed: (self: TestMeta, *, name: str) -> None ``` @@ -267,9 +311,11 @@ And for base-class-based transformers: @dataclass_transform(kw_only_default=True) class ModelBase: ... + class TestBase(ModelBase): name: str + reveal_type(TestBase.__init__) # revealed: (self: TestBase, *, name: str) -> None ``` @@ -280,12 +326,14 @@ When provided, sets the default value for the `frozen` parameter of `field()`. ```py from typing import dataclass_transform + @dataclass_transform(frozen_default=True) def create_model(*, frozen: bool = True): ... @create_model() class ImmutableModel: name: str + i = ImmutableModel(name="test") i.name = "new" # error: [invalid-assignment] ``` @@ -297,6 +345,7 @@ Again, this can be overridden by setting `frozen=False` when applying the decora class MutableModel: name: str + m = MutableModel(name="test") m.name = "new" # No error ``` @@ -307,11 +356,14 @@ This also works for metaclass-based transformers: @dataclass_transform(frozen_default=True) class ModelMeta(type): ... + class ModelBase(metaclass=ModelMeta): ... + class TestMeta(ModelBase): name: str + t = TestMeta(name="test") t.name = "new" # error: [invalid-assignment] ``` @@ -322,9 +374,11 @@ And for base-class-based transformers: @dataclass_transform(frozen_default=True) class ModelBase: ... + class TestMeta(ModelBase): name: str + t = TestMeta(name="test") t.name = "new" # error: [invalid-assignment] @@ -337,6 +391,7 @@ Combining several of these parameters also works as expected: ```py from typing import dataclass_transform + @dataclass_transform(eq_default=True, order_default=False, kw_only_default=True, frozen_default=True) def create_model(*, eq: bool = True, order: bool = False, kw_only: bool = True, frozen: bool = True): ... @create_model(eq=False, order=True, kw_only=False, frozen=False) @@ -344,6 +399,7 @@ class OverridesAllParametersModel: name: str age: int + # Positional arguments are allowed: model = OverridesAllParametersModel("test", 25) @@ -365,21 +421,25 @@ from `order=False` (default) to `order=True`: ```py from typing import dataclass_transform + @dataclass_transform(frozen_default=True) def default_frozen_model(*, frozen: bool = True, order: bool = False): ... @default_frozen_model() class Frozen: name: str + f = Frozen(name="test") f.name = "new" # error: [invalid-assignment] Frozen(name="A") < Frozen(name="B") # error: [unsupported-operator] + @default_frozen_model(frozen=False, order=True) class Mutable: name: str + m = Mutable(name="test") m.name = "new" # No error @@ -391,6 +451,7 @@ reveal_type(Mutable(name="A") < Mutable(name="B")) # revealed: bool ```py from typing import dataclass_transform + @dataclass_transform(frozen_default=True) class DefaultFrozenMeta(type): def __new__( @@ -403,19 +464,24 @@ class DefaultFrozenMeta(type): order: bool = False, ): ... + class DefaultFrozenModel(metaclass=DefaultFrozenMeta): ... + class Frozen(DefaultFrozenModel): name: str + f = Frozen(name="test") f.name = "new" # error: [invalid-assignment] Frozen(name="A") < Frozen(name="B") # error: [unsupported-operator] + class Mutable(DefaultFrozenModel, frozen=False, order=True): name: str + m = Mutable(name="test") # TODO: This should not be an error. In order to support this, we need to implement the precise `frozen` semantics of # `dataclass_transform` described here: https://typing.python.org/en/latest/spec/dataclasses.html#dataclass-semantics @@ -429,6 +495,7 @@ reveal_type(Mutable(name="A") < Mutable(name="B")) # revealed: bool ```py from typing import dataclass_transform + @dataclass_transform(frozen_default=True) class DefaultFrozenModel: def __init_subclass__( @@ -438,17 +505,21 @@ class DefaultFrozenModel: order: bool = False, ): ... + class Frozen(DefaultFrozenModel): name: str + f = Frozen(name="test") f.name = "new" # error: [invalid-assignment] Frozen(name="A") < Frozen(name="B") # error: [unsupported-operator] + class Mutable(DefaultFrozenModel, frozen=False, order=True): name: str + m = Mutable(name="test") m.name = "new" # No error @@ -467,20 +538,25 @@ from typing_extensions import dataclass_transform, TypeVar, Callable T = TypeVar("T", bound=type) + @dataclass_transform() def fancy_model(*, slots: bool = False) -> Callable[[T], T]: raise NotImplementedError + @fancy_model() class NoSlots: name: str + NoSlots.__slots__ # error: [unresolved-attribute] + @fancy_model(slots=True) class WithSlots: name: str + reveal_type(WithSlots.__slots__) # revealed: tuple[Literal["name"]] ``` @@ -489,23 +565,29 @@ reveal_type(WithSlots.__slots__) # revealed: tuple[Literal["name"]] ```py from typing_extensions import dataclass_transform + @dataclass_transform() class FancyMeta(type): def __new__(cls, name, bases, namespace, *, slots: bool = False): ... return super().__new__(cls, name, bases, namespace) + class FancyBase(metaclass=FancyMeta): ... + class NoSlots(FancyBase): name: str + # error: [unresolved-attribute] NoSlots.__slots__ + class WithSlots(FancyBase, slots=True): name: str + reveal_type(WithSlots.__slots__) # revealed: tuple[Literal["name"]] ``` @@ -514,20 +596,25 @@ reveal_type(WithSlots.__slots__) # revealed: tuple[Literal["name"]] ```py from typing_extensions import dataclass_transform + @dataclass_transform() class FancyBase: def __init_subclass__(cls, *, slots: bool = False): ... super().__init_subclass__() + class NoSlots(FancyBase): name: str + NoSlots.__slots__ # error: [unresolved-attribute] + class WithSlots(FancyBase, slots=True): name: str + reveal_type(WithSlots.__slots__) # revealed: tuple[Literal["name"]] ``` @@ -545,18 +632,21 @@ checkers do not seem to support this either. ```py from typing_extensions import dataclass_transform, Any + def fancy_field(*, init: bool = True, kw_only: bool = False, alias: str | None = None) -> Any: ... @dataclass_transform(field_specifiers=(fancy_field,)) def fancy_model[T](cls: type[T]) -> type[T]: ... return cls + @fancy_model class Person: id: int = fancy_field(init=False) internal_name: str = fancy_field(alias="name") age: int | None = fancy_field(kw_only=True) + reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None alice = Person("Alice", age=30) @@ -571,6 +661,7 @@ reveal_type(alice.age) # revealed: int | None ```py from typing_extensions import dataclass_transform, Any + def fancy_field(*, init: bool = True, kw_only: bool = False, alias: str | None = None) -> Any: ... @dataclass_transform(field_specifiers=(fancy_field,)) class FancyMeta(type): @@ -578,13 +669,16 @@ class FancyMeta(type): ... return super().__new__(cls, name, bases, namespace) + class FancyBase(metaclass=FancyMeta): ... + class Person(FancyBase): id: int = fancy_field(init=False) internal_name: str = fancy_field(alias="name") age: int | None = fancy_field(kw_only=True) + reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None alice = Person("Alice", age=30) @@ -599,6 +693,7 @@ reveal_type(alice.age) # revealed: int | None ```py from typing_extensions import dataclass_transform, Any + def fancy_field(*, init: bool = True, kw_only: bool = False, alias: str | None = None) -> Any: ... @dataclass_transform(field_specifiers=(fancy_field,)) class FancyBase: @@ -606,11 +701,13 @@ class FancyBase: ... super().__init_subclass__() + class Person(FancyBase): id: int = fancy_field(init=False) internal_name: str = fancy_field(alias="name") age: int | None = fancy_field(kw_only=True) + reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None alice = Person("Alice", age=30) @@ -627,17 +724,20 @@ Field specifiers can have default arguments that should be respected: ```py from typing_extensions import dataclass_transform, Any + def fancy_field(*, init: bool = False) -> Any: ... @dataclass_transform(field_specifiers=(fancy_field,)) def fancy_model[T](cls: type[T]) -> type[T]: ... return cls + @fancy_model class Person: id: int = fancy_field() name: str = fancy_field(init=True) + reveal_type(Person.__init__) # revealed: (self: Person, name: str) -> None Person(name="Alice") @@ -655,11 +755,13 @@ correctly when passed via `**kwargs` for all three kinds of transformers. from typing import Any from typing_extensions import dataclass_transform + def field(**kwargs: Any) -> Any: ... @dataclass_transform(field_specifiers=(field,)) def create_model[T](cls: type[T]) -> type[T]: return cls + @create_model class Person: id: int = field(init=False) @@ -669,6 +771,7 @@ class Person: email: str = field(kw_only=True) internal_notes: str = field(alias="notes") + # revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None reveal_type(Person.__init__) @@ -682,12 +785,15 @@ Person("Bob", email="bob@example.com", notes="other notes") from typing import Any from typing_extensions import dataclass_transform + def field(**kwargs: Any) -> Any: ... @dataclass_transform(field_specifiers=(field,)) class ModelMeta(type): ... + class ModelBase(metaclass=ModelMeta): ... + class Person(ModelBase): id: int = field(init=False) name: str @@ -696,6 +802,7 @@ class Person(ModelBase): email: str = field(kw_only=True) internal_notes: str = field(alias="notes") + # revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None reveal_type(Person.__init__) @@ -709,10 +816,12 @@ Person("Bob", email="bob@example.com", notes="other notes") from typing import Any from typing_extensions import dataclass_transform + def field(**kwargs: Any) -> Any: ... @dataclass_transform(field_specifiers=(field,)) class ModelBase: ... + class Person(ModelBase): id: int = field(init=False) name: str @@ -721,6 +830,7 @@ class Person(ModelBase): email: str = field(kw_only=True) internal_notes: str = field(alias="notes") + # revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None reveal_type(Person.__init__) @@ -736,11 +846,13 @@ the synthesized `__init__` method. ```py from typing_extensions import dataclass_transform, Any + def field_with_alias(*, alias: str | None = None, kw_only: bool = False) -> Any: ... @dataclass_transform(field_specifiers=(field_with_alias,)) def model[T](cls: type[T]) -> type[T]: return cls + @model class Person: internal_name: str = field_with_alias(alias="name") @@ -785,6 +897,7 @@ p = Person(name="Alice", internal_age=30) ```py from typing_extensions import dataclass_transform, overload, Any + @overload def fancy_field(*, init: bool = True) -> Any: ... @overload @@ -795,12 +908,14 @@ def fancy_model[T](cls: type[T]) -> type[T]: ... return cls + @fancy_model class Person: id: int = fancy_field(init=False) name: str = fancy_field() age: int | None = fancy_field(kw_only=True) + reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None ``` @@ -812,18 +927,21 @@ Make sure that models are only affected by the field specifiers of their own tra from typing_extensions import dataclass_transform, Any from dataclasses import field + def outer_field(*, init: bool = True, kw_only: bool = False) -> Any: ... @dataclass_transform(field_specifiers=(outer_field,)) def outer_model[T](cls: type[T]) -> type[T]: # ... return cls + def inner_field(*, init: bool = True, kw_only: bool = False) -> Any: ... @dataclass_transform(field_specifiers=(inner_field,)) def inner_model[T](cls: type[T]) -> type[T]: # ... return cls + @outer_model class Outer: @inner_model @@ -834,6 +952,7 @@ class Outer: outer_a: int = outer_field(init=False) outer_b: str = inner_field(init=False) + reveal_type(Outer.__init__) # revealed: (self: Outer, outer_b: str = ...) -> None reveal_type(Outer.Inner.__init__) # revealed: (self: Inner, inner_b: str = ...) -> None ``` @@ -850,6 +969,7 @@ from typing_extensions import dataclass_transform, TypeVar, Callable, overload T = TypeVar("T", bound=type) + @overload def versioned_class( cls: T, @@ -869,14 +989,17 @@ def versioned_class( ) -> T | Callable[[T], T]: raise NotImplementedError + @versioned_class class D1: x: str + @versioned_class(version=2) class D2: x: str + D1("a") D2("a") @@ -891,6 +1014,7 @@ from typing_extensions import dataclass_transform, TypeVar, Callable, overload T = TypeVar("T", bound=type) + @overload @dataclass_transform() def versioned_class( @@ -910,14 +1034,17 @@ def versioned_class( ) -> T | Callable[[T], T]: raise NotImplementedError + @versioned_class class D1: x: str + @versioned_class(version=2) class D2: x: str + D1("a") D2("a") @@ -937,17 +1064,21 @@ sure that we recognize all fields in a hierarchy like this: from dataclasses import dataclass from typing import dataclass_transform + @dataclass_transform() class ModelMeta(type): pass + class Sensor(metaclass=ModelMeta): key: int + @dataclass(frozen=True, kw_only=True) class TemperatureSensor(Sensor): name: str + t = TemperatureSensor(key=1, name="Temperature Sensor") reveal_type(t.key) # revealed: int reveal_type(t.name) # revealed: str @@ -965,15 +1096,18 @@ enables use of `dataclasses.fields`, `dataclasses.asdict`, `dataclasses.replace` from dataclasses import fields, asdict, replace, Field from typing import dataclass_transform, Any + @dataclass_transform() def create_model[T](cls: type[T]) -> type[T]: return cls + @create_model class Person: name: str age: int + p = Person("Alice", 30) reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]] @@ -990,15 +1124,19 @@ reveal_type(replace(p, name="Bob")) # revealed: Person from dataclasses import fields, asdict, replace, Field from typing import dataclass_transform, Any + @dataclass_transform() class ModelMeta(type): ... + class ModelBase(metaclass=ModelMeta): ... + class Person(ModelBase): name: str age: int + p = Person("Alice", 30) reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]] @@ -1015,13 +1153,16 @@ reveal_type(replace(p, name="Bob")) # revealed: Person from dataclasses import fields, asdict, replace, Field from typing import dataclass_transform, Any + @dataclass_transform() class ModelBase: ... + class Person(ModelBase): name: str age: int + p = Person("Alice", 30) reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]] @@ -1042,13 +1183,16 @@ When a function decorated with `@dataclass_transform()` is called directly with ```py from typing_extensions import dataclass_transform + @dataclass_transform() def my_dataclass[T](cls: type[T]) -> type[T]: return cls + class A: x: int + B = my_dataclass(A) reveal_type(B) # revealed: @@ -1061,13 +1205,16 @@ B(1) ```py from typing_extensions import dataclass_transform + @dataclass_transform() def my_dataclass[T](cls: type[T], *, order: bool = False) -> type[T]: return cls + class A: x: int + B = my_dataclass(A, order=True) reveal_type(B) # revealed: @@ -1083,6 +1230,7 @@ decorator), calling it with a class should return the class type. ```py from typing_extensions import dataclass_transform, Callable, overload + @overload @dataclass_transform() def my_dataclass[T](cls: type[T]) -> type[T]: ... @@ -1091,9 +1239,11 @@ def my_dataclass[T]() -> Callable[[type[T]], type[T]]: ... def my_dataclass[T](cls: type[T] | None = None) -> type[T] | Callable[[type[T]], type[T]]: raise NotImplementedError + class A: x: int + B = my_dataclass(A) reveal_type(B) # revealed: @@ -1109,13 +1259,16 @@ specialization should be preserved. ```py from typing_extensions import dataclass_transform + @dataclass_transform() def my_dataclass[T](cls: type[T]) -> type[T]: return cls + class A[T]: x: T + B = my_dataclass(A[int]) reveal_type(B) # revealed: @@ -1132,22 +1285,28 @@ class, not to the parameter class. ```py from typing_extensions import dataclass_transform + @dataclass_transform() def hydrated_dataclass[T](target: type[T], *, frozen: bool = False): def decorator[U](cls: type[U]) -> type[U]: return cls + return decorator + class Target: pass + decorator = hydrated_dataclass(Target) reveal_type(decorator) # revealed: + @hydrated_dataclass(Target) class Model: x: int + # Model should be a dataclass-like class with x as a field Model(x=1) reveal_type(Model.__init__) # revealed: (self: Model, x: int) -> None diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 6c352b1b75..b4dda32cff 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -9,11 +9,13 @@ decorator. By default, only the three mentioned methods are generated. ```py from dataclasses import dataclass + @dataclass class Person: name: str age: int | None = None + alice1 = Person("Alice", 30) alice2 = Person(name="Alice", age=30) alice3 = Person(age=30, name="Alice") @@ -63,12 +65,14 @@ the default value. ```py from dataclasses import dataclass + @dataclass class D: x: int y: str = "default" z: int | None = 1 + 2 + reveal_type(D.__init__) # revealed: (self: D, x: int, y: str = "default", z: int | None = 3) -> None ``` @@ -80,6 +84,7 @@ class D: x: int | None x = None + reveal_type(D.__init__) # revealed: (self: D, x: int | None = None) -> None ``` @@ -88,6 +93,7 @@ Non-fully static types are handled correctly: ```py from typing import Any + @dataclass class C: w: type[Any] @@ -95,6 +101,7 @@ class C: y: int | Any z: tuple[int, Any] + reveal_type(C.__init__) # revealed: (self: C, w: type[Any], x: Any, y: int | Any, z: tuple[int, Any]) -> None ``` @@ -106,6 +113,7 @@ class D: x: int y = 1 + reveal_type(D.__init__) # revealed: (self: D, x: int) -> None ``` @@ -125,12 +133,14 @@ Pure class attributes (`ClassVar`) are not included in the signature of `__init_ ```py from typing import ClassVar + @dataclass class D: x: int y: ClassVar[str] = "default" z: bool + reveal_type(D.__init__) # revealed: (self: D, x: int, z: bool) -> None d = D(1, True) @@ -149,6 +159,7 @@ class D: def y(self) -> str: return "" + reveal_type(D.__init__) # revealed: (self: D, x: int) -> None ``` @@ -162,6 +173,7 @@ class D: class Nested: y: str + reveal_type(D.__init__) # revealed: (self: D, x: int) -> None ``` @@ -171,8 +183,10 @@ But if there is a variable annotation with a function or class literal type, the ```py from ty_extensions import TypeOf + class SomeClass: ... + def some_function() -> None: ... @dataclass class D: @@ -180,6 +194,7 @@ class D: class_literal: TypeOf[SomeClass] class_subtype_of: type[SomeClass] + # revealed: (self: D, function_literal: def some_function() -> None, class_literal: , class_subtype_of: type[SomeClass]) -> None reveal_type(D.__init__) ``` @@ -189,10 +204,12 @@ More realistically, dataclasses can have `Callable` attributes: ```py from typing import Callable + @dataclass class D: c: Callable[[int], str] + reveal_type(D.__init__) # revealed: (self: D, c: (int, /) -> str) -> None ``` @@ -206,6 +223,7 @@ class D: def f(self, y: str) -> None: self.y: str = y + reveal_type(D(1).y) # revealed: str reveal_type(D.__init__) # revealed: (self: D, x: int) -> None @@ -220,6 +238,7 @@ class D: # (x) is an expression, not a "simple name" (x): int = 1 + # TODO: should ideally not include a `x` parameter reveal_type(D.__init__) # revealed:(self: D, x: int = 1) -> None ``` @@ -233,11 +252,13 @@ arguments are passed in: ```py from dataclasses import dataclass + @dataclass(init=True, repr=True, eq=True) class Person: name: str age: int | None = None + alice = Person("Alice", 30) reveal_type(repr(alice)) # revealed: str reveal_type(alice == alice) # revealed: bool @@ -248,10 +269,12 @@ If `init` is set to `False`, no `__init__` method is generated: ```py from dataclasses import dataclass + @dataclass(init=False) class C: x: int + C() # Okay # error: [too-many-positional-arguments] @@ -272,10 +295,12 @@ in that case `__repr__` is still available via `object.__repr__`: ```py from dataclasses import dataclass + @dataclass(repr=False) class WithoutRepr: x: int + reveal_type(WithoutRepr(1).__repr__) # revealed: bound method WithoutRepr.__repr__() -> str ``` @@ -287,10 +312,12 @@ The same is true for `__eq__`. Setting `eq=False` disables the generated `__eq__ ```py from dataclasses import dataclass + @dataclass(eq=False) class WithoutEq: x: int + reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool ``` @@ -307,19 +334,23 @@ methods will be generated: ```py from dataclasses import dataclass + @dataclass class WithoutOrder: x: int + WithoutOrder(1) < WithoutOrder(2) # error: [unsupported-operator] WithoutOrder(1) <= WithoutOrder(2) # error: [unsupported-operator] WithoutOrder(1) > WithoutOrder(2) # error: [unsupported-operator] WithoutOrder(1) >= WithoutOrder(2) # error: [unsupported-operator] + @dataclass(order=True) class WithOrder: x: int + WithOrder(1) < WithOrder(2) WithOrder(1) <= WithOrder(2) WithOrder(1) > WithOrder(2) @@ -340,10 +371,12 @@ This also works for generic dataclasses: ```py from dataclasses import dataclass + @dataclass(order=True) class GenericWithOrder[T]: x: T + GenericWithOrder[int](1) < GenericWithOrder[int](1) GenericWithOrder[int](1) < GenericWithOrder[str]("a") # error: [unsupported-operator] @@ -369,10 +402,12 @@ If `eq` and `frozen` are both `True`, a `__hash__` method is generated by defaul ```py from dataclasses import dataclass + @dataclass(eq=True, frozen=True) class WithHash: x: int + reveal_type(WithHash.__hash__) # revealed: (self: WithHash) -> int ``` @@ -382,10 +417,12 @@ is unhashable (because it is mutable): ```py from dataclasses import dataclass + @dataclass(eq=True, frozen=False) class WithoutHash: x: int + reveal_type(WithoutHash.__hash__) # revealed: None ``` @@ -397,21 +434,26 @@ not a synthetic method like in the first example. from dataclasses import dataclass from typing import Any + @dataclass(eq=False, frozen=False) class InheritHash: x: int + reveal_type(InheritHash.__hash__) # revealed: def __hash__(self) -> int + class Base: # Type the `self` parameter as `Any` to distinguish it from `object.__hash__` def __hash__(self: Any) -> int: return 42 + @dataclass(eq=False, frozen=False) class InheritHash(Base): x: int + reveal_type(InheritHash.__hash__) # revealed: def __hash__(self: Any) -> int ``` @@ -421,10 +463,12 @@ mutable: ```py from dataclasses import dataclass + @dataclass(eq=True, frozen=False, unsafe_hash=True) class WithUnsafeHash: x: int + reveal_type(WithUnsafeHash.__hash__) # revealed: (self: WithUnsafeHash) -> int ``` @@ -435,10 +479,12 @@ If true (the default is False), assigning to fields will generate a diagnostic. ```py from dataclasses import dataclass + @dataclass(frozen=True) class MyFrozenClass: x: int + frozen_instance = MyFrozenClass(1) frozen_instance.x = 2 # error: [invalid-assignment] ``` @@ -448,6 +494,7 @@ If `__setattr__()` or `__delattr__()` is defined in the class, we should emit a ```py from dataclasses import dataclass + @dataclass(frozen=True) class MyFrozenClass: x: int @@ -469,10 +516,12 @@ python-version = "3.12" ```py from dataclasses import dataclass + @dataclass(frozen=True) class MyFrozenGeneric[T]: x: T + frozen_instance = MyFrozenGeneric[int](1) frozen_instance.x = 2 # error: [invalid-assignment] ``` @@ -482,9 +531,11 @@ Attempting to mutate an unresolved attribute on a frozen dataclass: ```py from dataclasses import dataclass + @dataclass(frozen=True) class MyFrozenClass: ... + frozen = MyFrozenClass() frozen.x = 2 # error: [invalid-assignment] "Cannot assign to unresolved attribute `x` on type `MyFrozenClass`" ``` @@ -495,12 +546,15 @@ attribute in the child class: ```py from dataclasses import dataclass + @dataclass(frozen=True) class MyFrozenClass: x: int = 1 + class MyFrozenChildClass(MyFrozenClass): ... + frozen = MyFrozenChildClass() frozen.x = 2 # error: [invalid-assignment] ``` @@ -511,12 +565,15 @@ an attribute: ```py from dataclasses import dataclass + @dataclass(frozen=True) class MyFrozenClass: x: int = 1 + class MyFrozenChildClass(MyFrozenClass): ... + frozen = MyFrozenChildClass() del frozen.x # TODO this should emit an [invalid-assignment] ``` @@ -533,10 +590,12 @@ catch this error: ```py from dataclasses import dataclass + @dataclass(frozen=True) class FrozenBase: x: int + @dataclass # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`" class Child(FrozenBase): @@ -550,10 +609,12 @@ Frozen dataclasses inheriting from non-frozen dataclasses are also illegal: ```py from dataclasses import dataclass + @dataclass class Base: x: int + @dataclass(frozen=True) # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`" class FrozenChild(Base): @@ -567,6 +628,7 @@ Example of diagnostics when there are multiple files involved: ```py import dataclasses + @dataclasses.dataclass(frozen=False) class NotFrozenBase: x: int @@ -581,6 +643,7 @@ from dataclasses import dataclass from module import NotFrozenBase + @final @dataclass(frozen=True) @total_ordering # error: [invalid-total-ordering] @@ -597,31 +660,39 @@ from the list of non keyword-only parameters to the synthesized `__init__` metho ```py from dataclasses import dataclass, field + @dataclass class WithMatchArgs: normal_a: str normal_b: int kw_only: int = field(kw_only=True) + reveal_type(WithMatchArgs.__match_args__) # revealed: tuple[Literal["normal_a"], Literal["normal_b"]] + @dataclass(kw_only=True) class KwOnlyDefaultMatchArgs: normal_a: str = field(kw_only=False) normal_b: int = field(kw_only=False) kw_only: int + reveal_type(KwOnlyDefaultMatchArgs.__match_args__) # revealed: tuple[Literal["normal_a"], Literal["normal_b"]] + @dataclass(match_args=True) class ExplicitMatchArgs: normal: str + reveal_type(ExplicitMatchArgs.__match_args__) # revealed: tuple[Literal["normal"]] + @dataclass class Empty: ... + reveal_type(Empty.__match_args__) # revealed: tuple[()] ``` @@ -633,6 +704,7 @@ class NoMatchArgs: x: int y: str + NoMatchArgs.__match_args__ # error: [unresolved-attribute] ``` @@ -649,11 +721,13 @@ python-version = "3.10" ```py from dataclasses import dataclass + @dataclass(kw_only=True) class A: x: int y: int + # error: [missing-argument] "No arguments provided for required parameters `x`, `y`" # error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 2" a = A(1, 2) @@ -665,11 +739,13 @@ The class-level parameter can be overridden per-field. ```py from dataclasses import dataclass, field + @dataclass(kw_only=True) class A: a: str = field(kw_only=False) b: int = 0 + reveal_type(A.__init__) # revealed:(self: A, a: str, *, b: int = 0) -> None A("hi") @@ -684,6 +760,7 @@ class A: b: int = field(kw_only=True, default=3) a: str + A("hi") ``` @@ -692,11 +769,13 @@ The field-level `kw_only` value takes precedence over the `KW_ONLY` pseudo-type. ```py from dataclasses import field, dataclass, KW_ONLY + @dataclass class C: _: KW_ONLY x: int = field(kw_only=False) + C(x=1) C(1) ``` @@ -713,6 +792,7 @@ python-version = "3.9" ```py from dataclasses import dataclass + @dataclass(kw_only=True) # TODO: Emit a diagnostic here class A: x: int @@ -729,11 +809,13 @@ python-version = "3.13" ```py from dataclasses import dataclass, field + @dataclass class Employee: e_id: int = field(kw_only=True, default=0) name: str + Employee("Alice") Employee(name="Alice") Employee(name="Alice", e_id=1) @@ -753,12 +835,14 @@ python-version = "3.14" ```py from dataclasses import dataclass, field + @dataclass class Employee: # Python 3.14 introduces a new `doc` parameter for `dataclasses.field` e_id: int = field(kw_only=True, default=0, doc="Global employee ID") name: str + Employee("Alice") Employee(name="Alice") Employee(name="Alice", e_id=1) @@ -783,14 +867,17 @@ python-version = "3.10" ```py from dataclasses import dataclass + @dataclass class Inner: inner: int + @dataclass(kw_only=True) class Outer(Inner): outer: int + # Inherited field `inner` is positional, new field `outer` is keyword-only reveal_type(Outer.__init__) # revealed: (self: Outer, inner: int, *, outer: int) -> None @@ -806,16 +893,19 @@ This also works when the parent class uses the `KW_ONLY` sentinel: ```py from dataclasses import dataclass, KW_ONLY + @dataclass class Parent: a: int _: KW_ONLY b: str + @dataclass(kw_only=True) class Child(Parent): c: bytes + # `a` is positional (from parent), `b` is keyword-only (from parent's KW_ONLY), # `c` is keyword-only (from child's kw_only=True) reveal_type(Child.__init__) # revealed: (self: Child, a: int, *, b: str, c: bytes) -> None @@ -831,16 +921,19 @@ And when the child class uses the `KW_ONLY` sentinel while inheriting from a par ```py from dataclasses import dataclass, KW_ONLY + @dataclass class Base: x: int + @dataclass class Derived(Base): y: str _: KW_ONLY z: bytes + # `x` and `y` are positional, `z` is keyword-only (from Derived's KW_ONLY) reveal_type(Derived.__init__) # revealed: (self: Derived, x: int, y: str, *, z: bytes) -> None @@ -856,14 +949,17 @@ fields stay keyword-only while the child's fields are positional: ```py from dataclasses import dataclass + @dataclass(kw_only=True) class KwOnlyParent: parent_field: int + @dataclass class PositionalChild(KwOnlyParent): child_field: str + # `child_field` is positional (child's default), `parent_field` stays keyword-only reveal_type(PositionalChild.__init__) # revealed: (self: PositionalChild, child_field: str, *, parent_field: int) -> None @@ -882,20 +978,24 @@ is not present otherwise. from dataclasses import dataclass from typing import Tuple + @dataclass class A: x: int y: int + # revealed: Unknown # error: [unresolved-attribute] reveal_type(A.__slots__) + @dataclass(slots=True) class B: x: int y: int + reveal_type(B.__slots__) # revealed: tuple[Literal["x"], Literal["y"]] ``` @@ -912,10 +1012,12 @@ python-version = "3.11" ```py from dataclasses import dataclass + @dataclass(slots=True, weakref_slot=True) class C: x: int + reveal_type(C.__weakref__) # revealed: Any | None ``` @@ -925,9 +1027,11 @@ where the class definition was not marked with `weakref=True`: ```py from dataclasses import dataclass + @dataclass(slots=True) class C: ... + # error: [unresolved-attribute] reveal_type(C().__weakref__) # revealed: Unknown ``` @@ -981,6 +1085,7 @@ of the `__init__` signature. from dataclasses import dataclass from typing import Final, ClassVar + @dataclass class C: # a `Final` annotation without a right-hand side is not allowed in normal classes, @@ -991,6 +1096,7 @@ class C: class_variable1: ClassVar[Final[int]] = 1 class_variable2: ClassVar[Final[int]] = 1 + reveal_type(C.__init__) # revealed:(self: C, instance_variable_no_default: int, instance_variable: int = 1) -> None c = C(1) @@ -1005,12 +1111,15 @@ c.instance_variable = 2 ```py from dataclasses import dataclass + @dataclass class Base: x: int + class Derived(Base): ... + d = Derived(1) # OK reveal_type(d.x) # revealed: int ``` @@ -1020,13 +1129,16 @@ reveal_type(d.x) # revealed: int ```py from dataclasses import dataclass + class Base: x: int = 1 + @dataclass class Derived(Base): y: str + d = Derived("a") # error: [too-many-positional-arguments] @@ -1039,15 +1151,18 @@ Derived(1, "a") ```py from dataclasses import dataclass + @dataclass class Base: x: int y: str + @dataclass class Derived(Base): z: bool + d = Derived(1, "a", True) # OK reveal_type(d.x) # revealed: int @@ -1072,16 +1187,19 @@ derived class from dataclasses import dataclass from typing import Any + @dataclass class Base: x: Any = 15.0 y: int = 0 + @dataclass class C(Base): z: int = 10 x: int = 15 + reveal_type(C.__init__) # revealed:(self: C, x: int = 15, y: int = 0, z: int = 10) -> None ``` @@ -1095,6 +1213,7 @@ Fields that are defined in always-reachable branches are always present in the s ```py from dataclasses import dataclass + @dataclass class C: normal: int @@ -1105,6 +1224,7 @@ class C: if 1 + 2 == 4: never_present: bool + reveal_type(C.__init__) # revealed: (self: C, normal: int, always_present: str) -> None ``` @@ -1116,9 +1236,11 @@ alternative here would be to synthesized a union of all possible `__init__` sign ```py from dataclasses import dataclass + def flag() -> bool: return True + @dataclass class C: normal: int @@ -1126,6 +1248,7 @@ class C: if flag(): conditionally_present: str + reveal_type(C.__init__) # revealed: (self: C, normal: int, conditionally_present: str) -> None ``` @@ -1141,11 +1264,13 @@ python-version = "3.12" ```py from dataclasses import dataclass + @dataclass class DataWithDescription[T]: data: T description: str + reveal_type(DataWithDescription[int]) # revealed: d_int = DataWithDescription[int](1, "description") # OK @@ -1163,23 +1288,29 @@ This is a regression test for . ```py from dataclasses import dataclass + @dataclass class Wrap[T]: data: T + reveal_type(Wrap[int].__init__) # revealed: (self: Wrap[int], data: int) -> None + @dataclass class WrappedInt(Wrap[int]): other_field: str + reveal_type(WrappedInt.__init__) # revealed: (self: WrappedInt, data: int, other_field: str) -> None + # Make sure that another generic type parameter does not affect the `data` field @dataclass class WrappedIntAndExtraData[T](Wrap[int]): extra_data: T + # revealed: (self: WrappedIntAndExtraData[bytes], data: int, extra_data: bytes) -> None reveal_type(WrappedIntAndExtraData[bytes].__init__) ``` @@ -1194,16 +1325,20 @@ properly inferred when calling the inherited `__init__` method. ```py from dataclasses import dataclass + @dataclass class ParentDataclass[T]: value: T + # Non-dataclass inheriting from generic dataclass class ChildOfParentDataclass[T](ParentDataclass[T]): ... + def uses_dataclass[T](x: T) -> ChildOfParentDataclass[T]: return ChildOfParentDataclass(x) + # TODO: ParentDataclass.__init__ should show generic types, not Unknown # revealed: (self: ParentDataclass[Unknown], value: Unknown) -> None reveal_type(ParentDataclass.__init__) @@ -1230,6 +1365,7 @@ the type of the descriptor), and the default value is also of this type: from typing import overload from dataclasses import dataclass + class UppercaseString: _value: str = "" @@ -1239,10 +1375,12 @@ class UppercaseString: def __set__(self, instance: object, value: str) -> None: self._value = value.upper() + @dataclass class C: upper: UppercaseString = UppercaseString() + reveal_type(C.__init__) # revealed: (self: C, upper: str = ...) -> None c = C("abc") @@ -1269,6 +1407,7 @@ for the `instance` argument. from typing import Literal, overload from dataclasses import dataclass + class ConvertToLength: _len: int = 0 @@ -1285,10 +1424,12 @@ class ConvertToLength: def __set__(self, instance, value: str) -> None: self._len = len(value) + @dataclass class C: converter: ConvertToLength = ConvertToLength() + reveal_type(C.__init__) # revealed: (self: C, converter: str = "") -> None c = C("abc") @@ -1313,6 +1454,7 @@ union of all possible `value` parameter types: from typing import overload from dataclasses import dataclass + class AcceptsStrAndInt: def __get__(self, instance, owner) -> int: return 0 @@ -1324,10 +1466,12 @@ class AcceptsStrAndInt: def __set__(self, instance: object, value) -> None: pass + @dataclass class C: field: AcceptsStrAndInt = AcceptsStrAndInt() + reveal_type(C.__init__) # revealed: (self: C, field: str | int = ...) -> None ``` @@ -1344,10 +1488,12 @@ protocol checks for the presence of this attribute. It is used in the `dataclass ```py from dataclasses import dataclass, fields, asdict + @dataclass class Foo: x: int + foo = Foo(1) reveal_type(foo.__dataclass_fields__) # revealed: dict[str, Field[Any]] @@ -1393,12 +1539,14 @@ python-version = "3.10" ```py from dataclasses import dataclass, field, KW_ONLY + @dataclass class C: x: int _: KW_ONLY y: str + reveal_type(C.__init__) # revealed: (self: C, x: int, *, y: str) -> None # error: [missing-argument] @@ -1420,6 +1568,7 @@ class Fails: # error: [duplicate-kw-only] d: KW_ONLY e: bytes + reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None ``` @@ -1429,6 +1578,7 @@ This also works if `KW_ONLY` is used in a conditional branch: def flag() -> bool: return True + @dataclass class D: # error: [duplicate-kw-only] x: int @@ -1446,16 +1596,19 @@ subclasses: ```py from dataclasses import dataclass, KW_ONLY + @dataclass class D: x: int _: KW_ONLY y: str + @dataclass class E(D): z: bytes + # This should work: x=1 (positional), z=b"foo" (positional), y="foo" (keyword-only) E(1, b"foo", y="foo") @@ -1471,10 +1624,12 @@ We also understand dataclasses if they are decorated with the fully qualified na ```py import dataclasses + @dataclasses.dataclass class C: x: str + reveal_type(C.__init__) # revealed: (self: C, x: str) -> None ``` @@ -1485,6 +1640,7 @@ If a class already defines `__init__`, it is not replaced by the `dataclass` dec ```py from dataclasses import dataclass + @dataclass(init=True) class C: x: str @@ -1492,6 +1648,7 @@ class C: def __init__(self, x: int) -> None: self.x = str(x) + C(1) # OK # error: [invalid-argument-type] @@ -1506,6 +1663,7 @@ class D: def __init__(self, x: int) -> None: self.x = str(x) + D(1) # OK D() # error: [missing-argument] ``` @@ -1522,10 +1680,12 @@ dataclass_with_order = dataclass(order=True) reveal_type(dataclass_with_order) # revealed: + @dataclass_with_order class C: x: int + C(1) < C(2) # ok ``` @@ -1534,9 +1694,11 @@ C(1) < C(2) # ok ```py from dataclasses import dataclass + class B: x: int + # error: [missing-argument] dataclass(B)() @@ -1555,11 +1717,13 @@ and attributes like the MRO are unchanged: from dataclasses import dataclass from ty_extensions import reveal_mro + @dataclass class Person: name: str age: int | None = None + reveal_type(type(Person)) # revealed: reveal_type(Person.__mro__) # revealed: tuple[type, ...] reveal_mro(Person) # revealed: (, ) @@ -1590,10 +1754,12 @@ from typing import Callable from types import FunctionType from ty_extensions import CallableTypeOf, TypeOf, static_assert, is_subtype_of, is_assignable_to, is_equivalent_to + @dataclass(order=True) class C: x: int + reveal_type(C.__init__) # revealed: (self: C, x: int) -> None reveal_type(type(C.__init__)) # revealed: @@ -1601,9 +1767,11 @@ reveal_type(type(C.__init__)) # revealed: reveal_type(type(C.__init__).__code__) # revealed: CodeType reveal_type(C.__init__.__code__) # revealed: CodeType + def equivalent_signature(self: C, x: int) -> None: pass + type DunderInitType = TypeOf[C.__init__] type EquivalentPureCallableType = Callable[[C, int], None] type EquivalentFunctionLikeCallableType = CallableTypeOf[equivalent_signature] @@ -1630,6 +1798,7 @@ It should be possible to mock out synthesized methods: ```py from unittest.mock import Mock + def test_c(): c = C(1) c.__lt__ = Mock() @@ -1647,6 +1816,7 @@ from typing_extensions import TypeVar, dataclass_transform U = TypeVar("U") + @dataclass_transform(kw_only_default=True) def sequence(cls: type[U]) -> type[U]: d = dataclass( @@ -1658,6 +1828,7 @@ def sequence(cls: type[U]) -> type[U]: reveal_type(d) # revealed: type[U@sequence] & Any return d + @dataclass_transform(kw_only_default=True) def sequence2(cls: type) -> type: d = dataclass( @@ -1669,18 +1840,22 @@ def sequence2(cls: type) -> type: reveal_type(d) # revealed: type & Any return d + @dataclass_transform(kw_only_default=True) def sequence3(cls: type[U]) -> type[U]: # TODO: should reveal `type[U@sequence3]` return reveal_type(dataclass(cls)) # revealed: Unknown + @dataclass_transform(kw_only_default=True) def sequence4(cls: type) -> type: # TODO: should reveal `type` return reveal_type(dataclass(cls)) # revealed: Unknown + class Foo: ... + ordered_foo = dataclass(order=True)(Foo) reveal_type(ordered_foo) # revealed: reveal_type(ordered_foo()) # revealed: Foo @@ -1711,10 +1886,12 @@ Dynamic classes that inherit from a dataclass base also work: ```py from dataclasses import dataclass + @dataclass class Base: x: int + # Dynamic class inheriting from a dataclass DynamicChild = type("DynamicChild", (Base,), {}) DynamicChild = dataclass(DynamicChild) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md index 42a94cc36d..63070a9866 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md @@ -5,12 +5,14 @@ ```py from dataclasses import dataclass, field + @dataclass class Member: name: str role: str = field(default="user") tag: str | None = field(default=None, init=False) + # revealed: (self: Member, name: str, role: str = "user") -> None reveal_type(Member.__init__) @@ -32,11 +34,13 @@ field: from dataclasses import dataclass, field from datetime import datetime + @dataclass class Data: content: list[int] = field(default_factory=list) timestamp: datetime = field(default_factory=datetime.now, init=False) + # revealed: (self: Data, content: list[int] = ...) -> None reveal_type(Data.__init__) @@ -57,12 +61,14 @@ If `kw_only` is set to `True`, the field can only be set using keyword arguments ```py from dataclasses import dataclass, field + @dataclass class Person: name: str age: int | None = field(default=None, kw_only=True) role: str = field(default="user", kw_only=True) + # revealed: (self: Person, name: str, *, age: int | None = None, role: str = "user") -> None reveal_type(Person.__init__) @@ -77,9 +83,11 @@ bob = Person("Bob", 30) ```py from dataclasses import field + def get_default() -> str: return "default" + reveal_type(field(default=1)) # revealed: dataclasses.Field[Literal[1]] reveal_type(field(default=None)) # revealed: dataclasses.Field[None] reveal_type(field(default_factory=get_default)) # revealed: dataclasses.Field[str] @@ -97,23 +105,29 @@ from typing import TypeVar T = TypeVar("T") + @dataclass_transform() def create_model(*, init: bool = True): def deco(cls: type[T]) -> type[T]: return cls + return deco + @create_model() class A: name: str = field(init=False) + # field(init=False) should be ignored for dataclass_transform without explicit field_specifiers reveal_type(A.__init__) # revealed: (self: A, name: str) -> None + @dataclass class B: name: str = field(init=False) + # Regular @dataclass should respect field(init=False) reveal_type(B.__init__) # revealed: (self: B) -> None ``` diff --git a/crates/ty_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md index a8e353c5e6..f8bf84e148 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators.md @@ -13,9 +13,11 @@ of the decorator (which does not necessarily need to be a callable type): def custom_decorator(f) -> int: return 1 + @custom_decorator def f(x): ... + reveal_type(f) # revealed: int ``` @@ -26,13 +28,16 @@ More commonly, a decorator returns a modified callable type: ```py from typing import Callable + def ensure_positive(wrapped: Callable[[int], bool]) -> Callable[[int], bool]: return lambda x: wrapped(x) and x > 0 + @ensure_positive def even(x: int) -> bool: return x % 2 == 0 + reveal_type(even) # revealed: (int, /) -> bool reveal_type(even(4)) # revealed: bool ``` @@ -45,15 +50,19 @@ arguments: ```py from typing import Callable + def ensure_larger_than(lower_bound: int) -> Callable[[Callable[[int], bool]], Callable[[int], bool]]: def decorator(wrapped: Callable[[int], bool]) -> Callable[[int], bool]: return lambda x: wrapped(x) and x >= lower_bound + return decorator + @ensure_larger_than(10) def even(x: int) -> bool: return x % 2 == 0 + reveal_type(even) # revealed: (int, /) -> bool reveal_type(even(14)) # revealed: bool ``` @@ -67,17 +76,21 @@ meaning that the decorator closest to the function definition is applied first: def maps_to_str(f) -> str: return "a" + def maps_to_int(f) -> int: return 1 + def maps_to_bytes(f) -> bytes: return b"a" + @maps_to_str @maps_to_int @maps_to_bytes def f(x): ... + reveal_type(f) # revealed: str ``` @@ -97,10 +110,12 @@ class accept_strings: def __call__(self, x: str | int) -> bool: return self.f(int(x)) + @accept_strings def even(x: int) -> bool: return x > 0 + reveal_type(even) # revealed: accept_strings reveal_type(even.custom_attribute) # revealed: str reveal_type(even("1")) # revealed: bool @@ -121,17 +136,21 @@ implemented using `functools.wraps`. from typing import Callable from functools import wraps + def custom_decorator(f) -> Callable[[int], str]: @wraps(f) def wrapper(*args, **kwargs): print("Calling decorated function") return f(*args, **kwargs) + return wrapper + @custom_decorator def f(x: int) -> str: return str(x) + reveal_type(f) # revealed: (int, /) -> str ``` @@ -140,10 +159,12 @@ reveal_type(f) # revealed: (int, /) -> str ```py from functools import cache + @cache def f(x: int) -> int: return x**2 + # revealed: _lru_cache_wrapper[int] reveal_type(f) # revealed: int @@ -157,6 +178,7 @@ reveal_type(f(1)) def g(x: int) -> str: return "a" + # TODO: This should be `Literal[g]` or `(int, /) -> str` reveal_type(g) # revealed: Unknown ``` @@ -170,6 +192,7 @@ reveal_type(g) # revealed: Unknown @unknown_decorator def f(x): ... + reveal_type(f) # revealed: Unknown ``` @@ -180,6 +203,7 @@ reveal_type(f) # revealed: Unknown @(1 + "a") def f(x): ... + reveal_type(f) # revealed: Unknown ``` @@ -188,10 +212,12 @@ reveal_type(f) # revealed: Unknown ```py non_callable = 1 + # error: [call-non-callable] "Object of type `Literal[1]` is not callable" @non_callable def f(x): ... + reveal_type(f) # revealed: Unknown ``` @@ -206,10 +232,12 @@ first argument: def wrong_signature(f: int) -> str: return "a" + # error: [invalid-argument-type] "Argument to function `wrong_signature` is incorrect: Expected `int`, found `def f(x) -> Unknown`" @wrong_signature def f(x): ... + reveal_type(f) # revealed: str ``` @@ -221,15 +249,19 @@ Decorators need to be callable with a single argument. If they are not, we emit def takes_two_arguments(f, g) -> str: return "a" + # error: [missing-argument] "No argument provided for required parameter `g` of function `takes_two_arguments`" @takes_two_arguments def f(x): ... + reveal_type(f) # revealed: str + def takes_no_argument() -> str: return "a" + # error: [too-many-positional-arguments] "Too many positional arguments to function `takes_no_argument`: expected 0, got 1" @takes_no_argument def g(x): ... @@ -243,6 +275,7 @@ Class decorator calls are validated, emitting diagnostics for invalid arguments: def takes_int(x: int) -> int: return x + # error: [invalid-argument-type] @takes_int class Foo: ... @@ -262,10 +295,12 @@ A decorator can enforce type constraints on the class being decorated: def decorator(cls: type[int]) -> type[int]: return cls + # error: [invalid-argument-type] @decorator class Baz: ... + # TODO: the revealed type should ideally be `type[int]` (the decorator's return type) reveal_type(Baz) # revealed: ``` diff --git a/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md b/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md index 108c4b15ea..dcd478b06c 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md @@ -12,6 +12,7 @@ When a class defines `__eq__` and `__lt__`, the decorator synthesizes `__le__`, ```py from functools import total_ordering + @total_ordering class Student: def __init__(self, grade: int): @@ -25,6 +26,7 @@ class Student: def __lt__(self, other: "Student") -> bool: return self.grade < other.grade + s1 = Student(85) s2 = Student(90) @@ -47,6 +49,7 @@ other than the class itself: ```py from functools import total_ordering + @total_ordering class Comparable: def __init__(self, value: int): @@ -66,6 +69,7 @@ class Comparable: return self.value < other return NotImplemented + a = Comparable(10) b = Comparable(20) @@ -88,6 +92,7 @@ overridden. ```py from functools import total_ordering + @total_ordering class MultiSig: def __init__(self, value: int): @@ -95,13 +100,16 @@ class MultiSig: def __eq__(self, other: object) -> bool: return True + # __lt__ accepts `object` (highest priority, used as root) def __lt__(self, other: object) -> bool: return True + # __gt__ only accepts `MultiSig` (not overridden by decorator) def __gt__(self, other: "MultiSig") -> bool: return True + a = MultiSig(10) b = MultiSig(20) @@ -125,6 +133,7 @@ all overloads: from functools import total_ordering from typing import overload + @total_ordering class Flexible: def __init__(self, value: int): @@ -142,6 +151,7 @@ class Flexible: return self.value < other.value return self.value < other + a = Flexible(10) b = Flexible(20) @@ -165,6 +175,7 @@ When a class defines `__eq__` and `__gt__`, the decorator synthesizes `__lt__`, ```py from functools import total_ordering + @total_ordering class Priority: def __init__(self, level: int): @@ -178,6 +189,7 @@ class Priority: def __gt__(self, other: "Priority") -> bool: return self.level > other.level + p1 = Priority(1) p2 = Priority(2) @@ -199,6 +211,7 @@ A class only needs to define a single comparison method. The `__eq__` method can ```py from functools import total_ordering + @total_ordering class Score: def __init__(self, value: int): @@ -207,6 +220,7 @@ class Score: def __lt__(self, other: "Score") -> bool: return self.value < other.value + s1 = Score(85) s2 = Score(90) @@ -226,10 +240,12 @@ The decorator also works when the ordering method is inherited from a superclass ```py from functools import total_ordering + class Base: def __lt__(self, other: "Base") -> bool: return True + @total_ordering class Child(Base): def __eq__(self, other: object) -> bool: @@ -237,6 +253,7 @@ class Child(Base): return NotImplemented return True + c1 = Child() c2 = Child() @@ -256,16 +273,19 @@ over the locally-defined `__gt__`: from functools import total_ordering from typing import Literal + class Base: def __lt__(self, other: "Base") -> Literal[True]: return True + @total_ordering class Child(Base): # __gt__ is defined locally, but __lt__ (inherited) takes precedence def __gt__(self, other: "Child") -> Literal[False]: return False + c1 = Child() c2 = Child() @@ -290,6 +310,7 @@ We use a narrower return type (`Literal[True]`) to verify that the explicit meth from functools import total_ordering from typing import Literal + @total_ordering class Temperature: def __init__(self, celsius: float): @@ -301,6 +322,7 @@ class Temperature: def __gt__(self, other: "Temperature") -> Literal[True]: return True + t1 = Temperature(20.0) t2 = Temperature(25.0) @@ -321,6 +343,7 @@ The decorator works with `@dataclass`: from dataclasses import dataclass from functools import total_ordering + @total_ordering @dataclass class Point: @@ -330,6 +353,7 @@ class Point: def __lt__(self, other: "Point") -> bool: return (self.x, self.y) < (other.x, other.y) + p1 = Point(1, 2) p2 = Point(3, 4) @@ -353,11 +377,13 @@ a diagnostic is emitted at the decorator site: ```py from functools import total_ordering + @total_ordering # error: [invalid-total-ordering] class NoOrdering: def __eq__(self, other: object) -> bool: return True + n1 = NoOrdering() n2 = NoOrdering() @@ -384,6 +410,7 @@ class NoDecorator: def __lt__(self, other: "NoDecorator") -> bool: return self.value < other.value + n1 = NoDecorator(1) n2 = NoDecorator(2) @@ -416,6 +443,7 @@ simplifies to `int`: ```py from functools import total_ordering + @total_ordering class IntReturn: def __init__(self, value: int): @@ -429,6 +457,7 @@ class IntReturn: def __lt__(self, other: "IntReturn") -> int: return self.value - other.value + a = IntReturn(10) b = IntReturn(20) @@ -447,6 +476,7 @@ When the root method returns a type that is not a supertype of `bool`, the union ```py from functools import total_ordering + @total_ordering class StrReturn: def __init__(self, value: str): @@ -460,6 +490,7 @@ class StrReturn: def __lt__(self, other: "StrReturn") -> str: return self.value + a = StrReturn("a") b = StrReturn("b") @@ -480,10 +511,12 @@ performed: ```py from functools import total_ordering + class NoOrderingMethod: def __eq__(self, other: object) -> bool: return True + # error: [invalid-total-ordering] InvalidOrderedClass = total_ordering(NoOrderingMethod) ``` @@ -493,6 +526,7 @@ When the class does define an ordering method, no error is emitted: ```py from functools import total_ordering + class HasOrderingMethod: def __eq__(self, other: object) -> bool: return True @@ -500,6 +534,7 @@ class HasOrderingMethod: def __lt__(self, other: "HasOrderingMethod") -> bool: return True + # No error (class defines `__lt__`). ValidOrderedClass = total_ordering(HasOrderingMethod) reveal_type(ValidOrderedClass) # revealed: type[HasOrderingMethod] @@ -512,9 +547,11 @@ When `total_ordering` is called on a class created with `type()`, the same valid ```py from functools import total_ordering + def lt_impl(self, other) -> bool: return True + # No error: the functional class defines `__lt__` in its namespace ValidFunctional = total_ordering(type("ValidFunctional", (), {"__lt__": lt_impl})) @@ -531,21 +568,26 @@ correctly detects it: ```py from functools import total_ordering + def lt_impl(self, other) -> bool: return True + def eq_impl(self, other) -> bool: return True + # Functional class with __lt__ method OrderedBase = type("OrderedBase", (), {"__lt__": lt_impl}) + # A class inheriting from OrderedBase gets the ordering method @total_ordering class Ordered(OrderedBase): def __eq__(self, other: object) -> bool: return True + o1 = Ordered() o2 = Ordered() @@ -566,6 +608,7 @@ from functools import total_ordering # Dynamic class without ordering methods (invalid for @total_ordering) NoOrderBase = type("NoOrderBase", (), {}) + @total_ordering # error: [invalid-total-ordering] class NoOrder(NoOrderBase): def __eq__(self, other: object) -> bool: @@ -582,6 +625,7 @@ passed to `@total_ordering`: from functools import total_ordering from typing import Any + def f(ns: dict[str, Any]): # Dynamic class with dynamic namespace - might have ordering methods DynamicBase = type("DynamicBase", (), ns) diff --git a/crates/ty_python_semantic/resources/mdtest/del.md b/crates/ty_python_semantic/resources/mdtest/del.md index 945502ee82..17673bcbde 100644 --- a/crates/ty_python_semantic/resources/mdtest/del.md +++ b/crates/ty_python_semantic/resources/mdtest/del.md @@ -21,9 +21,11 @@ reveal_type(x) # revealed: Unknown # error: [unresolved-reference] reveal_type(y) # revealed: Unknown + def cond() -> bool: return True + b = 1 if cond(): del b @@ -42,17 +44,21 @@ reveal_type(c) # revealed: Literal[2] d = [1, 2, 3] + def delete(): del d # error: [unresolved-reference] "Name `d` used when not defined" + delete() reveal_type(d) # revealed: list[Unknown | int] + def delete_element(): # When the `del` target isn't a name, it doesn't force local resolution. del d[0] print(d) + def delete_global(): global d del d @@ -60,10 +66,12 @@ def delete_global(): # be careful about false positives if `d` got reinitialized somehow in between the two `del`s. del d + delete_global() # Again, the variable should have been removed, but we don't check it. reveal_type(d) # revealed: list[Unknown | int] + def delete_nonlocal(): e = 2 @@ -86,6 +94,7 @@ local error: ```py x = 1 + def foo(): print(x) # error: [unresolved-reference] "Name `x` used when not defined" if False: @@ -104,11 +113,14 @@ However, with `global x` in `foo`, `print(x)` in `bar` resolves in the global sc ```py x = 1 + def foo(): global x + def bar(): # allowed, refers to `x` in the global scope reveal_type(x) # revealed: Literal[1] + bar() del x # allowed, deletes `x` in the global scope (though we don't track that) ``` @@ -119,11 +131,14 @@ refer to: ```py def enclosing(): x = 2 + def foo(): nonlocal x + def bar(): # allowed, refers to `x` in `enclosing` reveal_type(x) # revealed: Literal[2] + bar() del x # allowed, deletes `x` in `enclosing` (though we don't track that) ``` @@ -141,6 +156,7 @@ assignment, and the attribute type will be the originally declared type. class C: x: int = 1 + c = C() del c.x reveal_type(c.x) # revealed: int @@ -160,6 +176,7 @@ reveal_type(c.x) # revealed: int class C: x: int = 1 + c = C() reveal_type(c.x) # revealed: int @@ -202,17 +219,21 @@ from typing import Protocol, TypeVar KT = TypeVar("KT") + class CanDelItem(Protocol[KT]): def __delitem__(self, k: KT, /) -> None: ... + def f(x: CanDelItem[int], k: int): # This should be valid - the object has __delitem__ del x[k] + class OnlyDelItem: def __delitem__(self, key: int) -> None: pass + d = OnlyDelItem() del d[0] # OK @@ -229,6 +250,7 @@ class OnlyGetItem: def __getitem__(self, key: int) -> str: return "value" + g = OnlyGetItem() reveal_type(g[0]) # revealed: str @@ -247,18 +269,22 @@ a valid instance of that TypedDict type. However, deleting `NotRequired` keys (o ```py from typing_extensions import TypedDict, NotRequired + class Movie(TypedDict): name: str year: int + class PartialMovie(TypedDict, total=False): name: str year: int + class MixedMovie(TypedDict): name: str year: NotRequired[int] + m: Movie = {"name": "Blade Runner", "year": 1982} p: PartialMovie = {"name": "Test"} mixed: MixedMovie = {"name": "Test"} diff --git a/crates/ty_python_semantic/resources/mdtest/deprecated.md b/crates/ty_python_semantic/resources/mdtest/deprecated.md index 9497c77fe4..8458afaafe 100644 --- a/crates/ty_python_semantic/resources/mdtest/deprecated.md +++ b/crates/ty_python_semantic/resources/mdtest/deprecated.md @@ -10,30 +10,36 @@ classes. Uses of these items should subsequently produce a warning. ```py from typing_extensions import deprecated + @deprecated("use OtherClass") def myfunc(x: int): ... + myfunc(1) # error: [deprecated] "use OtherClass" ``` ```py from typing_extensions import deprecated + @deprecated("use BetterClass") class MyClass: ... + MyClass() # error: [deprecated] "use BetterClass" ``` ```py from typing_extensions import deprecated + class MyClass: @deprecated("use something else") def afunc(): ... @deprecated("don't use this!") def amethod(self): ... + MyClass.afunc() # error: [deprecated] "use something else" MyClass().amethod() # error: [deprecated] "don't use this!" ``` @@ -59,18 +65,22 @@ runtime behavior. ```py from typing_extensions import deprecated + @deprecated # error: [invalid-argument-type] "LiteralString" def invalid_deco(): ... + invalid_deco() # error: [missing-argument] ``` ```py from typing_extensions import deprecated + @deprecated() # error: [missing-argument] "message" def invalid_deco(): ... + invalid_deco() ``` @@ -82,9 +92,11 @@ from typing_extensions import deprecated x = "message" + @deprecated(x) def invalid_deco(): ... + invalid_deco() # error: [deprecated] "message" ``` @@ -93,12 +105,15 @@ However sufficiently opaque LiteralStrings we can't resolve, and so we lose the ```py from typing_extensions import deprecated, LiteralString + def opaque() -> LiteralString: return "message" + @deprecated(opaque()) def valid_deco(): ... + valid_deco() # error: [deprecated] ``` @@ -108,12 +123,15 @@ LiteralString, so we can/should emit a diagnostic for this: ```py from typing_extensions import deprecated + def opaque() -> str: return "message" + @deprecated(opaque()) # error: [invalid-argument-type] "LiteralString" def dubious_deco(): ... + dubious_deco() ``` @@ -122,9 +140,11 @@ Although we have no use for the other arguments, we should still error if they'r ```py from typing_extensions import deprecated + @deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf" def invalid_deco(): ... + invalid_deco() ``` @@ -133,9 +153,11 @@ And we should always handle correct ones fine. ```py from typing_extensions import deprecated + @deprecated("some message", category=DeprecationWarning, stacklevel=1) def valid_deco(): ... + valid_deco() # error: [deprecated] "some message" ``` @@ -155,11 +177,13 @@ python-version = "3.13" import warnings import typing_extensions + @warnings.deprecated("nope") def func1(): ... @typing_extensions.deprecated("nada") def func2(): ... + func1() # error: [deprecated] "nope" func2() # error: [deprecated] "nada" ``` @@ -176,9 +200,11 @@ shouldn't produce a warning. ```py from typing_extensions import deprecated + @deprecated("Use OtherType instead") class DeprType: ... + @deprecated("Use other_func instead") def depr_func(): ... ``` @@ -194,8 +220,10 @@ from module import DeprType, depr_func DeprType() # error: [deprecated] "Use OtherType instead" depr_func() # error: [deprecated] "Use other_func instead" + def higher_order(x): ... + # TODO: these diagnostics ideally shouldn't fire since we warn on the import higher_order(DeprType) # error: [deprecated] "Use OtherType instead" higher_order(depr_func) # error: [deprecated] "Use other_func instead" @@ -215,9 +243,11 @@ a warning. ```py from typing_extensions import deprecated + @deprecated("Use OtherType instead") class DeprType: ... + @deprecated("Use other_func instead") def depr_func(): ... ``` @@ -230,8 +260,10 @@ import module module.DeprType() # error: [deprecated] "Use OtherType instead" module.depr_func() # error: [deprecated] "Use other_func instead" + def higher_order(x): ... + higher_order(module.DeprType) # error: [deprecated] "Use OtherType instead" higher_order(module.depr_func) # error: [deprecated] "Use other_func instead" @@ -248,9 +280,11 @@ If the items are instead star-imported, then the actual uses should warn. ```py from typing_extensions import deprecated + @deprecated("Use OtherType instead") class DeprType: ... + @deprecated("Use other_func instead") def depr_func(): ... ``` @@ -263,8 +297,10 @@ from module import * DeprType() # error: [deprecated] "Use OtherType instead" depr_func() # error: [deprecated] "Use other_func instead" + def higher_order(x): ... + higher_order(DeprType) # error: [deprecated] "Use OtherType instead" higher_order(depr_func) # error: [deprecated] "Use other_func instead" @@ -281,12 +317,15 @@ redundant and annoying. ```py from typing_extensions import deprecated + @deprecated("Use OtherType instead") class DeprType: ... + @deprecated("Use other_func instead") def depr_func(): ... + alias_func = depr_func # error: [deprecated] "Use other_func instead" AliasClass = DeprType # error: [deprecated] "Use OtherType instead" @@ -303,6 +342,7 @@ diagnostic. ```py from typing_extensions import deprecated + class MyInt: def __init__(self, val): self.val = val @@ -311,6 +351,7 @@ class MyInt: def __add__(self, other): return MyInt(self.val + other.val) + x = MyInt(1) y = MyInt(2) z = x + y # TODO error: [deprecated] "MyInt `+` support is broken" @@ -324,6 +365,7 @@ Overloads can be deprecated, but only trigger warnings when invoked. from typing_extensions import deprecated from typing_extensions import overload + @overload @deprecated("strings are no longer supported") def f(x: str): ... @@ -332,6 +374,7 @@ def f(x: int): ... def f(x): print(x) + f(1) f("hello") # TODO: error: [deprecated] "strings are no longer supported" ``` @@ -342,6 +385,7 @@ If the actual impl is deprecated, the deprecation always fires. from typing_extensions import deprecated from typing_extensions import overload + @overload def f(x: str): ... @overload @@ -350,6 +394,7 @@ def f(x: int): ... def f(x): print(x) + f(1) # error: [deprecated] "unusable" f("hello") # error: [deprecated] "unusable" ``` diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index ce59da02c5..1a363aa247 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -16,6 +16,7 @@ descriptor that returns a constant value: ```py from typing import Literal + class Ten: def __get__(self, instance: object, owner: type | None = None) -> Literal[10]: return 10 @@ -23,9 +24,11 @@ class Ten: def __set__(self, instance: object, value: Literal[10]) -> None: pass + class C: ten: Ten = Ten() + c = C() reveal_type(c.ten) # revealed: Literal[10] @@ -66,9 +69,11 @@ class FlexibleInt: def __set__(self, instance: object, value: int | str) -> None: self._value = int(value) + class C: flexible_int: FlexibleInt = FlexibleInt() + c = C() reveal_type(c.flexible_int) # revealed: int | None @@ -97,6 +102,7 @@ non-data descriptors. ```py from typing import Literal + class DataDescriptor: def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]: return "data" @@ -104,10 +110,12 @@ class DataDescriptor: def __set__(self, instance: object, value: int) -> None: pass + class NonDataDescriptor: def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]: return "non-data" + class C: data_descriptor = DataDescriptor() non_data_descriptor = NonDataDescriptor() @@ -123,6 +131,7 @@ class C: # So it is possible to override them. self.non_data_descriptor = 1 + c = C() reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data"] @@ -148,6 +157,7 @@ all possible results accordingly. We start by defining a data and a non-data des ```py from typing import Literal + class DataDescriptor: def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]: return "data" @@ -155,6 +165,7 @@ class DataDescriptor: def __set__(self, instance: object, value: int) -> None: pass + class NonDataDescriptor: def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]: return "non-data" @@ -186,8 +197,10 @@ descriptor here: class C2: def f(self): self.attr = "normal" + attr = NonDataDescriptor() + reveal_type(C2().attr) # revealed: Unknown | Literal["non-data", "normal"] # Assignments always go to the instance attribute in this case @@ -201,14 +214,17 @@ Descriptors only work when used as class variables. When put in instances, they ```py from typing import Literal + class Ten: def __get__(self, instance: object, owner: type | None = None) -> Literal[10]: return 10 + class C: def __init__(self): self.ten: Ten = Ten() + reveal_type(C().ten) # revealed: Ten C().ten = Ten() @@ -233,6 +249,7 @@ To verify this, we define a data and a non-data descriptor: ```py from typing import Literal, Any + class DataDescriptor: def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]: return "data" @@ -240,6 +257,7 @@ class DataDescriptor: def __set__(self, instance: object, value: int) -> None: pass + class NonDataDescriptor: def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]: return "non-data" @@ -253,10 +271,12 @@ class Meta1(type): meta_data_descriptor: DataDescriptor = DataDescriptor() meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor() + class C1(metaclass=Meta1): class_data_descriptor: DataDescriptor = DataDescriptor() class_non_data_descriptor: NonDataDescriptor = NonDataDescriptor() + reveal_type(C1.meta_data_descriptor) # revealed: Literal["data"] reveal_type(C1.meta_non_data_descriptor) # revealed: Literal["non-data"] @@ -293,6 +313,7 @@ class Meta2(type): meta_data_descriptor1: DataDescriptor = DataDescriptor() meta_data_descriptor2: DataDescriptor = DataDescriptor() + class ClassLevelDataDescriptor: def __get__(self, instance: object, owner: type | None = None) -> Literal["class level data descriptor"]: return "class level data descriptor" @@ -300,10 +321,12 @@ class ClassLevelDataDescriptor: def __set__(self, instance: object, value: str) -> None: pass + class C2(metaclass=Meta2): meta_data_descriptor1: Literal["value on class"] = "value on class" meta_data_descriptor2: ClassLevelDataDescriptor = ClassLevelDataDescriptor() + reveal_type(C2.meta_data_descriptor1) # revealed: Literal["data"] reveal_type(C2.meta_data_descriptor2) # revealed: Literal["data"] @@ -326,12 +349,14 @@ class Meta3(type): meta_non_data_descriptor1: NonDataDescriptor = NonDataDescriptor() meta_non_data_descriptor2: NonDataDescriptor = NonDataDescriptor() + class C3(metaclass=Meta3): meta_attribute1: Literal["value on class"] = "value on class" meta_attribute2: ClassLevelDataDescriptor = ClassLevelDataDescriptor() meta_non_data_descriptor1: Literal["value on class"] = "value on class" meta_non_data_descriptor2: ClassLevelDataDescriptor = ClassLevelDataDescriptor() + reveal_type(C3.meta_attribute1) # revealed: Literal["value on class"] reveal_type(C3.meta_attribute2) # revealed: Literal["class level data descriptor"] reveal_type(C3.meta_non_data_descriptor1) # revealed: Literal["value on class"] @@ -346,8 +371,10 @@ class Meta4(type): meta_attribute: Literal["value on metaclass"] = "value on metaclass" meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor() + class C4(metaclass=Meta4): ... + reveal_type(C4.meta_attribute) # revealed: Literal["value on metaclass"] reveal_type(C4.meta_non_data_descriptor) # revealed: Literal["non-data"] ``` @@ -386,6 +413,7 @@ metaclass attribute (unless it's a data descriptor, which always takes precedenc ```py from typing import Any + def _(flag: bool): class Meta6(type): attribute1: DataDescriptor = DataDescriptor() @@ -448,6 +476,7 @@ when it is accessed on an instance. A real-world example of this is the `__get__ ```py from typing_extensions import Literal, LiteralString, overload + class Descriptor: @overload def __get__(self, instance: None, owner: type, /) -> Literal["called on class object"]: ... @@ -459,9 +488,11 @@ class Descriptor: else: return "called on class object" + class C: d: Descriptor = Descriptor() + reveal_type(C.d) # revealed: Literal["called on class object"] reveal_type(C().d) # revealed: Literal["called on instance"] @@ -479,13 +510,16 @@ class SomeCallable: def __call__(self, x: int) -> str: return "a" + class Descriptor: def __get__(self, instance: object, owner: type | None = None) -> SomeCallable: return SomeCallable() + class B: __call__: Descriptor = Descriptor() + b_instance = B() reveal_type(b_instance(1)) # revealed: str @@ -512,6 +546,7 @@ class C: def name(self, value: str | None) -> None: self._value = value + c = C() reveal_type(c._name) # revealed: str | None @@ -550,6 +585,7 @@ class Base: def other(self, v: float) -> None: self.value = v + class Derived(Base): @property def other(self) -> float: @@ -573,6 +609,7 @@ class DontAssignToMe: @property def immutable(self): ... + # error: [invalid-assignment] DontAssignToMe().immutable = "the properties, they are a-changing" ``` @@ -595,6 +632,7 @@ class C: def get_name(cls) -> str: return cls.__name__ + c1 = C.factory("test") # okay reveal_type(c1) # revealed: C @@ -612,6 +650,7 @@ class C: def helper(value: str) -> str: return value + reveal_type(C.helper("42")) # revealed: str c = C() reveal_type(c.helper("string")) # revealed: str @@ -628,9 +667,11 @@ import types from inspect import getattr_static from ty_extensions import static_assert, is_subtype_of, TypeOf + def f(x: object) -> str: return "a" + reveal_type(f) # revealed: def f(x: object) -> str reveal_type(f.__get__) # revealed: static_assert(is_subtype_of(TypeOf[f.__get__], types.MethodWrapperType)) @@ -655,6 +696,7 @@ We can also bind the free function `f` to an instance of a class `C`: ```py class C: ... + bound_method = wrapper_descriptor(f, C(), C) reveal_type(bound_method) # revealed: bound method C.f() -> str @@ -708,25 +750,31 @@ This test makes sure that we call `__get__` with the right argument types for va ```py from __future__ import annotations + class TailoredForClassObjectAccess: def __get__(self, instance: None, owner: type[C]) -> int: return 1 + class TailoredForInstanceAccess: def __get__(self, instance: C, owner: type[C] | None = None) -> str: return "a" + class TailoredForMetaclassAccess: def __get__(self, instance: type[C], owner: type[Meta]) -> bytes: return b"a" + class Meta(type): metaclass_access: TailoredForMetaclassAccess = TailoredForMetaclassAccess() + class C(metaclass=Meta): class_object_access: TailoredForClassObjectAccess = TailoredForClassObjectAccess() instance_access: TailoredForInstanceAccess = TailoredForInstanceAccess() + reveal_type(C.class_object_access) # revealed: int reveal_type(C().instance_access) # revealed: str reveal_type(C.metaclass_access) # revealed: bytes @@ -752,9 +800,11 @@ class Descriptor: def __get__(self) -> int: return 1 + class C: descriptor: Descriptor = Descriptor() + # TODO: This should be an error reveal_type(C.descriptor) # revealed: int @@ -772,9 +822,11 @@ call `__get__`" on the descriptor object (leading us to infer `Unknown`): class BrokenDescriptor: __get__: None = None + class Foo: desc: BrokenDescriptor = BrokenDescriptor() + # TODO: this raises `TypeError` at runtime due to the implicit call to `__get__`; # we should emit a diagnostic reveal_type(Foo().desc) # revealed: Unknown @@ -794,9 +846,11 @@ class Descriptor: def __set__(self, instance: object, value: int) -> None: pass + class C: descriptor = Descriptor() + C.descriptor = "something else" reveal_type(C.descriptor) # revealed: Literal["something else"] ``` @@ -811,10 +865,12 @@ class DataDescriptor: def __set__(self, instance: int, value) -> None: pass + class NonDataDescriptor: def __get__(self, instance: object, owner: type | None = None) -> int: return 1 + def _(flag: bool): class PossiblyUnbound: if flag: @@ -840,6 +896,7 @@ def _(flag: bool): def _(flag: bool): class MaybeDescriptor: if flag: + def __get__(self, instance: object, owner: type | None = None) -> int: return 1 @@ -859,36 +916,46 @@ descriptor protocol on the callable's `__call__` method: ```py from __future__ import annotations + class ReturnedCallable2: def __call__(self, descriptor: Descriptor1, instance: None, owner: type[C]) -> int: return 1 + class ReturnedCallable1: def __call__(self, descriptor: Descriptor2, instance: Callable1, owner: type[Callable1]) -> ReturnedCallable2: return ReturnedCallable2() + class Callable3: def __call__(self, descriptor: Descriptor3, instance: Callable2, owner: type[Callable2]) -> ReturnedCallable1: return ReturnedCallable1() + class Descriptor3: __get__: Callable3 = Callable3() + class Callable2: __call__: Descriptor3 = Descriptor3() + class Descriptor2: __get__: Callable2 = Callable2() + class Callable1: __call__: Descriptor2 = Descriptor2() + class Descriptor1: __get__: Callable1 = Callable1() + class C: d: Descriptor1 = Descriptor1() + reveal_type(C.d) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md index 61816ff56f..e8031f1d38 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md @@ -13,6 +13,7 @@ These can be set on instances and on class objects. class C: attr: int = 0 + instance = C() instance.attr = 1 # fine instance.attr = "wrong" # error: [invalid-assignment] @@ -31,6 +32,7 @@ class C: def __init__(self): self.attr: int = 0 + instance = C() instance.attr = 1 # fine instance.attr = "wrong" # error: [invalid-assignment] @@ -46,9 +48,11 @@ diagnostic that mentions that the attribute is only available on class objects. ```py from typing import ClassVar + class C: attr: ClassVar[int] = 0 + C.attr = 1 # fine C.attr = "wrong" # error: [invalid-assignment] @@ -63,6 +67,7 @@ When trying to set an attribute that is not defined, we also emit errors: ```py class C: ... + C.non_existent = 1 # error: [unresolved-attribute] instance = C() @@ -97,9 +102,11 @@ class Descriptor: def __set__(self, instance: object, value: int) -> None: pass + class C: attr: Descriptor = Descriptor() + instance = C() instance.attr = 1 # fine @@ -114,9 +121,11 @@ class WrongDescriptor: def __set__(self, instance: object, value: int, extra: int) -> None: pass + class C: attr: WrongDescriptor = WrongDescriptor() + instance = C() # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) @@ -128,10 +137,12 @@ instance.attr = 1 # error: [invalid-assignment] ```py def _(flag: bool) -> None: if flag: + class C1: attr: int = 0 else: + class C1: attr: str = "" diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md index 8d4761dbde..205379fed6 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md @@ -11,6 +11,7 @@ to the invalid argument. def foo(x: int) -> int: return x * x + foo("hello") # error: [invalid-argument-type] ``` @@ -22,6 +23,7 @@ This is like the basic test, except we put the call site above the function defi def bar(): foo("hello") # error: [invalid-argument-type] + def foo(x: int) -> int: return x * x ``` @@ -52,6 +54,7 @@ This checks that a diagnostic renders reasonably when there are multiple paramet def foo(x: int, y: int, z: int) -> int: return x * y * z + foo(1, "hello", 3) # error: [invalid-argument-type] ``` @@ -68,6 +71,7 @@ def foo( ) -> int: return x * y * z + foo(1, "hello", 3) # error: [invalid-argument-type] ``` @@ -80,6 +84,7 @@ invalid argument types. def foo(x: int, y: int, z: int) -> int: return x * y * z + # error: [invalid-argument-type] # error: [invalid-argument-type] # error: [invalid-argument-type] @@ -114,6 +119,7 @@ Tests a function definition with only positional parameters. def foo(x: int, y: int, z: int, /) -> int: return x * y * z + foo(1, "hello", 3) # error: [invalid-argument-type] ``` @@ -125,6 +131,7 @@ Tests a function definition with variadic arguments. def foo(*numbers: int) -> int: return len(numbers) + foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type] ``` @@ -136,6 +143,7 @@ Tests a function definition with keyword-only arguments. def foo(x: int, y: int, *, z: int = 0) -> int: return x * y * z + foo(1, 2, z="hello") # error: [invalid-argument-type] ``` @@ -147,6 +155,7 @@ Tests a function definition with keyword-only arguments. def foo(x: int, y: int, z: int = 0) -> int: return x * y * z + foo(1, 2, "hello") # error: [invalid-argument-type] ``` @@ -156,6 +165,7 @@ foo(1, 2, "hello") # error: [invalid-argument-type] def foo(**numbers: int) -> int: return len(numbers) + foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type] ``` @@ -167,6 +177,7 @@ Tests a function definition with multiple different kinds of arguments. def foo(x: int, /, y: int, *, z: int = 0) -> int: return x * y * z + foo(1, 2, z="hello") # error: [invalid-argument-type] ``` @@ -179,6 +190,7 @@ class C: def __call__(self, x: int) -> int: return 1 + c = C() c("wrong") # error: [invalid-argument-type] ``` @@ -192,6 +204,7 @@ class C: def square(self, x: int) -> int: return x * x + c = C() c.square("hello") # error: [invalid-argument-type] ``` @@ -203,6 +216,7 @@ c.square("hello") # error: [invalid-argument-type] ```py class Foo: ... + def needs_a_foo(x: Foo): ... ``` @@ -211,8 +225,10 @@ def needs_a_foo(x: Foo): ... ```py from module import needs_a_foo + class Foo: ... + needs_a_foo(Foo()) # error: [invalid-argument-type] ``` @@ -230,6 +246,7 @@ python-version = "3.12" ```py class Foo: ... + def needs_a_foo(x: Foo): ... ``` @@ -238,8 +255,10 @@ def needs_a_foo(x: Foo): ... ```py from module import needs_a_foo + class Foo: ... + def f[T: Foo](x: T) -> T: needs_a_foo(x) # error: [invalid-argument-type] return x diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md index 24f5f98c49..fd010be5fb 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md @@ -19,6 +19,7 @@ This diagnostic also points to the class definition if available. class MissingAwait: pass + async def main() -> None: await MissingAwait() # error: [invalid-await] ``` @@ -30,11 +31,14 @@ This diagnostic also points to the method definition if available. ```py from datetime import datetime + class PossiblyUnbound: if datetime.today().weekday() == 0: + def __await__(self): yield + async def main() -> None: await PossiblyUnbound() # error: [invalid-await] ``` @@ -50,6 +54,7 @@ class InvalidAwaitArgs: def __await__(self, value: int): yield value + async def main() -> None: await InvalidAwaitArgs() # error: [invalid-await] ``` @@ -63,6 +68,7 @@ awaitable. class NonCallableAwait: __await__ = 42 + async def main() -> None: await NonCallableAwait() # error: [invalid-await] ``` @@ -77,6 +83,7 @@ class InvalidAwaitReturn: def __await__(self) -> int: return 5 + async def main() -> None: await InvalidAwaitReturn() # error: [invalid-await] ``` @@ -90,16 +97,19 @@ instance to be awaitable. In this specific case, no specific function definition import typing from datetime import datetime + class UnawaitableUnion: if datetime.today().weekday() == 6: def __await__(self) -> typing.Generator[typing.Any, None, None]: yield + else: def __await__(self) -> int: return 5 + async def main() -> None: await UnawaitableUnion() # error: [invalid-await] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md index 705ceae6b6..34c9cf3912 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md @@ -17,25 +17,32 @@ T3 = TypeVar("T3") DefaultStrT = TypeVar("DefaultStrT", default=str) + class SubclassMe(Generic[T1, DefaultStrT]): x: DefaultStrT + class Baz(SubclassMe[int, DefaultStrT]): pass + # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default" class Foo(Generic[T1, T2]): pass + class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class] pass + class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class] pass + class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class] pass + class VeryBad( Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class] Generic[T1, T2, DefaultStrT, T3], diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md index 02661d5cc0..56a4841d04 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md @@ -102,9 +102,11 @@ T = TypeVar("T", covariant=True, contravariant=True) ```py from typing_extensions import TypeVar + def cond() -> bool: return True + # error: [invalid-legacy-type-variable] T = TypeVar("T", covariant=cond()) diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md index 24a6e552b2..b55370a8fc 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md @@ -13,6 +13,7 @@ too verbose for it to be worth it. def f(a, b=42): ... def g(a, b): ... + class Foo: def method(self, a): ... ``` @@ -24,9 +25,11 @@ from module import f, g, Foo f() # error: [missing-argument] + def coinflip() -> bool: return True + h = f if coinflip() else g # error: [missing-argument] diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md index 19627f8351..5ee6620760 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md @@ -7,6 +7,7 @@ ```py from typing import overload + @overload def f(x: int) -> int: ... @overload @@ -14,6 +15,7 @@ def f(x: str) -> str: ... def f(x: int | str) -> int | str: return x + f(b"foo") # error: [no-matching-overload] ``` @@ -27,8 +29,10 @@ Which in turn makes snapshotting a bit annoying, since the output can depend on ```py from typing import overload + class Foo: ... + @overload def foo(a: int, b: int, c: int): ... @overload @@ -73,6 +77,7 @@ def foo(a: str, b: float, c: float): ... def foo(a: float, b: float, c: float): ... def foo(a, b, c): ... + foo(Foo(), Foo()) # error: [no-matching-overload] ``` @@ -84,8 +89,10 @@ cut off the list in the diagnostic and emit a message stating the number of omit ```py from typing import overload + class Foo: ... + @overload def foo(a: int, b: int, c: int): ... @overload @@ -210,6 +217,7 @@ def foo(a: float, b: float, c: bool): ... def foo(a: bool, b: float, c: float): ... def foo(a, b, c): ... + foo(Foo(), Foo()) # error: [no-matching-overload] ``` @@ -218,6 +226,7 @@ foo(Foo(), Foo()) # error: [no-matching-overload] ```py from typing import overload + @overload def f( lion: int, @@ -276,6 +285,7 @@ def f( ) -> int | str: return 0 + f(b"foo") # error: [no-matching-overload] ``` @@ -284,6 +294,7 @@ f(b"foo") # error: [no-matching-overload] ```py from typing import overload + class Foo: @overload def bar(self, x: int) -> int: ... @@ -292,6 +303,7 @@ class Foo: def bar(self, x: int | str) -> int | str: return x + foo = Foo() foo.bar(b"wat") # error: [no-matching-overload] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md index 92d6e33eb6..302bee3ddc 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md @@ -11,10 +11,12 @@ class A: class B: pass + class C: class B: pass + a: A.B = C.B() # error: [invalid-assignment] "Object of type `test.C.B` is not assignable to `test.A.B`" ``` @@ -26,6 +28,7 @@ a: A.B = C.B() # error: [invalid-assignment] "Object of type `test.C.B` is not class B: pass + def f(b: B): class B: pass @@ -42,6 +45,7 @@ import b df: a.DataFrame = b.DataFrame() # error: [invalid-assignment] "Object of type `b.DataFrame` is not assignable to `a.DataFrame`" + def _(dfs: list[b.DataFrame]): # error: [invalid-assignment] "Object of type `list[b.DataFrame]` is not assignable to `list[a.DataFrame]`" dataframes: list[a.DataFrame] = dfs @@ -68,6 +72,7 @@ class DataFrame: ```py from .foo import MyClass + def make_MyClass() -> MyClass: return MyClass() ``` @@ -83,6 +88,7 @@ class MyClass: ... ```py class MyClass: ... + def get_MyClass() -> MyClass: from . import make_MyClass @@ -105,6 +111,7 @@ s: status_a.Status = status_b.Status.ACTIVE ```py from enum import Enum + class Status(Enum): ACTIVE = 1 INACTIVE = 2 @@ -115,6 +122,7 @@ class Status(Enum): ```py from enum import Enum + class Status(Enum): ACTIVE = "active" INACTIVE = "inactive" @@ -127,16 +135,19 @@ class Status(Enum): ```py from enum import Enum + class A: class B(Enum): ACTIVE = "active" INACTIVE = "inactive" + class C: class B(Enum): ACTIVE = "active" INACTIVE = "inactive" + # error: [invalid-assignment] "Object of type `Literal[test.C.B.ACTIVE]` is not assignable to `test.A.B`" a: A.B = C.B.ACTIVE ``` @@ -182,6 +193,7 @@ from typing import Generic, TypeVar T = TypeVar("T") + class Container(Generic[T]): pass ``` @@ -193,6 +205,7 @@ from typing import Generic, TypeVar T = TypeVar("T") + class Container(Generic[T]): pass ``` @@ -208,9 +221,11 @@ from typing import Protocol, TypeVar T_co = TypeVar("T_co", covariant=True) + class Iterator(Protocol[T_co]): def __nexxt__(self) -> T_co: ... + def bad() -> Iterator[str]: raise NotImplementedError ``` @@ -220,6 +235,7 @@ def bad() -> Iterator[str]: ```py from typing import Iterator + def f() -> Iterator[str]: import bad @@ -234,6 +250,7 @@ from typing import Protocol import proto_a import proto_b + def _(drawable_b: proto_b.Drawable): # error: [invalid-assignment] "Object of type `proto_b.Drawable` is not assignable to `proto_a.Drawable`" drawable: proto_a.Drawable = drawable_b @@ -244,6 +261,7 @@ def _(drawable_b: proto_b.Drawable): ```py from typing import Protocol + class Drawable(Protocol): def draw(self) -> None: ... ``` @@ -253,6 +271,7 @@ class Drawable(Protocol): ```py from typing import Protocol + class Drawable(Protocol): def draw(self) -> int: ... ``` @@ -264,6 +283,7 @@ from typing import TypedDict import dict_a import dict_b + def _(b_person: dict_b.Person): # error: [invalid-assignment] "Object of type `dict_b.Person` is not assignable to `dict_a.Person`" person_var: dict_a.Person = b_person @@ -274,6 +294,7 @@ def _(b_person: dict_b.Person): ```py from typing import TypedDict + class Person(TypedDict): name: str ``` @@ -283,6 +304,7 @@ class Person(TypedDict): ```py from typing import TypedDict + class Person(TypedDict): name: bytes ``` @@ -298,6 +320,7 @@ class Model: ... ```py class Model: ... + def get_models_tuple() -> tuple[Model]: from module import Model diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md index b4e0b1ae24..d44d33d624 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -18,6 +18,7 @@ python-version = "3.10" async def elements(n): yield n + async def f(): # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)" return {n: [x async for x in elements(n)] for n in range(3)} @@ -38,9 +39,11 @@ correctly in the `SemanticSyntaxContext` trait: async def f(): [x for x in [1]] and [x async for x in elements(1)] + async def f(): def g(): pass + [x async for x in elements(1)] ``` @@ -57,6 +60,7 @@ python-version = "3.11" async def elements(n): yield n + async def f(): return {n: [x async for x in elements(n)] for n in range(3)} ``` @@ -82,6 +86,7 @@ python-version = "3.12" ```py from __future__ import annotations + # error: [invalid-type-form] "Named expressions are not allowed in type expressions" # error: [invalid-syntax] "named expression cannot be used within a type annotation" def f() -> (y := 3): ... @@ -114,6 +119,7 @@ python-version = "3.10" class Point: pass + obj = Point() match obj: # error: [invalid-syntax] "attribute name `x` repeated in class pattern" @@ -127,6 +133,7 @@ match obj: class C: def __await__(self): ... + # error: [invalid-syntax] "`return` statement outside of a function" return @@ -140,10 +147,12 @@ yield from [] # error: [invalid-syntax] "`await` outside of an asynchronous function" await C() + def f(): # error: [invalid-syntax] "`await` outside of an asynchronous function" await C() + (await cor async for cor in f()) # ok (await cor for cor in f()) # ok ([await c for c in cor] async for cor in f()) # ok @@ -155,6 +164,7 @@ Generators are evaluated lazily, so `await` is allowed, even outside of a functi async def g(): yield 1 + (x async for x in g()) ``` @@ -209,6 +219,7 @@ python-version = "3.12" class C[T, T]: pass + # error: [invalid-syntax] "duplicate type parameter" def f[X, Y, X](): pass @@ -223,10 +234,12 @@ def func(): # error: [invalid-syntax] "Starred expression cannot be used here" return *[1, 2, 3] + def gen(): # error: [invalid-syntax] "Starred expression cannot be used here" yield * [1, 2, 3] + # error: [invalid-syntax] "Starred expression cannot be used here" for *x in range(10): pass @@ -287,10 +300,12 @@ python-version = "3.12" # error: [invalid-syntax] "cannot assign to `__debug__`" __debug__ = False + # error: [invalid-syntax] "cannot assign to `__debug__`" def process(__debug__): pass + # error: [invalid-syntax] "cannot assign to `__debug__`" class Generic[__debug__]: pass @@ -311,16 +326,19 @@ def _(): # error: [invalid-syntax] "yield expression cannot be used within a TypeVar bound" type X[T: (yield 1)] = int + def _(): # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions" # error: [invalid-syntax] "yield expression cannot be used within a type alias" type Y = (yield 1) + # error: [invalid-type-form] "Named expressions are not allowed in type expressions" # error: [invalid-syntax] "named expression cannot be used within a generic definition" def f[T](x: int) -> (y := 3): return x + def _(): # error: [invalid-syntax] "yield expression cannot be used within a generic definition" class C[T]((yield from [object])): @@ -335,6 +353,7 @@ This error includes `await`, `async for`, `async with`, and `async` comprehensio async def elements(n): yield n + def _(): # error: [invalid-syntax] "`await` outside of an asynchronous function" await elements(1) @@ -354,6 +373,7 @@ def _(): ```py x: int + def f(): x = 1 global x # error: [invalid-syntax] "name `x` is used prior to global declaration" @@ -388,32 +408,39 @@ for x in range(42): ```py a = None + def f(a): global a # error: [invalid-syntax] + def g(a): if True: global a # error: [invalid-syntax] + def h(a): def inner(): global a + def i(a): try: global a # error: [invalid-syntax] except Exception: pass + def f(a): a = 1 global a # error: [invalid-syntax] + def f(a): a = 1 a = 2 global a # error: [invalid-syntax] + def f(a): class Inner: global a # ok diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md index c63631c1e4..e6f4a1c324 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md @@ -7,6 +7,7 @@ ```py class C: ... + C = 1 # error: [invalid-assignment] ``` @@ -15,5 +16,6 @@ C = 1 # error: [invalid-assignment] ```py def f(): ... + f = 1 # error: [invalid-assignment] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md index d19d2a8c12..8ef934f719 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md @@ -7,6 +7,7 @@ from typing_extensions import Any, Final, LiteralString, Self X = Any + class Foo: X: Final = LiteralString a: int @@ -16,6 +17,7 @@ class Foo: def __init__(self): self.y: Final = LiteralString + X.foo # error: [unresolved-attribute] X.aaaaooooooo # error: [unresolved-attribute] Foo.X.startswith # error: [unresolved-attribute] diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md index 07eebd1c8c..480c056c1f 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md @@ -13,6 +13,7 @@ too verbose for it to be worth it. def f(a, b=42): ... def g(a, b): ... + class Foo: def method(self, a): ... ``` @@ -24,9 +25,11 @@ from module import f, g, Foo f(1, 2, 3) # error: [too-many-positional-arguments] + def coinflip() -> bool: return True + h = f if coinflip() else g # error: [too-many-positional-arguments] diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md index a5e9b9370e..c6fe7f6bdf 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md @@ -13,9 +13,11 @@ python-version = "3.12" def f1() -> int: return 0 + def f2(name: str) -> int: return 0 + def _(flag: bool): if flag: f = f1 @@ -37,9 +39,11 @@ the end user.) def f1(a: int) -> int: return 0 + def f2(name: str) -> int: return 0 + def _(flag: bool): if flag: f = f1 @@ -61,18 +65,23 @@ just ensuring that we get test coverage for each of the possible diagnostic mess from inspect import getattr_static from typing import overload + def f1() -> int: return 0 + def f2(name: str) -> int: return 0 + def f3(a: int, b: int) -> int: return 0 + def f4[T: str](x: T) -> int: return 0 + @overload def f5() -> None: ... @overload @@ -80,6 +89,7 @@ def f5(x: str) -> str: ... def f5(x: str | None = None) -> str | None: return x + @overload def f6() -> None: ... @overload @@ -87,9 +97,11 @@ def f6(x: str, y: str) -> str: ... def f6(x: str | None = None, y: str | None = None) -> str | None: return x + y if x and y else None + def _(n: int): class PossiblyNotCallable: if n == 0: + def __call__(self) -> int: return 0 @@ -126,9 +138,11 @@ def _(n: int): def any(*args, **kwargs) -> int: return 0 + def f1(name: str) -> int: return 0 + def _(n: int): if n == 0: f = f1 @@ -147,16 +161,29 @@ therefore truncate the long expected union type to avoid overwhelming output. ```py from typing import Literal, Union + class A: ... + + class B: ... + + class C: ... + + class D: ... + + class E: ... + + class F: ... + def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int: return 0 + def _(n: int): x = n # error: [invalid-argument-type] diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unknown_argument.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unknown_argument.md index 5e709233ec..0b83f2ca5a 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/unknown_argument.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unknown_argument.md @@ -13,6 +13,7 @@ sub-diagnostic for each element would probably be too verbose for it to be worth def f(a, b, c=42): ... def g(a, b): ... + class Foo: def method(self, a, b): ... ``` @@ -24,9 +25,11 @@ from module import f, g, Foo f(a=1, b=2, c=3, d=42) # error: [unknown-argument] + def coinflip() -> bool: return True + h = f if coinflip() else g # error: [unknown-argument] diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md index 9cb91b89fd..468d56b22d 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md @@ -9,6 +9,7 @@ class NotBoolable: def __bool__(self, foo): return False + a = NotBoolable() # error: [unsupported-bool-conversion] @@ -22,6 +23,7 @@ class NotBoolable: def __bool__(self) -> str: return "wat" + a = NotBoolable() # error: [unsupported-bool-conversion] @@ -34,6 +36,7 @@ a = NotBoolable() class NotBoolable: __bool__: int = 3 + a = NotBoolable() # error: [unsupported-bool-conversion] @@ -47,15 +50,19 @@ class NotBoolable1: def __bool__(self) -> str: return "wat" + class NotBoolable2: pass + class NotBoolable3: __bool__: int = 3 + def get() -> NotBoolable1 | NotBoolable2 | NotBoolable3: return NotBoolable2() + # error: [unsupported-bool-conversion] 10 and get() and True ``` diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md index 67047850be..f73af73b21 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md @@ -10,6 +10,7 @@ from typing_extensions import assert_never, Never, Any from ty_extensions import Unknown + def _(never: Never): assert_never(never) # fine ``` @@ -24,24 +25,31 @@ If it is not, a `type-assertion-failure` diagnostic is emitted. from typing_extensions import assert_never, Never, Any from ty_extensions import Unknown + def _(): assert_never(0) # error: [type-assertion-failure] + def _(): assert_never("") # error: [type-assertion-failure] + def _(): assert_never(None) # error: [type-assertion-failure] + def _(): assert_never(()) # error: [type-assertion-failure] + def _(flag: bool, never: Never): assert_never(1 if flag else never) # error: [type-assertion-failure] + def _(any_: Any): assert_never(any_) # error: [type-assertion-failure] + def _(unknown: Unknown): assert_never(unknown) # error: [type-assertion-failure] ``` @@ -59,10 +67,16 @@ are handled in a series of `isinstance` checks or other narrowing patterns that ```py from typing_extensions import assert_never, Literal + class A: ... + + class B: ... + + class C: ... + def if_else_isinstance_success(obj: A | B): if isinstance(obj, A): pass @@ -73,6 +87,7 @@ def if_else_isinstance_success(obj: A | B): else: assert_never(obj) + def if_else_isinstance_error(obj: A | B): if isinstance(obj, A): pass @@ -83,6 +98,7 @@ def if_else_isinstance_error(obj: A | B): # error: [type-assertion-failure] "Type `B & ~A & ~C` is not equivalent to `Never`" assert_never(obj) + def if_else_singletons_success(obj: Literal[1, "a"] | None): if obj == 1: pass @@ -93,6 +109,7 @@ def if_else_singletons_success(obj: Literal[1, "a"] | None): else: assert_never(obj) + def if_else_singletons_error(obj: Literal[1, "a"] | None): if obj == 1: pass @@ -104,6 +121,7 @@ def if_else_singletons_error(obj: Literal[1, "a"] | None): # error: [type-assertion-failure] "Type `Literal["a"]` is not equivalent to `Never`" assert_never(obj) + def match_singletons_success(obj: Literal[1, "a"] | None): match obj: case 1: @@ -115,6 +133,7 @@ def match_singletons_success(obj: Literal[1, "a"] | None): case _ as obj: assert_never(obj) + def match_singletons_error(obj: Literal[1, "a"] | None): match obj: case 1: diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md index 7f99251017..00e8a2fd5b 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md @@ -7,6 +7,7 @@ ```py from typing_extensions import assert_type + def _(x: int, y: bool): assert_type(x, int) # fine assert_type(x, str) # error: [type-assertion-failure] @@ -26,6 +27,7 @@ python-version = "3.10" ```py from typing_extensions import assert_type + def _(x: int | str): if isinstance(x, int): reveal_type(x) # revealed: int @@ -40,14 +42,17 @@ The actual type must match the asserted type precisely. from typing import Any, Type, Union from typing_extensions import assert_type + # Subtype does not count def _(x: bool): assert_type(x, int) # error: [type-assertion-failure] "Type `int` does not match asserted type `bool`" + def _(a: type[int], b: type[Any]): assert_type(a, type[Any]) # error: [type-assertion-failure] "Type `type[Any]` does not match asserted type `type[int]`" assert_type(b, type[int]) # error: [type-assertion-failure] "Type `type[int]` does not match asserted type `type[Any]`" + # The expression constructing the type is not taken into account def _(a: type[int]): assert_type(a, Type[int]) # fine @@ -61,6 +66,7 @@ from typing_extensions import Literal, assert_type from ty_extensions import Unknown + # Any and Unknown are considered equivalent def _(a: Unknown, b: Any): reveal_type(a) # revealed: Unknown @@ -69,6 +75,7 @@ def _(a: Unknown, b: Any): reveal_type(b) # revealed: Any assert_type(b, Unknown) # fine + def _(a: type[Unknown], b: type[Any]): reveal_type(a) # revealed: type[Unknown] assert_type(a, type[Any]) # fine @@ -86,6 +93,7 @@ from typing_extensions import Any, assert_type from ty_extensions import Unknown + def _(a: tuple[int, str, bytes]): assert_type(a, tuple[int, str, bytes]) # fine @@ -93,6 +101,7 @@ def _(a: tuple[int, str, bytes]): assert_type(a, tuple[int, str, bytes, None]) # error: [type-assertion-failure] assert_type(a, tuple[int, bytes, str]) # error: [type-assertion-failure] + def _(a: tuple[Any, ...], b: tuple[Unknown, ...]): assert_type(a, tuple[Any, ...]) # fine assert_type(a, tuple[Unknown, ...]) # fine @@ -113,6 +122,7 @@ python-version = "3.10" ```py from typing_extensions import assert_type + def _(a: str | int): assert_type(a, str | int) assert_type(a, int | str) @@ -128,11 +138,19 @@ from typing_extensions import assert_type from ty_extensions import Intersection, Not + class A: ... + + class B: ... + + class C: ... + + class D: ... + def _(a: A): if isinstance(a, B) and not isinstance(a, C) and not isinstance(a, D): reveal_type(a) # revealed: A & B & ~C & ~D diff --git a/crates/ty_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md index 9be0c664d2..e1e1278062 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -28,15 +28,19 @@ cast(str) # error: [too-many-positional-arguments] "Too many positional arguments to function `cast`: expected 2, got 3" cast(str, b"ar", "foo") + def function_returning_int() -> int: return 10 + # error: [redundant-cast] "Value is already of type `int`" cast(int, function_returning_int()) + def function_returning_any() -> Any: return "blah" + # error: [redundant-cast] "Value is already of type `Any`" cast(Any, function_returning_any()) ``` @@ -47,6 +51,7 @@ diagnostics. ```py from typing import Callable + def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]): a = cast(Callable[[list[bytes]], None], x) b = cast(tuple[list[bytes]], y) @@ -65,6 +70,7 @@ the gradual guarantee and leads to cascading errors when an object is inferred a ```py from ty_extensions import Unknown + def f(x: Any, y: Unknown, z: Any | str | int): a = cast(dict[str, Any], x) reveal_type(a) # revealed: dict[str, Any] diff --git a/crates/ty_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md b/crates/ty_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md index 50e73bef0f..abd63d733b 100644 --- a/crates/ty_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md +++ b/crates/ty_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md @@ -9,6 +9,7 @@ types for undeclared symbols. This is best illustrated with an example: class Wrapper: value = None + wrapper = Wrapper() reveal_type(wrapper.value) # revealed: Unknown | None @@ -37,6 +38,7 @@ where `wrapper.value` is used in a way that is incompatible with `None`: def accepts_int(i: int) -> None: pass + def f(w: Wrapper) -> None: # This is fine v: int | None = w.value @@ -59,6 +61,7 @@ untyped module: class OptionalInt: value = 10 + def reset(o): o.value = None ``` @@ -91,6 +94,7 @@ class, this would probably be: class OptionalInt: value: int | None = 10 + o = OptionalInt() # The following public type is now @@ -118,6 +122,7 @@ class Wrapper: # Type as seen from the same scope: reveal_type(value) # revealed: None + # Type as seen from another scope: reveal_type(Wrapper.value) # revealed: Unknown | None ``` diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index c3b1e55c53..94c4e7c059 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -6,11 +6,13 @@ from enum import Enum from typing import Literal + class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 + reveal_type(Color.RED) # revealed: Literal[Color.RED] reveal_type(Color.RED.name) # revealed: Literal["RED"] reveal_type(Color.RED.value) # revealed: Literal[1] @@ -32,19 +34,23 @@ Simple enums with integer or string values: from enum import Enum from ty_extensions import enum_members + class ColorInt(Enum): RED = 1 GREEN = 2 BLUE = 3 + # revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]] reveal_type(enum_members(ColorInt)) + class ColorStr(Enum): RED = "red" GREEN = "green" BLUE = "blue" + # revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]] reveal_type(enum_members(ColorStr)) ``` @@ -55,11 +61,13 @@ reveal_type(enum_members(ColorStr)) from enum import IntEnum from ty_extensions import enum_members + class ColorInt(IntEnum): RED = 1 GREEN = 2 BLUE = 3 + # revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]] reveal_type(enum_members(ColorInt)) ``` @@ -72,6 +80,7 @@ Attributes on the enum class that are declared are not considered members of the from enum import Enum from ty_extensions import enum_members + class Answer(Enum): YES = 1 NO = 2 @@ -81,6 +90,7 @@ class Answer(Enum): # TODO: this could be considered an error: non_member_1: str = "some value" + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` @@ -92,10 +102,12 @@ from enum import Enum from typing import Final from ty_extensions import enum_members + class Answer(Enum): YES: Final = 1 NO: Final = 2 + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` @@ -110,13 +122,16 @@ from enum import Enum from ty_extensions import enum_members from typing import Callable, Literal + def identity(x) -> int: return x + class Descriptor: def __get__(self, instance, owner): return 0 + class Answer(Enum): YES = 1 NO = 2 @@ -139,6 +154,7 @@ class Answer(Enum): class NestedClass: ... + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` @@ -157,6 +173,7 @@ from enum import Enum, property as enum_property from typing import Any from ty_extensions import enum_members + class Answer(Enum): YES = 1 NO = 2 @@ -165,6 +182,7 @@ class Answer(Enum): def some_property(self) -> str: return "property value" + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` @@ -174,6 +192,7 @@ Enum attributes defined using `enum.property` take precedence over generated att ```py from enum import Enum, property as enum_property + class Choices(Enum): A = 1 B = 2 @@ -181,6 +200,7 @@ class Choices(Enum): @enum_property def value(self) -> Any: ... + # TODO: This should be `Any` - overridden by `@enum_property` reveal_type(Choices.A.value) # revealed: Literal[1] ``` @@ -194,6 +214,7 @@ from enum import Enum from ty_extensions import enum_members from types import DynamicClassAttribute + class Answer(Enum): YES = 1 NO = 2 @@ -202,6 +223,7 @@ class Answer(Enum): def dynamic_property(self) -> str: return "dynamic value" + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` @@ -232,12 +254,14 @@ Enum members can have aliases, which are not considered separate members: from enum import Enum from ty_extensions import enum_members + class Answer(Enum): YES = 1 NO = 2 DEFINITELY = YES + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) @@ -249,6 +273,7 @@ If a value is duplicated, we also treat that as an alias: ```py from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 @@ -256,6 +281,7 @@ class Color(Enum): red = 1 green = 2 + # revealed: tuple[Literal["RED"], Literal["GREEN"]] reveal_type(enum_members(Color)) @@ -269,6 +295,7 @@ Multiple aliases to the same member are also supported. This is a regression tes ```py from ty_extensions import enum_members + class ManyAliases(Enum): real_member = "real_member" alias1 = "real_member" @@ -277,6 +304,7 @@ class ManyAliases(Enum): other_member = "other_real_member" + # revealed: tuple[Literal["real_member"], Literal["other_member"]] reveal_type(enum_members(ManyAliases)) @@ -309,19 +337,23 @@ python-version = "3.11" from enum import Enum, auto from ty_extensions import enum_members + class Answer(Enum): YES = auto() NO = auto() + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) reveal_type(Answer.YES.value) # revealed: Literal[1] reveal_type(Answer.NO.value) # revealed: Literal[2] + class SingleMember(Enum): SINGLE = auto() + reveal_type(SingleMember.SINGLE.value) # revealed: Literal[1] ``` @@ -334,6 +366,7 @@ class Mixed(Enum): MANUAL_2 = -2 AUTO_2 = auto() + reveal_type(Mixed.MANUAL_1.value) # revealed: Literal[-1] reveal_type(Mixed.AUTO_1.value) # revealed: Literal[1] reveal_type(Mixed.MANUAL_2.value) # revealed: Literal[-2] @@ -345,16 +378,20 @@ When using `auto()` with `StrEnum`, the value is the lowercase name of the membe ```py from enum import StrEnum, auto + class Answer(StrEnum): YES = auto() NO = auto() + reveal_type(Answer.YES.value) # revealed: Literal["yes"] reveal_type(Answer.NO.value) # revealed: Literal["no"] + class SingleMember(StrEnum): SINGLE = auto() + reveal_type(SingleMember.SINGLE.value) # revealed: Literal["single"] ``` @@ -363,10 +400,12 @@ Using `auto()` with `IntEnum` also works as expected: ```py from enum import IntEnum, auto + class Answer(IntEnum): YES = auto() NO = auto() + reveal_type(Answer.YES.value) # revealed: Literal[1] reveal_type(Answer.NO.value) # revealed: Literal[2] ``` @@ -376,10 +415,12 @@ As does using `auto()` for other enums that use `int` as a mixin: ```py from enum import Enum, auto + class Answer(int, Enum): YES = auto() NO = auto() + reveal_type(Answer.YES.value) # revealed: Literal[1] reveal_type(Answer.NO.value) # revealed: Literal[2] ``` @@ -392,28 +433,36 @@ effect of using `auto()` will be for an arbitrary non-integer mixin, so for anyt ```python from enum import Enum, auto + class A(str, Enum): X = auto() Y = auto() + reveal_type(A.X.value) # revealed: Any + class B(bytes, Enum): X = auto() Y = auto() + reveal_type(B.X.value) # revealed: Any + class C(tuple, Enum): X = auto() Y = auto() + reveal_type(C.X.value) # revealed: Any + class D(float, Enum): X = auto() Y = auto() + reveal_type(D.X.value) # revealed: Any ``` @@ -422,12 +471,14 @@ Combining aliases with `auto()`: ```py from enum import Enum, auto + class Answer(Enum): YES = auto() NO = auto() DEFINITELY = YES + # TODO: This should ideally be `tuple[Literal["YES"], Literal["NO"]]` # revealed: tuple[Literal["YES"], Literal["NO"], Literal["DEFINITELY"]] reveal_type(enum_members(Answer)) @@ -444,11 +495,13 @@ python-version = "3.11" from enum import Enum, auto, member, nonmember from ty_extensions import enum_members + class Answer(Enum): YES = member(1) NO = member(2) OTHER = nonmember(17) + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) @@ -463,6 +516,7 @@ reveal_type(Answer.OTHER) from enum import Enum, member from ty_extensions import enum_members + class Answer(Enum): yes = member(1) no = member(2) @@ -471,6 +525,7 @@ class Answer(Enum): def maybe(self) -> None: return + # revealed: tuple[Literal["yes"], Literal["no"], Literal["maybe"]] reveal_type(enum_members(Answer)) ``` @@ -484,6 +539,7 @@ treated as a non-member: from enum import Enum from ty_extensions import enum_members + class Answer(Enum): YES = 1 NO = 2 @@ -491,6 +547,7 @@ class Answer(Enum): __private_member = 3 __maybe__ = 4 + # revealed: tuple[Literal["YES"], Literal["NO"], Literal["__maybe__"]] reveal_type(enum_members(Answer)) ``` @@ -504,6 +561,7 @@ whitespace-delimited list of names: from enum import Enum from ty_extensions import enum_members + class Answer(Enum): _ignore_ = "IGNORED _other_ignored also_ignored" @@ -514,6 +572,7 @@ class Answer(Enum): _other_ignored = "test" also_ignored = "test2" + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` @@ -530,6 +589,7 @@ class Answer2(Enum): MAYBE = 3 _other = "test" + # TODO: This should be `tuple[Literal["YES"], Literal["NO"]]` # revealed: tuple[Literal["YES"], Literal["NO"], Literal["MAYBE"], Literal["_other"]] reveal_type(enum_members(Answer2)) @@ -544,10 +604,12 @@ conflicting with `Enum.name` and `Enum.value`): from enum import Enum from ty_extensions import enum_members + class Answer(Enum): name = 1 value = 2 + # revealed: tuple[Literal["name"], Literal["value"]] reveal_type(enum_members(Answer)) @@ -560,11 +622,13 @@ reveal_type(Answer.value) # revealed: Literal[Answer.value] ```py from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 + for color in Color: reveal_type(color) # revealed: Color @@ -579,25 +643,31 @@ Methods and non-member attributes defined in the enum class can be accessed on e ```py from enum import Enum + class Answer(Enum): YES = 1 NO = 2 def is_yes(self) -> bool: return self == Answer.YES + constant: int = 1 + reveal_type(Answer.YES.is_yes()) # revealed: bool reveal_type(Answer.YES.constant) # revealed: int + class MyEnum(Enum): def some_method(self) -> None: pass + class MyAnswer(MyEnum): YES = 1 NO = 2 + reveal_type(MyAnswer.YES.some_method()) # revealed: None ``` @@ -606,10 +676,12 @@ reveal_type(MyAnswer.YES.some_method()) # revealed: None ```py from enum import Enum + class Answer(Enum): YES = 1 NO = 2 + def _(answer: type[Answer]) -> None: reveal_type(answer.YES) # revealed: Literal[Answer.YES] reveal_type(answer.NO) # revealed: Literal[Answer.NO] @@ -622,6 +694,7 @@ from enum import Enum from typing import Callable import sys + class Printer(Enum): STDOUT = 1 STDERR = 2 @@ -632,6 +705,7 @@ class Printer(Enum): elif self == Printer.STDERR: print(msg, file=sys.stderr) + Printer.STDOUT("Hello, world!") Printer.STDERR("An error occurred!") @@ -649,16 +723,20 @@ callable("Another error!") from enum import Enum from typing import Literal + class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 + reveal_type(Color.RED._name_) # revealed: Literal["RED"] + def _(red_or_blue: Literal[Color.RED, Color.BLUE]): reveal_type(red_or_blue.name) # revealed: Literal["RED", "BLUE"] + def _(any_color: Color): # TODO: Literal["RED", "GREEN", "BLUE"] reveal_type(any_color.name) # revealed: Any @@ -675,21 +753,25 @@ python-version = "3.11" from enum import Enum, StrEnum from typing import Literal + class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 + reveal_type(Color.RED.value) # revealed: Literal[1] reveal_type(Color.RED._value_) # revealed: Literal[1] reveal_type(Color.GREEN.value) # revealed: Literal[2] reveal_type(Color.GREEN._value_) # revealed: Literal[2] + class Answer(StrEnum): YES = "yes" NO = "no" + reveal_type(Answer.YES.value) # revealed: Literal["yes"] reveal_type(Answer.YES._value_) # revealed: Literal["yes"] @@ -706,15 +788,18 @@ An enum with one or more defined members cannot be subclassed. They are implicit ```py from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 + # error: [subclass-of-final-class] "Class `ExtendedColor` cannot inherit from final class `Color`" class ExtendedColor(Color): YELLOW = 4 + def f(color: Color): if isinstance(color, int): reveal_type(color) # revealed: Never @@ -726,14 +811,17 @@ An `Enum` subclass without any defined members can be subclassed: from enum import Enum from ty_extensions import enum_members + class MyEnum(Enum): def some_method(self) -> None: pass + class Answer(MyEnum): YES = 1 NO = 2 + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` @@ -743,14 +831,18 @@ reveal_type(enum_members(Answer)) ```py from enum import Enum + class Answer(Enum): YES = 1 NO = 2 + reveal_type(type(Answer.YES)) # revealed: + class NoMembers(Enum): ... + def _(answer: Answer, no_members: NoMembers): reveal_type(type(answer)) # revealed: reveal_type(type(no_members)) # revealed: type[NoMembers] @@ -763,6 +855,7 @@ from enum import Enum from typing import Literal from ty_extensions import enum_members + class Answer(Enum): YES = 1 NO = 2 @@ -771,6 +864,7 @@ class Answer(Enum): def yes(cls) -> "Literal[Answer.YES]": return Answer.YES + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` @@ -786,14 +880,17 @@ prior to Python 3.11. ```py from enum import Enum, EnumMeta + class CustomEnumSubclass(Enum): def custom_method(self) -> int: return 0 + class EnumWithCustomEnumSubclass(CustomEnumSubclass): NO = 0 YES = 1 + reveal_type(EnumWithCustomEnumSubclass.NO) # revealed: Literal[EnumWithCustomEnumSubclass.NO] reveal_type(EnumWithCustomEnumSubclass.NO.custom_method()) # revealed: int ``` @@ -808,18 +905,23 @@ python-version = "3.9" ```py from enum import Enum, EnumMeta + class EnumWithEnumMetaMetaclass(metaclass=EnumMeta): NO = 0 YES = 1 + reveal_type(EnumWithEnumMetaMetaclass.NO) # revealed: Literal[EnumWithEnumMetaMetaclass.NO] + class SubclassOfEnumMeta(EnumMeta): ... + class EnumWithSubclassOfEnumMetaMetaclass(metaclass=SubclassOfEnumMeta): NO = 0 YES = 1 + reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO) # revealed: Literal[EnumWithSubclassOfEnumMetaMetaclass.NO] # Attributes like `.value` can *not* be accessed on members of these enums: @@ -843,18 +945,23 @@ python-version = "3.11" ```py from enum import Enum, EnumType + class EnumWithEnumMetaMetaclass(metaclass=EnumType): NO = 0 YES = 1 + reveal_type(EnumWithEnumMetaMetaclass.NO) # revealed: Literal[EnumWithEnumMetaMetaclass.NO] + class SubclassOfEnumMeta(EnumType): ... + class EnumWithSubclassOfEnumMetaMetaclass(metaclass=SubclassOfEnumMeta): NO = 0 YES = 1 + reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO) # revealed: Literal[EnumWithSubclassOfEnumMetaMetaclass.NO] # error: [unresolved-attribute] @@ -873,11 +980,13 @@ To do: from enum import Enum from typing_extensions import assert_never + class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 + def color_name(color: Color) -> str: if color is Color.RED: return "Red" @@ -888,6 +997,7 @@ def color_name(color: Color) -> str: else: assert_never(color) + # No `invalid-return-type` error here because the implicit `else` branch is detected as unreachable: def color_name_without_assertion(color: Color) -> str: if color is Color.RED: @@ -897,6 +1007,7 @@ def color_name_without_assertion(color: Color) -> str: elif color is Color.BLUE: return "Blue" + def color_name_misses_one_variant(color: Color) -> str: if color is Color.RED: return "Red" @@ -905,9 +1016,11 @@ def color_name_misses_one_variant(color: Color) -> str: else: assert_never(color) # error: [type-assertion-failure] "Type `Literal[Color.BLUE]` is not equivalent to `Never`" + class Singleton(Enum): VALUE = 1 + def singleton_check(value: Singleton) -> str: if value is Singleton.VALUE: return "Singleton value" @@ -926,11 +1039,13 @@ python-version = "3.10" from enum import Enum from typing_extensions import assert_never + class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 + def color_name(color: Color) -> str: match color: case Color.RED: @@ -942,6 +1057,7 @@ def color_name(color: Color) -> str: case _: assert_never(color) + def color_name_without_assertion(color: Color) -> str: match color: case Color.RED: @@ -951,6 +1067,7 @@ def color_name_without_assertion(color: Color) -> str: case Color.BLUE: return "Blue" + def color_name_misses_one_variant(color: Color) -> str: match color: case Color.RED: @@ -960,9 +1077,11 @@ def color_name_misses_one_variant(color: Color) -> str: case _: assert_never(color) # error: [type-assertion-failure] "Type `Literal[Color.BLUE]` is not equivalent to `Never`" + class Singleton(Enum): VALUE = 1 + def singleton_check(value: Singleton) -> str: match value: case Singleton.VALUE: @@ -978,10 +1097,12 @@ def singleton_check(value: Singleton) -> str: ```py from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 + reveal_type(Color.RED == Color.RED) # revealed: Literal[True] reveal_type(Color.RED != Color.RED) # revealed: Literal[False] ``` @@ -991,6 +1112,7 @@ reveal_type(Color.RED != Color.RED) # revealed: Literal[False] ```py from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 @@ -998,6 +1120,7 @@ class Color(Enum): def __eq__(self, other: object) -> bool: return False + reveal_type(Color.RED == Color.RED) # revealed: bool ``` @@ -1006,6 +1129,7 @@ reveal_type(Color.RED == Color.RED) # revealed: bool ```py from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 @@ -1013,6 +1137,7 @@ class Color(Enum): def __ne__(self, other: object) -> bool: return False + reveal_type(Color.RED != Color.RED) # revealed: bool ``` @@ -1033,6 +1158,7 @@ python-version = "3.12" ```py from enum import Enum + # error: [invalid-generic-enum] "Enum class `E` cannot be generic" class E[T](Enum): A = 1 @@ -1049,6 +1175,7 @@ from typing import Generic, TypeVar T = TypeVar("T") + # error: [invalid-generic-enum] "Enum class `F` cannot be generic" class F(Enum, Generic[T]): A = 1 @@ -1065,6 +1192,7 @@ from typing import Generic, TypeVar T = TypeVar("T") + # error: [invalid-generic-enum] "Enum class `G` cannot be generic" class G(Generic[T], Enum): A = 1 @@ -1086,10 +1214,12 @@ from typing import Generic, TypeVar T = TypeVar("T") + # error: [invalid-generic-enum] "Enum class `MyIntEnum` cannot be generic" class MyIntEnum[T](IntEnum): A = 1 + # error: [invalid-generic-enum] "Enum class `MyFlagEnum` cannot be generic" class MyFlagEnum(IntEnum, Generic[T]): A = 1 @@ -1110,9 +1240,11 @@ from typing import Generic, TypeVar T = TypeVar("T") + class MyEnumBase(Enum): def some_method(self) -> None: ... + # error: [invalid-generic-enum] "Enum class `MyEnum` cannot be generic" class MyEnum[T](MyEnumBase): A = 1 diff --git a/crates/ty_python_semantic/resources/mdtest/exception/basic.md b/crates/ty_python_semantic/resources/mdtest/exception/basic.md index 4730887a3c..f7780f9024 100644 --- a/crates/ty_python_semantic/resources/mdtest/exception/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/exception/basic.md @@ -68,8 +68,10 @@ MRO, as the dynamic element in the MRO could materialize to some subclass of `Ba ```py from compat import BASE_EXCEPTION_CLASS # error: [unresolved-import] "Cannot resolve imported module `compat`" + class Error(BASE_EXCEPTION_CLASS): ... + try: ... except Error as err: @@ -95,6 +97,7 @@ python-version = "3.12" ```py from typing import Callable + def silence[T: type[BaseException]]( func: Callable[[], None], exception_type: T, @@ -104,6 +107,7 @@ def silence[T: type[BaseException]]( except exception_type as e: reveal_type(e) # revealed: T'instance@silence + def silence2[ T: ( type[ValueError], @@ -133,6 +137,7 @@ try: except (ValueError, OSError, "foo", b"bar") as e: reveal_type(e) # revealed: ValueError | OSError | Unknown + def foo( x: type[str], y: tuple[type[OSError], type[RuntimeError], int], @@ -150,6 +155,7 @@ def foo( except z as g: reveal_type(g) # revealed: Unknown + try: {}.get("foo") # error: [invalid-exception-caught] @@ -180,9 +186,11 @@ try: except: ... + def _(e: Exception | type[Exception]): raise e # fine + def _(e: Exception | type[Exception] | None): raise e # error: [invalid-raise] ``` @@ -196,30 +204,35 @@ def _(): except: ... + def _(): try: raise StopIteration from MemoryError() # fine except: ... + def _(): try: raise BufferError() from None # fine except: ... + def _(): try: raise ZeroDivisionError from False # error: [invalid-raise] except: ... + def _(): try: raise SystemExit from bool() # error: [invalid-raise] except: ... + def _(): try: raise @@ -227,6 +240,7 @@ def _(): reveal_type(e) # revealed: KeyboardInterrupt raise LookupError from e # fine + def _(): try: raise @@ -234,12 +248,15 @@ def _(): reveal_type(e) # revealed: Unknown raise KeyError from e + def _(e: Exception | type[Exception]): raise ModuleNotFoundError from e # fine + def _(e: Exception | type[Exception] | None): raise IndexError from e # fine + def _(e: int | None): raise IndexError from e # error: [invalid-raise] ``` @@ -259,9 +276,11 @@ reveal_type(e) # revealed: Unknown e = None + def cond() -> bool: return True + try: if cond(): raise ValueError() @@ -270,6 +289,7 @@ except ValueError as e: # error: [possibly-unresolved-reference] reveal_type(e) # revealed: None + def f(x: type[Exception]): e = None try: diff --git a/crates/ty_python_semantic/resources/mdtest/exception/control_flow.md b/crates/ty_python_semantic/resources/mdtest/exception/control_flow.md index 3cdc3d14cd..6a673034fd 100644 --- a/crates/ty_python_semantic/resources/mdtest/exception/control_flow.md +++ b/crates/ty_python_semantic/resources/mdtest/exception/control_flow.md @@ -33,6 +33,7 @@ completing. The type of `x` at the beginning of the `except` suite in this examp def could_raise_returns_str() -> str: return "foo" + x = 1 try: @@ -74,6 +75,7 @@ control-flow analysis. def could_raise_returns_str() -> str: return "foo" + x = 1 try: @@ -102,6 +104,7 @@ The inferred type of `x` at this point is the union of the types at the end of t def could_raise_returns_str() -> str: return "foo" + x = 1 try: @@ -134,6 +137,7 @@ the `except` suite: def could_raise_returns_str() -> str: return "foo" + x = 1 try: @@ -190,6 +194,7 @@ type of `x` at the end of the example is therefore `Literal[2]`: def could_raise_returns_str() -> str: return "foo" + x = 1 try: @@ -242,18 +247,26 @@ suites: ```py class A: ... + + class B: ... + + class C: ... + def could_raise_returns_A() -> A: return A() + def could_raise_returns_B() -> B: return B() + def could_raise_returns_C() -> C: return C() + x = 1 try: @@ -305,14 +318,19 @@ An example with multiple `except` branches and a `finally` branch: ```py class D: ... + + class E: ... + def could_raise_returns_D() -> D: return D() + def could_raise_returns_E() -> E: return E() + x = 1 try: @@ -346,26 +364,40 @@ an exception raised *there*. ```py class A: ... + + class B: ... + + class C: ... + + class D: ... + + class E: ... + def could_raise_returns_A() -> A: return A() + def could_raise_returns_B() -> B: return B() + def could_raise_returns_C() -> C: return C() + def could_raise_returns_D() -> D: return D() + def could_raise_returns_E() -> E: return E() + x = 1 try: @@ -395,14 +427,19 @@ The same again, this time with multiple `except` branches: ```py class F: ... + + class G: ... + def could_raise_returns_F() -> F: return F() + def could_raise_returns_G() -> G: return G() + x = 1 try: @@ -446,50 +483,82 @@ jumping out of that suite prior to the suite running to completion. ```py class A: ... + + class B: ... + + class C: ... + + class D: ... + + class E: ... + + class F: ... + + class G: ... + + class H: ... + + class I: ... + + class J: ... + + class K: ... + def could_raise_returns_A() -> A: return A() + def could_raise_returns_B() -> B: return B() + def could_raise_returns_C() -> C: return C() + def could_raise_returns_D() -> D: return D() + def could_raise_returns_E() -> E: return E() + def could_raise_returns_F() -> F: return F() + def could_raise_returns_G() -> G: return G() + def could_raise_returns_H() -> H: return H() + def could_raise_returns_I() -> I: return I() + def could_raise_returns_J() -> J: return J() + def could_raise_returns_K() -> K: return K() + x = 1 try: @@ -549,26 +618,40 @@ in the outer scope: ```py class A: ... + + class B: ... + + class C: ... + + class D: ... + + class E: ... + def could_raise_returns_A() -> A: return A() + def could_raise_returns_B() -> B: return B() + def could_raise_returns_C() -> C: return C() + def could_raise_returns_D() -> D: return D() + def could_raise_returns_E() -> E: return E() + x = 1 try: @@ -590,6 +673,7 @@ try: # TODO: should be `A | B | C | D` reveal_type(x) # revealed: B | D reveal_type(x) # revealed: B | D + x = foo reveal_type(x) # revealed: def foo(param=...) -> Unknown except: diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 1d7cbfd401..d83c26fe32 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -10,6 +10,7 @@ python-version = "3.11" ```py from typing import Literal, assert_never + def if_else_exhaustive(x: Literal[0, 1, "a"]): if x == 0: pass @@ -22,6 +23,7 @@ def if_else_exhaustive(x: Literal[0, 1, "a"]): assert_never(x) + def if_else_exhaustive_no_assertion(x: Literal[0, 1, "a"]) -> int: if x == 0: return 0 @@ -30,6 +32,7 @@ def if_else_exhaustive_no_assertion(x: Literal[0, 1, "a"]) -> int: elif x == "a": return 2 + def if_else_non_exhaustive(x: Literal[0, 1, "a"]): if x == 0: pass @@ -41,6 +44,7 @@ def if_else_non_exhaustive(x: Literal[0, 1, "a"]): # this diagnostic is correct: the inferred type of `x` is `Literal[1]` assert_never(x) # error: [type-assertion-failure] + def match_exhaustive(x: Literal[0, 1, "a"]): match x: case 0: @@ -54,6 +58,7 @@ def match_exhaustive(x: Literal[0, 1, "a"]): assert_never(x) + def match_exhaustive_no_assertion(x: Literal[0, 1, "a"]) -> int: match x: case 0: @@ -63,6 +68,7 @@ def match_exhaustive_no_assertion(x: Literal[0, 1, "a"]) -> int: case "a": return 2 + def match_non_exhaustive(x: Literal[0, 1, "a"]): match x: case 0: @@ -75,6 +81,7 @@ def match_non_exhaustive(x: Literal[0, 1, "a"]): # this diagnostic is correct: the inferred type of `x` is `Literal[1]` assert_never(x) # error: [type-assertion-failure] + # This is based on real-world code: # https://github.com/scipy/scipy/blob/99c0ef6af161a4d8157cae5276a20c30b7677c6f/scipy/linalg/tests/test_lapack.py#L147-L171 def exhaustiveness_using_containment_checks(): @@ -98,11 +105,13 @@ def exhaustiveness_using_containment_checks(): from enum import Enum from typing import assert_never + class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 + def if_else_exhaustive(x: Color): if x == Color.RED: pass @@ -115,6 +124,7 @@ def if_else_exhaustive(x: Color): assert_never(x) + def if_else_exhaustive_no_assertion(x: Color) -> int: if x == Color.RED: return 1 @@ -123,6 +133,7 @@ def if_else_exhaustive_no_assertion(x: Color) -> int: elif x == Color.BLUE: return 3 + def if_else_non_exhaustive(x: Color): if x == Color.RED: pass @@ -134,6 +145,7 @@ def if_else_non_exhaustive(x: Color): # this diagnostic is correct: inferred type of `x` is `Literal[Color.GREEN]` assert_never(x) # error: [type-assertion-failure] + def match_exhaustive(x: Color): match x: case Color.RED: @@ -147,6 +159,7 @@ def match_exhaustive(x: Color): assert_never(x) + def match_exhaustive_2(x: Color): match x: case Color.RED: @@ -158,6 +171,7 @@ def match_exhaustive_2(x: Color): assert_never(x) + def match_exhaustive_no_assertion(x: Color) -> int: match x: case Color.RED: @@ -167,6 +181,7 @@ def match_exhaustive_no_assertion(x: Color) -> int: case Color.BLUE: return 3 + def match_non_exhaustive(x: Color): match x: case Color.RED: @@ -190,13 +205,20 @@ python-version = "3.12" ```py from typing import assert_never + class A: ... + + class B: ... + + class C: ... + class GenericClass[T]: x: T + def if_else_exhaustive(x: A | B | C): if isinstance(x, A): pass @@ -209,6 +231,7 @@ def if_else_exhaustive(x: A | B | C): assert_never(x) + def if_else_exhaustive_no_assertion(x: A | B | C) -> int: if isinstance(x, A): return 0 @@ -217,6 +240,7 @@ def if_else_exhaustive_no_assertion(x: A | B | C) -> int: elif isinstance(x, C): return 2 + def if_else_non_exhaustive(x: A | B | C): if isinstance(x, A): pass @@ -228,6 +252,7 @@ def if_else_non_exhaustive(x: A | B | C): # this diagnostic is correct: the inferred type of `x` is `B & ~A & ~C` assert_never(x) # error: [type-assertion-failure] + def match_exhaustive(x: A | B | C): match x: case A(): @@ -241,6 +266,7 @@ def match_exhaustive(x: A | B | C): assert_never(x) + def match_exhaustive_no_assertion(x: A | B | C) -> int: match x: case A(): @@ -250,6 +276,7 @@ def match_exhaustive_no_assertion(x: A | B | C) -> int: case C(): return 2 + def match_non_exhaustive(x: A | B | C): match x: case A(): @@ -262,6 +289,7 @@ def match_non_exhaustive(x: A | B | C): # this diagnostic is correct: the inferred type of `x` is `B & ~A & ~C` assert_never(x) # error: [type-assertion-failure] + # Note: no invalid-return-type diagnostic; the `match` is exhaustive def match_exhaustive_generic[T](obj: GenericClass[T]) -> GenericClass[T]: match obj: @@ -284,21 +312,31 @@ python-version = "3.12" ```py from typing import assert_never + class A[T]: value: T + class ASub[T](A[T]): ... + class B[T]: value: T + class C[T]: value: T + class D: ... + + class E: ... + + class F: ... + def if_else_exhaustive(x: A[D] | B[E] | C[F]): if isinstance(x, A): pass @@ -310,6 +348,7 @@ def if_else_exhaustive(x: A[D] | B[E] | C[F]): no_diagnostic_here assert_never(x) + def if_else_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: if isinstance(x, A): return 0 @@ -318,6 +357,7 @@ def if_else_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: elif isinstance(x, C): return 2 + def if_else_non_exhaustive(x: A[D] | B[E] | C[F]): if isinstance(x, A): pass @@ -329,6 +369,7 @@ def if_else_non_exhaustive(x: A[D] | B[E] | C[F]): # this diagnostic is correct: the inferred type of `x` is `B[E] & ~A[D] & ~C[F]` assert_never(x) # error: [type-assertion-failure] + def match_exhaustive(x: A[D] | B[E] | C[F]): match x: case A(): @@ -341,6 +382,7 @@ def match_exhaustive(x: A[D] | B[E] | C[F]): no_diagnostic_here assert_never(x) + def match_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: match x: case A(): @@ -350,6 +392,7 @@ def match_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: case C(): return 2 + def match_non_exhaustive(x: A[D] | B[E] | C[F]): match x: case A(): @@ -362,6 +405,7 @@ def match_non_exhaustive(x: A[D] | B[E] | C[F]): # this diagnostic is correct: the inferred type of `x` is `B[E] & ~A[D] & ~C[F]` assert_never(x) # error: [type-assertion-failure] + # This function might seem a bit silly, but it's a pattern that exists in real-world code! # see https://github.com/bokeh/bokeh/blob/adef0157284696ce86961b2089c75fddda53c15c/src/bokeh/core/property/container.py#L130-L140 def no_invalid_return_diagnostic_here_either[T](x: A[T]) -> ASub[T]: @@ -384,6 +428,7 @@ def no_invalid_return_diagnostic_here_either[T](x: A[T]) -> ASub[T]: ```py from typing import assert_never + def as_pattern_exhaustive(subject: int | str): match subject: case int() as x: @@ -395,6 +440,7 @@ def as_pattern_exhaustive(subject: int | str): assert_never(subject) + def as_pattern_non_exhaustive(subject: int | str): match subject: case int() as x: @@ -411,6 +457,7 @@ def as_pattern_non_exhaustive(subject: int | str): ```py from enum import Enum + class Answer(Enum): YES = "yes" NO = "no" @@ -435,6 +482,7 @@ python-version = "3.12" ```py from typing import assert_never, Literal + def f[T: bool](x: T) -> T: match x: case True: @@ -445,6 +493,7 @@ def f[T: bool](x: T) -> T: reveal_type(x) # revealed: Never assert_never(x) + def g[T: Literal["foo", "bar"]](x: T) -> T: match x: case "foo": @@ -455,6 +504,7 @@ def g[T: Literal["foo", "bar"]](x: T) -> T: reveal_type(x) # revealed: Never assert_never(x) + def h[T: int | str](x: T) -> T: if isinstance(x, int): return x @@ -464,6 +514,7 @@ def h[T: int | str](x: T) -> T: reveal_type(x) # revealed: Never assert_never(x) + def i[T: (int, str)](x: T) -> T: match x: case int(): diff --git a/crates/ty_python_semantic/resources/mdtest/expression/assert.md b/crates/ty_python_semantic/resources/mdtest/expression/assert.md index 6fd9e7700f..c19b7993df 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/assert.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/assert.md @@ -4,6 +4,7 @@ class NotBoolable: __bool__: int = 3 + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" assert NotBoolable() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md index 67ba0f4f05..559644646d 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md @@ -59,6 +59,7 @@ reveal_type("x" or "y" and "") # revealed: Literal["x"] ```py redefined_builtin_bool: type[bool] = bool + def my_bool(x) -> bool: return True ``` @@ -86,56 +87,73 @@ reveal_type(bool((0,))) # revealed: Literal[True] reveal_type(bool("NON EMPTY")) # revealed: Literal[True] reveal_type(bool(True)) # revealed: Literal[True] + def foo(): ... + reveal_type(bool(foo)) # revealed: Literal[True] + class SingleElementTupleSubclass(tuple[int]): ... + reveal_type(bool(SingleElementTupleSubclass((0,)))) # revealed: Literal[True] + # Unknown length, but we know the length is guaranteed to be >=2 class MixedTupleSubclass(tuple[int, *tuple[str, ...], bytes]): ... + reveal_type(bool(MixedTupleSubclass((1, b"foo")))) # revealed: Literal[True] + # Unknown length with an overridden `__bool__`: class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]): def __bool__(self) -> Literal[True]: return True + reveal_type(bool(VariadicTupleSubclassWithDunderBoolOverride((1,)))) # revealed: Literal[True] + # Same again but for a subclass of a fixed-length tuple: class EmptyTupleSubclassWithDunderBoolOverride(tuple[()]): # TODO: we should reject this override as a Liskov violation: def __bool__(self) -> Literal[True]: return True + reveal_type(bool(EmptyTupleSubclassWithDunderBoolOverride(()))) # revealed: Literal[True] reveal_type(EmptyTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) -> Literal[True] # revealed: bound method EmptyTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True] reveal_type(EmptyTupleSubclassWithDunderBoolOverride().__bool__) + @final class FinalClassOverridingLenAndNotBool: def __len__(self) -> Literal[42]: return 42 + reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[True] + @final class FinalClassWithNoLenOrBool: ... + reveal_type(bool(FinalClassWithNoLenOrBool())) # revealed: Literal[True] + class EnumWithMembers(enum.Enum): A = 1 B = 2 + reveal_type(bool(EnumWithMembers.A)) # revealed: Literal[True] + def f(x: SingleElementTupleSubclass | FinalClassOverridingLenAndNotBool | FinalClassWithNoLenOrBool | Literal[EnumWithMembers.A]): reveal_type(bool(x)) # revealed: Literal[True] ``` @@ -153,17 +171,22 @@ reveal_type(bool("")) # revealed: Literal[False] reveal_type(bool(False)) # revealed: Literal[False] reveal_type(bool()) # revealed: Literal[False] + class EmptyTupleSubclass(tuple[()]): ... + reveal_type(bool(EmptyTupleSubclass())) # revealed: Literal[False] + @final class FinalClassOverridingLenAndNotBool: def __len__(self) -> Literal[0]: return 0 + reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[False] + class EnumWithMembersOverridingBool(enum.Enum): A = 1 B = 2 @@ -171,8 +194,10 @@ class EnumWithMembersOverridingBool(enum.Enum): def __bool__(self) -> Literal[False]: return False + reveal_type(bool(EnumWithMembersOverridingBool.A)) # revealed: Literal[False] + def f(x: EmptyTupleSubclass | FinalClassOverridingLenAndNotBool | Literal[EnumWithMembersOverridingBool.A]): reveal_type(bool(x)) # revealed: Literal[False] ``` @@ -187,20 +212,25 @@ reveal_type(bool([])) # revealed: bool reveal_type(bool({})) # revealed: bool reveal_type(bool(set())) # revealed: bool + class VariadicTupleSubclass(tuple[int, ...]): ... + def f(x: tuple[int, ...], y: VariadicTupleSubclass): reveal_type(bool(x)) # revealed: bool + class NonFinalOverridingLenAndNotBool: def __len__(self) -> Literal[42]: return 42 + # We cannot consider `__len__` for a non-`@final` type, # because a subclass might override `__bool__`, # and `__bool__` takes precedence over `__len__` reveal_type(bool(NonFinalOverridingLenAndNotBool())) # revealed: bool + class EnumWithMembersOverridingBool(enum.Enum): A = 1 B = 2 @@ -208,6 +238,7 @@ class EnumWithMembersOverridingBool(enum.Enum): def __bool__(self) -> bool: return False + reveal_type(bool(EnumWithMembersOverridingBool.A)) # revealed: bool ``` @@ -216,10 +247,12 @@ reveal_type(bool(EnumWithMembersOverridingBool.A)) # revealed: bool ```py from typing import NoReturn + class NotBoolable: def __bool__(self) -> NoReturn: raise NotImplementedError("This object can't be converted to a boolean") + # TODO: This should emit an error that `NotBoolable` can't be converted to a bool but it currently doesn't # because `Never` is assignable to `bool`. This probably requires dead code analysis to fix. if NotBoolable(): @@ -232,6 +265,7 @@ if NotBoolable(): class NotBoolable: __bool__: None = None + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" if NotBoolable(): ... diff --git a/crates/ty_python_semantic/resources/mdtest/expression/if.md b/crates/ty_python_semantic/resources/mdtest/expression/if.md index 50ba10270c..12c45a865c 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/if.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/if.md @@ -30,6 +30,7 @@ The test inside an if expression should not affect code outside of the expressio ```py from typing import Literal + def _(flag: bool): x: Literal[42, "hello"] = 42 if flag else "hello" diff --git a/crates/ty_python_semantic/resources/mdtest/expression/len.md b/crates/ty_python_semantic/resources/mdtest/expression/len.md index 43ea458b48..1de9e858be 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/len.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/len.md @@ -64,10 +64,17 @@ Tuple subclasses: ```py class EmptyTupleSubclass(tuple[()]): ... + + class Length1TupleSubclass(tuple[int]): ... + + class Length2TupleSubclass(tuple[int, str]): ... + + class UnknownLengthTupleSubclass(tuple[int, ...]): ... + reveal_type(len(EmptyTupleSubclass())) # revealed: Literal[0] reveal_type(len(Length1TupleSubclass((1,)))) # revealed: Literal[1] reveal_type(len(Length2TupleSubclass((1, "foo")))) # revealed: Literal[2] @@ -76,10 +83,12 @@ reveal_type(len(UnknownLengthTupleSubclass((1, 2, 3)))) # revealed: int reveal_type(tuple[int, int].__len__) # revealed: (self: tuple[int, int], /) -> Literal[2] reveal_type(tuple[int, ...].__len__) # revealed: (self: tuple[int, ...], /) -> int + def f(x: tuple[int, int], y: tuple[int, ...]): reveal_type(x.__len__) # revealed: () -> Literal[2] reveal_type(y.__len__) # revealed: () -> int + reveal_type(EmptyTupleSubclass.__len__) # revealed: (self: tuple[()], /) -> Literal[0] reveal_type(EmptyTupleSubclass().__len__) # revealed: () -> Literal[0] reveal_type(UnknownLengthTupleSubclass.__len__) # revealed: (self: tuple[int, ...], /) -> int @@ -91,16 +100,20 @@ If `__len__` is overridden, we use the overridden return type: ```py from typing import Literal + class UnknownLengthSubclassWithDunderLenOverridden(tuple[int, ...]): def __len__(self) -> Literal[42]: return 42 + reveal_type(len(UnknownLengthSubclassWithDunderLenOverridden())) # revealed: Literal[42] + class FixedLengthSubclassWithDunderLenOverridden(tuple[int]): def __len__(self) -> Literal[42]: # error: [invalid-method-override] return 42 + reveal_type(len(FixedLengthSubclassWithDunderLenOverridden((1,)))) # revealed: Literal[42] ``` @@ -135,30 +148,37 @@ The returned value of `__len__` is implicitly and recursively converted to `int` ```py from typing import Literal + class Zero: def __len__(self) -> Literal[0]: return 0 + class ZeroOrOne: def __len__(self) -> Literal[0, 1]: return 0 + class ZeroOrTrue: def __len__(self) -> Literal[0, True]: return 0 + class OneOrFalse: def __len__(self) -> Literal[1] | Literal[False]: return 1 + class OneOrFoo: def __len__(self) -> Literal[1, "foo"]: return 1 + class ZeroOrStr: def __len__(self) -> Literal[0] | str: return 0 + reveal_type(len(Zero())) # revealed: Literal[0] reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1] reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1] @@ -176,14 +196,17 @@ reveal_type(len(ZeroOrStr())) # revealed: int ```py from typing import Literal + class LiteralTrue: def __len__(self) -> Literal[True]: return True + class LiteralFalse: def __len__(self) -> Literal[False]: return False + reveal_type(len(LiteralTrue())) # revealed: Literal[1] reveal_type(len(LiteralFalse())) # revealed: Literal[0] ``` @@ -193,10 +216,12 @@ reveal_type(len(LiteralFalse())) # revealed: Literal[0] ```py from typing import Literal + class Negative: def __len__(self) -> Literal[-1]: return -1 + # TODO: Emit a diagnostic reveal_type(len(Negative())) # revealed: int ``` @@ -206,14 +231,17 @@ reveal_type(len(Negative())) # revealed: int ```py from typing import Literal + class SecondOptionalArgument: def __len__(self, v: int = 0) -> Literal[0]: return 0 + class SecondRequiredArgument: def __len__(self, v: int) -> Literal[1]: return 1 + # this is fine: the call succeeds at runtime since the second argument is optional reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0] @@ -226,6 +254,7 @@ reveal_type(len(SecondRequiredArgument())) # revealed: int ```py class NoDunderLen: ... + # error: [invalid-argument-type] reveal_type(len(NoDunderLen())) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md index a207b3414f..c8ba12d749 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md @@ -8,6 +8,7 @@ The type of a `yield` expression is the "send" type of the generator function. T ```py from typing import Generator + def inner_generator() -> Generator[int, bytes, str]: yield 1 yield 2 @@ -18,6 +19,7 @@ def inner_generator() -> Generator[int, bytes, str]: return "done" + def outer_generator(): result = yield from inner_generator() reveal_type(result) # revealed: str @@ -33,6 +35,7 @@ from typing import Generator, TypeVar, Generic T = TypeVar("T") + class OnceIterator(Generic[T]): def __init__(self, value: T): self.value = value @@ -45,6 +48,7 @@ class OnceIterator(Generic[T]): self.returned = True return self.value + class Once(Generic[T]): def __init__(self, value: T): self.value = value @@ -52,9 +56,11 @@ class Once(Generic[T]): def __iter__(self) -> OnceIterator[T]: return OnceIterator(self.value) + for x in Once("a"): reveal_type(x) # revealed: str + def generator() -> Generator: result = yield from Once("a") @@ -77,6 +83,7 @@ def generator() -> Generator: ```py from types import GeneratorType + def inner_generator() -> GeneratorType[int, bytes, str]: yield 1 yield 2 @@ -87,6 +94,7 @@ def inner_generator() -> GeneratorType[int, bytes, str]: return "done" + def outer_generator(): result = yield from inner_generator() reveal_type(result) # revealed: str @@ -99,6 +107,7 @@ def outer_generator(): ```py from typing import Generator + def generator() -> Generator: yield from 42 # error: [not-iterable] "Object of type `Literal[42]` is not iterable" ``` @@ -108,6 +117,7 @@ def generator() -> Generator: ```py from typing import Generator + # TODO: This should be an error. Claims to yield `int`, but yields `str`. def invalid_generator() -> Generator[int, None, None]: yield "not an int" # This should be an `int` @@ -118,10 +128,12 @@ def invalid_generator() -> Generator[int, None, None]: ```py from typing import Generator + # TODO: should emit an error (does not return `str`) def invalid_generator1() -> Generator[int, None, str]: yield 1 + # TODO: should emit an error (does not return `int`) def invalid_generator2() -> Generator[int, None, None]: yield 1 diff --git a/crates/ty_python_semantic/resources/mdtest/external/attrs.md b/crates/ty_python_semantic/resources/mdtest/external/attrs.md index 3b4bc342a6..e4d59a2020 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/attrs.md +++ b/crates/ty_python_semantic/resources/mdtest/external/attrs.md @@ -14,11 +14,13 @@ dependencies = ["attrs==25.4.0"] ```py import attr + @attr.s class User: id: int = attr.ib() name: str = attr.ib() + user = User(id=1, name="John Doe") reveal_type(user.id) # revealed: int @@ -30,11 +32,13 @@ reveal_type(user.name) # revealed: str ```py from attrs import define, field + @define class User: id: int = field() internal_name: str = field(alias="name") + user = User(id=1, name="John Doe") reveal_type(user.id) # revealed: int reveal_type(user.internal_name) # revealed: str @@ -45,12 +49,14 @@ reveal_type(user.internal_name) # revealed: str ```py from attrs import define, field + @define class Product: id: int = field(init=False) name: str = field() price_cent: int = field(kw_only=True) + reveal_type(Product.__init__) # revealed: (self: Product, name: str, *, price_cent: int) -> None ``` @@ -61,6 +67,7 @@ We currently do not support this: ```py from attrs import define, field + @define class Person: id: int = field() @@ -71,6 +78,7 @@ class Person: def _default_id(self) -> int: raise NotImplementedError + # error: [missing-argument] "No argument provided for required parameter `id`" person = Person(name="Alice") reveal_type(person.id) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic.md b/crates/ty_python_semantic/resources/mdtest/external/pydantic.md index a8a55f3a47..4de39f6ed8 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/pydantic.md +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic.md @@ -14,10 +14,12 @@ dependencies = ["pydantic==2.12.2"] ```py from pydantic import BaseModel + class User(BaseModel): id: int name: str + reveal_type(User.__init__) # revealed: (self: User, *, id: int, name: str) -> None user = User(id=1, name="John Doe") @@ -33,11 +35,13 @@ invalid_user = User(id=2) ```py from pydantic import BaseModel, Field + class Product(BaseModel): id: int = Field(init=False) name: str = Field(..., kw_only=False, min_length=1) internal_price_cent: int = Field(..., gt=0, alias="price_cent") + reveal_type(Product.__init__) # revealed: (self: Product, name: str = ..., *, price_cent: int = ...) -> None product = Product("Laptop", price_cent=999_00) diff --git a/crates/ty_python_semantic/resources/mdtest/external/pytest.md b/crates/ty_python_semantic/resources/mdtest/external/pytest.md index 823ef4d162..3d818dee91 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/pytest.md +++ b/crates/ty_python_semantic/resources/mdtest/external/pytest.md @@ -16,9 +16,11 @@ Make sure that we recognize `pytest.fail` calls as terminal: ```py import pytest + def some_runtime_condition() -> bool: return True + def test_something(): if not some_runtime_condition(): pytest.fail("Runtime condition failed") diff --git a/crates/ty_python_semantic/resources/mdtest/external/sqlalchemy.md b/crates/ty_python_semantic/resources/mdtest/external/sqlalchemy.md index 43fff45058..ad41baec2f 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/sqlalchemy.md +++ b/crates/ty_python_semantic/resources/mdtest/external/sqlalchemy.md @@ -16,15 +16,18 @@ This test makes sure that ty understands SQLAlchemy's `dataclass_transform` setu ```py from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + class Base(DeclarativeBase): pass + class User(Base): __tablename__ = "user" id: Mapped[int] = mapped_column(primary_key=True, init=False) internal_name: Mapped[str] = mapped_column(alias="name") + user = User(name="John Doe") reveal_type(user.id) # revealed: int reveal_type(user.internal_name) # revealed: str @@ -61,6 +64,7 @@ And define a simple model: class Base(DeclarativeBase): pass + class User(Base): __tablename__ = "users" @@ -173,15 +177,18 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, Integer, Text from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + class Base(DeclarativeBase): pass + class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(Text) + async def test_async(session: AsyncSession): stmt = select(User).where(User.name == "Alice") alice = await session.scalar(stmt) diff --git a/crates/ty_python_semantic/resources/mdtest/external/sqlmodel.md b/crates/ty_python_semantic/resources/mdtest/external/sqlmodel.md index 7dafa336db..a14bffcff2 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/sqlmodel.md +++ b/crates/ty_python_semantic/resources/mdtest/external/sqlmodel.md @@ -14,10 +14,12 @@ dependencies = ["sqlmodel==0.0.27"] ```py from sqlmodel import SQLModel + class User(SQLModel): id: int name: str + user = User(id=1, name="John Doe") reveal_type(user.id) # revealed: int reveal_type(user.name) # revealed: str diff --git a/crates/ty_python_semantic/resources/mdtest/external/strawberry.md b/crates/ty_python_semantic/resources/mdtest/external/strawberry.md index 58aef9325f..0c2cf9807b 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/strawberry.md +++ b/crates/ty_python_semantic/resources/mdtest/external/strawberry.md @@ -14,11 +14,13 @@ dependencies = ["strawberry-graphql==0.283.3"] ```py import strawberry + @strawberry.type class User: id: int role: str = strawberry.field(default="user") + reveal_type(User.__init__) # revealed: (self: User, *, id: int, role: str = ...) -> None user = User(id=1) diff --git a/crates/ty_python_semantic/resources/mdtest/final.md b/crates/ty_python_semantic/resources/mdtest/final.md index 98a26c3649..94cf5cecbf 100644 --- a/crates/ty_python_semantic/resources/mdtest/final.md +++ b/crates/ty_python_semantic/resources/mdtest/final.md @@ -8,19 +8,30 @@ Don't do this: import typing_extensions from typing import final + @final class A: ... + class B(A): ... # error: 9 [subclass-of-final-class] "Class `B` cannot inherit from final class `A`" + @typing_extensions.final class C: ... + class D(C): ... # error: [subclass-of-final-class] + + class E: ... + + class F: ... + + class G: ... + # fmt: off class H( E, @@ -175,6 +186,7 @@ class Baz(Foo): ```py from typing import final + class Foo: @final def f(self): ... @@ -185,6 +197,7 @@ class Foo: ```py import module1 + class Foo(module1.Foo): def f(self): ... # error: [override-of-final-method] ``` @@ -257,6 +270,7 @@ class ChildOfBad(Bad): ```py from typing import overload, final + class Good: @overload def f(self, x: str) -> str: ... @@ -266,6 +280,7 @@ class Good: def f(self, x: int | str) -> int | str: return x + class ChildOfGood(Good): @overload def f(self, x: str) -> str: ... @@ -276,6 +291,7 @@ class ChildOfGood(Good): def f(self, x: int | str) -> int | str: return x + class Bad: @overload @final @@ -317,6 +333,7 @@ class Bad: def i(self, x: int | str) -> int | str: return x + class ChildOfBad(Bad): # TODO: these should all cause us to emit Liskov violations as well f = None # error: [override-of-final-method] @@ -343,13 +360,16 @@ type qualifier as travelling *across* scopes. ```py from typing import final + class A: @final def method(self) -> None: ... + class B: method = A.method + class C(B): def method(self) -> None: ... # no diagnostic here (see prose discussion above) ``` @@ -359,10 +379,12 @@ class C(B): ```py from typing import final + class A: @final def __init__(self) -> None: ... + class B(A): def __init__(self) -> None: ... # error: [override-of-final-method] ``` @@ -376,14 +398,17 @@ class B(A): ```py from typing import final + class A: @final def f(self): ... + class B(A): @final def f(self): ... # error: [override-of-final-method] + class C(B): @final # we only emit one error here, not two @@ -395,6 +420,7 @@ class C(B): ```py from typing import final, Final + @final @final @final @@ -409,6 +435,7 @@ class A: @final def method(self): ... + @final @final @final @@ -417,9 +444,11 @@ class A: class B: method: Final = A.method + class C(A): # error: [subclass-of-final-class] def method(self): ... # error: [override-of-final-method] + class D(B): # error: [subclass-of-final-class] # TODO: we should emit a diagnostic here def method(self): ... @@ -430,10 +459,12 @@ class D(B): # error: [subclass-of-final-class] ```py from typing import final, Any + class Parent: @final def method(self) -> None: ... + class Child(Parent): def __init__(self) -> None: self.method: Any = 42 # TODO: we should emit `[override-of-final-method]` here @@ -446,37 +477,54 @@ class Child(Parent): ```py from typing import final + def coinflip() -> bool: return False + class A: if coinflip(): + @final def method1(self) -> None: ... + else: + def method1(self) -> None: ... if coinflip(): + def method2(self) -> None: ... + else: + @final def method2(self) -> None: ... if coinflip(): + @final def method3(self) -> None: ... + else: + @final def method3(self) -> None: ... if coinflip(): + def method4(self) -> None: ... + elif coinflip(): + @final def method4(self) -> None: ... + else: + def method4(self) -> None: ... + class B(A): def method1(self) -> None: ... # error: [override-of-final-method] def method2(self) -> None: ... # error: [override-of-final-method] @@ -490,19 +538,26 @@ class B(A): method4 = 42 unrelated = 56 # fmt: skip + # Possible overrides of possibly `@final` methods... class C(A): if coinflip(): + def method1(self) -> None: ... # error: [override-of-final-method] + else: pass if coinflip(): + def method2(self) -> None: ... # error: [override-of-final-method] + else: + def method2(self) -> None: ... if coinflip(): + def method3(self) -> None: ... # error: [override-of-final-method] # TODO: we should emit Liskov violations here too: @@ -523,15 +578,19 @@ python-version = "3.10" import sys from typing_extensions import final + class Parent: if sys.version_info >= (3, 10): + @final def foo(self) -> None: ... @final def foooo(self) -> None: ... @final def baaaaar(self) -> None: ... + else: + @final def bar(self) -> None: ... @final @@ -539,6 +598,7 @@ class Parent: @final def spam(self) -> None: ... + class Child(Parent): def foo(self) -> None: ... # error: [override-of-final-method] @@ -547,8 +607,10 @@ class Child(Parent): def bar(self) -> None: ... if sys.version_info >= (3, 10): + def foooo(self) -> None: ... # error: [override-of-final-method] def baz(self) -> None: ... + else: # Fine because this doesn't override any reachable definitions def foooo(self) -> None: ... diff --git a/crates/ty_python_semantic/resources/mdtest/function/parameters.md b/crates/ty_python_semantic/resources/mdtest/function/parameters.md index c629f26565..7010c2cda7 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/parameters.md +++ b/crates/ty_python_semantic/resources/mdtest/function/parameters.md @@ -13,6 +13,7 @@ a dictionary from strings to its annotated type. ```py from typing import Literal + def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, h: Literal[6] = 6, **kwargs: str): reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: int @@ -43,6 +44,7 @@ If there is an annotation, we respect it fully and don't union in the default va ```py from typing import Any + def f(x: Any = 1): reveal_type(x) # revealed: Any ``` @@ -57,9 +59,11 @@ fall back to inferring the annotated type, ignoring the default value type. def f(x: int = "foo"): reveal_type(x) # revealed: int + # The check is assignable-to, not subtype-of, so this is fine: from typing import Any + def g(x: Any = "foo"): reveal_type(x) # revealed: Any ``` @@ -76,10 +80,12 @@ python-version = "3.12" ```py from typing import Protocol + class Foo(Protocol): def x(self, y: bool = ...): ... def y[T](self, y: T = ...) -> T: ... + class GenericFoo[T](Protocol): def x(self, y: bool = ...) -> T: ... ``` @@ -89,6 +95,7 @@ class GenericFoo[T](Protocol): ```py from abc import abstractmethod + class Bar: @abstractmethod def x(self, y: bool = ...): ... @@ -101,6 +108,7 @@ class Bar: ```py from typing import overload + @overload def x(y: None = ...) -> None: ... @overload diff --git a/crates/ty_python_semantic/resources/mdtest/function/return_type.md b/crates/ty_python_semantic/resources/mdtest/function/return_type.md index 28bd509e44..cec2223f52 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/return_type.md +++ b/crates/ty_python_semantic/resources/mdtest/function/return_type.md @@ -25,6 +25,7 @@ A `raise` is equivalent to a return of `Never`, which is assignable to any annot def f() -> str: raise ValueError() + reveal_type(f()) # revealed: str ``` @@ -64,24 +65,31 @@ python-version = "3.12" ```py from typing import Protocol, TypeVar + class Bar(Protocol): def f(self) -> int: ... + class Baz(Bar): # error: [invalid-return-type] def f(self) -> int: ... + T = TypeVar("T") + class Qux(Protocol[T]): def f(self) -> int: ... + class Foo(Protocol): def f[T](self, v: T) -> T: ... + t = (Protocol, int) reveal_type(t[0]) # revealed: + class Lorem(t[0]): def f(self) -> int: ... ``` @@ -96,18 +104,21 @@ python-version = "3.12" ```py from abc import ABC, abstractmethod + class Foo(ABC): @abstractmethod def f(self) -> int: ... @abstractmethod def g[T](self, x: T) -> T: ... + class Bar[T](ABC): @abstractmethod def f(self) -> int: ... @abstractmethod def g[T](self, x: T) -> T: ... + # error: [invalid-return-type] def f() -> int: ... @abstractmethod # Semantically meaningless, accepted nevertheless @@ -119,6 +130,7 @@ def g() -> int: ... ```py from typing import overload + @overload def f(x: int) -> int: ... @overload @@ -157,86 +169,123 @@ import typing as t import compat.sub.sub if TYPE_CHECKING: + def f() -> int: ... else: + def f() -> str: return "hello" + reveal_type(f) # revealed: def f() -> int if not TYPE_CHECKING: ... elif True: + def g() -> str: ... else: + def h() -> str: ... + if not TYPE_CHECKING: + def i() -> int: return 1 else: + def i() -> str: ... + reveal_type(i) # revealed: def i() -> str if False: ... elif TYPE_CHECKING: + def j() -> str: ... else: + def j_() -> str: ... # error: [invalid-return-type] + if False: ... elif not TYPE_CHECKING: + def k_() -> str: ... # error: [invalid-return-type] else: + def k() -> str: ... + class Foo: if TYPE_CHECKING: + def f(self) -> int: ... + if TYPE_CHECKING: + class Bar: def f(self) -> int: ... + def get_bool() -> bool: return True + if TYPE_CHECKING: if get_bool(): + def l() -> str: ... + if get_bool(): if TYPE_CHECKING: + def m() -> str: ... + if TYPE_CHECKING: if not TYPE_CHECKING: + def n() -> str: ... + if typing.TYPE_CHECKING: + def o() -> str: ... + if not typing.TYPE_CHECKING: + def p() -> str: ... # error: [invalid-return-type] + if compat.sub.sub.TYPE_CHECKING: + def q() -> str: ... + if not compat.sub.sub.TYPE_CHECKING: + def r() -> str: ... # error: [invalid-return-type] + if t.TYPE_CHECKING: + def s() -> str: ... + if not t.TYPE_CHECKING: + def t() -> str: ... # error: [invalid-return-type] ``` @@ -249,18 +298,21 @@ def f(cond: bool) -> int: else: return 2 + def f(cond: bool) -> int | None: if cond: return 1 else: return + def f(cond: bool) -> int: if cond: return 1 else: raise ValueError() + def f(cond: bool) -> str | int: if cond: return "a" @@ -275,17 +327,20 @@ def f(cond: bool) -> int | None: if cond: return 1 + # no implicit return def f() -> int: if True: return 1 + # no implicit return def f(cond: bool) -> int: cond = True if cond: return 1 + def f(cond: bool) -> int: if cond: cond = True @@ -309,31 +364,41 @@ python-version = "3.12" def f() -> int: 1 + def f() -> str: # error: [invalid-return-type] return 1 + def f() -> int: # error: [invalid-return-type] return + from typing import TypeVar T = TypeVar("T") + # error: [invalid-return-type] def m(x: T) -> T: ... + class A[T]: ... + def f() -> A[int]: class A[T]: ... + return A[int]() # error: [invalid-return-type] + class B: ... + def g() -> B: class B: ... + return B() # error: [invalid-return-type] ``` @@ -369,6 +434,7 @@ def f(cond: bool) -> str: # error: [invalid-return-type] return 1 + def f(cond: bool) -> str: if cond: # error: [invalid-return-type] @@ -388,16 +454,19 @@ def f() -> None: # error: [invalid-return-type] return 1 + # error: [invalid-return-type] def f(cond: bool) -> int: if cond: return 1 + # error: [invalid-return-type] def f(cond: bool) -> int: if cond: raise ValueError() + # error: [invalid-return-type] def f(cond: bool) -> int: if cond: @@ -433,6 +502,7 @@ of special dunder methods. You can find more details in the ```py from __future__ import annotations + class A: def __add__(self, o: A) -> A: return NotImplemented @@ -444,12 +514,14 @@ However, as shown below, `NotImplemented` should not cause issues with the decla def f() -> int: return NotImplemented + def f(cond: bool) -> int: if cond: return 1 else: return NotImplemented + def f(x: int) -> int | str: if x < 0: return -1 @@ -458,9 +530,11 @@ def f(x: int) -> int | str: else: return "test" + def f(cond: bool) -> str: return "hello" if cond else NotImplemented + def f(cond: bool) -> int: # error: [invalid-return-type] "Return type does not match returned value: expected `int`, found `Literal["hello"]`" return "hello" if cond else NotImplemented @@ -493,6 +567,7 @@ python-version = "3.10" def f() -> int: return NotImplemented + def f(cond: bool) -> str: return "hello" if cond else NotImplemented ``` @@ -512,21 +587,27 @@ statements. import types import typing + def f() -> types.GeneratorType: yield 42 + def g() -> typing.Generator: yield 42 + def h() -> typing.Iterator: yield 42 + def i() -> typing.Iterable: yield 42 + def i2() -> typing.Generator: yield from i() + def j() -> str: # error: [invalid-return-type] yield 42 ``` @@ -542,18 +623,23 @@ if it does not contain any `return` statements. import types import typing + async def f() -> types.AsyncGeneratorType: yield 42 + async def g() -> typing.AsyncGenerator: yield 42 + async def h() -> typing.AsyncIterator: yield 42 + async def i() -> typing.AsyncIterable: yield 42 + async def j() -> str: # error: [invalid-return-type] yield 42 ``` @@ -567,9 +653,11 @@ We emit a nice subdiagnostic in this situation explaining the probable error her ```py from typing_extensions import Protocol + class Abstract(Protocol): def method(self) -> str: ... + class Concrete(Abstract): def method(self) -> str: ... # error: [invalid-return-type] ``` @@ -583,6 +671,7 @@ environment.python-version = "3.12" ```py from typing import Never, Any + def f(func: Any) -> Never: # error: [invalid-return-type] func() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/builtins.md b/crates/ty_python_semantic/resources/mdtest/generics/builtins.md index e98de384ab..b7c07a9fde 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/builtins.md @@ -31,6 +31,7 @@ arguments. def f(**kwargs): reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown] + def g(**kwargs: int): reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 464f7a3fc8..62315f0bd9 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -19,15 +19,31 @@ S = TypeVar("S") P = ParamSpec("P") Ts = TypeVarTuple("Ts") + class SingleTypevar(Generic[T]): ... + + class MultipleTypevars(Generic[T, S]): ... + + class SingleParamSpec(Generic[P]): ... + + class TypeVarAndParamSpec(Generic[P, T]): ... + + class SingleTypeVarTuple(Generic[Unpack[Ts]]): ... + + class StarredSingleTypeVarTuple(Generic[*Ts]): ... + + class TypeVarAndTypeVarTuple(Generic[T, Unpack[Ts]]): ... + + class StarredTypeVarAndTypeVarTuple(Generic[T, *Ts]): ... + # revealed: ty_extensions.GenericContext[T@SingleTypevar] reveal_type(generic_context(SingleTypevar)) # revealed: ty_extensions.GenericContext[T@MultipleTypevars, S@MultipleTypevars] @@ -50,6 +66,8 @@ class: ```py class Bad(Generic[T], Generic[T]): ... # error: [duplicate-base] + + class AlsoBad(Generic[T], Generic[S]): ... # error: [duplicate-base] ``` @@ -72,9 +90,14 @@ it with typevars. ```py class InheritedGeneric(MultipleTypevars[T, S]): ... + + class InheritedGenericPartiallySpecialized(MultipleTypevars[T, int]): ... + + class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ... + # revealed: ty_extensions.GenericContext[T@InheritedGeneric, S@InheritedGeneric] reveal_type(generic_context(InheritedGeneric)) # revealed: ty_extensions.GenericContext[T@InheritedGenericPartiallySpecialized] @@ -90,15 +113,18 @@ present, they are not included in the class's generic context. class OuterClass(Generic[T]): # error: [invalid-generic-class] "Generic class `InnerClass` must not reference type variables bound in an enclosing scope" class InnerClass(list[T]): ... + # revealed: None reveal_type(generic_context(InnerClass)) def method(self): # error: [invalid-generic-class] "Generic class `InnerClassInMethod` must not reference type variables bound in an enclosing scope" class InnerClassInMethod(list[T]): ... + # revealed: None reveal_type(generic_context(InnerClassInMethod)) + # revealed: ty_extensions.GenericContext[T@OuterClass] reveal_type(generic_context(OuterClass)) ``` @@ -110,6 +136,7 @@ the inheriting class generic. ```py class InheritedGenericDefaultSpecialization(MultipleTypevars): ... + reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None ``` @@ -119,14 +146,21 @@ if you do, you have to mention all of the typevars that you use in your other ba ```py class ExplicitInheritedGeneric(MultipleTypevars[T, S], Generic[T, S]): ... + # error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes" class ExplicitInheritedGenericMissingTypevar(MultipleTypevars[T, S], Generic[T]): ... + + class ExplicitInheritedGenericPartiallySpecialized(MultipleTypevars[T, int], Generic[T]): ... + + class ExplicitInheritedGenericPartiallySpecializedExtraTypevar(MultipleTypevars[T, int], Generic[T, S]): ... + # error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes" class ExplicitInheritedGenericPartiallySpecializedMissingTypevar(MultipleTypevars[T, int], Generic[S]): ... + # revealed: ty_extensions.GenericContext[T@ExplicitInheritedGeneric, S@ExplicitInheritedGeneric] reveal_type(generic_context(ExplicitInheritedGeneric)) # revealed: ty_extensions.GenericContext[T@ExplicitInheritedGenericPartiallySpecialized] @@ -144,9 +178,11 @@ from typing_extensions import Generic, Literal, TypeVar T = TypeVar("T") + class C(Generic[T]): x: T + reveal_type(C[int]()) # revealed: C[int] reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]] ``` @@ -166,10 +202,16 @@ from typing import Union BoundedT = TypeVar("BoundedT", bound=int) BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str]) + class Bounded(Generic[BoundedT]): ... + + class BoundedByUnion(Generic[BoundedByUnionT]): ... + + class IntSubclass(int): ... + reveal_type(Bounded[int]()) # revealed: Bounded[int] reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass] @@ -190,8 +232,10 @@ If the type variable is constrained, the specialized type must satisfy those con ```py ConstrainedT = TypeVar("ConstrainedT", int, str) + class Constrained(Generic[ConstrainedT]): ... + reveal_type(Constrained[int]()) # revealed: Constrained[int] # TODO: error: [invalid-argument-type] @@ -213,8 +257,10 @@ If the type variable has a default, it can be omitted: ```py WithDefaultU = TypeVar("WithDefaultU", default=int) + class WithDefault(Generic[T, WithDefaultU]): ... + reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] ``` @@ -234,9 +280,11 @@ from typing import TypeVar, Generic T = TypeVar("T", bound=str) U = TypeVar("U", int, bytes) + class Bounded(Generic[T]): x: T + class Constrained(Generic[U]): x: U ``` @@ -259,9 +307,11 @@ from typing_extensions import Generic, TypeVar T = TypeVar("T") + class C(Generic[T]): x: T + c: C[int] = C() # TODO: revealed: C[int] reveal_type(c) # revealed: C[Unknown] @@ -280,8 +330,10 @@ specific type, we infer the typevar's default type: ```py DefaultT = TypeVar("DefaultT", default=int) + class D(Generic[DefaultT]): ... + reveal_type(D()) # revealed: D[int] ``` @@ -305,10 +357,12 @@ from ty_extensions import generic_context, into_callable T = TypeVar("T") + class C(Generic[T]): def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C] @@ -328,9 +382,11 @@ from ty_extensions import generic_context, into_callable T = TypeVar("T") + class C(Generic[T]): def __init__(self, x: T) -> None: ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C] @@ -350,12 +406,14 @@ from ty_extensions import generic_context, into_callable T = TypeVar("T") + class C(Generic[T]): def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) def __init__(self, x: T) -> None: ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C] @@ -375,12 +433,14 @@ from ty_extensions import generic_context, into_callable T = TypeVar("T") + class C(Generic[T]): def __new__(cls, *args, **kwargs) -> "C[T]": return object.__new__(cls) def __init__(self, x: T) -> None: ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C] @@ -391,12 +451,14 @@ reveal_type(C(1)) # revealed: C[int] # error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") + class D(Generic[T]): def __new__(cls, x: T) -> "D[T]": return object.__new__(cls) def __init__(self, *args, **kwargs) -> None: ... + # revealed: ty_extensions.GenericContext[T@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[T@D] @@ -421,13 +483,16 @@ T = TypeVar("T") U = TypeVar("U") V = TypeVar("V") + class C(Generic[T, U]): def __new__(cls, *args, **kwargs) -> "C[T, U]": return object.__new__(cls) + class D(C[V, int]): def __init__(self, x: V) -> None: ... + # revealed: ty_extensions.GenericContext[V@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[V@D] @@ -445,12 +510,15 @@ from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U") + class C(Generic[T, U]): def __init__(self, t: T, u: U) -> None: ... + class D(C[T, U]): pass + # revealed: ty_extensions.GenericContext[T@D, U@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[T@D, U@D] @@ -471,9 +539,11 @@ from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U") + class D(dict[T, U]): pass + # revealed: ty_extensions.GenericContext[T@D, U@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[T@D, U@D] @@ -495,8 +565,10 @@ from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U") + class C(tuple[T, U]): ... + # revealed: ty_extensions.GenericContext[T@C, U@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C, U@C] @@ -520,9 +592,11 @@ from typing_extensions import TypeVar, Sequence, Never T = TypeVar("T") + def test_seq(x: Sequence[T]) -> Sequence[T]: return x + def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: tuple[()]): reveal_type(test_seq(t1)) # revealed: Sequence[int | float | complex | list[int]] reveal_type(test_seq(t2)) # revealed: Sequence[int | str] @@ -538,9 +612,11 @@ from ty_extensions import generic_context, into_callable S = TypeVar("S") T = TypeVar("T") + class C(Generic[T]): def __init__(self, x: T, y: S) -> None: ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C, S@__init__] @@ -563,6 +639,7 @@ from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U") + class C(Generic[T]): @overload def __init__(self: "C[str]", x: str) -> None: ... @@ -574,6 +651,7 @@ class C(Generic[T]): def __init__(self, x: int) -> None: ... def __init__(self, x: str | bytes | int) -> None: ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C] @@ -599,6 +677,7 @@ C[None]("string") # error: [no-matching-overload] C[None](b"bytes") # error: [no-matching-overload] C[None](12) + class D(Generic[T, U]): @overload def __init__(self: "D[str, U]", u: U) -> None: ... @@ -606,6 +685,7 @@ class D(Generic[T, U]): def __init__(self, t: T, u: U) -> None: ... def __init__(self, *args) -> None: ... + # revealed: ty_extensions.GenericContext[T@D, U@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[T@D, U@D] @@ -625,10 +705,12 @@ from ty_extensions import generic_context, into_callable T = TypeVar("T") + @dataclass class A(Generic[T]): x: T + # revealed: ty_extensions.GenericContext[T@A] reveal_type(generic_context(A)) # revealed: ty_extensions.GenericContext[T@A] @@ -646,8 +728,10 @@ from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U", default=T) + class C(Generic[T, U]): ... + # revealed: ty_extensions.GenericContext[T@C, U@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C, U@C] @@ -655,9 +739,11 @@ reveal_type(generic_context(into_callable(C))) reveal_type(C()) # revealed: C[Unknown, Unknown] + class D(Generic[T, U]): def __init__(self) -> None: ... + # revealed: ty_extensions.GenericContext[T@D, U@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[T@D, U@D] @@ -679,16 +765,29 @@ U = TypeVar("U") V = TypeVar("V") W = TypeVar("W") + class Parent(Generic[T]): x: T + class ExplicitlyGenericChild(Parent[U], Generic[U]): ... + + class ExplicitlyGenericGrandchild(ExplicitlyGenericChild[V], Generic[V]): ... + + class ExplicitlyGenericGreatgrandchild(ExplicitlyGenericGrandchild[W], Generic[W]): ... + + class ImplicitlyGenericChild(Parent[U]): ... + + class ImplicitlyGenericGrandchild(ImplicitlyGenericChild[V]): ... + + class ImplicitlyGenericGreatgrandchild(ImplicitlyGenericGrandchild[W]): ... + reveal_type(Parent[int]().x) # revealed: int reveal_type(ExplicitlyGenericChild[int]().x) # revealed: int reveal_type(ImplicitlyGenericChild[int]().x) # revealed: int @@ -711,6 +810,7 @@ from typing_extensions import Generic, TypeVar T = TypeVar("T") U = TypeVar("U") + class C(Generic[T]): def method(self, u: int) -> int: return u @@ -718,6 +818,7 @@ class C(Generic[T]): def generic_method(self, t: T, u: U) -> U: return u + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[Self@method] @@ -752,8 +853,10 @@ from typing_extensions import Generic, TypeVar, Protocol T = TypeVar("T") U = TypeVar("U") + class LinkedList(Generic[T]): ... + class C(Generic[T, U]): x: T y: U @@ -767,6 +870,7 @@ class C(Generic[T, U]): def method3(self) -> LinkedList[T]: return LinkedList[T]() + c = C[int, str]() reveal_type(c.x) # revealed: int reveal_type(c.y) # revealed: str @@ -774,12 +878,15 @@ reveal_type(c.method1()) # revealed: int reveal_type(c.method2()) # revealed: str reveal_type(c.method3()) # revealed: LinkedList[int] + class SomeProtocol(Protocol[T]): x: T + class Foo(Generic[T]): x: T + class D(Generic[T, U]): x: T y: U @@ -793,6 +900,7 @@ class D(Generic[T, U]): def method3(self) -> SomeProtocol[T]: return Foo() + d = D[int, str]() reveal_type(d.x) # revealed: int reveal_type(d.y) # revealed: str @@ -809,6 +917,7 @@ from typing_extensions import overload, Generic, TypeVar S = TypeVar("S") + class WithOverloadedMethod(Generic[T]): @overload def method(self, x: T) -> T: ... @@ -817,6 +926,7 @@ class WithOverloadedMethod(Generic[T]): def method(self, x: S | T) -> S | T: return x + # revealed: Overload[(self, x: int) -> int, [S](self, x: S) -> S | int] reveal_type(WithOverloadedMethod[int].method) ``` @@ -852,14 +962,21 @@ from typing_extensions import Generic, TypeVar T = TypeVar("T") + class Base(Generic[T]): ... + + class Sub(Base["Sub"]): ... + reveal_type(Sub) # revealed: U = TypeVar("U") + class Base2(Generic[T, U]): ... + + class Sub2(Base2["Sub2", U]): ... ``` @@ -872,8 +989,10 @@ from typing_extensions import Generic, TypeVar T = TypeVar("T") + class Base(Generic[T]): ... + # error: [unresolved-reference] class Sub(Base[Sub]): ... ``` @@ -897,9 +1016,11 @@ from typing_extensions import Generic, TypeVar T = TypeVar("T") + # error: [unresolved-reference] class C(C, Generic[T]): ... + # error: [unresolved-reference] class D(D[int], Generic[T]): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index c309a76cd9..58cf335bd9 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -10,12 +10,15 @@ from typing import TypeVar T = TypeVar("T") + # TODO: error, should be (x: object) def typevar_not_needed(x: T) -> None: pass + BoundedT = TypeVar("BoundedT", bound=int) + # TODO: error, should be (x: int) def bounded_typevar_not_needed(x: BoundedT) -> None: pass @@ -54,9 +57,11 @@ from typing import TypeVar T = TypeVar("T") + def f(x: T) -> T: return x + reveal_type(f(1)) # revealed: Literal[1] reveal_type(f(1.0)) # revealed: float reveal_type(f(True)) # revealed: Literal[True] @@ -76,49 +81,65 @@ from typing import Protocol, TypeVar T = TypeVar("T") + class CanIndex(Protocol[T]): def __getitem__(self, index: int, /) -> T: ... + class ExplicitlyImplements(CanIndex[T]): ... + + class SubProtocol(CanIndex[T], Protocol): ... + def takes_in_list(x: list[T]) -> list[T]: return x + def takes_in_protocol(x: CanIndex[T]) -> T: return x[0] + def deep_list(x: list[str]) -> None: reveal_type(takes_in_list(x)) # revealed: list[str] # TODO: revealed: str reveal_type(takes_in_protocol(x)) # revealed: Unknown + def deeper_list(x: list[set[str]]) -> None: reveal_type(takes_in_list(x)) # revealed: list[set[str]] # TODO: revealed: set[str] reveal_type(takes_in_protocol(x)) # revealed: Unknown + def deep_explicit(x: ExplicitlyImplements[str]) -> None: reveal_type(takes_in_protocol(x)) # revealed: str + def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None: reveal_type(takes_in_protocol(x)) # revealed: set[str] + def deep_subprotocol(x: SubProtocol[str]) -> None: reveal_type(takes_in_protocol(x)) # revealed: str + def deeper_subprotocol(x: SubProtocol[set[str]]) -> None: reveal_type(takes_in_protocol(x)) # revealed: set[str] + def itself(x: CanIndex[str]) -> None: reveal_type(takes_in_protocol(x)) # revealed: str + def deep_itself(x: CanIndex[set[str]]) -> None: reveal_type(takes_in_protocol(x)) # revealed: set[str] + def takes_in_type(x: type[T]) -> type[T]: return x + reveal_type(takes_in_type(int)) # revealed: type[int] ``` @@ -126,8 +147,11 @@ This also works when passing in arguments that are subclasses of the parameter t ```py class Sub(list[int]): ... + + class GenericSub(list[T]): ... + reveal_type(takes_in_list(Sub())) # revealed: list[int] # TODO: revealed: int reveal_type(takes_in_protocol(Sub())) # revealed: Unknown @@ -136,9 +160,13 @@ reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[str] # TODO: revealed: str reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown + class ExplicitSub(ExplicitlyImplements[int]): ... + + class ExplicitGenericSub(ExplicitlyImplements[T]): ... + reveal_type(takes_in_protocol(ExplicitSub())) # revealed: int reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str ``` @@ -155,35 +183,45 @@ from typing import TypeVar T = TypeVar("T") + def takes_mixed_tuple_suffix(x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T: return x[-2] + def takes_mixed_tuple_prefix(x: tuple[int, T, *tuple[str, ...], bool, int]) -> T: return x[1] + def _(x: tuple[int, bytes, *tuple[str, ...], bool, int]): reveal_type(takes_mixed_tuple_suffix(x)) # revealed: bool reveal_type(takes_mixed_tuple_prefix(x)) # revealed: bytes + reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[True] reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[b"foo"] + def takes_fixed_tuple(x: tuple[T, int]) -> T: return x[0] + def _(x: tuple[str, int]): reveal_type(takes_fixed_tuple(x)) # revealed: str + reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True] + def takes_homogeneous_tuple(x: tuple[T, ...]) -> T: return x[0] + def _(x: tuple[str, int], y: tuple[bool, ...], z: tuple[int, str, *tuple[range, ...], bytes]): reveal_type(takes_homogeneous_tuple(x)) # revealed: str | int reveal_type(takes_homogeneous_tuple(y)) # revealed: bool reveal_type(takes_homogeneous_tuple(z)) # revealed: int | str | range | bytes + reveal_type(takes_homogeneous_tuple((42,))) # revealed: Literal[42] reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Literal[42, 43] ``` @@ -197,9 +235,11 @@ from typing import TypeVar T = TypeVar("T", bound=int) + def f(x: T) -> T: return x + reveal_type(f(1)) # revealed: Literal[1] reveal_type(f(True)) # revealed: Literal[True] # error: [invalid-argument-type] @@ -215,9 +255,11 @@ from typing import TypeVar T = TypeVar("T", int, None) + def f(x: T) -> T: return x + reveal_type(f(1)) # revealed: int reveal_type(f(True)) # revealed: int reveal_type(f(None)) # revealed: None @@ -236,6 +278,7 @@ from typing import TypeVar T = TypeVar("T", bound=int) + def good_param(x: T) -> None: reveal_type(x) # revealed: T@good_param ``` @@ -249,6 +292,7 @@ return value is not guaranteed to be compatible for all `T: int`. def good_return(x: T) -> T: return x + def bad_return(x: T) -> T: # error: [invalid-return-type] "Return type does not match returned value: expected `T@bad_return`, found `int`" return x + 1 @@ -264,6 +308,7 @@ from typing import TypeVar T = TypeVar("T") S = TypeVar("S") + def different_types(cond: bool, t: T, s: S) -> T: if cond: return t @@ -271,6 +316,7 @@ def different_types(cond: bool, t: T, s: S) -> T: # error: [invalid-return-type] "Return type does not match returned value: expected `T@different_types`, found `S@different_types`" return s + def same_types(cond: bool, t1: T, t2: T) -> T: if cond: return t1 @@ -288,6 +334,7 @@ from typing import TypeVar T = TypeVar("T", int, str) + def same_constrained_types(t1: T, t2: T) -> T: # TODO: no error # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `T@same_constrained_types`" @@ -315,9 +362,11 @@ from typing import TypeVar T = TypeVar("T") + def two_params(x: T, y: T) -> T: return x + reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"] reveal_type(two_params("a", 1)) # revealed: Literal["a", 1] ``` @@ -331,10 +380,12 @@ def union_param(x: T | None) -> T: raise ValueError return x + reveal_type(union_param("a")) # revealed: Literal["a"] reveal_type(union_param(1)) # revealed: Literal[1] reveal_type(union_param(None)) # revealed: Unknown + def _(x: int | None): reveal_type(union_param(x)) # revealed: int ``` @@ -343,6 +394,7 @@ def _(x: int | None): def union_and_nonunion_params(x: T | int, y: T) -> T: return y + reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"] reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"] reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1] @@ -355,14 +407,18 @@ This also works if the typevar has a bound: ```py T_str = TypeVar("T_str", bound=str) + def accepts_t_or_int(x: T_str | int) -> T_str: raise NotImplementedError + reveal_type(accepts_t_or_int("a")) # revealed: Literal["a"] reveal_type(accepts_t_or_int(1)) # revealed: Unknown + class Unrelated: ... + # error: [invalid-argument-type] "Argument type `Unrelated` does not satisfy upper bound `str` of type variable `T_str`" reveal_type(accepts_t_or_int(Unrelated())) # revealed: Unknown ``` @@ -370,13 +426,16 @@ reveal_type(accepts_t_or_int(Unrelated())) # revealed: Unknown ```py T_str = TypeVar("T_str", bound=str) + def accepts_t_or_list_of_t(x: T_str | list[T_str]) -> T_str: raise NotImplementedError + reveal_type(accepts_t_or_list_of_t("a")) # revealed: Literal["a"] # error: [invalid-argument-type] "Argument type `Literal[1]` does not satisfy upper bound `str` of type variable `T_str`" reveal_type(accepts_t_or_list_of_t(1)) # revealed: Unknown + def _(list_ofstr: list[str], list_of_int: list[int]): reveal_type(accepts_t_or_list_of_t(list_ofstr)) # revealed: str @@ -391,9 +450,11 @@ would also be a valid solution: ```py S = TypeVar("S") + def tuple_param(x: T | S, y: tuple[T, S]) -> tuple[T, S]: return y + reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] ``` @@ -406,15 +467,19 @@ from typing import TypeVar, Generic T = TypeVar("T") + class P(Generic[T]): x: T + class Q(Generic[T]): x: T + def extract_t(x: P[T] | Q[T]) -> T: raise NotImplementedError + reveal_type(extract_t(P[int]())) # revealed: int reveal_type(extract_t(Q[str]())) # revealed: str ``` @@ -431,9 +496,11 @@ This also works when different union elements have different typevars: ```py S = TypeVar("S") + def extract_both(x: P[T] | Q[S]) -> tuple[T, S]: raise NotImplementedError + reveal_type(extract_both(P[int]())) # revealed: tuple[int, Unknown] reveal_type(extract_both(Q[str]())) # revealed: tuple[Unknown, str] ``` @@ -444,9 +511,11 @@ Inference also works when passing subclasses of the generic classes in the union class SubP(P[T]): pass + class SubQ(Q[T]): pass + reveal_type(extract_t(SubP[int]())) # revealed: int reveal_type(extract_t(SubQ[str]())) # revealed: str @@ -462,6 +531,7 @@ both types in a call to `extract_both`: class PandQ(P[int], Q[str]): pass + # TODO: Ideally, we would return `Unknown` here. # error: [invalid-argument-type] reveal_type(extract_t(PandQ())) # revealed: int | str @@ -476,6 +546,7 @@ types: def extract_optional_t(x: None | P[T]) -> T: raise NotImplementedError + reveal_type(extract_optional_t(None)) # revealed: Unknown reveal_type(extract_optional_t(P[int]())) # revealed: int ``` @@ -494,11 +565,14 @@ element that is more precise: class Base(Generic[T]): x: T + class Sub(Base[T]): ... + def f(t: Base[T] | Sub[T | None]) -> T: raise NotImplementedError + reveal_type(f(Base[int]())) # revealed: int # TODO: Should ideally be `str` reveal_type(f(Sub[str | None]())) # revealed: str | None @@ -513,12 +587,15 @@ from typing import TypeVar I_int = TypeVar("I_int", bound=int) S_str = TypeVar("S_str", bound=str) + class P(Generic[T]): value: T + def f(t: P[I_int] | P[S_str]) -> tuple[I_int, S_str]: raise NotImplementedError + reveal_type(f(P[int]())) # revealed: tuple[int, Unknown] reveal_type(f(P[str]())) # revealed: tuple[Unknown, str] ``` @@ -540,12 +617,15 @@ from typing import TypeVar T = TypeVar("T") + def f(x: T) -> tuple[T, int]: return (x, 1) + def g(x: T) -> T | None: return x + reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None ``` @@ -559,15 +639,19 @@ A = TypeVar("A") B = TypeVar("B") T = TypeVar("T") + def invoke(fn: Callable[[A], B], value: A) -> B: return fn(value) + def identity(x: T) -> T: return x + def head(xs: list[T]) -> T: return xs[0] + reveal_type(invoke(identity, 1)) # revealed: Literal[1] # TODO: this should be `Unknown | int` @@ -587,17 +671,21 @@ from typing import cast, Any, Callable, TypeVar F = TypeVar("F", bound=Callable[..., Any]) T = TypeVar("T") + def opaque_decorator(f: Any) -> Any: return f + def transparent_decorator(f: F) -> F: return f + @opaque_decorator def decorated(t: T) -> None: # error: [redundant-cast] reveal_type(cast(T, t)) # revealed: T@decorated + @transparent_decorator def decorated(t: T) -> None: # error: [redundant-cast] @@ -609,16 +697,21 @@ def decorated(t: T) -> None: ```py from typing import Generic, TypeVar + class A: ... + T = TypeVar("T", bound=A) + class B(Generic[T]): x: T + def f(c: T | None): return None + def g(b: B[T]): return f(b.x) # Fine ``` @@ -634,9 +727,11 @@ from typing import TypeVar T = TypeVar("T", str, bytes) + def NamedTemporaryFile(suffix: T | None, prefix: T | None) -> None: return None + def f(x: str): NamedTemporaryFile(prefix=x, suffix=".tar.gz") # Fine ``` @@ -649,11 +744,13 @@ from typing import TypeVar, overload T = TypeVar("T") S = TypeVar("S") + def outer(t: T) -> None: def inner(t: T) -> None: ... inner(t) + @overload def overloaded_outer() -> None: ... @overload @@ -664,9 +761,11 @@ def overloaded_outer(t: T | None = None) -> None: if t is not None: inner(t) + def outer(t: T) -> None: def inner(inner_t: T, s: S) -> tuple[T, S]: return inner_t, s + reveal_type(inner(t, 1)) # revealed: tuple[T@outer, Literal[1]] inner("wrong", 1) # error: [invalid-argument-type] @@ -683,29 +782,37 @@ from typing import NamedTuple, Final, TypeVar, Generic T = TypeVar("T", bound=tuple[int, str]) + def f(x: T) -> T: a, b = x reveal_type(a) # revealed: int reveal_type(b) # revealed: str return x + @dataclass class Team(Generic[T]): employees: list[T] + def x(team: Team[T]) -> Team[T]: age, name = team.employees[0] reveal_type(age) # revealed: int reveal_type(name) # revealed: str return team + class Age(int): ... + + class Name(str): ... + class Employee(NamedTuple): age: Age name: Name + EMPLOYEES: Final = (Employee(name=Name("alice"), age=Age(42)),) team = Team(employees=list(EMPLOYEES)) reveal_type(team.employees) # revealed: list[Employee] @@ -722,6 +829,7 @@ from ty_extensions import Not T = TypeVar("T") + def f(x: T, y: Not[T]) -> T: x = y # error: [invalid-assignment] y = x # error: [invalid-assignment] @@ -733,19 +841,26 @@ def f(x: T, y: Not[T]) -> T: ```py from typing import TypeVar + class Base: ... + + class Sub(Base): ... + # We solve to `Sub`, regardless of the order of constraints. T = TypeVar("T", Base, Sub) T2 = TypeVar("T2", Sub, Base) + def f(x: T) -> list[T]: return [x] + def f2(x: T2) -> list[T2]: return [x] + x: list[Sub] = f(Sub()) reveal_type(x) # revealed: list[Sub] @@ -763,17 +878,22 @@ See: ```py from typing import Callable, TypeVar + class Base: pass + class Derived(Base): attr: int + T = TypeVar("T", bound=Base) + def takes_factory(factory: Callable[[], T]) -> T: return factory() + # Passing a class as a factory: should infer Derived, not Base result = takes_factory(Derived) reveal_type(result) # revealed: Derived diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 5e3cbe3888..a7740292a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -158,12 +158,15 @@ from typing import ParamSpec, Callable, Concatenate, Protocol, Generic P = ParamSpec("P") + class ValidProtocol(Protocol[P]): def method(self, c: Callable[P, int]) -> None: ... + class ValidGeneric(Generic[P]): def method(self, c: Callable[P, int]) -> None: ... + def valid( a1: Callable[P, int], a2: Callable[Concatenate[int, P], int], @@ -192,6 +195,7 @@ from typing import Generic, Callable, ParamSpec P = ParamSpec("P") + def foo1(c: Callable[P, int]) -> None: def nested1(*args: P.args, **kwargs: P.kwargs) -> None: ... def nested2( @@ -210,10 +214,12 @@ def foo1(c: Callable[P, int]) -> None: # TODO: error def nested5(*args: P.args, x: int, **kwargs: P.kwargs) -> None: ... + # TODO: error def bar1(*args: P.args, **kwargs: P.kwargs) -> None: pass + class Foo1: # TODO: error def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... @@ -229,6 +235,7 @@ def foo2(c: Callable[P, int]) -> None: # TODO: error def nested2(**kwargs: P.kwargs) -> None: ... + class Foo2: # TODO: error args: P.args @@ -261,13 +268,16 @@ P1 = ParamSpec("P1") P2 = ParamSpec("P2") T1 = TypeVar("T1") + class OnlyParamSpec(Generic[P1]): attr: Callable[P1, None] + class TwoParamSpec(Generic[P1, P2]): attr1: Callable[P1, None] attr2: Callable[P2, None] + class TypeVarAndParamSpec(Generic[T1, P1]): attr: Callable[P1, T1] ``` @@ -280,9 +290,11 @@ reveal_type(OnlyParamSpec[[]]().attr) # revealed: () -> None reveal_type(OnlyParamSpec[[int, str]]().attr) # revealed: (int, str, /) -> None reveal_type(OnlyParamSpec[...]().attr) # revealed: (...) -> None + def func(c: Callable[P2, None]): reveal_type(OnlyParamSpec[P2]().attr) # revealed: (**P2@func) -> None + # error: [invalid-type-arguments] "ParamSpec `P2` is unbound" reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None @@ -378,6 +390,7 @@ PAnotherWithDefault = ParamSpec("PAnotherWithDefault", default=PList) class ParamSpecWithDefault1(Generic[PList]): attr: Callable[PList, None] + reveal_type(ParamSpecWithDefault1().attr) # revealed: (int, str, /) -> None reveal_type(ParamSpecWithDefault1[[int]]().attr) # revealed: (int, /) -> None ``` @@ -386,6 +399,7 @@ reveal_type(ParamSpecWithDefault1[[int]]().attr) # revealed: (int, /) -> None class ParamSpecWithDefault2(Generic[PEllipsis]): attr: Callable[PEllipsis, None] + reveal_type(ParamSpecWithDefault2().attr) # revealed: (...) -> None reveal_type(ParamSpecWithDefault2[[int, str]]().attr) # revealed: (int, str, /) -> None ``` @@ -395,6 +409,7 @@ class ParamSpecWithDefault3(Generic[P, PAnother]): attr1: Callable[P, None] attr2: Callable[PAnother, None] + # `P` hasn't been specialized, so it defaults to `Unknown` gradual form p1 = ParamSpecWithDefault3() reveal_type(p1.attr1) # revealed: (...) -> None @@ -408,10 +423,12 @@ p3 = ParamSpecWithDefault3[[int], [str]]() reveal_type(p3.attr1) # revealed: (int, /) -> None reveal_type(p3.attr2) # revealed: (str, /) -> None + class ParamSpecWithDefault4(Generic[PList, PAnotherWithDefault]): attr1: Callable[PList, None] attr2: Callable[PAnotherWithDefault, None] + p1 = ParamSpecWithDefault4() reveal_type(p1.attr1) # revealed: (int, str, /) -> None reveal_type(p1.attr2) # revealed: (int, str, /) -> None @@ -424,10 +441,12 @@ p3 = ParamSpecWithDefault4[[int], [str]]() reveal_type(p3.attr1) # revealed: (int, /) -> None reveal_type(p3.attr2) # revealed: (str, /) -> None + # Un-ordered type variables as the default of `PAnother` is `P` class ParamSpecWithDefault5(Generic[PAnother, P]): # error: [invalid-generic-class] attr: Callable[PAnother, None] + # TODO: error # PAnother has default as P (another ParamSpec) which is not in scope class ParamSpecWithDefault6(Generic[PAnother]): diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index 04dfdf648f..b9e47d3f6e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -117,6 +117,7 @@ T = TypeVar("T") ImplicitPositive = T Positive: TypeAlias = T + def _( # error: [invalid-type-form] "A type variable itself cannot be specialized" a: T[int], @@ -169,13 +170,16 @@ T = TypeVar("T") U = TypeVar("U", default=T) V = TypeVar("V", default=Union[T, U]) + class Valid(Generic[T, U, V]): ... + reveal_type(Valid()) # revealed: Valid[Unknown, Unknown, Unknown] reveal_type(Valid[int]()) # revealed: Valid[int, int, int] reveal_type(Valid[int, str]()) # revealed: Valid[int, str, int | str] reveal_type(Valid[int, str, None]()) # revealed: Valid[int, str, None] + # TODO: error, default value for U isn't available in the generic context class Invalid(Generic[U]): ... ``` @@ -267,9 +271,11 @@ T = TypeVar("T", covariant=True, contravariant=True) ```py from typing_extensions import TypeVar + def cond() -> bool: return True + # error: [invalid-legacy-type-variable] T = TypeVar("T", covariant=cond()) @@ -394,6 +400,7 @@ from typing import Callable, TypeVar T = TypeVar("T", bound=Callable[[], int]) + def bound(f: T): reveal_type(f) # revealed: T@bound reveal_type(f()) # revealed: int @@ -404,6 +411,7 @@ Same with a constrained typevar, as long as all constraints are callable: ```py T = TypeVar("T", Callable[[], int], Callable[[], str]) + def constrained(f: T): reveal_type(f) # revealed: T@constrained reveal_type(f()) # revealed: int | str @@ -418,21 +426,28 @@ from typing import TypeVar T_normal = TypeVar("T_normal") + def normal(x: T_normal): reveal_type(type(x)) # revealed: type[T_normal@normal] + T_bound_object = TypeVar("T_bound_object", bound=object) + def bound_object(x: T_bound_object): reveal_type(type(x)) # revealed: type[T_bound_object@bound_object] + T_bound_int = TypeVar("T_bound_int", bound=int) + def bound_int(x: T_bound_int): reveal_type(type(x)) # revealed: type[T_bound_int@bound_int] + T_constrained = TypeVar("T_constrained", int, str) + def constrained(x: T_constrained): reveal_type(type(x)) # revealed: type[T_constrained@constrained] ``` @@ -465,9 +480,11 @@ from typing import TypeVar, Generic T = TypeVar("T", bound=list["G"]) + class G(Generic[T]): x: T + reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]] ``` @@ -479,9 +496,11 @@ from typing import TypeVar, Generic # error: [invalid-type-arguments] T = TypeVar("T", bound="Node[int]") + class Node(Generic[T]): pass + # error: [invalid-type-arguments] def _(n: Node[str]): reveal_type(n) # revealed: Node[Unknown] @@ -503,10 +522,12 @@ from typing import Generic, TypeVar T = TypeVar("T") U = TypeVar("U", default=T) + class C(Generic[T, U]): x: T y: U + reveal_type(C[int, str]().x) # revealed: int reveal_type(C[int, str]().y) # revealed: str reveal_type(C[int]().x) # revealed: int @@ -515,9 +536,11 @@ reveal_type(C[int]().y) # revealed: int # TODO: error V = TypeVar("V", default="V") + class D(Generic[V]): x: V + reveal_type(D().x) # revealed: Unknown ``` @@ -535,10 +558,12 @@ from typing import Generic, TypeVar _DataT = TypeVar("_DataT", bound=int, default=int) + class Event(Generic[_DataT]): def __init__(self, data: _DataT) -> None: self.data = data + def async_fire_internal(event_data: _DataT): event: Event[_DataT] | None = None event = Event(event_data) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md index 732a2a878a..221a87beb1 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md @@ -25,19 +25,26 @@ get from the sequence is a valid `int`. from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any, Generic, TypeVar + class A: ... + + class B(A): ... + T = TypeVar("T", covariant=True) U = TypeVar("U", covariant=True) + class C(Generic[T]): def receive(self) -> T: raise ValueError + class D(C[U]): pass + static_assert(is_assignable_to(C[B], C[A])) static_assert(not is_assignable_to(C[A], C[B])) static_assert(is_assignable_to(C[A], C[Any])) @@ -106,18 +113,25 @@ that you pass into the consumer is a valid `int`. from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any, Generic, TypeVar + class A: ... + + class B(A): ... + T = TypeVar("T", contravariant=True) U = TypeVar("U", contravariant=True) + class C(Generic[T]): def send(self, value: T): ... + class D(C[U]): pass + static_assert(not is_assignable_to(C[B], C[A])) static_assert(is_assignable_to(C[A], C[B])) static_assert(is_assignable_to(C[A], C[Any])) @@ -183,11 +197,14 @@ from typing import Generic, TypeVar T = TypeVar("T", contravariant=True) T_int = TypeVar("T_int", bound=int) + class Contra(Generic[T]): ... + def f(x: Contra[T_int]) -> T_int: raise NotImplementedError + def _(x: Contra[str]): reveal_type(f(x)) # revealed: Never ``` @@ -218,20 +235,27 @@ since we can't know in advance which of the allowed methods you'll want to use. from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any, Generic, TypeVar + class A: ... + + class B(A): ... + T = TypeVar("T") U = TypeVar("U") + class C(Generic[T]): def send(self, value: T): ... def receive(self) -> T: raise ValueError + class D(C[U]): pass + static_assert(not is_assignable_to(C[B], C[A])) static_assert(not is_assignable_to(C[A], C[B])) static_assert(is_assignable_to(C[A], C[Any])) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index 857cdcd2b6..283e31b272 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -53,6 +53,7 @@ from typing import Literal type C[T] = T + def _(a: C[int], b: C[Literal[5]]): reveal_type(a) # revealed: int reveal_type(b) # revealed: Literal[5] @@ -75,81 +76,107 @@ type B = ... # error: [not-subscriptable] "Cannot subscript non-generic type alias" reveal_type(B[int]) # revealed: Unknown + # error: [not-subscriptable] "Cannot subscript non-generic type alias" def _(b: B[int]): reveal_type(b) # revealed: Unknown + type IntOrStr = int | str + # error: [not-subscriptable] "Cannot subscript non-generic type alias" def _(c: IntOrStr[int]): reveal_type(c) # revealed: Unknown + type ListOfInts = list[int] + # error: [not-subscriptable] "Cannot subscript non-generic type alias: `list[int]` is already specialized" def _(l: ListOfInts[int]): reveal_type(l) # revealed: Unknown + type List[T] = list[T] + # error: [not-subscriptable] "Cannot subscript non-generic type alias: Double specialization is not allowed" def _(l: List[int][int]): reveal_type(l) # revealed: Unknown + # error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" type DoubleSpecialization[T] = list[T][T] + def _(d: DoubleSpecialization[int]): reveal_type(d) # revealed: Unknown + type Tuple = tuple[int, str] + # error: [not-subscriptable] "Cannot subscript non-generic type alias: `tuple[int, str]` is already specialized" def _(doubly_specialized: Tuple[int]): reveal_type(doubly_specialized) # revealed: Unknown + T = TypeVar("T") + class LegacyProto(Protocol[T]): pass + type LegacyProtoInt = LegacyProto[int] + # error: [not-subscriptable] "Cannot subscript non-generic type alias: `LegacyProto[int]` is already specialized" def _(x: LegacyProtoInt[int]): reveal_type(x) # revealed: Unknown + class Proto[T](Protocol): pass + type ProtoInt = Proto[int] + # error: [not-subscriptable] "Cannot subscript non-generic type alias: `Proto[int]` is already specialized" def _(x: ProtoInt[int]): reveal_type(x) # revealed: Unknown + # TODO: TypedDict is just a function object at runtime, we should emit an error class LegacyDict(TypedDict[T]): x: T + type LegacyDictInt = LegacyDict[int] + # error: [not-subscriptable] "Cannot subscript non-generic type alias" def _(x: LegacyDictInt[int]): reveal_type(x) # revealed: Unknown + class Dict[T](TypedDict): x: T + type DictInt = Dict[int] + # error: [not-subscriptable] "Cannot subscript non-generic type alias: `Dict[int]` is already specialized" def _(x: DictInt[int]): reveal_type(x) # revealed: Unknown + type Union = list[str] | list[int] + # error: [not-subscriptable] "Cannot subscript non-generic type alias: `list[str] | list[int]` is already specialized" def _(x: Union[int]): reveal_type(x) # revealed: Unknown @@ -161,8 +188,10 @@ If the type variable has an upper bound, the specialized type must satisfy that type Bounded[T: int] = ... type BoundedByUnion[T: int | str] = ... + class IntSubclass(int): ... + reveal_type(Bounded[int]) # revealed: reveal_type(Bounded[IntSubclass]) # revealed: @@ -179,9 +208,11 @@ reveal_type(BoundedByUnion[int | str]) # revealed: , Unknown, ) + # error: [invalid-generic-class] "Cannot both inherit from `typing.Generic` and use PEP 695 type variables" # error: [invalid-base] "Cannot inherit from plain `Generic`" class DoublyInvalid[T](Generic): ... + reveal_mro(DoublyInvalid) # revealed: (, Unknown, ) ``` @@ -96,22 +118,31 @@ Generic classes implicitly inherit from `Generic`: ```py class Foo[T]: ... + # revealed: (, typing.Generic, ) reveal_mro(Foo) # revealed: (, typing.Generic, ) reveal_mro(Foo[int]) + class A: ... + + class Bar[T](A): ... + # revealed: (, , typing.Generic, ) reveal_mro(Bar) # revealed: (, , typing.Generic, ) reveal_mro(Bar[int]) + class B: ... + + class Baz[T](A, B): ... + # revealed: (, , , typing.Generic, ) reveal_mro(Baz) # revealed: (, , , typing.Generic, ) @@ -125,9 +156,11 @@ The type parameter can be specified explicitly: ```py from typing import Literal + class C[T]: x: T + reveal_type(C[int]()) # revealed: C[int] reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]] ``` @@ -143,9 +176,14 @@ If the type variable has an upper bound, the specialized type must satisfy that ```py class Bounded[T: int]: ... + + class BoundedByUnion[T: int | str]: ... + + class IntSubclass(int): ... + reveal_type(Bounded[int]()) # revealed: Bounded[int] reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass] @@ -166,6 +204,7 @@ If the type variable is constrained, the specialized type must satisfy those con ```py class Constrained[T: (int, str)]: ... + reveal_type(Constrained[int]()) # revealed: Constrained[int] # TODO: error: [invalid-argument-type] @@ -187,6 +226,7 @@ If the type variable has a default, it can be omitted: ```py class WithDefault[T, U = int]: ... + reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] ``` @@ -204,6 +244,7 @@ satisfy the type variable's upper bound or constraints: class Bounded[T: str]: x: T + class Constrained[U: (int, bytes)]: x: U ``` @@ -225,6 +266,7 @@ We can infer the type parameter from a type context: class C[T]: x: T + c: C[int] = C() # TODO: revealed: C[int] reveal_type(c) # revealed: C[Unknown] @@ -243,6 +285,7 @@ specific type, we infer the typevar's default type: ```py class D[T = int]: ... + reveal_type(D()) # revealed: D[int] ``` @@ -266,12 +309,14 @@ signatures don't count towards variance). ```py from ty_extensions import generic_context, into_callable + class C[T]: x: T def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C] @@ -288,11 +333,13 @@ wrong_innards: C[int] = C("five") ```py from ty_extensions import generic_context, into_callable + class C[T]: x: T def __init__(self, x: T) -> None: ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C] @@ -309,6 +356,7 @@ wrong_innards: C[int] = C("five") ```py from ty_extensions import generic_context, into_callable + class C[T]: x: T @@ -317,6 +365,7 @@ class C[T]: def __init__(self, x: T) -> None: ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C] @@ -333,6 +382,7 @@ wrong_innards: C[int] = C("five") ```py from ty_extensions import generic_context, into_callable + class C[T]: x: T @@ -341,6 +391,7 @@ class C[T]: def __init__(self, x: T) -> None: ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C] @@ -351,6 +402,7 @@ reveal_type(C(1)) # revealed: C[int] # error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") + class D[T]: x: T @@ -359,6 +411,7 @@ class D[T]: def __init__(self, *args, **kwargs) -> None: ... + # revealed: ty_extensions.GenericContext[T@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[T@D] @@ -378,13 +431,16 @@ to specialize the class. ```py from ty_extensions import generic_context, into_callable + class C[T, U]: def __new__(cls, *args, **kwargs) -> "C[T, U]": return object.__new__(cls) + class D[V](C[V, int]): def __init__(self, x: V) -> None: ... + # revealed: ty_extensions.GenericContext[V@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[V@D] @@ -398,12 +454,15 @@ reveal_type(D(1)) # revealed: D[Literal[1]] ```py from ty_extensions import generic_context, into_callable + class C[T, U]: def __init__(self, t: T, u: U) -> None: ... + class D[T, U](C[T, U]): pass + # revealed: ty_extensions.GenericContext[T@D, U@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[T@D, U@D] @@ -420,9 +479,11 @@ This is a specific example of the above, since it was reported specifically by a ```py from ty_extensions import generic_context, into_callable + class D[T, U](dict[T, U]): pass + # revealed: ty_extensions.GenericContext[T@D, U@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[T@D, U@D] @@ -440,8 +501,10 @@ context. But from the user's point of view, this is another example of the above ```py from ty_extensions import generic_context, into_callable + class C[T, U](tuple[T, U]): ... + # revealed: ty_extensions.GenericContext[T@C, U@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C, U@C] @@ -458,9 +521,11 @@ This test is taken from the ```py from typing import Sequence, Never + def test_seq[T](x: Sequence[T]) -> Sequence[T]: return x + def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: tuple[()]): reveal_type(test_seq(t1)) # revealed: Sequence[int | float | complex | list[int]] reveal_type(test_seq(t2)) # revealed: Sequence[int | str] @@ -472,11 +537,13 @@ def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: t ```py from ty_extensions import generic_context, into_callable + class C[T]: x: T def __init__[S](self, x: T, y: S) -> None: ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C, S@__init__] @@ -497,6 +564,7 @@ from __future__ import annotations from typing import overload from ty_extensions import generic_context, into_callable + class C[T]: # we need to use the type variable or else the class is bivariant in T, and # specializations become meaningless @@ -512,6 +580,7 @@ class C[T]: def __init__(self, x: int) -> None: ... def __init__(self, x: str | bytes | int) -> None: ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C] @@ -537,6 +606,7 @@ C[None]("string") # error: [no-matching-overload] C[None](b"bytes") # error: [no-matching-overload] C[None](12) + class D[T, U]: @overload def __init__(self: "D[str, U]", u: U) -> None: ... @@ -544,6 +614,7 @@ class D[T, U]: def __init__(self, t: T, u: U) -> None: ... def __init__(self, *args) -> None: ... + # revealed: ty_extensions.GenericContext[T@D, U@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[T@D, U@D] @@ -560,10 +631,12 @@ reveal_type(D(1, "string")) # revealed: D[Literal[1], Literal["string"]] from dataclasses import dataclass from ty_extensions import generic_context, into_callable + @dataclass class A[T]: x: T + # revealed: ty_extensions.GenericContext[T@A] reveal_type(generic_context(A)) # revealed: ty_extensions.GenericContext[T@A] @@ -577,8 +650,10 @@ reveal_type(A(x=1)) # revealed: A[int] ```py from ty_extensions import generic_context, into_callable + class C[T, U = T]: ... + # revealed: ty_extensions.GenericContext[T@C, U@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[T@C, U@C] @@ -586,9 +661,11 @@ reveal_type(generic_context(into_callable(C))) reveal_type(C()) # revealed: C[Unknown, Unknown] + class D[T, U = T]: def __init__(self) -> None: ... + # revealed: ty_extensions.GenericContext[T@D, U@D] reveal_type(generic_context(D)) # revealed: ty_extensions.GenericContext[T@D, U@D] @@ -606,10 +683,16 @@ propagate through: class Parent[T]: x: T + class Child[U](Parent[U]): ... + + class Grandchild[V](Child[V]): ... + + class Greatgrandchild[W](Child[W]): ... + reveal_type(Parent[int]().x) # revealed: int reveal_type(Child[int]().x) # revealed: int reveal_type(Grandchild[int]().x) # revealed: int @@ -625,18 +708,21 @@ scope for the method. ```py from ty_extensions import generic_context + class C[T]: def method(self, u: int) -> int: return u def generic_method[U](self, t: T, u: U) -> U: return u + # error: [unresolved-reference] def cannot_use_outside_of_method(self, u: U): ... # TODO: error def cannot_shadow_class_typevar[T](self, t: T): ... + # revealed: ty_extensions.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: ty_extensions.GenericContext[Self@method] @@ -668,6 +754,7 @@ class. ```py class LinkedList[T]: ... + class C[T, U]: x: T y: U @@ -681,6 +768,7 @@ class C[T, U]: def method3(self) -> LinkedList[T]: return LinkedList[T]() + c = C[int, str]() reveal_type(c.x) # revealed: int reveal_type(c.y) # revealed: str @@ -694,6 +782,7 @@ When a method is overloaded, the specialization is applied to all overloads. ```py from typing import overload + class WithOverloadedMethod[T]: @overload def method(self, x: T) -> T: ... @@ -702,6 +791,7 @@ class WithOverloadedMethod[T]: def method[S](self, x: S | T) -> S | T: return x + # revealed: Overload[(self, x: int) -> int, [S](self, x: S) -> S | int] reveal_type(WithOverloadedMethod[int].method) ``` @@ -717,9 +807,11 @@ Typevar bounds/constraints/defaults are lazy, but cannot refer to later typevars class C[S: T, T]: pass + class D[S: X]: pass + X = int ``` @@ -747,8 +839,11 @@ A similar case can work in a non-stub file, if forward references are stringifie ```py class Base[T]: ... + + class Sub(Base["Sub"]): ... + reveal_type(Sub) # revealed: ``` @@ -759,6 +854,7 @@ In a non-stub file, without stringified forward references, this raises a `NameE ```py class Base[T]: ... + # error: [unresolved-reference] class Sub(Base[Sub]): ... ``` @@ -809,15 +905,18 @@ from __future__ import annotations from typing import Protocol from ty_extensions import generic_context + class A[S, R](Protocol): def get(self, s: S) -> R: ... def set(self, s: S, r: R) -> S: ... def merge[R2](self, other: A[S, R2]) -> A[S, tuple[R, R2]]: ... + class Impl[S, R](A[S, R]): def foo(self, s: S) -> S: return self.set(s, self.get(s)) + reveal_type(generic_context(A.get)) # revealed: ty_extensions.GenericContext[Self@get] reveal_type(generic_context(A.merge)) # revealed: ty_extensions.GenericContext[Self@merge, R2@merge] reveal_type(generic_context(Impl.foo)) # revealed: ty_extensions.GenericContext[Self@foo] diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 180af1497a..85c25869b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -15,6 +15,7 @@ If you're only using a typevar for a single parameter, you don't need the typeva def typevar_not_needed[T](x: T) -> None: pass + # TODO: error, should be (x: int) def bounded_typevar_not_needed[T: int](x: T) -> None: pass @@ -52,6 +53,7 @@ is bound to at each call site. def f[T](x: T) -> T: return x + reveal_type(f(1)) # revealed: Literal[1] reveal_type(f(1.0)) # revealed: float reveal_type(f(True)) # revealed: Literal[True] @@ -71,49 +73,65 @@ from typing import Protocol, TypeVar S = TypeVar("S") + class CanIndex(Protocol[S]): def __getitem__(self, index: int, /) -> S: ... + class ExplicitlyImplements[T](CanIndex[T]): ... + + class SubProtocol[T](CanIndex[T], Protocol): ... + def takes_in_list[T](x: list[T]) -> list[T]: return x + def takes_in_protocol[T](x: CanIndex[T]) -> T: return x[0] + def deep_list(x: list[str]) -> None: reveal_type(takes_in_list(x)) # revealed: list[str] # TODO: revealed: str reveal_type(takes_in_protocol(x)) # revealed: Unknown + def deeper_list(x: list[set[str]]) -> None: reveal_type(takes_in_list(x)) # revealed: list[set[str]] # TODO: revealed: set[str] reveal_type(takes_in_protocol(x)) # revealed: Unknown + def deep_explicit(x: ExplicitlyImplements[str]) -> None: reveal_type(takes_in_protocol(x)) # revealed: str + def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None: reveal_type(takes_in_protocol(x)) # revealed: set[str] + def deep_subprotocol(x: SubProtocol[str]) -> None: reveal_type(takes_in_protocol(x)) # revealed: str + def deeper_subprotocol(x: SubProtocol[set[str]]) -> None: reveal_type(takes_in_protocol(x)) # revealed: set[str] + def itself(x: CanIndex[str]) -> None: reveal_type(takes_in_protocol(x)) # revealed: str + def deep_itself(x: CanIndex[set[str]]) -> None: reveal_type(takes_in_protocol(x)) # revealed: set[str] + def takes_in_type[T](x: type[T]) -> type[T]: return x + reveal_type(takes_in_type(int)) # revealed: type[int] ``` @@ -121,8 +139,11 @@ This also works when passing in arguments that are subclasses of the parameter t ```py class Sub(list[int]): ... + + class GenericSub[T](list[T]): ... + reveal_type(takes_in_list(Sub())) # revealed: list[int] # TODO: revealed: int reveal_type(takes_in_protocol(Sub())) # revealed: Unknown @@ -131,9 +152,13 @@ reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[str] # TODO: revealed: str reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown + class ExplicitSub(ExplicitlyImplements[int]): ... + + class ExplicitGenericSub[T](ExplicitlyImplements[T]): ... + reveal_type(takes_in_protocol(ExplicitSub())) # revealed: int reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str ``` @@ -144,32 +169,41 @@ reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str def takes_mixed_tuple_suffix[T](x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T: return x[-2] + def takes_mixed_tuple_prefix[T](x: tuple[int, T, *tuple[str, ...], bool, int]) -> T: return x[1] + def _(x: tuple[int, bytes, *tuple[str, ...], bool, int]): reveal_type(takes_mixed_tuple_suffix(x)) # revealed: bool reveal_type(takes_mixed_tuple_prefix(x)) # revealed: bytes + reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[True] reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[b"foo"] + def takes_fixed_tuple[T](x: tuple[T, int]) -> T: return x[0] + def _(x: tuple[str, int]): reveal_type(takes_fixed_tuple(x)) # revealed: str + reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True] + def takes_homogeneous_tuple[T](x: tuple[T, ...]) -> T: return x[0] + def _(x: tuple[str, int], y: tuple[bool, ...], z: tuple[int, str, *tuple[range, ...], bytes]): reveal_type(takes_homogeneous_tuple(x)) # revealed: str | int reveal_type(takes_homogeneous_tuple(y)) # revealed: bool reveal_type(takes_homogeneous_tuple(z)) # revealed: int | str | range | bytes + reveal_type(takes_homogeneous_tuple((42,))) # revealed: Literal[42] reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Literal[42, 43] ``` @@ -181,9 +215,11 @@ reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Literal[42, 43] ```py from typing_extensions import reveal_type + def f[T: int](x: T) -> T: return x + reveal_type(f(1)) # revealed: Literal[1] reveal_type(f(True)) # revealed: Literal[True] # error: [invalid-argument-type] @@ -197,9 +233,11 @@ reveal_type(f("string")) # revealed: Unknown ```py from typing_extensions import reveal_type + def f[T: (int, None)](x: T) -> T: return x + reveal_type(f(1)) # revealed: int reveal_type(f(True)) # revealed: int reveal_type(f(None)) # revealed: None @@ -227,6 +265,7 @@ return value is not guaranteed to be compatible for all `T: int`. def good_return[T: int](x: T) -> T: return x + def bad_return[T: int](x: T) -> T: # error: [invalid-return-type] "Return type does not match returned value: expected `T@bad_return`, found `int`" return x + 1 @@ -244,6 +283,7 @@ def different_types[T, S](cond: bool, t: T, s: S) -> T: # error: [invalid-return-type] "Return type does not match returned value: expected `T@different_types`, found `S@different_types`" return s + def same_types[T](cond: bool, t1: T, t2: T) -> T: if cond: return t1 @@ -283,6 +323,7 @@ parameters simultaneously. def two_params[T](x: T, y: T) -> T: return x + reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"] reveal_type(two_params("a", 1)) # revealed: Literal["a", 1] ``` @@ -296,10 +337,12 @@ def union_param[T](x: T | None) -> T: raise ValueError return x + reveal_type(union_param("a")) # revealed: Literal["a"] reveal_type(union_param(1)) # revealed: Literal[1] reveal_type(union_param(None)) # revealed: Unknown + def _(x: int | None): reveal_type(union_param(x)) # revealed: int ``` @@ -308,6 +351,7 @@ def _(x: int | None): def union_and_nonunion_params[T](x: T | int, y: T) -> T: return y + reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"] reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"] reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1] @@ -321,21 +365,27 @@ This also works if the typevar has a bound: def accepts_t_or_int[T_str: str](x: T_str | int) -> T_str: raise NotImplementedError + reveal_type(accepts_t_or_int("a")) # revealed: Literal["a"] reveal_type(accepts_t_or_int(1)) # revealed: Unknown + class Unrelated: ... + # error: [invalid-argument-type] "Argument type `Unrelated` does not satisfy upper bound `str` of type variable `T_str`" reveal_type(accepts_t_or_int(Unrelated())) # revealed: Unknown + def accepts_t_or_list_of_t[T: str](x: T | list[T]) -> T: raise NotImplementedError + reveal_type(accepts_t_or_list_of_t("a")) # revealed: Literal["a"] # error: [invalid-argument-type] "Argument type `Literal[1]` does not satisfy upper bound `str` of type variable `T`" reveal_type(accepts_t_or_list_of_t(1)) # revealed: Unknown + def _(list_ofstr: list[str], list_of_int: list[int]): reveal_type(accepts_t_or_list_of_t(list_ofstr)) # revealed: str @@ -351,6 +401,7 @@ would also be a valid solution: def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]: return y + reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] ``` @@ -362,12 +413,15 @@ the actual argument even for non-final classes. class P[T]: x: T # invariant + class Q[T]: x: T # invariant + def extract_t[T](x: P[T] | Q[T]) -> T: raise NotImplementedError + reveal_type(extract_t(P[int]())) # revealed: int reveal_type(extract_t(Q[str]())) # revealed: str ``` @@ -385,6 +439,7 @@ This also works when different union elements have different typevars: def extract_both[T, S](x: P[T] | Q[S]) -> tuple[T, S]: raise NotImplementedError + reveal_type(extract_both(P[int]())) # revealed: tuple[int, Unknown] reveal_type(extract_both(Q[str]())) # revealed: tuple[Unknown, str] ``` @@ -395,9 +450,11 @@ Inference also works when passing subclasses of the generic classes in the union class SubP[T](P[T]): pass + class SubQ[T](Q[T]): pass + reveal_type(extract_t(SubP[int]())) # revealed: int reveal_type(extract_t(SubQ[str]())) # revealed: str @@ -413,6 +470,7 @@ both types in a call to `extract_both`: class PandQ(P[int], Q[str]): pass + # TODO: Ideally, we would return `Unknown` here. # error: [invalid-argument-type] reveal_type(extract_t(PandQ())) # revealed: int | str @@ -427,6 +485,7 @@ types: def extract_optional_t[T](x: None | P[T]) -> T: raise NotImplementedError + reveal_type(extract_optional_t(None)) # revealed: Unknown reveal_type(extract_optional_t(P[int]())) # revealed: int ``` @@ -445,11 +504,14 @@ element that is more precise: class Base[T]: x: T + class Sub[T](Base[T]): ... + def f[T](t: Base[T] | Sub[T | None]) -> T: raise NotImplementedError + reveal_type(f(Base[int]())) # revealed: int # TODO: Should ideally be `str` reveal_type(f(Sub[str | None]())) # revealed: str | None @@ -462,9 +524,11 @@ typevar bound, we do not emit a specialization error: class P[T]: value: T + def f[I: int, S: str](t: P[I] | P[S]) -> tuple[I, S]: raise NotImplementedError + reveal_type(f(P[int]())) # revealed: tuple[int, Unknown] reveal_type(f(P[str]())) # revealed: tuple[Unknown, str] ``` @@ -485,9 +549,11 @@ type variable, we do not confuse the two; `T@f` and `T@g` have separate types in def f[T](x: T) -> tuple[T, int]: return (x, 1) + def g[T](x: T) -> T | None: return x + reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None ``` @@ -497,15 +563,19 @@ reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None ```py from typing import Callable + def invoke[A, B](fn: Callable[[A], B], value: A) -> B: return fn(value) + def identity[T](x: T) -> T: return x + def head[T](xs: list[T]) -> T: return xs[0] + reveal_type(invoke(identity, 1)) # revealed: Literal[1] # TODO: this should be `Unknown | int` @@ -520,32 +590,46 @@ Protocol types can be used as TypeVar bounds, just like nominal types. from typing import Any, Protocol from ty_extensions import static_assert, is_assignable_to + class SupportsClose(Protocol): def close(self) -> None: ... + class ClosableFullyStaticProtocol(Protocol): x: int + def close(self) -> None: ... + class ClosableNonFullyStaticProtocol(Protocol): x: Any + def close(self) -> None: ... + class ClosableFullyStaticNominal: x: int + def close(self) -> None: ... + class ClosableNonFullyStaticNominal: x: int + def close(self) -> None: ... + class NotClosableProtocol(Protocol): ... + + class NotClosableNominal: ... + def close_and_return[T: SupportsClose](x: T) -> T: x.close() return x + def f( a: SupportsClose, b: ClosableFullyStaticProtocol, @@ -577,17 +661,21 @@ decorator "hides" the function type from outside callers. ```py from typing import cast, Any, Callable + def opaque_decorator(f: Any) -> Any: return f + def transparent_decorator[F: Callable[..., Any]](f: F) -> F: return f + @opaque_decorator def decorated[T](t: T) -> None: # error: [redundant-cast] reveal_type(cast(T, t)) # revealed: T@decorated + @transparent_decorator def decorated[T](t: T) -> None: # error: [redundant-cast] @@ -599,12 +687,15 @@ def decorated[T](t: T) -> None: ```py class A: ... + class B[T: A]: x: T + def f[T: A](c: T | None): return None + def g[T: A](b: B[T]): return f(b.x) # Fine ``` @@ -615,13 +706,16 @@ def g[T: A](b: B[T]): def takes_in_union[T](t: T | None) -> T: raise NotImplementedError + def takes_in_bigger_union[T](t: T | int | None) -> T: raise NotImplementedError + def _(x: str | None) -> None: reveal_type(takes_in_union(x)) # revealed: str reveal_type(takes_in_bigger_union(x)) # revealed: str + def _(x: str | int | None) -> None: reveal_type(takes_in_union(x)) # revealed: str | int reveal_type(takes_in_bigger_union(x)) # revealed: str @@ -635,6 +729,7 @@ the fact that it only appears in the function's type annotations as part of a un def f[T: (str, bytes)](suffix: T | None, prefix: T | None): return None + def g(x: str): f(prefix=x, suffix=".tar.gz") ``` @@ -654,11 +749,13 @@ def _(x: list[int], y: dict[int, int]): ```py from typing import overload + def outer[T](t: T) -> None: def inner[T](t: T) -> None: ... inner(t) + @overload def overloaded_outer() -> None: ... @overload @@ -669,9 +766,11 @@ def overloaded_outer[T](t: T | None = None) -> None: if t is not None: inner(t) + def outer[T](t: T) -> None: def inner[S](inner_t: T, s: S) -> tuple[T, S]: return inner_t, s + reveal_type(inner(t, 1)) # revealed: tuple[T@outer, Literal[1]] inner("wrong", 1) # error: [invalid-argument-type] @@ -686,29 +785,37 @@ TypeVar if the TypeVar's upper bound is a type with a precise tuple spec: from dataclasses import dataclass from typing import NamedTuple, Final + def f[T: tuple[int, str]](x: T) -> T: a, b = x reveal_type(a) # revealed: int reveal_type(b) # revealed: str return x + @dataclass class Team[T: tuple[int, str]]: employees: list[T] + def x[T: tuple[int, str]](team: Team[T]) -> Team[T]: age, name = team.employees[0] reveal_type(age) # revealed: int reveal_type(name) # revealed: str return team + class Age(int): ... + + class Name(str): ... + class Employee(NamedTuple): age: Age name: Name + EMPLOYEES: Final = (Employee(name=Name("alice"), age=Age(42)),) team = Team(employees=list(EMPLOYEES)) reveal_type(team.employees) # revealed: list[Employee] @@ -725,6 +832,7 @@ When a generic method uses a PEP 695 generic context, an implict or explicit ann ```py from typing import Self + class C: def explicit_self[T](self: Self, x: T) -> tuple[Self, T]: return self, x @@ -732,6 +840,7 @@ class C: def implicit_self[T](self, x: T) -> tuple[Self, T]: return self, x + def _(x: int): reveal_type(C().explicit_self(x)) # revealed: tuple[C, int] @@ -743,6 +852,7 @@ def _(x: int): ```py from ty_extensions import Not + def f[T](x: T, y: Not[T]) -> T: x = y # error: [invalid-assignment] y = x # error: [invalid-assignment] @@ -760,12 +870,15 @@ specializations of a generic function. from typing import Any, Callable, NoReturn, overload, Self from ty_extensions import generic_context, into_callable + def accepts_callable[**P, R](callable: Callable[P, R]) -> Callable[P, R]: return callable + def returns_int() -> int: raise NotImplementedError + # revealed: () -> int reveal_type(into_callable(returns_int)) # revealed: () -> int @@ -773,8 +886,10 @@ reveal_type(accepts_callable(returns_int)) # revealed: int reveal_type(accepts_callable(returns_int)()) + class ClassWithoutConstructor: ... + # revealed: () -> ClassWithoutConstructor reveal_type(into_callable(ClassWithoutConstructor)) # revealed: () -> ClassWithoutConstructor @@ -782,10 +897,12 @@ reveal_type(accepts_callable(ClassWithoutConstructor)) # revealed: ClassWithoutConstructor reveal_type(accepts_callable(ClassWithoutConstructor)()) + class ClassWithNew: def __new__(cls, *args, **kwargs) -> Self: raise NotImplementedError + # revealed: (...) -> ClassWithNew reveal_type(into_callable(ClassWithNew)) # revealed: (...) -> ClassWithNew @@ -793,9 +910,11 @@ reveal_type(accepts_callable(ClassWithNew)) # revealed: ClassWithNew reveal_type(accepts_callable(ClassWithNew)()) + class ClassWithInit: def __init__(self) -> None: ... + # revealed: () -> ClassWithInit reveal_type(into_callable(ClassWithInit)) # revealed: () -> ClassWithInit @@ -803,12 +922,14 @@ reveal_type(accepts_callable(ClassWithInit)) # revealed: ClassWithInit reveal_type(accepts_callable(ClassWithInit)()) + class ClassWithNewAndInit: def __new__(cls, *args, **kwargs) -> Self: raise NotImplementedError def __init__(self, x: int) -> None: ... + # TODO: We do not currently solve a common behavioral supertype for the two solutions of P. # revealed: ((...) -> ClassWithNewAndInit) | ((x: int) -> ClassWithNewAndInit) reveal_type(into_callable(ClassWithNewAndInit)) @@ -818,14 +939,17 @@ reveal_type(accepts_callable(ClassWithNewAndInit)) # revealed: ClassWithNewAndInit reveal_type(accepts_callable(ClassWithNewAndInit)()) + class Meta(type): def __call__(cls, *args: Any, **kwargs: Any) -> NoReturn: raise NotImplementedError + class ClassWithNoReturnMetatype(metaclass=Meta): def __new__(cls, *args: Any, **kwargs: Any) -> Self: raise NotImplementedError + # TODO: The return types here are wrong, because we end up creating a constraint (Never ≤ R), which # we confuse with "R has no lower bound". # revealed: (...) -> Never @@ -837,14 +961,17 @@ reveal_type(accepts_callable(ClassWithNoReturnMetatype)) # revealed: Unknown reveal_type(accepts_callable(ClassWithNoReturnMetatype)()) + class Proxy: ... + class ClassWithIgnoredInit: def __new__(cls) -> Proxy: return Proxy() def __init__(self, x: int) -> None: ... + # revealed: () -> Proxy reveal_type(into_callable(ClassWithIgnoredInit)) # revealed: () -> Proxy @@ -852,6 +979,7 @@ reveal_type(accepts_callable(ClassWithIgnoredInit)) # revealed: Proxy reveal_type(accepts_callable(ClassWithIgnoredInit)()) + class ClassWithOverloadedInit[T]: t: T # invariant @@ -861,6 +989,7 @@ class ClassWithOverloadedInit[T]: def __init__(self: "ClassWithOverloadedInit[str]", x: str) -> None: ... def __init__(self, x: int | str) -> None: ... + # TODO: The old solver cannot handle this overloaded constructor. The ideal solution is that we # would solve **P once, and map it to the entire overloaded signature of the constructor. This # mapping would have to include the return types, since there are different return types for each @@ -879,12 +1008,14 @@ reveal_type(accepts_callable(ClassWithOverloadedInit)(0)) # revealed: ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str] reveal_type(accepts_callable(ClassWithOverloadedInit)("")) + class GenericClass[T]: t: T # invariant def __new__(cls, x: list[T], y: list[T]) -> Self: raise NotImplementedError + def _(x: list[str]): # TODO: This fails because we are not propagating GenericClass's generic context into the # Callable that we create for it. @@ -919,25 +1050,42 @@ bound more than once, since we know the extra copies cannot affect the result. ```py from typing import Callable, Generic, TypeVar, Union + class M1: ... + + class M2: ... + + class M3: ... + + class M4: ... + + class M5: ... + + class M6: ... + + class M7: ... + Msg = Union[M1, M2, M3, M4, M5, M6, M7] T = TypeVar("T") U_co = TypeVar("U_co", covariant=True) + class Stream(Generic[T]): def apply(self, func: Callable[["Stream[T]"], "Stream[U_co]"]) -> "Stream[U_co]": return func(self) + TMsg = TypeVar("TMsg", bound=Msg) + class Builder(Generic[TMsg]): def build(self) -> Stream[TMsg]: stream: Stream[TMsg] = Stream() @@ -967,6 +1115,7 @@ regression test. ```py from functools import reduce + def _(keys: list[str]): # TODO: revealed: int # revealed: Unknown | Literal[0] diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 9117e09e27..ba4b51a493 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -47,9 +47,11 @@ The default value for a `ParamSpec` can be either a list of types, `...`, or ano def foo2[**P = ...]() -> None: reveal_type(P) # revealed: ParamSpec + def foo3[**P = [int, str]]() -> None: reveal_type(P) # revealed: ParamSpec + def foo4[**P, **Q = P](): reveal_type(P) # revealed: ParamSpec reveal_type(Q) # revealed: ParamSpec @@ -70,6 +72,7 @@ def foo[**P = int]() -> None: ```py from typing import ParamSpec, Callable, Concatenate + def valid[**P]( a1: Callable[P, int], a2: Callable[Concatenate[int, P], int], @@ -96,6 +99,7 @@ annotated types of `*args` and `**kwargs` respectively. ```py from typing import Callable + def foo[**P](c: Callable[P, int]) -> None: def nested1(*args: P.args, **kwargs: P.kwargs) -> None: ... @@ -123,6 +127,7 @@ def foo[**P](c: Callable[P, int]) -> None: # TODO: error def nested2(**kwargs: P.kwargs) -> None: ... + class Foo[**P]: # TODO: error args: P.args @@ -168,6 +173,7 @@ of `tuple[P.args, ...]` and `dict[str, P.kwargs]`. ```py from typing import Callable + def f[**P](func: Callable[P, int]) -> Callable[P, None]: def wrapper(*args: P.args, **kwargs: P.kwargs) -> None: reveal_type(args) # revealed: P@f.args @@ -186,6 +192,7 @@ def f[**P](func: Callable[P, int]) -> Callable[P, None]: reveal_type(func()) # revealed: int reveal_type(func(*args)) # revealed: int reveal_type(func(**kwargs)) # revealed: int + return wrapper ``` @@ -198,6 +205,7 @@ are represented as a type variable that has an upper bound of `tuple[object, ... ```py from typing import Callable, Any + def f[**P](func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: reveal_type(args + ("extra",)) # revealed: tuple[object, ...] reveal_type(args + (1, 2, 3)) # revealed: tuple[object, ...] @@ -213,13 +221,16 @@ def f[**P](func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: ```py from typing import Any, Callable, ParamSpec + class OnlyParamSpec[**P1]: attr: Callable[P1, None] + class TwoParamSpec[**P1, **P2]: attr1: Callable[P1, None] attr2: Callable[P2, None] + class TypeVarAndParamSpec[T1, **P1]: attr: Callable[P1, T1] ``` @@ -232,9 +243,11 @@ reveal_type(OnlyParamSpec[[]]().attr) # revealed: () -> None reveal_type(OnlyParamSpec[[int, str]]().attr) # revealed: (int, str, /) -> None reveal_type(OnlyParamSpec[...]().attr) # revealed: (...) -> None + def func[**P2](c: Callable[P2, None]): reveal_type(OnlyParamSpec[P2]().attr) # revealed: (**P2@func) -> None + P2 = ParamSpec("P2") # error: [invalid-type-arguments] "ParamSpec `P2` is unbound" @@ -316,9 +329,11 @@ reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int ```py from typing import Callable, ParamSpec + class ParamSpecWithDefault1[**P1 = [int, str]]: attr: Callable[P1, None] + reveal_type(ParamSpecWithDefault1().attr) # revealed: (int, str, /) -> None reveal_type(ParamSpecWithDefault1[int]().attr) # revealed: (int, /) -> None ``` @@ -327,6 +342,7 @@ reveal_type(ParamSpecWithDefault1[int]().attr) # revealed: (int, /) -> None class ParamSpecWithDefault2[**P1 = ...]: attr: Callable[P1, None] + reveal_type(ParamSpecWithDefault2().attr) # revealed: (...) -> None reveal_type(ParamSpecWithDefault2[int, str]().attr) # revealed: (int, str, /) -> None ``` @@ -336,6 +352,7 @@ class ParamSpecWithDefault3[**P1, **P2 = P1]: attr1: Callable[P1, None] attr2: Callable[P2, None] + # `P1` hasn't been specialized, so it defaults to `...` gradual form p1 = ParamSpecWithDefault3() reveal_type(p1.attr1) # revealed: (...) -> None @@ -349,10 +366,12 @@ p3 = ParamSpecWithDefault3[[int], [str]]() reveal_type(p3.attr1) # revealed: (int, /) -> None reveal_type(p3.attr2) # revealed: (str, /) -> None + class ParamSpecWithDefault4[**P1 = [int, str], **P2 = P1]: attr1: Callable[P1, None] attr2: Callable[P2, None] + p1 = ParamSpecWithDefault4() reveal_type(p1.attr1) # revealed: (int, str, /) -> None reveal_type(p1.attr2) # revealed: (int, str, /) -> None @@ -367,6 +386,7 @@ reveal_type(p3.attr2) # revealed: (str, /) -> None P2 = ParamSpec("P2") + # TODO: error: paramspec is out of scope class ParamSpecWithDefault5[**P1 = P2]: attr: Callable[P1, None] @@ -382,15 +402,19 @@ Most of these test cases are adopted from the ```py from typing import Callable + def converter[**P](func: Callable[P, int]) -> Callable[P, bool]: def wrapper(*args: P.args, **kwargs: P.kwargs) -> bool: func(*args, **kwargs) return True + return wrapper + def f1(x: int, y: str) -> int: return 1 + # This should preserve all the information about the parameters of `f1` f2 = converter(f1) @@ -417,6 +441,7 @@ The `converter` function act as a decorator here: def f3(x: int, y: str) -> int: return 1 + reveal_type(f3) # revealed: (x: int, y: str) -> bool reveal_type(f3(1, "a")) # revealed: bool @@ -435,11 +460,13 @@ f3("a", "b") ```py from typing import Callable + def multiple[**P](func1: Callable[P, int], func2: Callable[P, int]) -> Callable[P, bool]: def wrapper(*args: P.args, **kwargs: P.kwargs) -> bool: func1(*args, **kwargs) func2(*args, **kwargs) return True + return wrapper ``` @@ -456,9 +483,11 @@ TODO: Currently, we don't do this def xy(x: int, y: str) -> int: return 1 + def yx(y: int, x: str) -> int: return 2 + reveal_type(multiple(xy, xy)) # revealed: (x: int, y: str) -> bool # The common supertype is `(int, str, /)` which is converting the positional-or-keyword parameters @@ -467,12 +496,15 @@ reveal_type(multiple(xy, xy)) # revealed: (x: int, y: str) -> bool # error: [invalid-argument-type] reveal_type(multiple(xy, yx)) # revealed: (x: int, y: str) -> bool + def keyword_only_with_default_1(*, x: int = 42) -> int: return 1 + def keyword_only_with_default_2(*, y: int = 42) -> int: return 2 + # The common supertype for two functions with only keyword-only parameters would be an empty # parameter list i.e., `()` # TODO: This shouldn't error @@ -480,12 +512,15 @@ def keyword_only_with_default_2(*, y: int = 42) -> int: # revealed: (*, x: int = 42) -> bool reveal_type(multiple(keyword_only_with_default_1, keyword_only_with_default_2)) + def keyword_only1(*, x: int) -> int: return 1 + def keyword_only2(*, y: int) -> int: return 2 + # On the other hand, combining two functions with only keyword-only parameters does not have a # common supertype, so it should result in an error. # error: [invalid-argument-type] "Argument to function `multiple` is incorrect: Expected `(*, x: int) -> int`, found `def keyword_only2(*, y: int) -> int`" @@ -497,16 +532,19 @@ reveal_type(multiple(keyword_only1, keyword_only2)) # revealed: (*, x: int) -> ```py from typing import Callable + class C[**P]: f: Callable[P, int] def __init__(self, f: Callable[P, int]) -> None: self.f = f + # Note that the return type must match exactly, since C is invariant on the return type of C.f. def f(x: int, y: str) -> int: return True + c = C(f) reveal_type(c.f) # revealed: (x: int, y: str) -> int ``` @@ -519,12 +557,15 @@ reveal_type(c.f) # revealed: (x: int, y: str) -> int ```py from typing import Callable + def foo1[**P1](func: Callable[P1, int], *args: P1.args, **kwargs: P1.kwargs) -> int: return func(*args, **kwargs) + def foo1_with_extra_arg[**P1](func: Callable[P1, int], extra: str, *args: P1.args, **kwargs: P1.kwargs) -> int: return func(*args, **kwargs) + def foo2[**P2](func: Callable[P2, int], *args: P2.args, **kwargs: P2.kwargs) -> None: foo1(func, *args, **kwargs) @@ -544,6 +585,7 @@ which is then used to type the `ParamSpec` components used in `*args` and `**kwa def f1(x: int, y: str) -> int: return 1 + foo1(f1, 1, "a") foo1(f1, x=1, y="a") foo1(f1, 1, y="a") @@ -573,6 +615,7 @@ class Foo[**P]: self.args = args self.kwargs = kwargs + def bar[**P](foo: Foo[P]) -> None: reveal_type(foo) # revealed: Foo[P@bar] reveal_type(foo.args) # revealed: Unknown | P@bar.args @@ -585,6 +628,7 @@ unioned with `Unknown`, it shouldn't error here. ```py from typing import Callable + def baz[**P](fn: Callable[P, None], foo: Foo[P]) -> None: fn(*foo.args, **foo.kwargs) ``` @@ -594,11 +638,13 @@ The `Unknown` can be eliminated by using annotating these attributes with `Final ```py from typing import Final + class FooWithFinal[**P]: def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None: self.args: Final = args self.kwargs: Final = kwargs + def with_final[**P](foo: FooWithFinal[P]) -> None: reveal_type(foo) # revealed: FooWithFinal[P@with_final] reveal_type(foo.args) # revealed: P@with_final.args @@ -612,6 +658,7 @@ class Foo[**P]: def method(self, *args: P.args, **kwargs: P.kwargs) -> str: return "hello" + foo = Foo[int, str]() reveal_type(foo) # revealed: Foo[(int, str, /)] @@ -624,13 +671,16 @@ reveal_type(foo.method(1, "a")) # revealed: str ```py from typing import Callable + def callable_identity[**P, R](func: Callable[P, R]) -> Callable[P, R]: return func + @callable_identity def f(env: dict) -> None: pass + # revealed: (env: dict[Unknown, Unknown]) -> None reveal_type(f) ``` @@ -662,16 +712,21 @@ def str_str(x: str) -> str: ... from typing import Callable from overloaded import int_int, int_str, str_str + def change_return_type[**P](f: Callable[P, int]) -> Callable[P, str]: def nested(*args: P.args, **kwargs: P.kwargs) -> str: return str(f(*args, **kwargs)) + return nested + def with_parameters[**P](f: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> Callable[P, str]: def nested(*args: P.args, **kwargs: P.kwargs) -> str: return str(f(*args, **kwargs)) + return nested + reveal_type(change_return_type(int_int)) # revealed: Overload[(x: int) -> str, (x: str) -> str] # TODO: This shouldn't error and should pick the first overload because of the return type @@ -695,6 +750,7 @@ This is regression test for ```py from typing import Callable, Never, overload + class Task[**P, R]: def __init__(self, func: Callable[P, R]) -> None: self.func = func @@ -706,12 +762,15 @@ class Task[**P, R]: def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R | None: return self.func(*args, **kwargs) + def returns_str(x: int) -> str: return str(x) + def never_returns(x: int) -> Never: raise Exception() + t1 = Task(returns_str) reveal_type(t1) # revealed: Task[(x: int), str] reveal_type(t1(1)) # revealed: str @@ -739,19 +798,23 @@ method overrides where both methods have their own `ParamSpec`. ```py from typing import Callable + class Parent: def method[**P](self, callback: Callable[P, None]) -> Callable[P, None]: return callback + class Child1(Parent): # This is a valid override: Q.args matches P.args, Q.kwargs matches P.kwargs def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]: return callback + # Both signatures use ParamSpec, so they should be compatible def outer[**P](f: Callable[P, int]) -> Callable[P, int]: def inner[**Q](g: Callable[Q, int]) -> Callable[Q, int]: return g + return inner(f) ``` @@ -760,6 +823,7 @@ We can explicitly mark it as an override using the `@override` decorator. ```py from typing import override + class Child2(Parent): @override def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]: @@ -774,6 +838,7 @@ assignable. ```py from typing import Callable + class Container[**P]: def method(self, f: Callable[P, None]) -> Callable[P, None]: return f @@ -792,11 +857,14 @@ from did not have an annotated return type. ```py from typing import Callable + def infer_paramspec[**P](func: Callable[P, None]) -> Callable[P, None]: return func + def f(x: int, y: str): pass + reveal_type(infer_paramspec(f)) # revealed: (x: int, y: str) -> None ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md index cd24eb5f06..dc17698355 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -38,6 +38,7 @@ def f[T = int](): reveal_type(T.__bound__) # revealed: None reveal_type(T.__constraints__) # revealed: tuple[()] + def g[S](): reveal_type(S.__default__) # revealed: NoDefault ``` @@ -52,11 +53,13 @@ python-version = "3.13" ```py class Valid[T, U = T, V = T | U]: ... + reveal_type(Valid()) # revealed: Valid[Unknown, Unknown, Unknown] reveal_type(Valid[int]()) # revealed: Valid[int, int, int] reveal_type(Valid[int, str]()) # revealed: Valid[int, str, int | str] reveal_type(Valid[int, str, None]()) # revealed: Valid[int, str, None] + # error: [unresolved-reference] class Invalid[S = T]: ... ``` @@ -70,6 +73,7 @@ def f[T: int](): reveal_type(T.__bound__) # revealed: int reveal_type(T.__constraints__) # revealed: tuple[()] + def g[S](): reveal_type(S.__bound__) # revealed: None ``` @@ -83,6 +87,7 @@ def f[T: (int, str)](): reveal_type(T.__constraints__) # revealed: tuple[int, str] reveal_type(T.__bound__) # revealed: None + def g[S](): reveal_type(S.__constraints__) # revealed: tuple[()] ``` @@ -106,6 +111,7 @@ A type variable itself cannot be explicitly specialized; the result of the speci ```py type Positive[T] = T + def _[T]( # error: [invalid-type-form] "A type variable itself cannot be specialized" a: T[int], @@ -132,6 +138,7 @@ different uses of the same typevar. def f[T](x: T, y: T) -> None: reveal_type(x) # revealed: T@f + class C[T]: def m(self, x: T) -> None: reveal_type(x) # revealed: T@C @@ -151,11 +158,19 @@ specialization. Thus, the typevar is a subtype of itself and of `object`, but no ```py from ty_extensions import is_assignable_to, is_subtype_of, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + + class Unrelated: ... + def unbounded_unconstrained[T, U](t: T, u: U) -> None: static_assert(is_assignable_to(T, T)) static_assert(is_assignable_to(T, object)) @@ -190,6 +205,7 @@ is a final class, since the typevar can still be specialized to `Never`.) from typing import Any from typing_extensions import final + def bounded[T: Super](t: T) -> None: static_assert(is_assignable_to(T, Any)) static_assert(is_assignable_to(Any, T)) @@ -205,6 +221,7 @@ def bounded[T: Super](t: T) -> None: static_assert(not is_subtype_of(Super, T)) static_assert(not is_subtype_of(Sub, T)) + def bounded_by_gradual[T: Any](t: T) -> None: static_assert(is_assignable_to(T, Any)) static_assert(is_assignable_to(Any, T)) @@ -220,9 +237,11 @@ def bounded_by_gradual[T: Any](t: T) -> None: static_assert(not is_subtype_of(T, Sub)) static_assert(not is_subtype_of(Sub, T)) + @final class FinalClass: ... + def bounded_final[T: FinalClass](t: T) -> None: static_assert(is_assignable_to(T, Any)) static_assert(is_assignable_to(Any, T)) @@ -248,6 +267,7 @@ def two_bounded[T: Super, U: Super](t: T, u: U) -> None: static_assert(not is_subtype_of(T, U)) static_assert(not is_subtype_of(U, T)) + def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -263,6 +283,7 @@ intersection of all of its constraints is a subtype of the typevar. ```py from ty_extensions import Intersection + def constrained[T: (Base, Unrelated)](t: T) -> None: static_assert(not is_assignable_to(T, Super)) static_assert(not is_assignable_to(T, Base)) @@ -292,6 +313,7 @@ def constrained[T: (Base, Unrelated)](t: T) -> None: static_assert(not is_subtype_of(Super | Unrelated, T)) static_assert(is_subtype_of(Intersection[Base, Unrelated], T)) + def constrained_by_gradual[T: (Base, Any)](t: T) -> None: static_assert(is_assignable_to(T, Super)) static_assert(is_assignable_to(T, Base)) @@ -341,9 +363,11 @@ def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None: static_assert(not is_subtype_of(T, U)) static_assert(not is_subtype_of(U, T)) + @final class AnotherFinalClass: ... + def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -379,8 +403,10 @@ And an intersection of a typevar with another type is always a subtype of the Ty ```py from ty_extensions import Intersection, Not, is_disjoint_from + class A: ... + def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: static_assert(is_assignable_to(Intersection[T, Unrelated], T)) static_assert(is_subtype_of(Intersection[T, Unrelated], T)) @@ -404,12 +430,15 @@ that final class.) from typing import final from ty_extensions import is_equivalent_to, static_assert + @final class FinalClass: ... + @final class SecondFinalClass: ... + def f[A, B, C: FinalClass, D: FinalClass, E: (FinalClass, SecondFinalClass), F: (FinalClass, SecondFinalClass)](): static_assert(is_equivalent_to(A, A)) static_assert(is_equivalent_to(B, B)) @@ -456,6 +485,7 @@ non-singleton type. ```py from ty_extensions import is_singleton, is_single_valued, static_assert + def unbounded_unconstrained[T](t: T) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) @@ -476,13 +506,16 @@ specialize a constrained typevar to a subtype of a constraint.) ```py from typing_extensions import Literal + def constrained_non_singletons[T: (int, str)](t: T) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) + def constrained_singletons[T: (Literal[True], Literal[False])](t: T) -> None: static_assert(is_singleton(T)) + def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None: static_assert(is_single_valued(T)) ``` @@ -495,11 +528,19 @@ there is no guarantee what type the typevar will be specialized to. ```py from typing import Any + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + + class Unrelated: ... + def unbounded_unconstrained[T](t: T) -> None: def _(x: T | Super) -> None: reveal_type(x) # revealed: T@unbounded_unconstrained | Super @@ -571,11 +612,19 @@ since there is no guarantee what type the typevar will be specialized to. from ty_extensions import Intersection from typing import Any + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + + class Unrelated: ... + def unbounded_unconstrained[T](t: T) -> None: def _(x: Intersection[T, Super]) -> None: reveal_type(x) # revealed: T@unbounded_unconstrained & Super @@ -657,6 +706,7 @@ this is modeled internally as an intersection with a negation. ```py from ty_extensions import Not + def remove_constraint[T: (int, str, bool)](t: T) -> None: def _(x: Intersection[T, Not[int]]) -> None: reveal_type(x) # revealed: str @@ -684,6 +734,7 @@ of) itself. ```py from ty_extensions import is_assignable_to, is_subtype_of, Not, static_assert + def intersection_is_assignable[T](t: T) -> None: static_assert(is_assignable_to(Intersection[T, None], T)) static_assert(is_assignable_to(Intersection[T, Not[None]], T)) @@ -698,9 +749,14 @@ We can use narrowing expressions to eliminate some of the possibilities of a con ```py class P: ... + + class Q: ... + + class R: ... + def f[T: (P, Q)](t: T) -> None: if isinstance(t, P): reveal_type(t) # revealed: P @@ -716,6 +772,7 @@ def f[T: (P, Q)](t: T) -> None: reveal_type(t) # revealed: P & ~Q p: P = t + def g[T: (P, Q, R)](t: T) -> None: if isinstance(t, P): reveal_type(t) # revealed: P @@ -759,6 +816,7 @@ A typevar bound to a Callable type is callable: ```py from typing import Callable + def bound[T: Callable[[], int]](f: T): reveal_type(f) # revealed: T@bound reveal_type(f()) # revealed: int @@ -780,12 +838,15 @@ The meta-type of a typevar is `type[T]`. def normal[T](x: T): reveal_type(type(x)) # revealed: type[T@normal] + def bound_object[T: object](x: T): reveal_type(type(x)) # revealed: type[T@bound_object] + def bound_int[T: int](x: T): reveal_type(type(x)) # revealed: type[T@bound_int] + def constrained[T: (int, str)](x: T): reveal_type(type(x)) # revealed: type[T@constrained] ``` @@ -799,48 +860,60 @@ A typevar's bounds and constraints cannot be generic, cyclic or otherwise: ```py from typing import Any + # TODO: error def f[S, T: list[S]](x: S, y: T) -> S | T: return x or y + # TODO: error class C[S, T: list[S]]: x: S y: T + reveal_type(C[int, list[Any]]().x) # revealed: int reveal_type(C[int, list[Any]]().y) # revealed: list[Any] + # TODO: error def g[T: list[T]](x: T) -> T: return x + # TODO: error class D[T: list[T]]: x: T + reveal_type(D[list[Any]]().x) # revealed: list[Any] + # TODO: error def h[S, T: (list[S], str)](x: S, y: T) -> S | T: return x or y + # TODO: error class E[S, T: (list[S], str)]: x: S y: T + reveal_type(E[int, str]().x) # revealed: int reveal_type(E[int, str]().y) # revealed: str + # TODO: error def i[T: (list[T], str)](x: T) -> T: return x + # TODO: error class F[T: (list[T], str)]: x: T + reveal_type(F[list[Any]]().x) # revealed: list[Any] ``` @@ -850,6 +923,7 @@ However, they are lazily evaluated and can cyclically refer to their own type: class G[T: list[G]]: x: T + reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]] ``` @@ -860,6 +934,7 @@ An invalid specialization in a recursive bound doesn't cause a panic: class Node[T: "Node[int]"]: pass + # error: [invalid-type-arguments] def _(n: Node[str]): reveal_type(n) # revealed: Node[Unknown] @@ -874,15 +949,18 @@ class C[T, U = T]: x: T y: U + reveal_type(C[int, str]().x) # revealed: int reveal_type(C[int, str]().y) # revealed: str reveal_type(C[int]().x) # revealed: int reveal_type(C[int]().y) # revealed: int + # TODO: error class D[T = T]: x: T + reveal_type(D().x) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md index cb55bc5cd7..20621c9cc4 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -30,16 +30,22 @@ get from the sequence is a valid `int`. from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any, Never + class A: ... + + class B(A): ... + class C[T]: def receive(self) -> T: raise ValueError + class D[U](C[U]): pass + static_assert(is_assignable_to(C[B], C[A])) static_assert(not is_assignable_to(C[A], C[B])) static_assert(is_assignable_to(C[A], C[Any])) @@ -108,15 +114,21 @@ that you pass into the consumer is a valid `int`. from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any, Never + class A: ... + + class B(A): ... + class C[T]: def send(self, value: T): ... + class D[U](C[U]): pass + static_assert(not is_assignable_to(C[B], C[A])) static_assert(is_assignable_to(C[A], C[B])) static_assert(is_assignable_to(C[A], C[Any])) @@ -182,9 +194,11 @@ the bound directly. The typevar can be solved to the intersection of the actual class Contra[T]: def append(self, x: T): ... + def f[T: int](x: Contra[T]) -> T: raise NotImplementedError + def _(x: Contra[str]): reveal_type(f(x)) # revealed: Never ``` @@ -215,17 +229,23 @@ since we can't know in advance which of the allowed methods you'll want to use. from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any, Never + class A: ... + + class B(A): ... + class C[T]: def send(self, value: T): ... def receive(self) -> T: raise ValueError + class D[U](C[U]): pass + static_assert(not is_assignable_to(C[B], C[A])) static_assert(not is_assignable_to(C[A], C[B])) static_assert(is_assignable_to(C[A], C[Any])) @@ -295,15 +315,21 @@ the typevar was used.) from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any, Never + class A: ... + + class B(A): ... + class C[T]: pass + class D[U](C[U]): pass + static_assert(is_assignable_to(C[B], C[A])) static_assert(is_assignable_to(C[A], C[B])) static_assert(is_assignable_to(C[A], C[Any])) @@ -370,15 +396,20 @@ This example due to Martin Huschenbett's PyCon 2025 talk, from ty_extensions import is_subtype_of, static_assert from typing import Any + class A: ... + + class B(A): ... + class C[X]: def f(self) -> "D[X]": return D() def g(self, x: X) -> None: ... + class D[Y]: def h(self) -> C[Y]: return C() @@ -440,12 +471,17 @@ Normal attributes are mutable, and so make the enclosing class invariant in this ```py from ty_extensions import is_subtype_of, static_assert + class A: ... + + class B(A): ... + class C[T]: x: T + static_assert(not is_subtype_of(C[B], C[A])) static_assert(not is_subtype_of(C[A], C[B])) ``` @@ -464,12 +500,17 @@ invariance. from typing import Final from ty_extensions import is_subtype_of, static_assert + class A: ... + + class B(A): ... + class C[T]: x: Final[T] + static_assert(is_subtype_of(C[B], C[A])) static_assert(not is_subtype_of(C[A], C[B])) ``` @@ -482,9 +523,13 @@ mutated. ```py from ty_extensions import is_subtype_of, static_assert + class A: ... + + class B(A): ... + class C[T]: _x: T @@ -492,9 +537,11 @@ class C[T]: def x(self) -> T: return self._x + static_assert(is_subtype_of(C[B], C[A])) static_assert(not is_subtype_of(C[A], C[B])) + class D[T]: def __init__(self, x: T): self._x = x @@ -503,6 +550,7 @@ class D[T]: def x(self) -> T: return self._x + static_assert(is_subtype_of(D[B], D[A])) static_assert(not is_subtype_of(D[A], D[B])) ``` @@ -513,20 +561,27 @@ static_assert(not is_subtype_of(D[A], D[B])) from dataclasses import dataclass, field from ty_extensions import is_subtype_of, static_assert + class A: ... + + class B(A): ... + @dataclass(frozen=True) class D[U]: y: U + static_assert(is_subtype_of(D[B], D[A])) static_assert(not is_subtype_of(D[A], D[B])) + @dataclass(frozen=True) class E[U]: y: U = field() + static_assert(is_subtype_of(E[B], E[A])) static_assert(not is_subtype_of(E[A], E[B])) ``` @@ -546,13 +601,18 @@ dataclasses on Python 3.13+ can't be covariant in their field types. from dataclasses import dataclass from ty_extensions import is_subtype_of, static_assert + class A: ... + + class B(A): ... + @dataclass(frozen=True) class D[U]: y: U + static_assert(not is_subtype_of(D[B], D[A])) static_assert(not is_subtype_of(D[A], D[B])) ``` @@ -563,12 +623,17 @@ static_assert(not is_subtype_of(D[A], D[B])) from typing import NamedTuple from ty_extensions import is_subtype_of, static_assert + class A: ... + + class B(A): ... + class E[V](NamedTuple): z: V + static_assert(is_subtype_of(E[B], E[A])) static_assert(not is_subtype_of(E[A], E[B])) ``` @@ -579,6 +644,7 @@ A subclass of a `NamedTuple` can still be covariant: class D[T](E[T]): pass + static_assert(is_subtype_of(D[B], D[A])) static_assert(not is_subtype_of(D[A], D[B])) ``` @@ -590,6 +656,7 @@ But adding a new generic attribute on the subclass makes it invariant (the added class C[T](E[T]): w: T + static_assert(not is_subtype_of(C[B], C[A])) static_assert(not is_subtype_of(C[A], C[B])) ``` @@ -601,14 +668,19 @@ Properties constrain to covariance if they are get-only and invariant if they ar ```py from ty_extensions import static_assert, is_subtype_of + class A: ... + + class B(A): ... + class C[T]: @property def x(self) -> T | None: return None + class D[U]: @property def y(self) -> U | None: @@ -617,6 +689,7 @@ class D[U]: @y.setter def y(self, value: U): ... + static_assert(is_subtype_of(C[B], C[A])) static_assert(not is_subtype_of(C[A], C[B])) static_assert(not is_subtype_of(D[B], D[A])) @@ -630,13 +703,18 @@ Implicit attributes work like normal ones ```py from ty_extensions import static_assert, is_subtype_of + class A: ... + + class B(A): ... + class C[T]: def f(self) -> None: self.x: T | None = None + static_assert(not is_subtype_of(C[B], C[A])) static_assert(not is_subtype_of(C[A], C[B])) ``` @@ -650,13 +728,18 @@ impact of these methods. ```py from ty_extensions import static_assert, is_subtype_of + class A: ... + + class B(A): ... + class C[T]: def __init__(self, x: T): ... def __new__(self, x: T): ... + static_assert(is_subtype_of(C[B], C[A])) static_assert(is_subtype_of(C[A], C[B])) ``` @@ -668,10 +751,12 @@ This holds likewise for dataclasses with synthesized `__init__`: ```py from dataclasses import dataclass + @dataclass(init=True, frozen=True) class D[T]: x: T + # Covariant due to the read-only T-typed attribute; the `__init__` is ignored and doesn't make it # invariant: @@ -687,10 +772,16 @@ Union types are covariant in all their members. If `A <: B`, then `A | C <: B | ```py from ty_extensions import is_assignable_to, is_subtype_of, static_assert + class A: ... + + class B(A): ... + + class C: ... + # Union types are covariant in their members static_assert(is_subtype_of(B | C, A | C)) static_assert(is_subtype_of(C | B, C | A)) @@ -713,10 +804,16 @@ in their positive conjuncts and contravariant in their negative conjuncts. ```py from ty_extensions import is_assignable_to, is_subtype_of, static_assert, Intersection, Not + class A: ... + + class B(A): ... + + class C: ... + # Test covariance in positive conjuncts # If B <: A, then Intersection[X, B] <: Intersection[X, A] static_assert(is_subtype_of(Intersection[C, B], Intersection[C, A])) @@ -743,9 +840,13 @@ in `T` because if `A <: B`, then `type[A] <: type[B]` holds. ```py from ty_extensions import is_assignable_to, is_subtype_of, static_assert + class A: ... + + class B(A): ... + # type[T] is covariant in T static_assert(is_subtype_of(type[B], type[A])) static_assert(not is_subtype_of(type[A], type[B])) @@ -753,6 +854,7 @@ static_assert(not is_subtype_of(type[A], type[B])) static_assert(is_assignable_to(type[B], type[A])) static_assert(not is_assignable_to(type[A], type[B])) + # With generic classes using type[T] class ClassContainer[T]: def __init__(self, cls: type[T]) -> None: @@ -761,6 +863,7 @@ class ClassContainer[T]: def create_instance(self) -> T: return self.cls() + # ClassContainer is covariant in T due to type[T] static_assert(is_subtype_of(ClassContainer[B], ClassContainer[A])) static_assert(not is_subtype_of(ClassContainer[A], ClassContainer[B])) @@ -768,11 +871,13 @@ static_assert(not is_subtype_of(ClassContainer[A], ClassContainer[B])) static_assert(is_assignable_to(ClassContainer[B], ClassContainer[A])) static_assert(not is_assignable_to(ClassContainer[A], ClassContainer[B])) + # Practical example: you can pass a ClassContainer[B] where ClassContainer[A] is expected # because type[B] can safely be used where type[A] is expected def use_a_class_container(container: ClassContainer[A]) -> A: return container.create_instance() + b_container = ClassContainer[B](B) a_instance: A = use_a_class_container(b_container) # This should work ``` @@ -790,17 +895,21 @@ python-version = "3.13" from typing import TypeIs from ty_extensions import is_assignable_to, is_subtype_of, static_assert + class A: pass + class B(A): pass + class C[T]: def check(self, x: object) -> TypeIs[T]: # this is a bad check, but we only care about it type-checking return False + static_assert(not is_subtype_of(C[B], C[A])) static_assert(not is_subtype_of(C[A], C[B])) static_assert(not is_assignable_to(C[B], C[A])) @@ -828,17 +937,21 @@ be concluding `x: B` from `x: A`, which is an unsafe downcast. from typing import TypeGuard from ty_extensions import is_assignable_to, is_subtype_of, static_assert + class A: pass + class B(A): pass + class C[T]: def check(self, x: object) -> TypeGuard[T]: # this is a bad check, but we only care about it type-checking return False + static_assert(is_subtype_of(C[B], C[A])) static_assert(not is_subtype_of(C[A], C[B])) static_assert(is_assignable_to(C[B], C[A])) @@ -853,10 +966,12 @@ The variance of the type alias matches the variance of the value type (RHS type) from ty_extensions import static_assert, is_subtype_of from typing import Literal + class Covariant[T]: def get(self) -> T: raise ValueError + type CovariantLiteral1 = Covariant[Literal[1]] type CovariantInt = Covariant[int] type MyCovariant[T] = Covariant[T] @@ -864,10 +979,12 @@ type MyCovariant[T] = Covariant[T] static_assert(is_subtype_of(CovariantLiteral1, CovariantInt)) static_assert(is_subtype_of(MyCovariant[Literal[1]], MyCovariant[int])) + class Contravariant[T]: def set(self, value: T): pass + type ContravariantLiteral1 = Contravariant[Literal[1]] type ContravariantInt = Contravariant[int] type MyContravariant[T] = Contravariant[T] @@ -875,6 +992,7 @@ type MyContravariant[T] = Contravariant[T] static_assert(is_subtype_of(ContravariantInt, ContravariantLiteral1)) static_assert(is_subtype_of(MyContravariant[int], MyContravariant[Literal[1]])) + class Invariant[T]: def get(self) -> T: raise ValueError @@ -882,6 +1000,7 @@ class Invariant[T]: def set(self, value: T): pass + type InvariantLiteral1 = Invariant[Literal[1]] type InvariantInt = Invariant[int] type MyInvariant[T] = Invariant[T] @@ -891,9 +1010,11 @@ static_assert(not is_subtype_of(InvariantLiteral1, InvariantInt)) static_assert(not is_subtype_of(MyInvariant[Literal[1]], MyInvariant[int])) static_assert(not is_subtype_of(MyInvariant[int], MyInvariant[Literal[1]])) + class Bivariant[T]: pass + type BivariantLiteral1 = Bivariant[Literal[1]] type BivariantInt = Bivariant[int] type MyBivariant[T] = Bivariant[T] @@ -913,23 +1034,29 @@ you only count its own occurrences. Because we count both then, `T` is invariant ```py from ty_extensions import is_subtype_of, static_assert + class A: pass + class B(A): pass + class C[T]: def f() -> T | None: pass + static_assert(is_subtype_of(C[B], C[A])) static_assert(not is_subtype_of(C[A], C[B])) + class D[T](C[T]): def g(x: T) -> None: pass + static_assert(not is_subtype_of(D[B], D[A])) static_assert(not is_subtype_of(D[A], D[B])) ``` @@ -944,45 +1071,59 @@ T = TypeVar("T") T_co = TypeVar("T_co", covariant=True) T_contra = TypeVar("T_contra", contravariant=True) + class A: pass + class B(A): pass + class Invariant(Generic[T]): pass + static_assert(not is_subtype_of(Invariant[B], Invariant[A])) static_assert(not is_subtype_of(Invariant[A], Invariant[B])) + class DerivedInvariant[T](Invariant[T]): pass + static_assert(not is_subtype_of(DerivedInvariant[B], DerivedInvariant[A])) static_assert(not is_subtype_of(DerivedInvariant[A], DerivedInvariant[B])) + class Covariant(Generic[T_co]): pass + static_assert(is_subtype_of(Covariant[B], Covariant[A])) static_assert(not is_subtype_of(Covariant[A], Covariant[B])) + class DerivedCovariant[T](Covariant[T]): pass + static_assert(is_subtype_of(DerivedCovariant[B], DerivedCovariant[A])) static_assert(not is_subtype_of(DerivedCovariant[A], DerivedCovariant[B])) + class Contravariant(Generic[T_contra]): pass + static_assert(not is_subtype_of(Contravariant[B], Contravariant[A])) static_assert(is_subtype_of(Contravariant[A], Contravariant[B])) + class DerivedContravariant[T](Contravariant[T]): pass + static_assert(not is_subtype_of(DerivedContravariant[B], DerivedContravariant[A])) static_assert(is_subtype_of(DerivedContravariant[A], DerivedContravariant[B])) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index f4a8b3b889..e12b321c67 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -20,10 +20,12 @@ T = TypeVar("T") # TODO: error x: T + class C: # TODO: error x: T + def f() -> None: # TODO: error x: T @@ -42,12 +44,15 @@ from typing import TypeVar T = TypeVar("T") + def f1(x: T) -> T: return x + def f2(x: T) -> T: return x + f1(1) f2("a") ``` @@ -64,6 +69,7 @@ to a different type each time. def f[T](x: T) -> T: return x + reveal_type(f(1)) # revealed: Literal[1] reveal_type(f("a")) # revealed: Literal["a"] ``` @@ -81,6 +87,7 @@ class C[T]: def m2(self, x: T) -> T: return x + c: C[int] = C[int]() c.m1(1) c.m2(1) @@ -97,10 +104,12 @@ descriptor protocol, which is how `self` parameters are bound to instance method ```py from inspect import getattr_static + class C[T]: def f(self, x: T) -> str: return "a" + reveal_type(getattr_static(C[int], "f")) # revealed: def f(self, x: int) -> str reveal_type(getattr_static(C[int], "f").__get__) # revealed: reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: def f(self, x: int) -> str @@ -121,9 +130,11 @@ reveal_type(bound_method(1)) # revealed: str C[int].f(1) # error: [missing-argument] reveal_type(C[int].f(C[int](), 1)) # revealed: str + class D[U](C[U]): pass + reveal_type(D[int]().f) # revealed: bound method D[int].f(x: int) -> str ``` @@ -138,10 +149,12 @@ from typing import TypeVar, Generic T = TypeVar("T") S = TypeVar("S") + class Legacy(Generic[T]): def m(self, x: T, y: S) -> S: return y + legacy: Legacy[int] = Legacy[int]() reveal_type(legacy.m(1, "string")) # revealed: Literal["string"] ``` @@ -167,6 +180,7 @@ class C[T]: def m[S](self, x: T, y: S) -> S: return y + c: C[int] = C() reveal_type(c.m(1, "string")) # revealed: Literal["string"] ``` @@ -184,11 +198,13 @@ from typing import TypeVar, Generic T = TypeVar("T") S = TypeVar("S") + def f(x: T) -> None: x: list[T] = [] # TODO: invalid-assignment error y: list[S] = [] + class C(Generic[T]): # TODO: error: cannot use S if it's not in the current generic context x: list[S] = [] @@ -208,11 +224,13 @@ from typing import TypeVar S = TypeVar("S") + def f[T](x: T) -> None: x: list[T] = [] # TODO: invalid assignment error y: list[S] = [] + class C[T]: # TODO: error: cannot use S if it's not in the current generic context x: list[S] = [] @@ -267,10 +285,13 @@ class C[T]: ```py from typing import Iterable + def f[T](x: T, y: T) -> None: class Ok[S]: ... + # error: [invalid-generic-class] class Bad1[T]: ... + # error: [invalid-generic-class] class Bad2(Iterable[T]): ... ``` @@ -282,10 +303,13 @@ def f[T](x: T, y: T) -> None: ```py from typing import Iterable + class C[T]: class Ok1[S]: ... + # error: [invalid-generic-class] class Bad1[T]: ... + # error: [invalid-generic-class] class Bad2(Iterable[T]): ... ``` @@ -298,29 +322,37 @@ class C[_T]( C ): ... + # `D` in `list[D]` is resolved to be a type variable of class `D`. class D[D](list[D]): ... + # error: [unresolved-reference] "Name `E` used when not defined" if E: + class E[_T]( # error: [unresolved-reference] "Name `E` used when not defined" E ): ... + # error: [unresolved-reference] "Name `F` used when not defined" F + # error: [unresolved-reference] "Name `F` used when not defined" class F[_T](F): ... + def foo(): class G[_T]( # error: [unresolved-reference] "Name `G` used when not defined" G ): ... + # error: [unresolved-reference] "Name `H` used when not defined" if H: + class H[_T]( # error: [unresolved-reference] "Name `H` used when not defined" H @@ -341,6 +373,7 @@ class C[T]: bad: list[T] = [] class Inner[S]: ... + ok2: Inner[T] ``` @@ -357,10 +390,12 @@ from ty_extensions import into_callable T = TypeVar("T") S = TypeVar("S") + class Foo(Generic[T]): def bar(self, x: T, y: S) -> tuple[T, S]: raise NotImplementedError + def f(x: type[Foo[T]]) -> T: # revealed: [S](self, x: T@f, y: S) -> tuple[T@f, S] reveal_type(into_callable(x.bar)) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/specialize_constrained.md b/crates/ty_python_semantic/resources/mdtest/generics/specialize_constrained.md index 32956cdfa8..21e9180bbb 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/specialize_constrained.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/specialize_constrained.md @@ -60,13 +60,20 @@ If a typevar has an upper bound, then it must specialize to a type that is a sub from typing import final, Never from ty_extensions import ConstraintSet, generic_context + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + @final class Unrelated: ... + def bounded[T: Base](): # revealed: ty_extensions.Specialization[T@bounded = Base] reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.always())) @@ -96,6 +103,7 @@ that makes the test succeed. ```py from typing import Any + def bounded_by_gradual[T: Any](): # TODO: revealed: ty_extensions.Specialization[T@bounded_by_gradual = Any] # revealed: ty_extensions.Specialization[T@bounded_by_gradual = object] @@ -115,6 +123,7 @@ def bounded_by_gradual[T: Any](): # revealed: ty_extensions.Specialization[T@bounded_by_gradual = Unrelated] reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Unrelated))) + def bounded_by_gradual_list[T: list[Any]](): # revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = Top[list[Any]]] reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.always())) @@ -153,13 +162,20 @@ information at the moment. from typing import final, Never from ty_extensions import ConstraintSet, generic_context + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + @final class Unrelated: ... + def constrained[T: (Base, Unrelated)](): # revealed: None reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.always())) @@ -388,13 +404,20 @@ the other. from typing import final, Never from ty_extensions import ConstraintSet, generic_context + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + @final class Unrelated: ... + # fmt: off def mutually_bound[T: Base, U](): @@ -423,6 +446,7 @@ must be specialized to `list[T]`, but it cannot affect what `T` is specialized t from typing import Never from ty_extensions import ConstraintSet, generic_context + def mentions[T, U](): # (T@mentions ≤ int) ∧ (U@mentions = list[T@mentions]) constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(list[T], U, list[T]) diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md index 2d9734455b..4ba4845c44 100644 --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -59,18 +59,21 @@ understood as being available: ```py from ty_extensions import has_member, static_assert + class Base: base_class_attr: int = 1 def f_base(self): self.base_instance_attr: str = "Base" + class Intermediate(Base): intermediate_attr: int = 2 def f_intermediate(self): self.intermediate_instance_attr: str = "Intermediate" + class C(Intermediate): class_attr: int = 3 @@ -89,6 +92,7 @@ class C(Intermediate): def static_method() -> int: return 1 + static_assert(has_member(C(), "base_class_attr")) static_assert(has_member(C(), "intermediate_attr")) static_assert(has_member(C(), "class_attr")) @@ -120,15 +124,18 @@ Class-level attributes can also be accessed through the class itself: ```py from ty_extensions import has_member, static_assert + class Base: base_attr: int = 1 + class C(Base): class_attr: str = "c" def f(self): self.instance_attr = True + static_assert(has_member(C, "class_attr")) static_assert(has_member(C, "base_attr")) @@ -148,23 +155,28 @@ accessible: class MetaBase(type): meta_base_attr = 1 + class Meta(MetaBase): meta_attr = 2 + class D(Base, metaclass=Meta): class_attr = 3 + static_assert(has_member(D, "meta_base_attr")) static_assert(has_member(D, "meta_attr")) static_assert(has_member(D, "base_attr")) static_assert(has_member(D, "class_attr")) + def _(x: type[D]): static_assert(has_member(x, "meta_base_attr")) static_assert(has_member(x, "meta_attr")) static_assert(has_member(x, "base_attr")) static_assert(has_member(x, "class_attr")) + def _[T: D](x: type[T]): static_assert(has_member(x, "meta_base_attr")) static_assert(has_member(x, "meta_attr")) @@ -180,9 +192,11 @@ from typing import Generic, TypeVar T = TypeVar("T") + class C(Generic[T]): base_attr: T + static_assert(has_member(C[int], "base_attr")) static_assert(has_member(C[int](), "base_attr")) ``` @@ -193,10 +207,13 @@ Generic classes can also have metaclasses: class Meta(type): FOO = 42 + class E(Generic[T], metaclass=Meta): ... + static_assert(has_member(E[int], "FOO")) + def f(x: type[E[str]]): static_assert(has_member(x, "FOO")) ``` @@ -209,6 +226,7 @@ def f(x: type[E[str]]): from typing import Any from ty_extensions import has_member, static_assert + def f(x: type[Any]): static_assert(has_member(x, "__base__")) static_assert(has_member(x, "__qualname__")) @@ -233,9 +251,11 @@ static_assert(has_member("a", "startswith")) static_assert(has_member(b"a", "__buffer__")) static_assert(has_member(3.14, "is_integer")) + def _(literal_string: LiteralString): static_assert(has_member(literal_string, "startswith")) + static_assert(has_member(("some", "tuple", 1, 2), "count")) static_assert(has_member(len, "__doc__")) @@ -248,10 +268,12 @@ static_assert(has_member("a".startswith, "__doc__")) from ty_extensions import has_member, static_assert from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + static_assert(has_member(Answer, "NO")) static_assert(has_member(Answer, "YES")) static_assert(has_member(Answer, "__members__")) @@ -263,14 +285,17 @@ static_assert(has_member(Answer, "__members__")) from ty_extensions import has_member, static_assert from typing import TypedDict + class Person(TypedDict): name: str age: int | None + static_assert(not has_member(Person, "name")) static_assert(has_member(Person, "keys")) static_assert(has_member(Person, "__total__")) + def _(person: Person): static_assert(not has_member(person, "name")) static_assert(not has_member(person, "__total__")) @@ -281,6 +306,7 @@ def _(person: Person): static_assert(not has_member(type(person), "__total__")) static_assert(has_member(type(person), "keys")) + def _(t_person: type[Person]): static_assert(not has_member(t_person, "name")) static_assert(has_member(t_person, "__total__")) @@ -293,10 +319,12 @@ def _(t_person: type[Person]): from ty_extensions import has_member, static_assert from typing import NamedTuple, Generic, TypeVar + class Person(NamedTuple): id: int name: str + static_assert(has_member(Person, "id")) static_assert(has_member(Person, "name")) @@ -304,6 +332,7 @@ static_assert(has_member(Person, "_make")) static_assert(has_member(Person, "_asdict")) static_assert(has_member(Person, "_replace")) + def _(person: Person): static_assert(has_member(person, "id")) static_assert(has_member(person, "name")) @@ -312,6 +341,7 @@ def _(person: Person): static_assert(has_member(person, "_asdict")) static_assert(has_member(person, "_replace")) + def _(t_person: type[Person]): static_assert(has_member(t_person, "id")) static_assert(has_member(t_person, "name")) @@ -320,17 +350,21 @@ def _(t_person: type[Person]): static_assert(has_member(t_person, "_asdict")) static_assert(has_member(t_person, "_replace")) + T = TypeVar("T") + class Box(NamedTuple, Generic[T]): item: T + static_assert(has_member(Box, "item")) static_assert(has_member(Box, "_make")) static_assert(has_member(Box, "_asdict")) static_assert(has_member(Box, "_replace")) + def _(box: Box[int]): static_assert(has_member(box, "item")) @@ -347,14 +381,17 @@ the union. ```py from ty_extensions import has_member, static_assert + class A: on_both: int = 1 only_on_a: str = "a" + class B: on_both: int = 2 only_on_b: str = "b" + def f(union: A | B): static_assert(has_member(union, "on_both")) static_assert(not has_member(union, "only_on_a")) @@ -368,18 +405,22 @@ items on the intersection of the non-`Any` elements: from typing import Any from ty_extensions import has_member, static_assert + class A: on_both: int = 1 only_on_a: str = "a" + class B: on_both: int = 2 only_on_b: str = "b" + def f(union: Any | A): static_assert(has_member(union, "on_both")) static_assert(has_member(union, "only_on_a")) + def g(union: Any | A | B): static_assert(has_member(union, "on_both")) static_assert(not has_member(union, "only_on_a")) @@ -393,14 +434,17 @@ unioned with `Any`: from typing import Any from ty_extensions import Intersection, has_member, static_assert + class A: on_both: int = 1 only_on_a: str = "a" + class B: on_both: int = 2 only_on_b: str = "b" + def f(x: Intersection[Any, A] | B): static_assert(has_member(x, "on_both")) static_assert(not has_member(x, "only_on_a")) @@ -417,14 +461,17 @@ the elements: ```py from ty_extensions import has_member, static_assert + class A: on_both: int = 1 only_on_a: str = "a" + class B: on_both: int = 2 only_on_b: str = "b" + def f(intersection: object): if isinstance(intersection, A): if isinstance(intersection, B): @@ -440,24 +487,28 @@ It also works when negative types are introduced: ```py from ty_extensions import has_member, static_assert + class A: on_all: int = 1 only_on_a: str = "a" only_on_ab: str = "a" only_on_ac: str = "a" + class B: on_all: int = 2 only_on_b: str = "b" only_on_ab: str = "b" only_on_bc: str = "b" + class C: on_all: int = 3 only_on_c: str = "c" only_on_ac: str = "c" only_on_bc: str = "c" + def f(intersection: object): if isinstance(intersection, A): if isinstance(intersection, B): @@ -627,6 +678,7 @@ Dynamically added members cannot be accessed: ```py from ty_extensions import has_member, static_assert + class C: static_attr = 1 @@ -636,6 +688,7 @@ class C: def __getattr__(self, name: str) -> str: return "a" + c = C() c.dynamic_attr = "a" @@ -658,11 +711,13 @@ python-version = "3.9" from ty_extensions import has_member, static_assert from dataclasses import dataclass + @dataclass class Person: age: int name: str + static_assert(has_member(Person, "name")) static_assert(has_member(Person, "age")) @@ -723,10 +778,12 @@ def _(person: Person): from ty_extensions import has_member, static_assert from dataclasses import dataclass + @dataclass(init=False, repr=False, eq=False) class C: x: int + static_assert(has_member(C, "__init__")) static_assert(has_member(C, "__repr__")) static_assert(has_member(C, "__eq__")) @@ -745,15 +802,18 @@ When `order=True` is set, comparison dunder methods become available: from ty_extensions import has_member, static_assert from dataclasses import dataclass + @dataclass(order=True) class C: x: int + static_assert(has_member(C, "__lt__")) static_assert(has_member(C, "__le__")) static_assert(has_member(C, "__gt__")) static_assert(has_member(C, "__ge__")) + def _(c: C): static_assert(has_member(c, "__lt__")) static_assert(has_member(c, "__le__")) @@ -769,10 +829,12 @@ When `slots=True`, the corresponding dunder attribute becomes available: from ty_extensions import has_member, static_assert from dataclasses import dataclass + @dataclass(slots=True) class C: x: int + static_assert(has_member(C, "__slots__")) static_assert(has_member(C(1), "__slots__")) ``` @@ -790,10 +852,12 @@ python-version = "3.11" from ty_extensions import has_member, static_assert from dataclasses import dataclass + @dataclass(slots=True, weakref_slot=True) class C: x: int + static_assert(has_member(C, "__weakref__")) static_assert(has_member(C(1), "__weakref__")) ``` @@ -811,12 +875,15 @@ python-version = "3.13" from ty_extensions import has_member, static_assert from dataclasses import dataclass + @dataclass class C: x: int + static_assert(has_member(C, "__replace__")) + def _(c: C): static_assert(has_member(c, "__replace__")) ``` @@ -834,12 +901,15 @@ python-version = "3.10" from ty_extensions import has_member, static_assert from dataclasses import dataclass + @dataclass class C: x: int + static_assert(has_member(C, "__match_args__")) + def _(c: C): static_assert(has_member(c, "__match_args__")) ``` @@ -855,11 +925,13 @@ python-version = "3.9" from dataclasses import dataclass from ty_extensions import static_assert, has_member + # TODO: these parameters don't exist on Python 3.9; # we should emit a diagnostic (or two) @dataclass(slots=True, weakref_slot=True) class F: ... + static_assert(not has_member(F, "__slots__")) static_assert(not has_member(F, "__match_args__")) @@ -878,15 +950,18 @@ inherited from their base classes on the class object: ```py from ty_extensions import has_member, static_assert + class Base: base_attr: int = 1 def base_method(self) -> str: return "hello" + class Mixin: mixin_attr: str = "mixin" + # Dynamic class with a single base DynamicSingle = type("DynamicSingle", (Base,), {}) @@ -936,12 +1011,15 @@ Dynamic classes inheriting from classes with custom metaclasses get metaclass me ```py from ty_extensions import has_member, static_assert + class MyMeta(type): meta_attr: str = "meta" + class Base(metaclass=MyMeta): base_attr: int = 1 + Dynamic = type("Dynamic", (Base,), {}) # Metaclass attributes are available on the class @@ -954,9 +1032,11 @@ However, instances of dynamic classes currently do not expose members for autoco ```py from ty_extensions import has_member, static_assert + class Base: base_attr: int = 1 + DynamicSingle = type("DynamicSingle", (Base,), {}) instance = DynamicSingle() diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index e3fff99f0d..29fd43b3e8 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -11,9 +11,11 @@ valid type for use in a type expression: ```py MyInt = int + def f(x: MyInt): reveal_type(x) # revealed: int + f(1) ``` @@ -22,9 +24,11 @@ f(1) ```py MyNone = None + def g(x: MyNone): reveal_type(x) # revealed: None + g(None) ``` @@ -116,6 +120,7 @@ reveal_type(IntOrTypeVar) # revealed: reveal_type(NoneOrTypeVar) # revealed: + def _( int_or_str: IntOrStr, int_or_str_or_bytes1: IntOrStrOrBytes1, @@ -206,6 +211,7 @@ ListOfIntOrListOfInt = list[int] | list[int] reveal_type(IntOrInt) # revealed: reveal_type(ListOfIntOrListOfInt) # revealed: + def _(int_or_int: IntOrInt, list_of_int_or_list_of_int: ListOfIntOrListOfInt): reveal_type(int_or_int) # revealed: int reveal_type(list_of_int_or_list_of_int) # revealed: list[int] @@ -226,6 +232,7 @@ IntOrOne = int | 1 # error: [unsupported-operator] reveal_type(IntOrOne) # revealed: Unknown + def _(int_or_one: IntOrOne): reveal_type(int_or_one) # revealed: Unknown ``` @@ -236,10 +243,12 @@ as a type expression: ```py from types import UnionType + def f(SomeUnionType: UnionType): # error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression" some_union: SomeUnionType + f(int | str) ``` @@ -253,16 +262,20 @@ class Foo: def __or__(self, other) -> str: return "foo" + reveal_type(Foo() | int) # revealed: str reveal_type(Foo() | list[int]) # revealed: str + class Bar: def __ror__(self, other) -> str: return "bar" + reveal_type(int | Bar()) # revealed: str reveal_type(list[int] | Bar()) # revealed: str + class Invalid: def __or__(self, other: "Invalid") -> str: return "Invalid" @@ -270,6 +283,7 @@ class Invalid: def __ror__(self, other: "Invalid") -> str: return "Invalid" + # error: [unsupported-operator] reveal_type(int | Invalid()) # revealed: Unknown # error: [unsupported-operator] @@ -287,9 +301,13 @@ class Meta(type): def __or__(self, other) -> str: return "Meta" + class Foo(metaclass=Meta): ... + + class Bar(metaclass=Meta): ... + X = Foo | Bar # In an ideal world, perhaps we would respect `Meta.__or__` here and reveal `str`? @@ -297,9 +315,11 @@ X = Foo | Bar # `X` is still a valid type alias reveal_type(X) # revealed: + def f(obj: X): reveal_type(obj) # revealed: Foo | Bar + # We do respect the metaclass `__or__` if it's used between a class and a non-class, however: Y = Foo | 42 @@ -308,6 +328,7 @@ reveal_type(Y) # revealed: str Z = Bar | 56 reveal_type(Z) # revealed: str + def g( arg1: Y, # error: [invalid-type-form] arg2: Z, # error: [invalid-type-form] @@ -341,6 +362,7 @@ from bar import GLOBAL_CONSTANT reveal_type(GLOBAL_CONSTANT) # revealed: int | str if TYPE_CHECKING: + class ItsQuiteCloudyInManchester: X = int | str @@ -356,8 +378,10 @@ if TYPE_CHECKING: # TODO: should be `int | str` reveal_type(obj) # revealed: Unknown + Y = list["int | str"] + def g(obj: Y): reveal_type(obj) # revealed: list[int | str] ``` @@ -403,6 +427,7 @@ reveal_type(AnnotatedType) # revealed: + def _( list_of_ints: MyList[int], dict_str_to_int: MyDict[str, int], @@ -439,6 +464,7 @@ DictStrTo = MyDict[str, U] reveal_type(DictStrTo) # revealed: + def _( dict_str_to_int: DictStrTo[int], ): @@ -465,6 +491,7 @@ reveal_type(AnnotatedInt) # revealed: reveal_type(CallableIntToStr) # revealed: str'> + def _( ints_or_none: IntsOrNone, ints_or_strs: IntsOrStrs, @@ -498,6 +525,7 @@ reveal_type(MyOtherList) # revealed: reveal_type(MyOtherType) # revealed: reveal_type(TypeOrList) # revealed: + def _( list_of_ints: MyOtherList[int], subclass_of_int: MyOtherType[int], @@ -545,6 +573,7 @@ T_default = TypeVar("T_default", default=int) MyListWithDefault = list[T_default] + def _( list_of_str: MyListWithDefault[str], list_of_int: MyListWithDefault, @@ -563,19 +592,24 @@ def _( from typing_extensions import Generic from ty_extensions import reveal_mro + class GenericBase(Generic[T]): pass + ConcreteBase = GenericBase[int] + class Derived1(ConcreteBase): pass + # revealed: (, , typing.Generic, ) reveal_mro(Derived1) GenericBaseAlias = GenericBase[T] + class Derived2(GenericBaseAlias[int]): pass ``` @@ -600,6 +634,7 @@ MyList = list[T] from my_types import MyList import my_types as mt + def _( list_of_ints1: MyList[int], list_of_ints2: mt.MyList[int], @@ -623,6 +658,7 @@ T = TypeVar("T") MyList = list[T] + def _( list_of_ints: "MyList[int]", ): @@ -646,6 +682,7 @@ V = TypeVar("V") X = tuple[T, *tuple[U, ...], V] Y = X[T, tuple[int, str, U], bytes] + def g(obj: Y[bool, range]): reveal_type(obj) # revealed: tuple[bool, *tuple[tuple[int, str, range], ...], bytes] ``` @@ -664,79 +701,103 @@ from typing import Protocol, TypeVar, TypedDict ListOfInts = list[int] + # error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" def _(doubly_specialized: ListOfInts[int]): reveal_type(doubly_specialized) # revealed: Unknown + type ListOfInts2 = list[int] # error: [not-subscriptable] "Cannot subscript non-generic type alias: `list[int]` is already specialized" DoublySpecialized = ListOfInts2[int] + def _(doubly_specialized: DoublySpecialized): reveal_type(doubly_specialized) # revealed: Unknown + # error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" List = list[int][int] + def _(doubly_specialized: List): reveal_type(doubly_specialized) # revealed: Unknown + Tuple = tuple[int, str] + # error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" def _(doubly_specialized: Tuple[int]): reveal_type(doubly_specialized) # revealed: Unknown + T = TypeVar("T") + class LegacyProto(Protocol[T]): pass + LegacyProtoInt = LegacyProto[int] + # error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" def _(doubly_specialized: LegacyProtoInt[int]): reveal_type(doubly_specialized) # revealed: Unknown + class Proto[T](Protocol): pass + ProtoInt = Proto[int] + # error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" def _(doubly_specialized: ProtoInt[int]): reveal_type(doubly_specialized) # revealed: Unknown + # TODO: TypedDict is just a function object at runtime, we should emit an error class LegacyDict(TypedDict[T]): x: T + # TODO: should be a `not-subscriptable` error LegacyDictInt = LegacyDict[int] + # TODO: should be a `not-subscriptable` error def _(doubly_specialized: LegacyDictInt[int]): # TODO: should be `Unknown` reveal_type(doubly_specialized) # revealed: @Todo(Inference of subscript on special form) + class Dict[T](TypedDict): x: T + DictInt = Dict[int] + # error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" def _(doubly_specialized: DictInt[int]): reveal_type(doubly_specialized) # revealed: Unknown + Union = list[str] | list[int] + # error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" def _(doubly_specialized: Union[int]): reveal_type(doubly_specialized) # revealed: Unknown + type MyListAlias[T] = list[T] MyListOfInts = MyListAlias[int] + # error: [not-subscriptable] "Cannot subscript non-generic type alias: Double specialization is not allowed" def _(doubly_specialized: MyListOfInts[int]): reveal_type(doubly_specialized) # revealed: Unknown @@ -754,6 +815,7 @@ U = TypeVar("U") MyList = list[T] MyDict = dict[T, U] + def _( # error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" list_too_many_args: MyList[int, str], @@ -771,9 +833,11 @@ from ty_extensions import TypeOf IntOrStr = int | str + def this_does_not_work() -> TypeOf[IntOrStr]: raise NotImplementedError() + def _( # error: [not-subscriptable] "Cannot subscript non-generic type" specialized: this_does_not_work()[int], @@ -787,6 +851,7 @@ Similarly, if you try to specialize a union type without a binding context, we e # error: [not-subscriptable] "Cannot subscript non-generic type" x: (list[T] | set[T])[int] + def _(): # TODO: `list[Unknown] | set[Unknown]` might be better reveal_type(x) # revealed: Unknown @@ -805,6 +870,7 @@ T = TypeVar("T") MyAlias = list[T] + def outer(): MyAlias = set[T] @@ -829,6 +895,7 @@ if False: else: MyAlias2 = set[T] + def _( x1: MyAlias1[int], x2: MyAlias2[int], @@ -846,14 +913,17 @@ from typing_extensions import TypeVar T = TypeVar("T") + def flag() -> bool: return True + if flag(): MyAlias = list[T] else: MyAlias = set[T] + # It is questionable whether this should be supported or not. It might also be reasonable to # emit an error here (e.g. "Invalid subscript of object of type ` | # ` in type expression"). If we ever choose to do so, the revealed @@ -879,13 +949,16 @@ BytesLiteral = Literal[b"b"] BoolLiteral = Literal[True] MixedLiterals = Literal[1, "a", True, None] + class Color(Enum): RED = 0 GREEN = 1 BLUE = 2 + EnumLiteral = Literal[Color.RED] + def _( int_literal1: IntLiteral1, int_literal2: IntLiteral2, @@ -916,9 +989,11 @@ LiteralInt = Literal[int] reveal_type(LiteralInt) # revealed: Unknown + def _(weird: LiteralInt): reveal_type(weird) # revealed: Unknown + # error: [invalid-type-form] "`Literal[26]` is not a generic class" def _(weird: IntLiteral1[int]): reveal_type(weird) # revealed: Unknown @@ -933,6 +1008,7 @@ from typing import Annotated MyAnnotatedInt = Annotated[int, "some metadata", 1, 2, 3] + def _(annotated_int: MyAnnotatedInt): reveal_type(annotated_int) # revealed: int ``` @@ -946,9 +1022,11 @@ T = TypeVar("T") Deprecated = Annotated[T, "deprecated attribute"] + class C: old: Deprecated[int] + reveal_type(C().old) # revealed: int ``` @@ -959,6 +1037,7 @@ still use the first element as the type, when used in annotations: # error: [invalid-type-form] "Special form `typing.Annotated` expected at least 2 arguments (one type and at least one metadata element)" WronglyAnnotatedInt = Annotated[int] + def _(wrongly_annotated_int: WronglyAnnotatedInt): reveal_type(wrongly_annotated_int) # revealed: int ``` @@ -976,6 +1055,7 @@ MyOptionalInt = Optional[int] reveal_type(MyOptionalInt) # revealed: + def _(optional_int: MyOptionalInt): reveal_type(optional_int) # revealed: int | None ``` @@ -987,6 +1067,7 @@ JustNone = Optional[None] reveal_type(JustNone) # revealed: None + def _(just_none: JustNone): reveal_type(just_none) # revealed: None ``` @@ -1011,6 +1092,7 @@ reveal_type(MyLiteralString) # revealed: reveal_type(MyNoReturn) # revealed: reveal_type(MyNever) # revealed: + def _( ls: MyLiteralString, nr: MyNoReturn, @@ -1033,6 +1115,7 @@ SingleInt = Tuple[int] Ints = Tuple[int, ...] EmptyTuple = Tuple[()] + def _(int_and_str: IntAndStr, single_int: SingleInt, ints: Ints, empty_tuple: EmptyTuple): reveal_type(int_and_str) # revealed: tuple[int, str] reveal_type(single_int) # revealed: tuple[int] @@ -1048,6 +1131,7 @@ from typing import Tuple # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" Invalid = Tuple[int, 1] + def _(invalid: Invalid): reveal_type(invalid) # revealed: tuple[int, Unknown] ``` @@ -1065,6 +1149,7 @@ IntOrStrOrBytes = Union[int, Union[str, bytes]] reveal_type(IntOrStr) # revealed: reveal_type(IntOrStrOrBytes) # revealed: + def _( int_or_str: IntOrStr, int_or_str_or_bytes: IntOrStrOrBytes, @@ -1080,6 +1165,7 @@ JustInt = Union[int] reveal_type(JustInt) # revealed: + def _(just_int: JustInt): reveal_type(just_int) # revealed: int ``` @@ -1093,6 +1179,7 @@ EmptyUnion = Union[()] reveal_type(EmptyUnion) # revealed: + def _(empty: EmptyUnion): reveal_type(empty) # revealed: Never ``` @@ -1103,6 +1190,7 @@ Other invalid uses are also caught: # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" Invalid = Union[str, 1] + def _( invalid: Invalid, ): @@ -1120,13 +1208,20 @@ from typing import Any, Union, Protocol, TypeVar, Generic T = TypeVar("T") + class A: ... + + class B: ... + + class G(Generic[T]): ... + class P(Protocol): def method(self) -> None: ... + SubclassOfA = type[A] SubclassOfAny = type[Any] SubclassOfAOrB1 = type[A | B] @@ -1145,6 +1240,7 @@ reveal_type(SubclassOfG) # revealed: reveal_type(SubclassOfGInt) # revealed: reveal_type(SubclassOfP) # revealed: + def _( subclass_of_a: SubclassOfA, subclass_of_any: SubclassOfAny, @@ -1184,9 +1280,13 @@ Using `type[]` with a union type alias distributes the `type[]` over the union e ```py from typing import Union + class C: ... + + class D: ... + UnionAlias1 = C | D UnionAlias2 = Union[C, D] @@ -1196,6 +1296,7 @@ SubclassOfUnionAlias2 = type[UnionAlias2] reveal_type(SubclassOfUnionAlias1) # revealed: reveal_type(SubclassOfUnionAlias2) # revealed: + def _( subclass_of_union_alias1: SubclassOfUnionAlias1, subclass_of_union_alias2: SubclassOfUnionAlias2, @@ -1218,6 +1319,7 @@ InvalidSubclassOf1 = type[1] # TODO: This should be an error InvalidSubclassOfLiteral = type[Literal[42]] + def _( invalid_subclass_of_1: InvalidSubclassOf1, invalid_subclass_of_literal: InvalidSubclassOfLiteral, @@ -1236,13 +1338,20 @@ from typing import Any, Union, Protocol, TypeVar, Generic, Type T = TypeVar("T") + class A: ... + + class B: ... + + class G(Generic[T]): ... + class P(Protocol): def method(self) -> None: ... + SubclassOfA = Type[A] SubclassOfAny = Type[Any] SubclassOfAOrB1 = Type[A | B] @@ -1261,6 +1370,7 @@ reveal_type(SubclassOfG) # revealed: reveal_type(SubclassOfGInt) # revealed: reveal_type(SubclassOfP) # revealed: + def _( subclass_of_a: SubclassOfA, subclass_of_any: SubclassOfAny, @@ -1329,6 +1439,7 @@ reveal_type(MyDefaultDict) # revealed: reveal_type(MyDeque) # revealed: reveal_type(MyOrderedDict) # revealed: + def _( my_list: MyList, my_set: MySet, @@ -1394,6 +1505,7 @@ reveal_type(DefaultDictOrNone) # revealed: reveal_type(OrderedDictOrNone) # revealed: + def _( none_or_list: NoneOrList, none_or_set: NoneOrSet, @@ -1458,6 +1570,7 @@ DictTooFewArgs = Dict[str] # error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 3" DictTooManyArgs = Dict[str, int, float] + def _( invalid_list: InvalidList, list_too_many_args: ListTooManyArgs, @@ -1489,6 +1602,7 @@ reveal_type(CallableNoArgs) # revealed: No reveal_type(BasicCallable) # revealed: bytes'> reveal_type(GradualCallable) # revealed: str'> + def _( callable_no_args: CallableNoArgs, basic_callable: BasicCallable, @@ -1505,6 +1619,7 @@ Nested callables work as expected: TakesCallable = Callable[[Callable[[int], str]], bytes] ReturnsCallable = Callable[[int], Callable[[str], bytes]] + def _(takes_callable: TakesCallable, returns_callable: ReturnsCallable): reveal_type(takes_callable) # revealed: ((int, /) -> str, /) -> bytes reveal_type(returns_callable) # revealed: (int, /) -> (str, /) -> bytes @@ -1522,6 +1637,7 @@ InvalidCallable2 = Callable[int, str] reveal_type(InvalidCallable1) # revealed: Unknown'> reveal_type(InvalidCallable2) # revealed: Unknown'> + def _(invalid_callable1: InvalidCallable1, invalid_callable2: InvalidCallable2): reveal_type(invalid_callable1) # revealed: (...) -> Unknown reveal_type(invalid_callable2) # revealed: (...) -> Unknown @@ -1541,14 +1657,17 @@ errors: ```py AliasForStr = "str" + # error: [invalid-type-form] "Variable of type `Literal["str"]` is not allowed in a type expression" def _(s: AliasForStr): reveal_type(s) # revealed: Unknown + IntOrStr = int | "str" # error: [unsupported-operator] reveal_type(IntOrStr) # revealed: Unknown + def _(int_or_str: IntOrStr): reveal_type(int_or_str) # revealed: Unknown ``` @@ -1567,8 +1686,10 @@ DictStrToStyle = Dict[str, "Style"] AnnotatedStyle = Annotated["Style", "metadata"] CallableStyleToStyle = Callable[["Style"], "Style"] + class Style: ... + def _( list_of_ints1: ListOfInts1, list_of_ints2: ListOfInts2, @@ -1596,6 +1717,7 @@ from typing import Union Recursive = list[Union["Recursive", None]] + def _(r: Recursive): reveal_type(r) # revealed: list[Divergent] ``` @@ -1617,6 +1739,7 @@ RecursiveDict2 = Dict[str, "RecursiveDict2" | None] RecursiveDict3 = dict["RecursiveDict3", int] RecursiveDict4 = Dict["RecursiveDict4", int] + def _( recursive_list1: RecursiveList1, recursive_list2: RecursiveList2, @@ -1643,6 +1766,7 @@ T = TypeVar("T") NestedDict = dict[str, "NestedDict[T] | T"] NestedList = list["NestedList[T] | None"] + def _( nested_dict_int: NestedDict[int], nested_list_str: NestedList[str], diff --git a/crates/ty_python_semantic/resources/mdtest/import/conditional.md b/crates/ty_python_semantic/resources/mdtest/import/conditional.md index 02ca3a5cef..76f66889ea 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/conditional.md +++ b/crates/ty_python_semantic/resources/mdtest/import/conditional.md @@ -8,6 +8,7 @@ def coinflip() -> bool: return True + if coinflip(): y = 3 @@ -37,6 +38,7 @@ reveal_type(y) # revealed: Literal[3] def coinflip() -> bool: return True + if coinflip(): y: int = 3 @@ -70,6 +72,7 @@ Importing a possibly undeclared name still gives us its declared type: def coinflip() -> bool: return True + if coinflip(): x: int ``` @@ -94,9 +97,11 @@ def f(): ... def coinflip() -> bool: return True + if coinflip(): from c import f else: + def f(): ... ``` @@ -124,6 +129,7 @@ x: int def coinflip() -> bool: return True + if coinflip(): from c import x else: diff --git a/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md b/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md index 2dc3bf4839..feb7072b32 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md +++ b/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md @@ -13,6 +13,8 @@ NOTE: This file only includes the usage of `__all__` for named-imports i.e., ```py class A: ... + + class B: ... ``` @@ -36,16 +38,24 @@ from the local scope of a function or class. ```py __all__ = ["A"] + def foo(): __all__.append("B") + class Foo: __all__ += ["C"] + class A: ... + + class B: ... + + class C: ... + foo() ``` @@ -70,7 +80,10 @@ According to the [specification], the following idioms are supported: ```py __all__ = ["A", "B"] + class A: ... + + class B: ... ``` @@ -79,7 +92,10 @@ class B: ... ```py __all__: list[str] = ["C", "D"] + class C: ... + + class D: ... ``` @@ -104,12 +120,19 @@ reveal_type(dunder_all_names(exporter_annotated)) ```py __all__ = ["A", "B"] + class A: ... + + class B: ... + __all__ = ["C", "D"] + class C: ... + + class D: ... ``` @@ -118,11 +141,16 @@ class D: ... ```py __all__ = ["X"] + class X: ... + __all__: list[str] = ["Y", "Z"] + class Y: ... + + class Z: ... ``` @@ -147,7 +175,10 @@ reveal_type(dunder_all_names(exporter_annotated)) ```py __all__ = ("A", "B") + class A: ... + + class B: ... ``` @@ -156,7 +187,10 @@ class B: ... ```py __all__: tuple[str, ...] = ("C", "D") + class C: ... + + class D: ... ``` @@ -181,12 +215,19 @@ reveal_type(dunder_all_names(exporter_annotated)) ```py __all__ = ("A", "B") + class A: ... + + class B: ... + __all__ = ("C", "D") + class C: ... + + class D: ... ``` @@ -195,11 +236,16 @@ class D: ... ```py __all__ = ("X",) + class X: ... + __all__: tuple[str, ...] = ("Y", "Z") + class Y: ... + + class Z: ... ``` @@ -224,7 +270,10 @@ reveal_type(dunder_all_names(exporter_annotated)) ```py __all__ = ["A", "B"] + class A: ... + + class B: ... ``` @@ -237,7 +286,10 @@ __all__ = [] __all__ += ["C", "D"] __all__ += subexporter.__all__ + class C: ... + + class D: ... ``` @@ -305,7 +357,10 @@ reveal_type(dunder_all_names(module)) # revealed: tuple[Literal["bar"], Literal ```py __all__ = ["A", "B"] + class A: ... + + class B: ... ``` @@ -320,7 +375,10 @@ __all__.extend(("E", "F")) __all__.extend({"G", "H"}) __all__.extend(subexporter.__all__) + class C: ... + + class D: ... ``` @@ -342,7 +400,10 @@ reveal_type(dunder_all_names(exporter)) __all__ = ["A"] __all__.append("B") + class A: ... + + class B: ... ``` @@ -368,7 +429,10 @@ __all__.remove("A") # TODO: This raises `ValueError` at runtime, maybe we should raise a diagnostic as well? __all__.remove("C") + class A: ... + + class B: ... ``` @@ -394,8 +458,13 @@ __all__.append("B") __all__.extend(["C"]) __all__.remove("B") + class A: ... + + class B: ... + + class C: ... ``` @@ -408,6 +477,7 @@ __all__ = [] __all__ += ["D"] __all__ += subexporter.__all__ + class D: ... ``` @@ -433,7 +503,10 @@ Idioms that are not mentioned in the [specification] are not recognized by `ty` ```py __all__ = ["A", "B"] + class A: ... + + class B: ... ``` @@ -479,7 +552,10 @@ __all__.pop() # TODO: warning diagnostic __all__ = {"C", "D"} + class C: ... + + class D: ... ``` @@ -503,7 +579,10 @@ defined for that module. This is also to avoid false positives. ```py __all__ = ("A", "B") + class A: ... + + class B: ... ``` @@ -519,6 +598,7 @@ reveal_type(dunder_all_names(subexporter)) # TODO: warning diagnostic __all__ = ("C", *subexporter.__all__) + class C: ... ``` @@ -555,15 +635,20 @@ elif sys.version_info >= (3, 11): else: __all__ += ["Python310"] + class AllVersion: ... + if sys.version_info >= (3, 12): + class Python312: ... elif sys.version_info >= (3, 11): + class Python311: ... else: + class Python310: ... ``` @@ -598,15 +683,20 @@ elif sys.version_info >= (3, 11): else: __all__ += ["Python310"] + class AllVersion: ... + if sys.version_info >= (3, 12): + class Python312: ... elif sys.version_info >= (3, 11): + class Python311: ... else: + class Python310: ... ``` @@ -641,15 +731,20 @@ elif sys.version_info >= (3, 11): else: __all__ += ["Python310"] + class AllVersion: ... + if sys.version_info >= (3, 12): + class Python312: ... elif sys.version_info >= (3, 11): + class Python311: ... else: + class Python310: ... ``` @@ -686,15 +781,22 @@ if sys.version_info >= (3, 11): if sys.version_info >= (3, 10): __all__ += ["Python310"] + class AllVersion: ... + if sys.version_info >= (3, 12): + class Python312: ... + if sys.version_info >= (3, 11): + class Python311: ... + if sys.version_info >= (3, 10): + class Python310: ... ``` @@ -719,6 +821,7 @@ reveal_type(dunder_all_names(exporter)) ```py __all__ = ["A"] + class A: ... ``` @@ -739,6 +842,7 @@ reveal_type(dunder_all_names(exporter)) ```py __all__ = ["A"] + class A: ... ``` @@ -749,6 +853,7 @@ from subexporter import __all__ __all__.append("B") + class B: ... ``` @@ -775,6 +880,7 @@ module. ```py __all__ = ["A", "__all__"] + class A: ... ``` @@ -788,6 +894,7 @@ reveal_type(__all__) # revealed: list[Unknown | str] __all__.append("B") + class B: ... ``` @@ -811,6 +918,7 @@ reveal_type(dunder_all_names(exporter)) ```py __all__ = ["A"] + class A: ... ``` @@ -825,6 +933,7 @@ reveal_type(__all__) # revealed: Unknown # error: [unresolved-reference] __all__.append("B") + class B: ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/errors.md b/crates/ty_python_semantic/resources/mdtest/import/errors.md index 41b311370a..d0621aa323 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/errors.md +++ b/crates/ty_python_semantic/resources/mdtest/import/errors.md @@ -69,13 +69,17 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]" ```py from ty_extensions import reveal_mro + class A: ... + reveal_mro(A) # revealed: (, ) import b + class C(b.B): ... + reveal_mro(C) # revealed: (, , , ) ``` @@ -85,7 +89,9 @@ reveal_mro(C) # revealed: (, , , , , ) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/module_getattr.md b/crates/ty_python_semantic/resources/mdtest/import/module_getattr.md index 79cce812fe..82a6f1ce0c 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/module_getattr.md +++ b/crates/ty_python_semantic/resources/mdtest/import/module_getattr.md @@ -51,6 +51,7 @@ reveal_type(mixed_module.dynamic_attr) # revealed: str ```py explicit_attr = "explicit" + def __getattr__(name: str) -> str: return "dynamic" ``` @@ -131,6 +132,7 @@ reveal_type(unknown_attr) # revealed: Unknown ```py from typing import Literal + def __getattr__(name: Literal["known_attr"]) -> int: return 3 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md b/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md index e17a026e32..0860d0016b 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md +++ b/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md @@ -847,6 +847,7 @@ __all__ = ["funcmod"] ```py __all__ = ["funcmod"] + def funcmod(x: int) -> int: return x ``` @@ -870,6 +871,7 @@ from .funcmod import funcmod funcmod(1) + def run(): funcmod(2) ``` @@ -922,15 +924,18 @@ def run1(): funcmod(1) + def run2(): from .funcmod import funcmod funcmod(2) + def run3(): # error: [unresolved-reference] funcmod(3) + # error: [unresolved-reference] funcmod(4) ``` @@ -957,6 +962,7 @@ def run1(): # error: [unresolved-reference] funcmod.funcmod(1) + def run2(): from .funcmod import other @@ -964,10 +970,12 @@ def run2(): # error: [unresolved-reference] funcmod.funcmod(2) + def run3(): # error: [unresolved-reference] funcmod.funcmod(3) + # error: [unresolved-reference] funcmod.funcmod(4) ``` @@ -977,6 +985,7 @@ funcmod.funcmod(4) ```py other: int = 1 + def funcmod(x: int) -> int: return x ``` @@ -1039,6 +1048,7 @@ x = funcmod(1) ```py from .funcmod import other + def funcmod(x: int) -> int: return x ``` @@ -1155,6 +1165,7 @@ from .funcmod import other def other(x: int) -> int: return x + def funcmod(x: int) -> int: return x ``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/star.md b/crates/ty_python_semantic/resources/mdtest/import/star.md index 72c2a97e49..e3422d984d 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/star.md +++ b/crates/ty_python_semantic/resources/mdtest/import/star.md @@ -166,12 +166,16 @@ for G in [1]: for (H := 4).whatever in [2]: # error: [unresolved-attribute] ... + class I: ... + def J(): ... + type K = int + class ContextManagerThatMightNotRunToCompletion: def __enter__(self) -> "ContextManagerThatMightNotRunToCompletion": return self @@ -179,12 +183,15 @@ class ContextManagerThatMightNotRunToCompletion: def __exit__(self, *args) -> typing.Literal[True]: return True + with ContextManagerThatMightNotRunToCompletion() as L: U = ... + def get_object() -> object: pass + match get_object(): case {"something": M}: ... @@ -211,9 +218,11 @@ match 12345: case T: ... + def boolean_condition() -> bool: return True + if boolean_condition(): V = ... @@ -303,15 +312,18 @@ match 42: case I: ... + def boolean_condition() -> bool: return True + if boolean_condition(): J = ... while boolean_condition(): K = ... + class ContextManagerThatMightNotRunToCompletion: def __enter__(self) -> "ContextManagerThatMightNotRunToCompletion": return self @@ -319,6 +331,7 @@ class ContextManagerThatMightNotRunToCompletion: def __exit__(self, *args) -> Literal[True]: return True + with ContextManagerThatMightNotRunToCompletion(): L = ... ``` @@ -373,15 +386,18 @@ match 42: case I: ... + def boolean_condition() -> bool: return True + if boolean_condition(): J = ... while boolean_condition(): K = ... + class ContextManagerThatMightNotRunToCompletion: def __enter__(self) -> "ContextManagerThatMightNotRunToCompletion": return self @@ -389,6 +405,7 @@ class ContextManagerThatMightNotRunToCompletion: def __exit__(self, *args) -> Literal[True]: return True + with ContextManagerThatMightNotRunToCompletion(): L = ... @@ -436,10 +453,12 @@ class Iterator: def __next__(self) -> int: return 42 + class Iterable: def __iter__(self) -> Iterator: return Iterator() + [a for a in Iterable()] {b for b in Iterable()} {c: c for c in Iterable()} @@ -720,6 +739,7 @@ reveal_type(Y) # revealed: Unknown # Thus this still reveals `Literal[True]`. reveal_type(Z) # revealed: Literal[True] + # Make sure that reachability constraints are also correctly applied # for nonlocal lookups: def _(): @@ -772,6 +792,7 @@ the `*` import occurs. def coinflip() -> bool: return True + if coinflip(): A = 1 B = 2 @@ -804,6 +825,7 @@ A = 1 def coinflip() -> bool: return True + if coinflip(): from exporter import * @@ -826,9 +848,11 @@ import sys if sys.version_info >= (3, 12): A: bool = True + def coinflip() -> bool: return True + if coinflip(): B: bool = True ``` @@ -1055,9 +1079,11 @@ user that we cannot statically determine the elements of `__all__`. def f() -> str: return "f" + def g() -> int: return 42 + # TODO we should emit a warning here for the dynamically constructed `__all__` member. __all__ = [f()] ``` @@ -1316,6 +1342,7 @@ def f(): g = True + f() ``` @@ -1355,9 +1382,11 @@ class C: ```py from common import C + def flag() -> bool: return True + should_be_imported: C = C() if flag(): diff --git a/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md b/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md index 3f99e5d92b..9eb78fff62 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md +++ b/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md @@ -264,6 +264,7 @@ class Pentagon: sides: int area: float + class Hexagon: sides: int area: float @@ -273,6 +274,8 @@ class Hexagon: ```py class Pentagon: ... + + class Hexagon: ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md index bb8e013083..3332150486 100644 --- a/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md +++ b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md @@ -5,15 +5,24 @@ ```py class A: ... + class B: __slots__ = () + class C: __slots__ = ("lorem", "ipsum") + class AB(A, B): ... # fine + + class AC(A, C): ... # fine + + class BC(B, C): ... # fine + + class ABC(A, B, C): ... # fine ``` @@ -25,9 +34,11 @@ class ABC(A, B, C): ... # fine class A: __slots__ = ("a", "b") + class B: __slots__ = ("c", "d") + class C( # error: [instance-layout-conflict] A, B, @@ -40,9 +51,11 @@ class C( # error: [instance-layout-conflict] class A: __slots__ = ("a", "b") + class B: __slots__ = ("a", "b") + class C( # error: [instance-layout-conflict] A, B, @@ -55,9 +68,11 @@ class C( # error: [instance-layout-conflict] class A: __slots__ = "abc" + class B: __slots__ = ("abc",) + class AB( # error: [instance-layout-conflict] A, B, @@ -69,22 +84,28 @@ class AB( # error: [instance-layout-conflict] ```py from dataclasses import dataclass + @dataclass(slots=True) class F: ... + @dataclass(slots=True) class G: ... + class H(F, G): ... # fine because both classes have empty `__slots__` + @dataclass(slots=True) class I: x: int + @dataclass(slots=True) class J: y: int + class K(I, J): ... # error: [instance-layout-conflict] ``` @@ -96,15 +117,19 @@ TODO: Emit diagnostics class NonString1: __slots__ = 42 + class NonString2: __slots__ = b"ar" + class NonIdentifier1: __slots__ = "42" + class NonIdentifier2: __slots__ = ("lorem", "42") + class NonIdentifier3: __slots__ = (e for e in ("lorem", "42")) ``` @@ -115,12 +140,17 @@ class NonIdentifier3: class A: __slots__ = ("a", "b") + class B(A): ... + class C: __slots__ = ("c", "d") + class D(C): ... + + class E( # error: [instance-layout-conflict] B, D, @@ -133,9 +163,16 @@ class E( # error: [instance-layout-conflict] class A: __slots__ = ("a", "b") + class B(A): ... + + class C(A): ... + + class D(B, A): ... # fine + + class E(B, C, A): ... # fine ``` @@ -146,11 +183,14 @@ class A: __slots__ = () __slots__ += ("a", "b") + reveal_type(A.__slots__) # revealed: tuple[Literal["a", "b"], ...] + class B: __slots__ = ("c", "d") + # TODO: ideally this would trigger `[instance-layout-conflict]` # (but it's also not high-priority) class C(A, B): ... @@ -165,9 +205,11 @@ We do not emit false positives on classes with empty `__slots__` definitions, ev class Foo: __slots__: tuple[str, ...] = () + class Bar: __slots__: tuple[str, ...] = () + class Baz(Foo, Bar): ... # fine ``` @@ -238,9 +280,11 @@ other: class A: __slots__ = ("a",) + class B(A): __slots__ = ("b",) + class C(B, A): ... # fine ``` @@ -250,11 +294,17 @@ The same principle, but a more complex example: class AA: __slots__ = ("a",) + class BB(AA): __slots__ = ("b",) + class CC(BB): ... + + class DD(AA): ... + + class FF(CC, DD): ... # fine ``` @@ -298,9 +348,11 @@ def _(flag: bool): class A: __slots__ = ["a", "b"] # This is treated as "dynamic" + class B: __slots__ = ("c", "d") + # False negative: [incompatible-slots] class C(A, B): ... ``` @@ -321,6 +373,7 @@ class A: # Modifying `__slots__` from within the class body is fine: __slots__ = ("a", "b") + # No `Unknown` here: reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]] diff --git a/crates/ty_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md index 022e09c43b..f51b5e2c00 100644 --- a/crates/ty_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md @@ -10,9 +10,13 @@ matter): ```py from ty_extensions import Intersection, Not + class P: ... + + class Q: ... + def _( i1: Intersection[P, Q], i2: Intersection[P, Not[Q]], @@ -37,10 +41,16 @@ We use `P`, `Q`, `R`, … to denote types that are non-disjoint: ```py from ty_extensions import static_assert, is_disjoint_from + class P: ... + + class Q: ... + + class R: ... + static_assert(not is_disjoint_from(P, Q)) static_assert(not is_disjoint_from(P, R)) static_assert(not is_disjoint_from(Q, R)) @@ -75,10 +85,16 @@ Finally, we use `A <: B <: C` and `A <: B1`, `A <: B2` to denote hierarchies of ```py from ty_extensions import static_assert, is_subtype_of, is_disjoint_from + class A: ... + + class B(A): ... + + class C(B): ... + static_assert(is_subtype_of(B, A)) static_assert(is_subtype_of(C, B)) static_assert(is_subtype_of(C, A)) @@ -87,9 +103,13 @@ static_assert(not is_subtype_of(A, B)) static_assert(not is_subtype_of(B, C)) static_assert(not is_subtype_of(A, C)) + class B1(A): ... + + class B2(A): ... + static_assert(is_subtype_of(B1, A)) static_assert(is_subtype_of(B2, A)) @@ -113,8 +133,10 @@ show an intersection with a single negative contribution as just the negation of ```py from ty_extensions import Intersection, Not + class P: ... + def _( i1: Intersection[P], i2: Intersection[Not[P]], @@ -130,11 +152,19 @@ We eagerly flatten nested intersections types. ```py from ty_extensions import Intersection, Not + class P: ... + + class Q: ... + + class R: ... + + class S: ... + def positive_contributions( i1: Intersection[P, Intersection[Q, R]], i2: Intersection[Intersection[P, Q], R], @@ -142,6 +172,7 @@ def positive_contributions( reveal_type(i1) # revealed: P & Q & R reveal_type(i2) # revealed: P & Q & R + def negative_contributions( i1: Intersection[Not[P], Intersection[Not[Q], Not[R]]], i2: Intersection[Intersection[Not[P], Not[Q]], Not[R]], @@ -149,6 +180,7 @@ def negative_contributions( reveal_type(i1) # revealed: ~P & ~Q & ~R reveal_type(i2) # revealed: ~P & ~Q & ~R + def mixed( i1: Intersection[P, Intersection[Not[Q], R]], i2: Intersection[Intersection[P, Not[Q]], R], @@ -160,11 +192,13 @@ def mixed( reveal_type(i3) # revealed: Q & ~P & ~R reveal_type(i4) # revealed: Q & ~R & ~P + def multiple( i1: Intersection[Intersection[P, Q], Intersection[R, S]], ): reveal_type(i1) # revealed: P & Q & R & S + def nested( i1: Intersection[Intersection[Intersection[P, Q], R], S], i2: Intersection[P, Intersection[Q, Intersection[R, S]]], @@ -181,11 +215,19 @@ intersection_, we distribute the union over the respective elements: ```py from ty_extensions import Intersection, Not + class P: ... + + class Q: ... + + class R: ... + + class S: ... + def _( i1: Intersection[P, Q | R | S], i2: Intersection[P | Q | R, S], @@ -195,6 +237,7 @@ def _( reveal_type(i2) # revealed: (P & S) | (Q & S) | (R & S) reveal_type(i3) # revealed: (P & R) | (Q & R) | (P & S) | (Q & S) + def simplifications_for_same_elements( i1: Intersection[P, Q | P], i2: Intersection[Q, P | Q], @@ -234,14 +277,21 @@ Distribution also applies to a negation operation. This is a manifestation of on from ty_extensions import Not from typing import Literal + class P: ... + + class Q: ... + + class R: ... + def _(i1: Not[P | Q], i2: Not[P | Q | R]) -> None: reveal_type(i1) # revealed: ~P & ~Q reveal_type(i2) # revealed: ~P & ~Q & ~R + def example_literals(i: Not[Literal[1, 2]]) -> None: reveal_type(i) # revealed: ~Literal[1] & ~Literal[2] ``` @@ -253,10 +303,16 @@ The other of [De Morgan's laws], `~(P & Q) = ~P | ~Q`, also holds: ```py from ty_extensions import Intersection, Not + class P: ... + + class Q: ... + + class R: ... + def _( i1: Not[Intersection[P, Q]], i2: Not[Intersection[P, Q, R]], @@ -275,6 +331,7 @@ of the [complement laws] of set theory. from ty_extensions import Intersection, Not from typing_extensions import Never + def _( not_never: Not[Never], not_object: Not[object], @@ -292,8 +349,10 @@ in intersections, and can be eagerly simplified out. `object & P` is equivalent ```py from ty_extensions import Intersection, Not, is_equivalent_to, static_assert + class P: ... + static_assert(is_equivalent_to(Intersection[object, P], P)) static_assert(is_equivalent_to(Intersection[object, Not[P]], Not[P])) ``` @@ -309,10 +368,16 @@ from typing import Any, Generic, TypeVar T_co = TypeVar("T_co", covariant=True) + class P: ... + + class Q: ... + + class R(Generic[T_co]): ... + def _( i1: Intersection[P, Not[P]], i2: Intersection[Not[P], P], @@ -343,10 +408,16 @@ from typing import Generic, TypeVar T_co = TypeVar("T_co", covariant=True) + class P: ... + + class Q: ... + + class R(Generic[T_co]): ... + def _( i1: P | Not[P], i2: Not[P] | P, @@ -370,8 +441,10 @@ The final of the [complement laws] states that negating twice is equivalent to n ```py from ty_extensions import Not + class P: ... + def _( i1: Not[P], i2: Not[Not[P]], @@ -398,9 +471,13 @@ dynamic types involved: from ty_extensions import Intersection, Not from typing_extensions import Never, Any + class P: ... + + class Q: ... + def _( i1: Intersection[P, Never], i2: Intersection[Never, P], @@ -423,8 +500,10 @@ If we intersect disjoint types, we can simplify to `Never`, even in the presence from ty_extensions import Intersection, Not from typing import Literal, Any + class P: ... + def _( i01: Intersection[Literal[1], Literal[2]], i02: Intersection[Literal[2], Literal[1]], @@ -444,6 +523,7 @@ def _( reveal_type(i07) # revealed: Never reveal_type(i08) # revealed: Never + # `bool` is final and cannot be subclassed, so `type[bool]` is equivalent to `Literal[bool]`, which # is disjoint from `type[str]`: def example_type_bool_type_str( @@ -461,6 +541,7 @@ contribution `~Y`, as `~Y` must fully contain the positive contribution `X` as a from ty_extensions import Intersection, Not from typing import Literal + def _( i1: Intersection[Literal[1], Not[Literal[2]]], i2: Intersection[Not[Literal[2]], Literal[1]], @@ -474,6 +555,7 @@ def _( reveal_type(i4) # revealed: Literal[1] reveal_type(i5) # revealed: Literal[1] + # None is disjoint from int, so this simplification applies here def example_none( i1: Intersection[int, Not[None]], @@ -494,11 +576,19 @@ superfluous supertypes: from ty_extensions import Intersection, Not from typing import Any + class A: ... + + class B(A): ... + + class C(B): ... + + class Unrelated: ... + def _( i01: Intersection[A, B], i02: Intersection[B, A], @@ -553,11 +643,19 @@ For negative contributions, this property is reversed. Here we can remove superf from ty_extensions import Intersection, Not from typing import Any + class A: ... + + class B(A): ... + + class C(B): ... + + class Unrelated: ... + def _( i01: Intersection[Not[B], Not[A]], i02: Intersection[Not[A], Not[B]], @@ -611,10 +709,16 @@ If there are multiple negative subtypes, all of them can be removed: ```py from ty_extensions import Intersection, Not + class A: ... + + class B1(A): ... + + class B2(A): ... + def _( i1: Intersection[Not[A], Not[B1], Not[B2]], i2: Intersection[Not[A], Not[B2], Not[B1]], @@ -640,11 +744,19 @@ intersection to `Never`: from ty_extensions import Intersection, Not from typing import Any + class A: ... + + class B(A): ... + + class C(B): ... + + class Unrelated: ... + def _( i1: Intersection[Not[A], B], i2: Intersection[B, Not[A]], @@ -680,8 +792,10 @@ to the fact that `bool` is a `@final` class at runtime that cannot be subclassed from ty_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy from typing_extensions import Literal + class P: ... + def f( a: Intersection[bool, AlwaysTruthy], b: Intersection[bool, AlwaysFalsy], @@ -703,6 +817,7 @@ def f( reveal_type(g) # revealed: Never reveal_type(h) # revealed: Never + def never( a: Intersection[Intersection[AlwaysFalsy, Not[Literal[False]]], bool], b: Intersection[Intersection[AlwaysTruthy, Not[Literal[True]]], bool], @@ -722,9 +837,11 @@ Regression tests for complex nested simplifications: ```py from typing_extensions import Any, assert_type + def _(x: Intersection[bool, Not[Intersection[Any, Not[AlwaysTruthy], Not[AlwaysFalsy]]]]): assert_type(x, bool) + def _(x: Intersection[bool, Any] | Literal[True] | Literal[False]): assert_type(x, bool) ``` @@ -739,6 +856,7 @@ exactly `str` (and not a subclass of `str`): from ty_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy, Unknown from typing_extensions import LiteralString + def f( a: Intersection[LiteralString, AlwaysTruthy], b: Intersection[LiteralString, AlwaysFalsy], @@ -775,15 +893,18 @@ from ty_extensions import Intersection, Not from typing import Literal from enum import Enum + class Color(Enum): RED = "red" GREEN = "green" BLUE = "blue" + type Red = Literal[Color.RED] type Green = Literal[Color.GREEN] type Blue = Literal[Color.BLUE] + def f( a: Intersection[Color, Red], b: Intersection[Color, Not[Red]], @@ -805,9 +926,11 @@ def f( reveal_type(h) # revealed: Never reveal_type(i) # revealed: Literal[Color.GREEN] + class Single(Enum): VALUE = 0 + def g( a: Intersection[Single, Literal[Single.VALUE]], b: Intersection[Single, Not[Literal[Single.VALUE]]], @@ -831,6 +954,7 @@ This slightly strange-looking test is a regression test for a mistake that was n from ty_extensions import AlwaysFalsy, Intersection, Unknown from typing_extensions import Literal + def _(x: Intersection[str, Unknown, AlwaysFalsy, Literal[""]]): reveal_type(x) # revealed: Unknown & Literal[""] ``` @@ -847,8 +971,10 @@ simplify `~Any` to `Any` in intersections. The same applies to `Unknown`. from ty_extensions import Intersection, Not, Unknown from typing_extensions import Any, Never + class P: ... + def any( i1: Not[Any], i2: Intersection[P, Not[Any]], @@ -858,6 +984,7 @@ def any( reveal_type(i2) # revealed: P & Any reveal_type(i3) # revealed: Never + def unknown( i1: Not[Unknown], i2: Intersection[P, Not[Unknown]], @@ -877,8 +1004,10 @@ still an unknown set of runtime values: from ty_extensions import Intersection, Not, Unknown from typing_extensions import Any + class P: ... + def any( i1: Intersection[Any, Any], i2: Intersection[P, Any, Any], @@ -890,6 +1019,7 @@ def any( reveal_type(i3) # revealed: Any & P reveal_type(i4) # revealed: Any & P + def unknown( i1: Intersection[Unknown, Unknown], i2: Intersection[P, Unknown, Unknown], @@ -911,6 +1041,7 @@ of another unknown set of values is not necessarily empty, so we keep the positi from typing import Any from ty_extensions import Intersection, Not, Unknown + def any( i1: Intersection[Any, Not[Any]], i2: Intersection[Not[Any], Any], @@ -918,6 +1049,7 @@ def any( reveal_type(i1) # revealed: Any reveal_type(i2) # revealed: Any + def unknown( i1: Intersection[Unknown, Not[Unknown]], i2: Intersection[Not[Unknown], Unknown], @@ -934,6 +1066,7 @@ Gradually-equivalent types can be simplified out of intersections: from typing import Any from ty_extensions import Intersection, Not, Unknown + def mixed( i1: Intersection[Any, Unknown], i2: Intersection[Any, Not[Unknown]], @@ -951,10 +1084,12 @@ def mixed( ```py from ty_extensions import Intersection, Not + # error: [invalid-type-form] "`ty_extensions.Intersection` requires at least one argument when used in a type expression" def f(x: Intersection) -> None: reveal_type(x) # revealed: Unknown + # error: [invalid-type-form] "`ty_extensions.Not` requires exactly one argument when used in a type expression" def f(x: Not) -> None: reveal_type(x) # revealed: Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md index 2b657a34f7..ff21db24eb 100644 --- a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md +++ b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md @@ -19,31 +19,40 @@ import builtins _ItemT_co = TypeVar("_ItemT_co", default=Any, covariant=True) + class generic(Generic[_ItemT_co]): @property def dtype(self) -> _DTypeT_co: raise NotImplementedError + _BoolItemT_co = TypeVar("_BoolItemT_co", bound=builtins.bool, default=builtins.bool, covariant=True) + class bool(generic[_BoolItemT_co], Generic[_BoolItemT_co]): ... + @final class object_(generic): ... + _ScalarT = TypeVar("_ScalarT", bound=generic) _ScalarT_co = TypeVar("_ScalarT_co", bound=generic, default=Any, covariant=True) + @final class dtype(Generic[_ScalarT_co]): ... + _DTypeT_co = TypeVar("_DTypeT_co", bound=dtype, default=dtype, covariant=True) + @runtime_checkable class _SupportsDType(Protocol[_DTypeT_co]): @property def dtype(self) -> _DTypeT_co: ... + _DTypeLike: TypeAlias = type[_ScalarT] | dtype[_ScalarT] | _SupportsDType[dtype[_ScalarT]] DTypeLike: TypeAlias = _DTypeLike[Any] | str | None @@ -54,8 +63,10 @@ Now we can make sure that a function which accepts `DTypeLike | None` works as e ```py import mini_numpy as np + def accepts_dtype(dtype: np.DTypeLike | None) -> None: ... + accepts_dtype(dtype=np.bool) accepts_dtype(dtype=np.dtype[np.bool]) accepts_dtype(dtype=object) diff --git a/crates/ty_python_semantic/resources/mdtest/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index dc0e09989a..0c1be342f1 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -474,6 +474,7 @@ class D(C): ```py class Bad: x: int + def __eq__(self, other: "Bad") -> bool: # error: [invalid-method-override] return self.x == other.x ``` @@ -615,8 +616,10 @@ have bigger problems: ```py from __future__ import annotations + class MaybeEqWhile: while ...: + def __eq__(self, other: MaybeEqWhile) -> bool: return True ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md index ad5829da1f..8df48202c9 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md @@ -34,9 +34,11 @@ reveal_type(d) # revealed: dict[Unknown | str, Unknown | int] def a(_: int) -> int: return 0 + def b(_: int) -> int: return 1 + x = {1: a, 2: b} reveal_type(x) # revealed: dict[Unknown | int, Unknown | ((_: int) -> int)] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/generator_expressions.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/generator_expressions.md index bd2ccf14b0..15fd4590e7 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/generator_expressions.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/generator_expressions.md @@ -27,8 +27,10 @@ expected: ```py from typing import Iterator + def process_numbers(x: Iterator[float]): ... + numbers = (x for x in range(10)) reveal_type(numbers) # revealed: GeneratorType[int, None, None] process_numbers(numbers) @@ -42,6 +44,7 @@ For async generator expressions, we infer specialized `AsyncGeneratorType` insta import asyncio from typing import AsyncGenerator + async def slow_numbers() -> AsyncGenerator[int, None]: current = 0 while True: @@ -49,6 +52,7 @@ async def slow_numbers() -> AsyncGenerator[int, None]: yield current current += 1 + async def main() -> None: slow_squares = (x**2 async for x in slow_numbers()) @@ -58,5 +62,6 @@ async def main() -> None: reveal_type(s) # revealed: int print(s) + asyncio.run(main()) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md index 325caba10d..103cf8d33c 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md @@ -18,9 +18,11 @@ reveal_type([(1, 2), (3, 4)]) # revealed: list[Unknown | tuple[int, int]] def a(_: int) -> int: return 0 + def b(_: int) -> int: return 1 + x = [a, b] reveal_type(x) # revealed: list[Unknown | ((_: int) -> int)] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md index d80112ee84..48a9da7e44 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md @@ -18,9 +18,11 @@ reveal_type({(1, 2), (3, 4)}) # revealed: set[Unknown | tuple[int, int]] def a(_: int) -> int: return 0 + def b(_: int) -> int: return 1 + x = {a, b} reveal_type(x) # revealed: set[Unknown | ((_: int) -> int)] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/f_string.md b/crates/ty_python_semantic/resources/mdtest/literal/f_string.md index ac51ba56d7..954f316fa9 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/f_string.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/f_string.md @@ -5,6 +5,7 @@ ```py from typing_extensions import Literal + def _(x: Literal[0], y: str, z: Literal[False]): reveal_type(f"hello") # revealed: Literal["hello"] reveal_type(f"h {x}") # revealed: Literal["h 0"] diff --git a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md index 250b5e8770..d7db2f4b09 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md @@ -16,12 +16,15 @@ a type annotation. from enum import Enum from typing import Literal, LiteralString + class MyEnum(Enum): A = 1 + def promote[T](x: T) -> list[T]: return [x] + def _( lit1: Literal["x"], lit2: LiteralString, @@ -35,6 +38,7 @@ def _( reveal_type(promote(lit4)) # revealed: list[bytes] reveal_type(promote(lit5)) # revealed: list[MyEnum] + reveal_type(promote(3.14)) # revealed: list[int | float] reveal_type(promote(3.14j)) # revealed: list[int | float | complex] ``` @@ -45,6 +49,7 @@ Function types are also promoted to their `Callable` form: def lit6(_: int) -> int: return 0 + reveal_type(promote(lit6)) # revealed: list[(_: int) -> int] ``` @@ -74,21 +79,25 @@ function, or constructor of a generic class: class Bivariant[T]: def __init__(self, value: T): ... + class Covariant[T]: def __init__(self, value: T): ... def pop(self) -> T: raise NotImplementedError + class Contravariant[T]: def __init__(self, value: T): ... def push(self, value: T) -> None: pass + class Invariant[T]: x: T def __init__(self, value: T): ... + def f1[T](x: T) -> Bivariant[T] | None: ... def f2[T](x: T) -> Covariant[T] | None: ... def f3[T](x: T) -> Covariant[T] | Bivariant[T] | None: ... @@ -101,6 +110,7 @@ def f9[T](x: T) -> tuple[Invariant[T], Invariant[T]] | None: ... def f10[T, U](x: T, y: U) -> tuple[Invariant[T], Covariant[U]] | None: ... def f11[T, U](x: T, y: U) -> tuple[Invariant[Covariant[T] | None], Covariant[U]] | None: ... + reveal_type(Bivariant(1)) # revealed: Bivariant[Literal[1]] reveal_type(Covariant(1)) # revealed: Covariant[Literal[1]] @@ -130,17 +140,21 @@ position in an argument type, we respect the explicitly annotated argument, and ```py from typing import Literal + class Covariant[T]: def pop(self) -> T: raise NotImplementedError + class Contravariant[T]: def push(self, value: T) -> None: pass + class Invariant[T]: x: T + def f1[T](x: T) -> Invariant[T] | None: ... def f2[T](x: Covariant[T]) -> Invariant[T] | None: ... def f3[T](x: Invariant[T]) -> Invariant[T] | None: ... @@ -178,9 +192,11 @@ promotion: ```py from typing import Iterable + class X[T]: def __init__(self, x: Iterable[T]): ... + def _(x: list[Literal[1]]): reveal_type(X(x)) # revealed: X[Literal[1]] ``` @@ -190,12 +206,15 @@ def _(x: list[Literal[1]]): ```py from typing import Literal + def promote[T](x: T) -> list[T]: return [x] + def _(x: tuple[tuple[tuple[Literal[1]]]]): reveal_type(promote(x)) # revealed: list[tuple[tuple[tuple[int]]]] + x1 = ([1, 2], [(3,), (4,)], ["5", "6"]) reveal_type(x1) # revealed: tuple[list[Unknown | int], list[Unknown | tuple[int]], list[Unknown | str]] ``` @@ -205,6 +224,7 @@ However, this promotion should not take place if the literal type appears in con ```py from typing import Callable, Literal + def in_negated_position(non_zero_number: int): if non_zero_number == 0: raise ValueError() @@ -213,11 +233,13 @@ def in_negated_position(non_zero_number: int): reveal_type([non_zero_number]) # revealed: list[Unknown | (int & ~Literal[0])] + def in_parameter_position(callback: Callable[[Literal[1]], None]): reveal_type(callback) # revealed: (Literal[1], /) -> None reveal_type([callback]) # revealed: list[Unknown | ((Literal[1], /) -> None)] + def double_negation(callback: Callable[[Callable[[Literal[1]], None]], None]): reveal_type(callback) # revealed: ((Literal[1], /) -> None, /) -> None @@ -231,17 +253,21 @@ position: class Bivariant[T]: pass + class Covariant[T]: def pop(self) -> T: raise NotImplementedError + class Contravariant[T]: def push(self, value: T) -> None: pass + class Invariant[T]: x: T + def _( bivariant: Bivariant[Literal[1]], covariant: Covariant[Literal[1]], @@ -263,19 +289,24 @@ Explicitly annotated `Literal` types will prevent literal promotion: from enum import Enum from typing_extensions import Literal, LiteralString + class Color(Enum): RED = "red" + type Y[T] = list[T] + class X[T]: value: T def __init__(self, value: T): ... + def x[T](x: T) -> X[T]: return X(x) + x1: list[Literal[1]] = [1] reveal_type(x1) # revealed: list[Literal[1]] @@ -362,14 +393,18 @@ reveal_type(x2) # revealed: list[Literal[1, 2, 3]] x3: Iterable[Literal[1, 2, 3]] = [1, 2, 3] reveal_type(x3) # revealed: list[Literal[1, 2, 3]] + class Sup1[T]: value: T + class Sub1[T](Sup1[T]): ... + def sub1[T](value: T) -> Sub1[T]: return Sub1() + x4: Sub1[Literal[1]] = sub1(1) reveal_type(x4) # revealed: Sub1[Literal[1]] @@ -382,17 +417,22 @@ reveal_type(x6) # revealed: Sub1[Literal[1]] x7: Sup1[Literal[1]] | None = sub1(1) reveal_type(x7) # revealed: Sub1[Literal[1]] + class Sup2A[T, U]: value: tuple[T, U] + class Sup2B[T, U]: value: tuple[T, U] + class Sub2[T, U](Sup2A[T, Any], Sup2B[Any, U]): ... + def sub2[T, U](x: T, y: U) -> Sub2[T, U]: return Sub2() + x8 = sub2(1, 2) reveal_type(x8) # revealed: Sub2[int, int] diff --git a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md index 29fba1ce77..577efacaff 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md @@ -44,6 +44,7 @@ async def foo(): ```py class NotAsyncIterable: ... + async def foo(): # error: [not-iterable] "Object of type `NotAsyncIterable` is not async-iterable" async for x in NotAsyncIterable(): @@ -72,10 +73,12 @@ async def foo(): ```py class NoAnext: ... + class AsyncIterable: def __aiter__(self) -> NoAnext: return NoAnext() + async def foo(): # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable" async for x in AsyncIterable(): @@ -88,6 +91,7 @@ async def foo(): async def foo(flag: bool): class PossiblyUnboundAnext: if flag: + async def __anext__(self) -> int: return 42 @@ -110,6 +114,7 @@ async def foo(flag: bool): class PossiblyUnboundAiter: if flag: + def __aiter__(self) -> AsyncIterable: return AsyncIterable() @@ -125,10 +130,12 @@ class AsyncIterator: async def __anext__(self) -> int: return 42 + class AsyncIterable: def __aiter__(self, arg: int) -> AsyncIterator: # wrong return AsyncIterator() + async def foo(): # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable" async for x in AsyncIterable(): @@ -142,10 +149,12 @@ class AsyncIterator: async def __anext__(self, arg: int) -> int: # wrong return 42 + class AsyncIterable: def __aiter__(self) -> AsyncIterator: return AsyncIterator() + async def foo(): # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable" async for x in AsyncIterable(): diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index 3916fa884a..7ff25de3c7 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -7,10 +7,12 @@ class IntIterator: def __next__(self) -> int: return 42 + class IntIterable: def __iter__(self) -> IntIterator: return IntIterator() + for x in IntIterable(): pass @@ -26,10 +28,12 @@ class IntIterator: def __next__(self) -> int: return 42 + class IntIterable: def __iter__(self) -> IntIterator: return IntIterator() + x = "foo" for x in IntIterable(): @@ -45,10 +49,12 @@ class IntIterator: def __next__(self) -> int: return 42 + class IntIterable: def __iter__(self) -> IntIterator: return IntIterator() + for x in IntIterable(): pass else: @@ -64,10 +70,12 @@ class IntIterator: def __next__(self) -> int: return 42 + class IntIterable: def __iter__(self) -> IntIterator: return IntIterator() + for x in IntIterable(): if x > 5: break @@ -84,6 +92,7 @@ class OldStyleIterable: def __getitem__(self, key: int) -> int: return 42 + for x in OldStyleIterable(): pass @@ -142,8 +151,10 @@ for x in nonsense: # error: [not-iterable] class NotIterable: def __getitem__(self, key: int) -> int: return 42 + __iter__: None = None + for x in NotIterable(): # error: [not-iterable] pass ``` @@ -155,14 +166,17 @@ class TestIter: def __next__(self) -> int: return 42 + class Test: def __iter__(self) -> TestIter: return TestIter() + class Test2: def __iter__(self) -> TestIter: return TestIter() + def _(flag: bool): for x in Test() if flag else Test2(): reveal_type(x) # revealed: int @@ -175,14 +189,17 @@ class TestIter: def __next__(self) -> int: return 42 + class TestIter2: def __next__(self) -> int: return 42 + class Test: def __iter__(self) -> TestIter | TestIter2: return TestIter() + for x in Test(): reveal_type(x) # revealed: int ``` @@ -191,36 +208,53 @@ for x in Test(): ```py class Result1A: ... + + class Result1B: ... + + class Result2A: ... + + class Result2B: ... + + class Result3: ... + + class Result4: ... + class TestIter1: def __next__(self) -> Result1A | Result1B: return Result1B() + class TestIter2: def __next__(self) -> Result2A | Result2B: return Result2B() + class TestIter3: def __next__(self) -> Result3: return Result3() + class TestIter4: def __next__(self) -> Result4: return Result4() + class Test: def __iter__(self) -> TestIter1 | TestIter2: return TestIter1() + class Test2: def __iter__(self) -> TestIter3 | TestIter4: return TestIter3() + def _(flag: bool): for x in Test() if flag else Test2(): reveal_type(x) # revealed: Result1A | Result1B | Result2A | Result2B | Result3 | Result4 @@ -235,14 +269,17 @@ annotation. ```py from typing import Iterator, Literal + class IntIterator: def __iter__(self) -> Iterator[int]: return iter(range(42)) + class StrIterator: def __iter__(self) -> Iterator[str]: return iter("foo") + def f(x: IntIterator | StrIterator): for a in x: reveal_type(a) # revealed: int | str @@ -271,6 +308,7 @@ infer a precise type for the iterable element when iterating over a `Literal` st ```py from typing import Literal + def f(x: Literal["foo", b"bar"], y: Literal["foo"] | range): for item in x: reveal_type(item) # revealed: Literal["f", "o", 98, 97, 114] @@ -287,10 +325,12 @@ class TestIter: def __next__(self) -> int: return 42 + class Test: def __iter__(self) -> TestIter: return TestIter() + def _(flag: bool): # error: [not-iterable] for x in Test() if flag else 42: @@ -306,14 +346,17 @@ class TestIter: def __next__(self) -> int: return 42 + class Test: def __iter__(self) -> TestIter: return TestIter() + class Test2: def __iter__(self) -> int: return 42 + def _(flag: bool): # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989) # error: [not-iterable] @@ -328,10 +371,12 @@ class TestIter: def __next__(self) -> int: return 42 + class Test: def __iter__(self) -> TestIter | int: return TestIter() + # error: [not-iterable] "Object of type `Test` may not be iterable" for x in Test(): reveal_type(x) # revealed: int @@ -345,6 +390,7 @@ iterable element type precisely: ```py from typing import Sequence + def _(x: Sequence[int], y: object): reveal_type(x) # revealed: Sequence[int] for item in x: @@ -371,9 +417,11 @@ and intersect their element types. ```py from ty_extensions import Intersection + class NotIterable: pass + def _(x: Intersection[list[int], NotIterable]): # `list[int]` is iterable (yielding `int`), but `NotIterable` is not. # We should still be able to iterate over the intersection. @@ -389,12 +437,15 @@ fail to iterate. ```py from ty_extensions import Intersection + class NotIterable1: pass + class NotIterable2: pass + def _(x: Intersection[NotIterable1, NotIterable2]): # error: [not-iterable] for item in x: @@ -409,6 +460,7 @@ intersect the element types position-by-position. ```py from ty_extensions import Intersection + def _(x: Intersection[tuple[int, str], tuple[object, object]]): # `tuple[int, str]` yields `int | str` when iterated. # `tuple[object, object]` yields `object` when iterated. @@ -426,10 +478,12 @@ element type with the iterator's element type. ```py from collections.abc import Iterator + class Foo: def __iter__(self) -> Iterator[object]: raise NotImplementedError + def _(x: tuple[int, str, bytes]): if isinstance(x, Foo): # The intersection `tuple[int, str, bytes] & Foo` should iterate as @@ -449,10 +503,12 @@ specs, we should intersect their element types. ```py from collections.abc import Iterator + class Foo: def __iter__(self) -> Iterator[object]: raise NotImplementedError + def _(x: list[int]): if isinstance(x, Foo): # `list[int]` yields `int`, `Foo` yields `object`. @@ -471,8 +527,10 @@ def _(flag: bool): class CustomCallable: if flag: + def __call__(self, *args, **kwargs) -> Iterator: return Iterator() + else: __call__: None = None @@ -481,8 +539,10 @@ def _(flag: bool): class Iterable2: if flag: + def __iter__(self) -> Iterator: return Iterator() + else: __iter__: None = None @@ -506,10 +566,12 @@ class Iterator: def __next__(self) -> int: return 42 + class Iterable: def __iter__(self, extra_arg) -> Iterator: return Iterator() + # error: [not-iterable] for x in Iterable(): reveal_type(x) # revealed: int @@ -524,6 +586,7 @@ class Bad: def __iter__(self) -> int: return 42 + # error: [not-iterable] for x in Bad(): reveal_type(x) # revealed: Unknown @@ -535,6 +598,7 @@ for x in Bad(): def _(flag: bool): class Iterator: if flag: + def __next__(self) -> int: return 42 @@ -556,17 +620,21 @@ class Iterator1: def __next__(self, extra_arg) -> int: return 42 + class Iterator2: __next__: None = None + class Iterable1: def __iter__(self) -> Iterator1: return Iterator1() + class Iterable2: def __iter__(self) -> Iterator2: return Iterator2() + # error: [not-iterable] for x in Iterable1(): reveal_type(x) # revealed: int @@ -588,8 +656,10 @@ def _(flag: bool): class Iterable: if flag: + def __iter__(self) -> Iterator: return Iterator() + # invalid signature because it only accepts a `str`, # but the old-style iteration protocol will pass it an `int` def __getitem__(self, key: str) -> bytes: @@ -619,8 +689,10 @@ def _(flag: bool): class Iterable: if flag: + def __iter__(self) -> Iterator: return Iterator() + __getitem__: None = None # error: [not-iterable] "Object of type `Iterable` may not be iterable" @@ -637,12 +709,16 @@ class Iterator: def __next__(self) -> int: return 42 + def _(flag1: bool, flag2: bool): class Iterable: if flag1: + def __iter__(self) -> Iterator: return Iterator() + if flag2: + def __getitem__(self, key: int) -> bytes: return bytes() @@ -659,6 +735,7 @@ def _(flag1: bool, flag2: bool): class Bad: __getitem__: None = None + # error: [not-iterable] for x in Bad(): reveal_type(x) # revealed: Unknown @@ -672,8 +749,10 @@ for x in Bad(): def _(flag: bool): class CustomCallable: if flag: + def __call__(self, *args, **kwargs) -> int: return 42 + else: __call__: None = None @@ -682,8 +761,10 @@ def _(flag: bool): class Iterable2: if flag: + def __getitem__(self, key: int) -> int: return 42 + else: __getitem__: None = None @@ -709,6 +790,7 @@ class Iterable: def __getitem__(self, key: str) -> int: return 42 + # error: [not-iterable] for x in Iterable(): reveal_type(x) # revealed: int @@ -724,9 +806,11 @@ class Iterator: def __next__(self) -> str: return "foo" + def _(flag: bool): class Iterable: if flag: + def __iter__(self) -> Iterator: return Iterator() @@ -746,12 +830,16 @@ class Iterator: def __next__(self) -> int: return 42 + def _(flag: bool): class Iterable1: if flag: + def __iter__(self) -> Iterator: return Iterator() + else: + def __iter__(self, invalid_extra_arg) -> Iterator: return Iterator() @@ -761,8 +849,10 @@ def _(flag: bool): class Iterable2: if flag: + def __iter__(self) -> Iterator: return Iterator() + else: __iter__: None = None @@ -780,16 +870,21 @@ def _(flag: bool): def _(flag: bool): class Iterator1: if flag: + def __next__(self) -> int: return 42 + else: + def __next__(self, invalid_extra_arg) -> str: return "foo" class Iterator2: if flag: + def __next__(self) -> int: return 42 + else: __next__: None = None @@ -819,16 +914,21 @@ def _(flag: bool): def _(flag: bool): class Iterable1: if flag: + def __getitem__(self, item: int) -> str: return "foo" + else: __getitem__: None = None class Iterable2: if flag: + def __getitem__(self, item: int) -> str: return "foo" + else: + def __getitem__(self, item: str) -> int: return 42 @@ -851,26 +951,35 @@ class Iterator: def __next__(self) -> bytes: return b"foo" + def _(flag: bool, flag2: bool): class Iterable1: if flag: + def __getitem__(self, item: int) -> str: return "foo" + else: __getitem__: None = None if flag2: + def __iter__(self) -> Iterator: return Iterator() class Iterable2: if flag: + def __getitem__(self, item: int) -> str: return "foo" + else: + def __getitem__(self, item: str) -> int: return 42 + if flag2: + def __iter__(self) -> Iterator: return Iterator() @@ -896,6 +1005,7 @@ for x in (): ```py from typing_extensions import Never + def f(never: Never): for x in never: reveal_type(x) # revealed: Unknown @@ -924,8 +1034,10 @@ from unresolved_module import SomethingUnknown # error: [unresolved-import] from typing import Any, Iterable from ty_extensions import static_assert, is_assignable_to, TypeOf, Unknown, reveal_mro + class Foo(SomethingUnknown): ... + reveal_mro(Foo) # revealed: (, Unknown, ) # TODO: these should pass @@ -937,8 +1049,10 @@ static_assert(is_assignable_to(type[Foo], Iterable[Unknown])) # error: [static- for x in Foo: reveal_type(x) # revealed: Unknown + class Bar(Any): ... + reveal_mro(Bar) # revealed: (, Any, ) # TODO: these should pass diff --git a/crates/ty_python_semantic/resources/mdtest/loops/iterators.md b/crates/ty_python_semantic/resources/mdtest/loops/iterators.md index 5b8c009fe3..28811b7524 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/iterators.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/iterators.md @@ -5,14 +5,17 @@ ```py class NotIterable: ... + class Iterator: def __next__(self) -> int: return 42 + class Iterable: def __iter__(self) -> Iterator: return Iterator() + def generator_function(): yield from Iterable() yield from NotIterable() # error: "Object of type `NotIterable` is not iterable" diff --git a/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md index 41e48b1404..043e15b2e6 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md @@ -50,6 +50,7 @@ def _(flag: bool, flag2: bool): def flag() -> bool: return True + x = 1 while flag(): @@ -123,6 +124,7 @@ def _(flag: bool, flag2: bool): class NotBoolable: __bool__: int = 3 + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" while NotBoolable(): ... diff --git a/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md b/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md index 6de0978fc9..b7619f9bf9 100644 --- a/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md +++ b/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md @@ -40,8 +40,10 @@ And finally write a normal Python code block that makes use of the custom stubs: ```py b: BuiltinClass = builtin_symbol + class OtherClass: ... + o: OtherClass = builtin_symbol # error: [invalid-assignment] # Make sure that 'sys' has a proper entry in the auto-generated 'VERSIONS' file diff --git a/crates/ty_python_semantic/resources/mdtest/metaclass.md b/crates/ty_python_semantic/resources/mdtest/metaclass.md index 3c4762fe8f..64ae3d7ac8 100644 --- a/crates/ty_python_semantic/resources/mdtest/metaclass.md +++ b/crates/ty_python_semantic/resources/mdtest/metaclass.md @@ -3,6 +3,7 @@ ```py class M(type): ... + reveal_type(M.__class__) # revealed: ``` @@ -22,8 +23,11 @@ reveal_type(type.__class__) # revealed: ```py class M(type): ... + + class B(metaclass=M): ... + reveal_type(B.__class__) # revealed: ``` @@ -34,8 +38,11 @@ arguments as `type.__new__`) isn't a valid metaclass. ```py class M: ... + + class A(metaclass=M): ... + # TODO: emit a diagnostic for the invalid metaclass reveal_type(A.__class__) # revealed: ``` @@ -47,9 +54,14 @@ metaclass. ```py class M(type): ... + + class A(metaclass=M): ... + + class B(A): ... + reveal_type(B.__class__) # revealed: ``` @@ -64,10 +76,17 @@ python-version = "3.13" ```py class M(type): ... + + class A[T](metaclass=M): ... + + class B(A): ... + + class C(A[int]): ... + reveal_type(B.__class__) # revealed: reveal_type(C.__class__) # revealed: ``` @@ -80,13 +99,21 @@ subclass or the class itself.) ```py class M1(type): ... + + class M2(type): ... + + class A(metaclass=M1): ... + + class B(metaclass=M2): ... + # error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship" class C(A, B): ... + reveal_type(C.__class__) # revealed: type[Unknown] ``` @@ -98,12 +125,18 @@ subclass or the class itself.) ```py class M1(type): ... + + class M2(type): ... + + class A(metaclass=M1): ... + # error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` (metaclass of `B`) and `M1` (metaclass of base class `A`) have no subclass relationship" class B(A, metaclass=M2): ... + reveal_type(B.__class__) # revealed: type[Unknown] ``` @@ -113,10 +146,17 @@ A class has two explicit bases, both of which have the same metaclass. ```py class M(type): ... + + class A(metaclass=M): ... + + class B(metaclass=M): ... + + class C(A, B): ... + reveal_type(C.__class__) # revealed: ``` @@ -126,11 +166,20 @@ A class has an explicit base with a custom metaclass. That metaclass itself has ```py class M1(type): ... + + class M2(type, metaclass=M1): ... + + class M3(M2): ... + + class A(metaclass=M3): ... + + class B(A): ... + reveal_type(A.__class__) # revealed: ``` @@ -138,16 +187,30 @@ reveal_type(A.__class__) # revealed: ```py class M(type): ... + + class M1(M): ... + + class M2(M): ... + + class M12(M1, M2): ... + + class A(metaclass=M1): ... + + class B(metaclass=M2): ... + + class C(metaclass=M12): ... + # error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship" class D(A, B, C): ... + reveal_type(D.__class__) # revealed: type[Unknown] ``` @@ -156,15 +219,23 @@ reveal_type(D.__class__) # revealed: type[Unknown] ```py from nonexistent_module import UnknownClass # error: [unresolved-import] + class C(UnknownClass): ... + # TODO: should be `type[type] & Unknown` reveal_type(C.__class__) # revealed: + class M(type): ... + + class A(metaclass=M): ... + + class B(A, UnknownClass): ... + # TODO: should be `type[M] & Unknown` reveal_type(B.__class__) # revealed: ``` @@ -173,9 +244,14 @@ reveal_type(B.__class__) # revealed: ```py class M(type): ... + + class A(metaclass=M): ... + + class B(A, A): ... # error: [duplicate-base] "Duplicate base class `A`" + reveal_type(B.__class__) # revealed: ``` @@ -188,33 +264,42 @@ When a class has an explicit `metaclass` that is not a class, but is a callable def f(*args, **kwargs) -> int: return 1 + class A(metaclass=f): ... + # TODO: Should be `int` reveal_type(A) # revealed: reveal_type(A.__class__) # revealed: type[int] + def _(n: int): # error: [invalid-metaclass] class B(metaclass=n): ... + # TODO: Should be `Unknown` reveal_type(B) # revealed: reveal_type(B.__class__) # revealed: type[Unknown] + def _(flag: bool): m = f if flag else 42 # error: [invalid-metaclass] class C(metaclass=m): ... + # TODO: Should be `int | Unknown` reveal_type(C) # revealed: reveal_type(C.__class__) # revealed: type[Unknown] + class SignatureMismatch: ... + # TODO: Emit a diagnostic class D(metaclass=SignatureMismatch): ... + # TODO: Should be `Unknown` reveal_type(D) # revealed: # TODO: Should be `type[Unknown]` @@ -242,8 +327,11 @@ python-version = "3.12" ```py class M(type): ... + + class A[T: str](metaclass=M): ... + reveal_type(A.__class__) # revealed: ``` @@ -251,14 +339,22 @@ reveal_type(A.__class__) # revealed: ```py class Foo(type): ... + + class Bar(type, metaclass=Foo): ... + + class Baz(type, metaclass=Bar): ... + + class Spam(metaclass=Baz): ... + reveal_type(Spam.__class__) # revealed: reveal_type(Spam.__class__.__class__) # revealed: reveal_type(Spam.__class__.__class__.__class__) # revealed: + def test(x: Spam): reveal_type(x.__class__) # revealed: type[Spam] reveal_type(x.__class__.__class__) # revealed: type[Baz] diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md index f1f38aac30..6043c46b2f 100644 --- a/crates/ty_python_semantic/resources/mdtest/mro.md +++ b/crates/ty_python_semantic/resources/mdtest/mro.md @@ -25,8 +25,10 @@ aliases, `Any` and `Unknown`. ```py from ty_extensions import reveal_mro + class C: ... + reveal_mro(C) # revealed: (, ) ``` @@ -43,8 +45,10 @@ reveal_mro(object) # revealed: (,) ```py from ty_extensions import reveal_mro + class C(object): ... + reveal_mro(C) # revealed: (, ) ``` @@ -53,9 +57,13 @@ reveal_mro(C) # revealed: (, ) ```py from ty_extensions import reveal_mro + class A: ... + + class B(A): ... + reveal_mro(B) # revealed: (, , ) ``` @@ -64,10 +72,16 @@ reveal_mro(B) # revealed: (, , ) ```py from ty_extensions import reveal_mro + class A: ... + + class B: ... + + class C(A, B): ... + reveal_mro(C) # revealed: (, , , ) ``` @@ -78,12 +92,22 @@ This is "ex_2" from ```py from ty_extensions import reveal_mro + class O: ... + + class X(O): ... + + class Y(O): ... + + class A(X, Y): ... + + class B(Y, X): ... + reveal_mro(A) # revealed: (, , , , ) reveal_mro(B) # revealed: (, , , , ) ``` @@ -95,14 +119,28 @@ This is "ex_5" from ```py from ty_extensions import reveal_mro + class O: ... + + class F(O): ... + + class E(O): ... + + class D(O): ... + + class C(D, F): ... + + class B(D, E): ... + + class A(B, C): ... + # revealed: (, , , , ) reveal_mro(C) # revealed: (, , , , ) @@ -118,14 +156,28 @@ This is "ex_6" from ```py from ty_extensions import reveal_mro + class O: ... + + class F(O): ... + + class E(O): ... + + class D(O): ... + + class C(D, F): ... + + class B(E, D): ... + + class A(B, C): ... + # revealed: (, , , , ) reveal_mro(C) # revealed: (, , , , ) @@ -141,17 +193,37 @@ This is "ex_9" from ```py from ty_extensions import reveal_mro + class O: ... + + class A(O): ... + + class B(O): ... + + class C(O): ... + + class D(O): ... + + class E(O): ... + + class K1(A, B, C): ... + + class K2(D, B, E): ... + + class K3(D, A): ... + + class Z(K1, K2, K3): ... + # revealed: (, , , , , ) reveal_mro(K1) # revealed: (, , , , , ) @@ -168,13 +240,25 @@ reveal_mro(Z) from ty_extensions import reveal_mro from does_not_exist import DoesNotExist # error: [unresolved-import] + class A(DoesNotExist): ... + + class B: ... + + class C: ... + + class D(A, B, C): ... + + class E(B, C): ... + + class F(E, A): ... + reveal_mro(A) # revealed: (, Unknown, ) reveal_mro(D) # revealed: (, , Unknown, , , ) reveal_mro(E) # revealed: (, , , ) @@ -197,12 +281,14 @@ if hasattr(DoesNotExist, "__mro__"): reveal_type(DoesNotExist) # revealed: Unknown & class Foo(DoesNotExist): ... # no error! + reveal_mro(Foo) # revealed: (, Unknown, ) if not isinstance(DoesNotExist, type): reveal_type(DoesNotExist) # revealed: Unknown & ~type class Foo(DoesNotExist): ... # error: [unsupported-base] + reveal_mro(Foo) # revealed: (, Unknown, ) ``` @@ -215,11 +301,14 @@ guarantee: from typing import Any from ty_extensions import Unknown, Intersection, reveal_mro + def f(x: type[Any], y: Intersection[Unknown, type[Any]]): class Foo(x): ... + reveal_mro(Foo) # revealed: (, Any, ) class Bar(y): ... + reveal_mro(Bar) # revealed: (, Unknown, ) ``` @@ -231,33 +320,51 @@ creation to fail, we infer the class's `__mro__` as being `[, Unknown, ob ```py from ty_extensions import reveal_mro + # error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[, ]`" class Foo(object, int): ... + reveal_mro(Foo) # revealed: (, Unknown, ) + class Bar(Foo): ... + reveal_mro(Bar) # revealed: (, , Unknown, ) + # This is the `TypeError` at the bottom of "ex_2" # in the examples at class O: ... + + class X(O): ... + + class Y(O): ... + + class A(X, Y): ... + + class B(Y, X): ... + reveal_mro(A) # revealed: (, , , , ) reveal_mro(B) # revealed: (, , , , ) + # error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Z` with bases list `[, ]`" class Z(A, B): ... + reveal_mro(Z) # revealed: (, Unknown, ) + class AA(Z): ... + reveal_mro(AA) # revealed: (, , Unknown, ) ``` @@ -272,12 +379,17 @@ find a union type in a class's bases, we infer the class's `__mro__` as being ```py from ty_extensions import reveal_mro + def returns_bool() -> bool: return True + class A: ... + + class B: ... + if returns_bool(): x = A else: @@ -285,15 +397,21 @@ else: reveal_type(x) # revealed: | + # error: 11 [unsupported-base] "Unsupported class base with type ` | `" class Foo(x): ... + reveal_mro(Foo) # revealed: (, Unknown, ) + def f(): if returns_bool(): + class C: ... + else: + class C: ... class D(C): ... # error: [unsupported-base] @@ -305,10 +423,14 @@ This is not legal: ```py class A: ... + + class B: ... + EitherOr = A | B + # error: [invalid-base] "Invalid class base with type ``" class Foo(EitherOr): ... ``` @@ -323,13 +445,16 @@ diagnostic, and we use the dynamic type as a base to prevent further downstream from typing import Any from ty_extensions import reveal_mro + def _(flag: bool, any: Any): if flag: Base = any else: + class Base: ... class Foo(Base): ... + reveal_mro(Foo) # revealed: (, Any, ) ``` @@ -338,14 +463,23 @@ def _(flag: bool, any: Any): ```py from ty_extensions import reveal_mro + def returns_bool() -> bool: return True + class A: ... + + class B: ... + + class C: ... + + class D: ... + if returns_bool(): x = A else: @@ -359,10 +493,12 @@ else: reveal_type(x) # revealed: | reveal_type(y) # revealed: | + # error: 11 [unsupported-base] "Unsupported class base with type ` | `" # error: 14 [unsupported-base] "Unsupported class base with type ` | `" class Foo(x, y): ... + reveal_mro(Foo) # revealed: (, Unknown, ) ``` @@ -371,39 +507,55 @@ reveal_mro(Foo) # revealed: (, Unknown, ) ```py from ty_extensions import reveal_mro + def returns_bool() -> bool: return True + class O: ... + + class X(O): ... + + class Y(O): ... + if returns_bool(): foo = Y else: foo = object + # error: 21 [unsupported-base] "Unsupported class base with type ` | `" class PossibleError(foo, X): ... + reveal_mro(PossibleError) # revealed: (, Unknown, ) + class A(X, Y): ... + reveal_mro(A) # revealed: (, , , , ) if returns_bool(): + class B(X, Y): ... else: + class B(Y, X): ... + # revealed: (, , , , ) | (, , , , ) reveal_mro(B) + # error: 12 [unsupported-base] "Unsupported class base with type ` | `" class Z(A, B): ... + reveal_mro(Z) # revealed: (, Unknown, ) ``` @@ -423,6 +575,7 @@ class Foo: def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]: return () + class Bar(Foo()): ... # error: [unsupported-base] ``` @@ -434,11 +587,15 @@ class Bad1: def __mro_entries__(self, bases, extra_arg): return () + class Bad2: def __mro_entries__(self, bases) -> int: return 42 + class BadSub1(Bad1()): ... # error: [invalid-base] + + class BadSub2(Bad2()): ... # error: [invalid-base] ``` @@ -449,15 +606,25 @@ class BadSub2(Bad2()): ... # error: [invalid-base] ```py from ty_extensions import reveal_mro + class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" + reveal_mro(Foo) # revealed: (, Unknown, ) + class Spam: ... + + class Eggs: ... + + class Bar: ... + + class Baz: ... + # fmt: off # error: [duplicate-base] "Duplicate base class `Spam`" @@ -475,9 +642,13 @@ class Ham( reveal_mro(Ham) # revealed: (, Unknown, ) + class Mushrooms: ... + + class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] + reveal_mro(Omelette) # revealed: (, Unknown, ) # fmt: off @@ -560,9 +731,11 @@ from unresolvable_module import UnknownBase1, UnknownBase2 # error: [unresolved reveal_type(UnknownBase1) # revealed: Unknown reveal_type(UnknownBase2) # revealed: Unknown + # no error here -- we respect the gradual guarantee: class Foo(UnknownBase1, UnknownBase2): ... + reveal_mro(Foo) # revealed: (, Unknown, ) ``` @@ -574,6 +747,7 @@ bases materialize to: # error: [duplicate-base] "Duplicate base class `Foo`" class Bar(UnknownBase1, Foo, UnknownBase2, Foo): ... + reveal_mro(Bar) # revealed: (, Unknown, ) ``` @@ -594,20 +768,30 @@ from ty_extensions import reveal_mro T = TypeVar("T") + class peekable(Generic[T], Iterator[T]): ... + # revealed: (, , , typing.Protocol, typing.Generic, ) reveal_mro(peekable) + class peekable2(Iterator[T], Generic[T]): ... + # revealed: (, , , typing.Protocol, typing.Generic, ) reveal_mro(peekable2) + class Base: ... + + class Intermediate(Base, Generic[T]): ... + + class Sub(Intermediate[T], Base): ... + # revealed: (, , , typing.Generic, ) reveal_mro(Sub) ``` @@ -621,8 +805,13 @@ from typing_extensions import Protocol, TypeVar, Generic T = TypeVar("T") + class Foo(Protocol): ... + + class Bar(Protocol[T]): ... + + class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index cb0f201989..edfc1efc36 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -11,11 +11,13 @@ name, and not just by its numeric position within the tuple: from typing import NamedTuple from ty_extensions import static_assert, is_subtype_of, is_assignable_to, reveal_mro + class Person(NamedTuple): id: int name: str age: int | None = None + alice = Person(1, "Alice", 42) alice = Person(id=1, name="Alice", age=42) bob = Person(2, "Bob") @@ -106,6 +108,7 @@ Fields without default values must come before fields with. ```py from typing import NamedTuple + class Location(NamedTuple): altitude: float = 0.0 # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitude` defined here without a default value" @@ -113,6 +116,7 @@ class Location(NamedTuple): # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitude` defined here without a default value" longitude: float + class StrangeLocation(NamedTuple): altitude: float altitude: float = 0.0 @@ -121,6 +125,7 @@ class StrangeLocation(NamedTuple): latitude: float # error: [invalid-named-tuple] longitude: float # error: [invalid-named-tuple] + class VeryStrangeLocation(NamedTuple): altitude: float = 0.0 latitude: float # error: [invalid-named-tuple] @@ -137,10 +142,12 @@ Multiple inheritance is not supported for `NamedTuple` classes except with `Gene ```py from typing import NamedTuple, Protocol + # error: [invalid-named-tuple] "NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`" class C(NamedTuple, object): id: int + # fmt: off class D( @@ -150,6 +157,7 @@ class D( # fmt: on + # error: [invalid-named-tuple] class E(NamedTuple, Protocol): ... ``` @@ -162,13 +170,16 @@ synthesized `__new__` signature: ```py from typing import NamedTuple + class User(NamedTuple): id: int name: str + class SuperUser(User): level: int + # This is fine: alice = SuperUser(1, "Alice") reveal_type(alice.level) # revealed: int @@ -184,12 +195,14 @@ flagged. ```py from typing import NamedTuple + class User(NamedTuple): id: int name: str age: int | None nickname: str + class SuperUser(User): # TODO: this should be an error because it implies that the `id` attribute on # `SuperUser` is mutable, but the read-only `id` property from the superclass @@ -211,6 +224,7 @@ class SuperUser(User): # error: 9 [invalid-assignment] "Cannot assign to read-only property `nickname` on object of type `Self@now_called_robert`" self.nickname = "Bob" + james = SuperUser(0, "James", 42, "Jimmy") # fine because the property on the superclass was overridden with a mutable attribute @@ -231,10 +245,12 @@ python-version = "3.12" ```py from typing import NamedTuple, Generic, TypeVar + class Property[T](NamedTuple): name: str value: T + reveal_type(Property("height", 3.4)) # revealed: Property[float] reveal_type(Property.value) # revealed: property reveal_type(Property.value.fget) # revealed: (self, /) -> Unknown @@ -243,10 +259,12 @@ reveal_type(Property("height", 3.4).value) # revealed: float T = TypeVar("T") + class LegacyProperty(NamedTuple, Generic[T]): name: str value: T + reveal_type(LegacyProperty("height", 42)) # revealed: LegacyProperty[int] reveal_type(LegacyProperty.value) # revealed: property reveal_type(LegacyProperty.value.fget) # revealed: (self, /) -> Unknown @@ -261,10 +279,12 @@ The following attributes are available on `NamedTuple` classes / instances: ```py from typing import NamedTuple + class Person(NamedTuple): name: str age: int | None = None + reveal_type(Person._field_defaults) # revealed: dict[str, Any] reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]] reveal_type(Person._make) # revealed: bound method ._make(iterable: Iterable[Any]) -> Person @@ -291,12 +311,15 @@ from typing import NamedTuple, Generic, TypeVar T = TypeVar("T") + class Box(NamedTuple, Generic[T]): content: T + class IntBox(Box[int]): pass + reveal_type(IntBox(1)._replace(content=42)) # revealed: IntBox ``` @@ -319,8 +342,10 @@ At runtime, `NamedTuple` is a function, and we understand this: import types import typing + def expects_functiontype(x: types.FunctionType): ... + expects_functiontype(typing.NamedTuple) ``` @@ -352,9 +377,11 @@ def expects_named_tuple(x: typing.NamedTuple): reveal_type(x.__add__) reveal_type(x.__iter__) # revealed: bound method tuple[object, ...].__iter__() -> Iterator[object] + def _(y: type[typing.NamedTuple]): reveal_type(y) # revealed: @Todo(unsupported type[X] special form) + # error: [invalid-type-form] "Special form `typing.NamedTuple` expected no type parameter" def _(z: typing.NamedTuple[int]): ... ``` @@ -367,10 +394,12 @@ all NamedTuple implementations automatically compatible: from typing import NamedTuple, Protocol, Iterable, Any from ty_extensions import static_assert, is_assignable_to + class Point(NamedTuple): x: int y: int + reveal_type(Point._make) # revealed: bound method ._make(iterable: Iterable[Any]) -> Point reveal_type(Point._asdict) # revealed: def _asdict(self) -> dict[str, Any] reveal_type(Point._replace) # revealed: (self: Self, *, x: int = ..., y: int = ...) -> Self @@ -394,6 +423,7 @@ static_assert(is_assignable_to(NamedTuple, tuple)) static_assert(is_assignable_to(NamedTuple, tuple[object, ...])) static_assert(is_assignable_to(NamedTuple, tuple[Any, ...])) + def expects_tuple(x: tuple[object, ...]): ... def _(x: NamedTuple): expects_tuple(x) # fine @@ -407,12 +437,14 @@ This is a regression test for . Make ```py from typing import NamedTuple + class Vec2(NamedTuple): x: float = 0.0 y: float = 0.0 def __getattr__(self, attrs: str): ... + Vec2(0.0, 0.0) ``` @@ -425,6 +457,7 @@ Using `super()` in a method of a `NamedTuple` class will raise an exception at r ```py from typing import NamedTuple + class F(NamedTuple): x: int @@ -463,9 +496,11 @@ However, classes that **inherit from** a `NamedTuple` class (but don't directly ```py from typing import NamedTuple + class Base(NamedTuple): x: int + class Child(Base): def method(self): super() @@ -484,9 +519,11 @@ Using `super()` on a `NamedTuple` class also works fine if it occurs outside the ```py from typing import NamedTuple + class F(NamedTuple): x: int + super(F, F(42)) # fine ``` @@ -497,13 +534,16 @@ super(F, F(42)) # fine ```py from typing import NamedTuple + class Foo(NamedTuple): # error: [invalid-named-tuple] "NamedTuple field `_bar` cannot start with an underscore" _bar: int + class Bar(NamedTuple): x: int + class Baz(Bar): _whatever: str # `Baz` is not a NamedTuple class, so this is fine ``` @@ -516,6 +556,7 @@ assign to these attributes (without type annotations) will raise an `AttributeEr ```py from typing import NamedTuple + class F(NamedTuple): x: int @@ -549,6 +590,7 @@ However, other attributes (including those starting with underscores) can be ass ```py from typing import NamedTuple + class G(NamedTuple): x: int @@ -565,6 +607,7 @@ underscore field name check): ```py from typing import NamedTuple + class H(NamedTuple): x: int # This is a field declaration, not an override. It's not flagged as an override, @@ -578,6 +621,7 @@ The check also applies to assignments within conditional blocks: ```py from typing import NamedTuple + class I(NamedTuple): x: int @@ -591,6 +635,7 @@ Method definitions with prohibited names are also flagged: ```py from typing import NamedTuple + class J(NamedTuple): x: int @@ -610,9 +655,11 @@ not subject to these restrictions: ```py from typing import NamedTuple + class Base(NamedTuple): x: int + class Child(Base): # This is fine - Child is not directly a NamedTuple _asdict = 42 @@ -625,9 +672,11 @@ class Child(Base): ```py from typing import NamedTuple + def coinflip() -> bool: return True + class Foo(NamedTuple): if coinflip(): _asdict: bool # error: [invalid-named-tuple] "NamedTuple field `_asdict` cannot start with an underscore" diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/assert.md b/crates/ty_python_semantic/resources/mdtest/narrow/assert.md index 0fab83880e..bb00f658a6 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/assert.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/assert.md @@ -25,6 +25,7 @@ def _(x: bool, y: bool): ```py from typing import Literal + def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]): assert x is 2 reveal_type(x) # revealed: Literal[2] @@ -45,6 +46,7 @@ def _(x: int | str): ```py from typing import Literal + def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]): assert x in (1, 2) reveal_type(x) # revealed: Literal[1, 2] @@ -74,6 +76,7 @@ def one(x: int | None): # error: [unresolved-reference] reveal_type(y) # revealed: Unknown + def two(x: int | None, y: int | None): assert x is None, (y := 42) * reveal_type(y) # revealed: Literal[42] reveal_type(y) # revealed: int | None @@ -108,6 +111,7 @@ def one(x: int | None): assert (y := x), reveal_type(y) # revealed: (int & ~AlwaysTruthy) | None reveal_type(y) # revealed: int & ~AlwaysFalsy + def two(x: int | None): assert isinstance((y := x), int), reveal_type(y) # revealed: None reveal_type(y) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md index 752d08d216..ac61d52451 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md @@ -12,6 +12,7 @@ class A: def __init__(self): self.z = None + a = A() a.x = 0 a.y = 0 @@ -21,6 +22,7 @@ reveal_type(a.x) # revealed: Literal[0] reveal_type(a.y) # revealed: Literal[0] reveal_type(a.z) # revealed: Literal[0] + # Make sure that we infer the narrowed type for eager # scopes (class, comprehension) and the non-narrowed # public type for lazy scopes (function) @@ -29,15 +31,18 @@ class _: reveal_type(a.y) # revealed: Literal[0] reveal_type(a.z) # revealed: Literal[0] + [reveal_type(a.x) for _ in range(1)] # revealed: Literal[0] [reveal_type(a.y) for _ in range(1)] # revealed: Literal[0] [reveal_type(a.z) for _ in range(1)] # revealed: Literal[0] + def _(): reveal_type(a.x) # revealed: int | None reveal_type(a.y) # revealed: Unknown | None reveal_type(a.z) # revealed: Unknown | None + if False: a = A() reveal_type(a.x) # revealed: Literal[0] @@ -57,15 +62,18 @@ reveal_type(a.x) # revealed: Literal[0] reveal_type(a.y) # revealed: Literal[0] reveal_type(a.z) # revealed: Literal[0] + class _: a = A() reveal_type(a.x) # revealed: int | None reveal_type(a.y) # revealed: Unknown | None reveal_type(a.z) # revealed: Unknown | None + def cond() -> bool: return True + class _: if False: a = A() @@ -79,6 +87,7 @@ class _: reveal_type(a.y) # revealed: Unknown | None reveal_type(a.z) # revealed: Unknown | None + class _: a = A() @@ -87,6 +96,7 @@ class _: reveal_type(a.y) # revealed: Unknown | None reveal_type(a.z) # revealed: Unknown | None + a = A() # error: [unresolved-attribute] a.dynamically_added = 0 @@ -104,16 +114,20 @@ reveal_type(does.nt.exist) # revealed: Unknown ```py class D: ... + class C: d: D | None = None + class B: c1: C | None = None c2: C | None = None + class A: b: B | None = None + a = A() a.b = B() a.b.c1 = C() @@ -156,6 +170,7 @@ class C: def x(self, value: int) -> None: self._x = abs(value) + c = C() c.x = -1 # Don't infer `c.x` to be `Literal[-1]` @@ -172,9 +187,11 @@ class Descriptor: def __set__(self, instance: object, value: int) -> None: pass + class C: desc: Descriptor = Descriptor() + c = C() c.desc = -1 # Don't infer `c.desc` to be `Literal[-1]` @@ -214,6 +231,7 @@ reveal_type(b[0]) # revealed: Literal[0] reveal_type(dd[0]) # revealed: Literal[0] reveal_type(cm[0]) # revealed: Literal[0] + class C: reveal_type(l[0]) # revealed: Literal[0] reveal_type(d[0]) # revealed: Literal[0] @@ -221,12 +239,14 @@ class C: reveal_type(dd[0]) # revealed: Literal[0] reveal_type(cm[0]) # revealed: Literal[0] + [reveal_type(l[0]) for _ in range(1)] # revealed: Literal[0] [reveal_type(d[0]) for _ in range(1)] # revealed: Literal[0] [reveal_type(b[0]) for _ in range(1)] # revealed: Literal[0] [reveal_type(dd[0]) for _ in range(1)] # revealed: Literal[0] [reveal_type(cm[0]) for _ in range(1)] # revealed: Literal[0] + def _(): reveal_type(l[0]) # revealed: int | None reveal_type(d[0]) # revealed: int @@ -234,10 +254,12 @@ def _(): reveal_type(dd[0]) # revealed: int reveal_type(cm[0]) # revealed: int + class D(TypedDict): x: int label: str + td = D(x=1, label="a") td["x"] = 0 reveal_type(td["x"]) # revealed: Literal[0] @@ -270,6 +292,7 @@ class C: else: self.l[index] = str(value) + c = C() c[0] = 0 reveal_type(c[0]) # revealed: str @@ -281,18 +304,22 @@ reveal_type(c[0]) # revealed: str class A: x: list[int | None] = [] + class B: a: A | None = None + b = B() b.a = A() b.a.x[0] = 0 reveal_type(b.a.x[0]) # revealed: Literal[0] + class C: reveal_type(b.a.x[0]) # revealed: Literal[0] + def _(): # error: [possibly-missing-attribute] reveal_type(b.a.x[0]) # revealed: int | None @@ -308,6 +335,7 @@ class C: x: int | None l: list[int] + def f(c: C, s: str): c.x = s # error: [invalid-assignment] reveal_type(c.x) # revealed: int | None diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/boolean.md b/crates/ty_python_semantic/resources/mdtest/narrow/boolean.md index dd86762ee5..4a4bdb987c 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/boolean.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/boolean.md @@ -11,6 +11,7 @@ Similarly, in `and` expressions, the right-hand side is evaluated only if the le ```py def _(flag: bool): class A: ... + x: A | None = A() if flag else None isinstance(x, A) or reveal_type(x) # revealed: None @@ -23,17 +24,21 @@ def _(flag: bool): ```py from typing import final + def _(flag: bool): class A: ... + x: A | None = A() if flag else None isinstance(x, A) and reveal_type(x) # revealed: A x is None and reveal_type(x) # revealed: None reveal_type(x) # revealed: A | None + @final class FinalClass: ... + # We know that no subclass of `FinalClass` can exist, # therefore no subtype of `FinalClass` can define `__bool__` # or `__len__`, therefore `FinalClass` can safely be considered @@ -46,6 +51,7 @@ reveal_type(FinalClass() and None) # revealed: None ```py def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool): class A: ... + x: A | None = A() if flag1 else None flag2 and isinstance(x, A) and reveal_type(x) # revealed: A @@ -58,6 +64,7 @@ def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool): ```py def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool): class A: ... + x: A | None = A() if flag1 else None flag2 or isinstance(x, A) or reveal_type(x) # revealed: None @@ -70,8 +77,10 @@ def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool): ```py from typing import Literal + def _(flag1: bool, flag2: bool): class A: ... + x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1 x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1] @@ -82,8 +91,10 @@ def _(flag1: bool, flag2: bool): ```py from typing import Literal + def _(flag1: bool, flag2: bool): class A: ... + x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1 isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1] diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index 9326539e0c..cf2b0f41cc 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -12,6 +12,7 @@ simplify to just the original callable type. ```py from typing import Any, Callable + def f(x: Callable[..., Any] | None): if callable(x): # The intersection simplifies because `(...) -> Any` is a subtype of @@ -28,6 +29,7 @@ def f(x: Callable[..., Any] | None): ```py from typing import Any, Callable + def g(x: Callable[[int], str] | None): if callable(x): # All callables are subtypes of `Top[(...) -> object]`, so the intersection simplifies. @@ -35,6 +37,7 @@ def g(x: Callable[[int], str] | None): else: reveal_type(x) # revealed: None + def h(x: Callable[..., int] | None): if callable(x): reveal_type(x) # revealed: (...) -> int @@ -62,6 +65,7 @@ be valid. ```py import typing as t + def call_with_args(y: object, a: int, b: str) -> object: if isinstance(y, t.Callable): # error: [call-top-callable] @@ -77,9 +81,11 @@ narrowed. ```py from typing import Any + class Foo: func: Any | None + def f(foo: Foo): first = getattr(foo, "func", None) if callable(first): @@ -107,9 +113,11 @@ static_assert(is_assignable_to(Top[Callable[..., bool]], Callable[..., int])) F = TypeVar("F", bound=Callable[..., Any]) + def wrap(f: F) -> F: return f + def f(x: object): if callable(x): # x has type `Top[(...) -> object]`, which should be assignable to `Callable[..., Any]` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md index b1a324e557..0bf55128db 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md @@ -9,9 +9,11 @@ We support type narrowing for attributes and subscripts. ```py from ty_extensions import Unknown + class C: x: int | None = None + c = C() reveal_type(c.x) # revealed: int | None @@ -33,25 +35,32 @@ if c.x is None: reveal_type(c.x) # revealed: int + class _: reveal_type(c.x) # revealed: int + c = C() + class _: if c.x is None: c.x = 1 reveal_type(c.x) # revealed: int + # TODO: should be `int` reveal_type(c.x) # revealed: int | None + class D: x = None + def unknown() -> Unknown: return 1 + d = D() reveal_type(d.x) # revealed: Unknown | None d.x = 1 @@ -59,12 +68,15 @@ reveal_type(d.x) # revealed: Literal[1] d.x = unknown() reveal_type(d.x) # revealed: Unknown + class E: x: int | None = None + e = E() if e.x is not None: + class _: reveal_type(e.x) # revealed: int ``` @@ -103,6 +115,7 @@ reveal_type(c.x) # revealed: int | None class C: value: str | None + def foo(c: C): # The truthiness check `c.value` narrows to `str & ~AlwaysFalsy`. # The subsequent `len(c.value)` doesn't narrow further since `str` is not narrowable by len(). @@ -136,6 +149,7 @@ class C[T]: self.x = x self.y = x + def f(a: int | None): c = C(a) reveal_type(c.x) # revealed: int | None @@ -146,6 +160,7 @@ def f(a: int | None): # but different values ​​may be reassigned to `x` and `y` in another place. reveal_type(c.y) # revealed: int | None + def g[T](c: C[T]): reveal_type(c.x) # revealed: T@g reveal_type(c.y) # revealed: T@g @@ -170,6 +185,7 @@ class C: self.x: int | None = None self.y: int | None = None + c = C() reveal_type(c.x) # revealed: int | None if c.x is not None: @@ -177,9 +193,11 @@ if c.x is not None: reveal_type(c.y) # revealed: int | None if c.x is not None: + def _(): reveal_type(c.x) # revealed: int | None + def _(): if c.x is not None: reveal_type(c.x) # revealed: int @@ -220,6 +238,7 @@ def _(t1: tuple[int | None, int | None], t2: tuple[int, int] | tuple[None, None] else: reveal_type(t2) # revealed: tuple[int, int] + def _(t3: tuple[int, str] | tuple[None, None] | tuple[bool, bytes]): # Narrow to tuples where first element is not None if t3[0] is not None: @@ -229,12 +248,14 @@ def _(t3: tuple[int, str] | tuple[None, None] | tuple[bool, bytes]): if t3[0] is None: reveal_type(t3) # revealed: tuple[None, None] + def _(t4: tuple[bool, int] | tuple[bool, str]): # Both tuples have bool at index 0, which is not disjoint from True, # so neither gets filtered out when checking `is True` if t4[0] is True: reveal_type(t4) # revealed: tuple[bool, int] | tuple[bool, str] + def _(t5: tuple[int, None] | tuple[None, int]): # Narrow on second element (index 1) if t5[1] is not None: @@ -246,12 +267,14 @@ def _(t5: tuple[int, None] | tuple[None, int]): if t5[-1] is None: reveal_type(t5) # revealed: tuple[int, None] + def _(t6: tuple[int, ...] | tuple[None, None]): # Variadic tuple at index 0 has element type `int` (not a union), # so `tuple[None, None]` gets filtered out if t6[0] is not None: reveal_type(t6) # revealed: tuple[int, ...] + def _(t6b: tuple[int, ...] | tuple[None, ...]): # Both variadic: `int` is disjoint from None, `None` is not disjoint from None if t6b[0] is not None: @@ -259,6 +282,7 @@ def _(t6b: tuple[int, ...] | tuple[None, ...]): else: reveal_type(t6b) # revealed: tuple[None, ...] + def _(t7: tuple[int, int] | tuple[None, None]): # Index out of range for both tuples - no narrowing, but errors are emitted # error: [index-out-of-bounds] "Index 5 is out of bounds for tuple `tuple[int, int]` with length 2" @@ -266,12 +290,14 @@ def _(t7: tuple[int, int] | tuple[None, None]): if t7[5] is not None: reveal_type(t7) # revealed: tuple[int, int] | tuple[None, None] + def _(t8: tuple[int, int, int] | tuple[None, None]): # Index in range for first tuple but out of range for second # error: [index-out-of-bounds] "Index 2 is out of bounds for tuple `tuple[None, None]` with length 2" if t8[2] is not None: reveal_type(t8) # revealed: tuple[int, int, int] | tuple[None, None] + def _(t9: tuple[int | None, str] | tuple[str, int]): # When the element type is a union (like `int | None`), we can't filter # out the tuple. @@ -286,10 +312,16 @@ Narrow unions of tuples based on literal tag elements using `==` comparison: ```py from typing import Literal + class A: ... + + class B: ... + + class C: ... + def _(x: tuple[Literal["tag1"], A] | tuple[Literal["tag2"], B, C]): if x[0] == "tag1": reveal_type(x) # revealed: tuple[Literal["tag1"], A] @@ -299,12 +331,14 @@ def _(x: tuple[Literal["tag1"], A] | tuple[Literal["tag2"], B, C]): reveal_type(x[1]) # revealed: B reveal_type(x[2]) # revealed: C + def _(x: tuple[Literal["tag1"], A] | tuple[Literal["tag2"], B, C]): if x[0] != "tag1": reveal_type(x) # revealed: tuple[Literal["tag2"], B, C] else: reveal_type(x) # revealed: tuple[Literal["tag1"], A] + # With int literals def _(x: tuple[Literal[1], A] | tuple[Literal[2], B]): if x[0] == 1: @@ -312,6 +346,7 @@ def _(x: tuple[Literal[1], A] | tuple[Literal[2], B]): else: reveal_type(x) # revealed: tuple[Literal[2], B] + # With bytes literals def _(x: tuple[Literal[b"a"], A] | tuple[Literal[b"b"], B]): if x[0] == b"a": @@ -319,6 +354,7 @@ def _(x: tuple[Literal[b"a"], A] | tuple[Literal[b"b"], B]): else: reveal_type(x) # revealed: tuple[Literal[b"b"], B] + # Multiple tuple variants def _(x: tuple[Literal["a"], A] | tuple[Literal["b"], B] | tuple[Literal["c"], C]): if x[0] == "a": @@ -328,6 +364,7 @@ def _(x: tuple[Literal["a"], A] | tuple[Literal["b"], B] | tuple[Literal["c"], C else: reveal_type(x) # revealed: tuple[Literal["c"], C] + # Using index 1 instead of 0 def _(x: tuple[A, Literal["tag1"]] | tuple[B, Literal["tag2"]]): if x[1] == "tag1": @@ -390,10 +427,12 @@ class C: def __init__(self): self.x: tuple[int | None, int | None] = (None, None) + class D: def __init__(self): self.c: tuple[C] | None = None + d = D() if d.c is not None and d.c[0].x[0] is not None: reveal_type(d.c[0].x[0]) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/boolean.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/boolean.md index 666c5e6b68..df01521bb5 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/boolean.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/boolean.md @@ -4,8 +4,11 @@ ```py class A: ... + + class B: ... + def _(x: A | B): if isinstance(x, A) and isinstance(x, B): reveal_type(x) # revealed: A & B @@ -17,8 +20,11 @@ def _(x: A | B): ```py class A: ... + + class B: ... + def _(flag: bool, x: A | B): if isinstance(x, A) and flag: reveal_type(x) # revealed: A @@ -37,8 +43,11 @@ def _(flag: bool, x: A | B): ```py class A: ... + + class B: ... + def _(x: A | B): if isinstance(x, A) and True: reveal_type(x) # revealed: A @@ -74,8 +83,11 @@ def _(x: A | B): ```py class A: ... + + class B: ... + def _(x: A | B, y: A | B): if isinstance(x, A) and isinstance(y, B): reveal_type(x) # revealed: A @@ -93,9 +105,14 @@ def _(x: A | B, y: A | B): ```py class A: ... + + class B: ... + + class C: ... + def _(x: A | B | C): if isinstance(x, A) or isinstance(x, B): reveal_type(x) # revealed: A | B @@ -107,9 +124,14 @@ def _(x: A | B | C): ```py class A: ... + + class B: ... + + class C: ... + def _(flag: bool, x: A | B | C): if isinstance(x, A) or isinstance(x, B) or flag: reveal_type(x) # revealed: A | B | C @@ -121,9 +143,14 @@ def _(flag: bool, x: A | B | C): ```py class A: ... + + class B: ... + + class C: ... + def _(x: A | B | C, y: A | B | C): if isinstance(x, A) or isinstance(y, A): # The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here. @@ -147,9 +174,14 @@ def _(x: A | B | C, y: A | B | C): ```py class A: ... + + class B: ... + + class C: ... + def _(x: A | B | C): if isinstance(x, B) and not isinstance(x, C): reveal_type(x) # revealed: B & ~C @@ -162,9 +194,14 @@ def _(x: A | B | C): ```py class A: ... + + class B: ... + + class C: ... + def _(x: A | B | C): if isinstance(x, B) or not isinstance(x, C): reveal_type(x) # revealed: B | (A & ~C) @@ -176,9 +213,14 @@ def _(x: A | B | C): ```py class A: ... + + class B: ... + + class C: ... + def _(x: A | B | C): if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)): reveal_type(x) # revealed: A | (B & ~C) @@ -191,9 +233,14 @@ def _(x: A | B | C): ```py class A: ... + + class B: ... + + class C: ... + def _(x: A | B | C): if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)): # A & (B | ~C) -> (A & B) | (A & ~C) @@ -230,6 +277,7 @@ def _(x: str | None, y: str | None): def f() -> bool: return True + if x := f(): reveal_type(x) # revealed: Literal[True] else: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md index 96b2153b90..71964970de 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md @@ -47,10 +47,14 @@ def _(flag1: bool, flag2: bool): ```py class Foo: ... + + class Bar: ... + def f() -> Foo | Bar | None: ... + if isinstance(x := f(), Foo): reveal_type(x) # revealed: Foo elif isinstance(x, Bar): diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index cb61ecf5f6..aba5f2d1ef 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -47,6 +47,7 @@ def _(x: bool): else: reveal_type(x) # revealed: Literal[False] + def _(x: bool): if x == False: reveal_type(x) # revealed: Literal[False] @@ -59,25 +60,30 @@ def _(x: bool): ```py from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + def _(answer: Answer): if answer != Answer.NO: reveal_type(answer) # revealed: Literal[Answer.YES] else: reveal_type(answer) # revealed: Literal[Answer.NO] + def _(answer: Answer): if answer == Answer.NO: reveal_type(answer) # revealed: Literal[Answer.NO] else: reveal_type(answer) # revealed: Literal[Answer.YES] + class Single(Enum): VALUE = 1 + def _(x: Single | int): if x != Single.VALUE: reveal_type(x) # revealed: int @@ -85,6 +91,7 @@ def _(x: Single | int): # `int` is not eliminated here because there could be subclasses of `int` with custom `__eq__`/`__ne__` methods reveal_type(x) # revealed: Single | int + def _(x: Single | int): if x == Single.VALUE: reveal_type(x) # revealed: Single | int @@ -97,6 +104,7 @@ This narrowing behavior is only safe if the enum has no custom `__eq__`/`__ne__` ```py from enum import Enum + class AmbiguousEnum(Enum): NO = 0 YES = 1 @@ -104,6 +112,7 @@ class AmbiguousEnum(Enum): def __ne__(self, other) -> bool: return True + def _(answer: AmbiguousEnum): if answer != AmbiguousEnum.NO: reveal_type(answer) # revealed: AmbiguousEnum @@ -116,14 +125,17 @@ Similar if that method is inherited from a base class: ```py from enum import Enum + class Mixin: def __eq__(self, other) -> bool: return True + class AmbiguousEnum(Mixin, Enum): NO = 0 YES = 1 + def _(answer: AmbiguousEnum): if answer == AmbiguousEnum.NO: reveal_type(answer) # revealed: AmbiguousEnum @@ -146,7 +158,9 @@ def _(flag: bool): ```py def _(flag: bool): class A: ... + class B: ... + C = A if flag else B if C != A: @@ -198,9 +212,11 @@ def _(flag1: bool, flag2: bool, a: int): ```py from typing import Literal + def f() -> Literal[1, 2, 3]: return 1 + if (x := f()) != 1: reveal_type(x) # revealed: Literal[2, 3] else: @@ -212,6 +228,7 @@ else: ```py from typing import Any + def _(x: Any | None, y: Any | None): if x != 1: reveal_type(x) # revealed: (Any & ~Literal[1]) | None @@ -224,6 +241,7 @@ def _(x: Any | None, y: Any | None): ```py from typing import Literal + def _(b: bool, i: Literal[1, 2]): if b == 1: reveal_type(b) # revealed: Literal[True] @@ -251,6 +269,7 @@ def _(b: bool, i: Literal[1, 2]): ```py from typing_extensions import Literal, LiteralString, Any + def _(s: LiteralString | None, t: LiteralString | Any): if s == "foo": reveal_type(s) # revealed: Literal["foo"] @@ -271,6 +290,7 @@ tuples. So they are excluded from the narrowed type when comparing to non-tuple ```py from typing import Literal + def _(x: Literal["a", "b"] | tuple[int, int]): if x == "a": # tuple type is excluded because it's disjoint from the string literal diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md index 660ef375e7..630269405d 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -21,6 +21,7 @@ def _(x: str): ```py from typing import Literal + def _(x: Literal[1, 2, "a", "b", False, b"abc"]): if x in (1,): reveal_type(x) # revealed: Literal[1] @@ -55,6 +56,7 @@ def _(x: str): ```py from typing import Literal + def _(x: Literal["a", "b", "c", "d"]): if x in "abc": reveal_type(x) # revealed: Literal["a", "b", "c"] @@ -84,9 +86,11 @@ def _(x: Literal[1, "a", "b", "c", "d"]): ```py from typing import Literal + def f() -> Literal[1, 2, 3]: return 1 + if (x := f()) in (1,): reveal_type(x) # revealed: Literal[1] else: @@ -98,6 +102,7 @@ else: ```py from typing import Literal + def test(x: Literal["a", "b", "c"] | None | int = None): if x in ("a", "b"): # int is included because custom __eq__ methods could make @@ -112,6 +117,7 @@ def test(x: Literal["a", "b", "c"] | None | int = None): ```py from typing import Literal + def test(x: Literal["a", "b", "c"] | None | int = None): if x not in ("a", "c"): # int is included because custom __eq__ methods could make @@ -130,6 +136,7 @@ future: ```py from typing import Literal + def f(x: Literal["abc", "def"]): if "a" in x: # `x` could also be validly narrowed to `Literal["abc"]` here: @@ -155,6 +162,7 @@ def _(x: bool): else: reveal_type(x) # revealed: Literal[False] + def _(x: bool | str): if x in (False,): # `str` remains due to possible custom __eq__ methods on a subclass @@ -168,12 +176,14 @@ def _(x: bool | str): ```py from typing_extensions import LiteralString + def _(x: LiteralString): if x in ("a", "b", "c"): reveal_type(x) # revealed: Literal["a", "b", "c"] else: reveal_type(x) # revealed: LiteralString & ~Literal["a"] & ~Literal["b"] & ~Literal["c"] + def _(x: LiteralString | int): if x in ("a", "b", "c"): reveal_type(x) # revealed: Literal["a", "b", "c"] | int @@ -186,11 +196,13 @@ def _(x: LiteralString | int): ```py from enum import Enum + class Color(Enum): RED = "red" GREEN = "green" BLUE = "blue" + def _(x: Color): if x in (Color.RED, Color.GREEN): reveal_type(x) # revealed: Literal[Color.RED, Color.GREEN] @@ -203,11 +215,13 @@ def _(x: Color): ```py from enum import Enum + class Status(Enum): PENDING = 1 APPROVED = 2 REJECTED = 3 + def test(x: Status | int): if x in (Status.PENDING, Status.APPROVED): # int is included because custom __eq__ methods could make @@ -225,6 +239,7 @@ tuples. So they are excluded from the narrowed type when disjoint from the RHS v ```py from typing import Literal + def test(x: Literal["none", "auto", "required"] | tuple[list[str], Literal["auto", "required"]]): if x in ("auto", "required"): # tuple type is excluded because it's disjoint from the string literals diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md index e3c40104f1..54a6135d34 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -19,6 +19,7 @@ def _(flag: bool): ```py def _(flag: bool): class A: ... + x = A() y = x if flag else None @@ -70,19 +71,23 @@ def _(flag1: bool, flag2: bool): ```py from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + def _(answer: Answer): if answer is Answer.NO: reveal_type(answer) # revealed: Literal[Answer.NO] else: reveal_type(answer) # revealed: Literal[Answer.YES] + class Single(Enum): VALUE = 1 + def _(x: Single | int): if x is Single.VALUE: reveal_type(x) # revealed: Single @@ -100,6 +105,7 @@ python-version = "3.10" ```py from types import EllipsisType + def _(x: int | EllipsisType): if x is ...: reveal_type(x) # revealed: EllipsisType @@ -131,8 +137,10 @@ def _(flag: bool): ```py from typing import Literal + def f() -> Literal[1, 2] | None: ... + if (x := f()) is None: reveal_type(x) # revealed: None else: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md index c85e42fc6f..d04ecbed85 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md @@ -62,10 +62,12 @@ Enum literals: ```py from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + def _(answer: Answer): if answer is not Answer.NO: reveal_type(answer) # revealed: Literal[Answer.YES] @@ -74,9 +76,11 @@ def _(answer: Answer): reveal_type(answer) # revealed: Answer + class Single(Enum): VALUE = 1 + def _(x: Single | int): if x is not Single.VALUE: reveal_type(x) # revealed: int @@ -104,6 +108,7 @@ else: ```py def _(flag: bool): class A: ... + x = A() y = x if flag else None @@ -143,6 +148,7 @@ def _(x_flag: bool, y_flag: bool): ```py def f() -> int | str | None: ... + if (x := f()) is not None: reveal_type(x) # revealed: int | str else: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md index 2cb4585b3b..744bad7acb 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md @@ -79,38 +79,48 @@ class A: def update_x(self, value: str | None): self.x = value + a = A() a.x = "a" + class B: reveal_type(a.x) # revealed: Literal["a"] + def f(): reveal_type(a.x) # revealed: str | None + [reveal_type(a.x) for _ in range(1)] # revealed: Literal["a"] a = A() + class C: reveal_type(a.x) # revealed: str | None + def g(): reveal_type(a.x) # revealed: str | None + [reveal_type(a.x) for _ in range(1)] # revealed: str | None a = A() a.x = "a" a.update_x("b") + class D: # TODO: should be `str | None` reveal_type(a.x) # revealed: Literal["a"] + def h(): reveal_type(a.x) # revealed: str | None + # TODO: should be `str | None` [reveal_type(a.x) for _ in range(1)] # revealed: Literal["a"] ``` @@ -120,19 +130,24 @@ def h(): ```py class D: ... + class C: d: D | None = None + class B: c1: C | None = None c2: C | None = None + class A: b: B | None = None + a = A() a.b = B() + class _: a.b.c1 = C() @@ -154,9 +169,11 @@ class _: # TODO: should be `D | None` reveal_type(a.b.c1.d) # revealed: Unknown + a.b.c1 = C() a.b.c1.d = D() + class _: a.b = B() @@ -171,13 +188,16 @@ class _: ```py g: str | None = "a" + class A: x: str | None = None + a = A() l: list[str | None] = [None] + def f(x: str | None): def _(): if x is not None: @@ -219,15 +239,19 @@ def f(x: str | None): ```py g: str | None = "a" + class A: x: str | None = None + a = A() l: list[str | None] = [None] + def f(x: str | None): if x is not None: + def _(): # If there is a possibility that `x` may be rewritten after this function definition, # the constraint `x is not None` outside the function is no longer be applicable for narrowing. @@ -241,26 +265,35 @@ def f(x: str | None): # When there is a reassignment, any narrowing constraints on the place are invalidated in lazy scopes. x = None + def f(x: str | None): def _(): if x is not None: + def closure(): reveal_type(x) # revealed: str | None + x = None + def f(x: str | None): def _(x: str | None): if x is not None: + def closure(): reveal_type(x) # revealed: str + x = None + def f(x: str | None): class C: def _(): if x is not None: + def closure(): reveal_type(x) # revealed: str + x = None # This assignment is not visible in the inner lazy scope, so narrowing is still valid. ``` @@ -270,6 +303,7 @@ inner lazy scope. ```py def f(const: str | None): if const is not None: + def _(): # The `const is not None` narrowing constraint is still valid since `const` has not been reassigned reveal_type(const) # revealed: str @@ -279,9 +313,11 @@ def f(const: str | None): [reveal_type(const) for _ in range(1)] # revealed: str + def f(const: str | None): def _(): if const is not None: + def closure(): reveal_type(const) # revealed: str ``` @@ -292,14 +328,19 @@ is still valid in the inner lazy scope. ```py def f(l: list[str | None] | None): if l is not None: + def _(): reveal_type(l) # revealed: list[str | None] + l[0] = None + def f(a: A): if a: + def _(): reveal_type(a) # revealed: A & ~AlwaysFalsy + a.x = None ``` @@ -309,38 +350,53 @@ no longer valid in the inner lazy scope. ```py def f(l: list[str | None]): if l[0] is not None: + def _(): reveal_type(l[0]) # revealed: str | None + l = [None] + def f(l: list[str | None]): l[0] = "a" + def _(): reveal_type(l[0]) # revealed: str | None + l = [None] + def f(l: list[str | None]): l[0] = "a" + def _(): l: list[str | None] = [None] + def _(): reveal_type(l[0]) # revealed: str | None def _(): def _(): reveal_type(l[0]) # revealed: str | None + l: list[str | None] = [None] + def f(a: A): if a.x is not None: + def _(): reveal_type(a.x) # revealed: str | None + a = A() + def f(a: A): a.x = "a" + def _(): reveal_type(a.x) # revealed: str | None + a = A() ``` @@ -349,6 +405,7 @@ Narrowing is also invalidated if a `nonlocal` declaration is made within a lazy ```py def f(non_local: str | None): if non_local is not None: + def _(): nonlocal non_local non_local = None @@ -356,11 +413,14 @@ def f(non_local: str | None): def _(): reveal_type(non_local) # revealed: str | None + def f(non_local: str | None): def _(): nonlocal non_local non_local = None + if non_local is not None: + def _(): reveal_type(non_local) # revealed: str | None ``` @@ -371,6 +431,7 @@ of their changes. ```py def f(): if g is not None: + def _(): reveal_type(g) # revealed: str | None @@ -380,6 +441,7 @@ def f(): [reveal_type(g) for _ in range(1)] # revealed: str if a.x is not None: + def _(): # Lazy nested scope narrowing is not performed on attributes/subscripts because it's difficult to track their changes. reveal_type(a.x) # revealed: str | None @@ -390,6 +452,7 @@ def f(): [reveal_type(a.x) for _ in range(1)] # revealed: str if l[0] is not None: + def _(): reveal_type(l[0]) # revealed: str | None @@ -406,19 +469,23 @@ from typing import Literal g: str | Literal[1] | None = "a" + class A: x: str | Literal[1] | None = None + a = A() l: list[str | Literal[1] | None] = [None] + def f(x: str | Literal[1] | None): class C: # If we try to access a variable in a class before it has been defined, # the lookup will fall back to global. # error: [unresolved-reference] if x is not None: + def _(): if x != 1: reveal_type(x) # revealed: str | None @@ -435,14 +502,18 @@ def f(x: str | Literal[1] | None): # No narrowing is performed on unresolved references. # error: [unresolved-reference] if x is not None: + def _(): if x != 1: reveal_type(x) # revealed: None + x = None + def f(const: str | Literal[1] | None): class C: if const is not None: + def _(): if const != 1: reveal_type(const) # revealed: str @@ -455,13 +526,16 @@ def f(const: str | Literal[1] | None): def _(): if const is not None: + def _(): if const != 1: reveal_type(const) # revealed: str + def f(): class C: if g is not None: + def _(): if g != 1: reveal_type(g) # revealed: str | None @@ -471,6 +545,7 @@ def f(): reveal_type(g) # revealed: str if a.x is not None: + def _(): if a.x != 1: reveal_type(a.x) # revealed: str | None @@ -480,6 +555,7 @@ def f(): reveal_type(a.x) # revealed: str if l[0] is not None: + def _(): if l[0] != 1: reveal_type(l[0]) # revealed: str | None @@ -496,11 +572,13 @@ from typing import Literal g: str | Literal[1] | None = "a" + def f(flag: bool): class C: (g := None) if flag else (g := None) # `g` is always bound here, so narrowing checks don't apply to nested scopes if g is not None: + class F: reveal_type(g) # revealed: str | Literal[1] | None @@ -509,6 +587,7 @@ def f(flag: bool): None if flag else (g := None) if g is not None: + class F: reveal_type(g) # revealed: str | Literal[1] @@ -518,6 +597,7 @@ def f(flag: bool): # This additional constraint is not relevant to nested scopes, since it only applies to # a binding of `g` that they cannot see: if g is None: + class E: reveal_type(g) # revealed: str | Literal[1] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md b/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md index 633dd83bd2..9fd8d9b4d7 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md @@ -7,8 +7,10 @@ accomplished using an intersection with a synthesized protocol: from typing import final from typing_extensions import LiteralString + class NonFinalClass: ... + def _(obj: NonFinalClass): if hasattr(obj, "spam"): reveal_type(obj) # revealed: NonFinalClass & @@ -32,6 +34,7 @@ a `spam` attribute, so the type is narrowed to `Never`: @final class FinalClass: ... + def _(obj: FinalClass): if hasattr(obj, "spam"): reveal_type(obj) # revealed: Never @@ -51,6 +54,7 @@ change the type. `` is a supertype of `WithSpam`, class WithSpam: spam: int = 42 + def _(obj: WithSpam): if hasattr(obj, "spam"): reveal_type(obj) # revealed: WithSpam @@ -66,10 +70,12 @@ the attribute exists. Here, no `possibly-missing-attribute` error is emitted in def returns_bool() -> bool: return False + class MaybeWithSpam: if returns_bool(): spam: int = 42 + def _(obj: MaybeWithSpam): # error: [possibly-missing-attribute] reveal_type(obj.spam) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 4e7efb7a10..39226e2f19 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -114,6 +114,7 @@ python-version = "3.10" ```py from typing import Any, Literal, NamedTuple + def _(x: int | list[int] | bytes): # error: [invalid-argument-type] if isinstance(x, list[int] | int): @@ -158,6 +159,7 @@ IntOrStr = Union[int, str] reveal_type(IntOrStr) # revealed: + def _(x: int | str | bytes | memoryview | range): if isinstance(x, IntOrStr): reveal_type(x) # revealed: int | str @@ -166,14 +168,17 @@ def _(x: int | str | bytes | memoryview | range): else: reveal_type(x) # revealed: range + def _(x: int | str | None): if isinstance(x, Union[int, None]): reveal_type(x) # revealed: int | None else: reveal_type(x) # revealed: str + ListStrOrInt = Union[list[str], int] + def _(x: dict[int, str] | ListStrOrInt): # TODO: this should ideally be an error if isinstance(x, ListStrOrInt): @@ -191,6 +196,7 @@ def _(x: dict[int, str] | ListStrOrInt): ```py from typing import Optional + def _(x: int | str | None): if isinstance(x, Optional[int]): reveal_type(x) # revealed: int | None @@ -206,6 +212,7 @@ can be used in `isinstance()` and `issubclass()` checks. We support narrowing us ```py import typing as t + def f(x: dict[str, int] | list[str], y: object): if isinstance(x, t.Dict): reveal_type(x) # revealed: dict[str, int] @@ -220,9 +227,14 @@ def f(x: dict[str, int] | list[str], y: object): ```py class A: ... + + class B: ... + + class C: ... + x = object() if isinstance(x, A): @@ -256,6 +268,7 @@ def _(flag: bool, t: type): def _(flag: bool): def isinstance(x, t): return True + x = 1 if flag else "a" if isinstance(x, int): @@ -279,6 +292,7 @@ def _(flag: bool): ```py from builtins import isinstance as imported_isinstance + def _(flag: bool): x = 1 if flag else "a" @@ -344,8 +358,10 @@ We used to incorrectly infer `Literal` booleans for some of these. ```py from ty_extensions import Not, Intersection, AlwaysTruthy, AlwaysFalsy + class P: ... + def f( a: Intersection[P, AlwaysTruthy], b: Intersection[P, AlwaysFalsy], @@ -382,8 +398,10 @@ type of the second argument is a dynamic type: from typing import Any from something_unresolvable import SomethingUnknown # error: [unresolved-import] + class Foo: ... + def f(a: Foo, b: Any): if isinstance(a, SomethingUnknown): reveal_type(a) # revealed: Foo & Unknown @@ -407,14 +425,18 @@ python-version = "3.12" from typing import Any from ty_extensions import Intersection + class Foo: ... + class Bar: attribute: int + class Baz: attribute: str + def f(x: Foo, y: Intersection[type[Bar], type[Baz]], z: type[Any]): if isinstance(x, y): reveal_type(x) # revealed: Foo & Bar & Baz @@ -438,6 +460,7 @@ from typing import TypeVar T = TypeVar("T", bound=type[Bar]) + def h_old_syntax(x: Foo, y: T) -> T: if isinstance(x, y): reveal_type(x) # revealed: Foo & Bar @@ -445,6 +468,7 @@ def h_old_syntax(x: Foo, y: T) -> T: return y + def h[U: type[Bar | Baz]](x: Foo, y: U) -> U: if isinstance(x, y): reveal_type(x) # revealed: (Foo & Bar) | (Foo & Baz) @@ -458,11 +482,19 @@ Or even a tuple of tuple of typevars that have intersection bounds... ```py from ty_extensions import Intersection + class Spam: ... + + class Eggs: ... + + class Ham: ... + + class Mushrooms: ... + def i[T: Intersection[type[Bar], type[Baz | Spam]], U: (type[Eggs], type[Ham])](x: Foo, y: T, z: U) -> tuple[T, U]: if isinstance(x, (y, (z, Mushrooms))): reveal_type(x) # revealed: (Foo & Bar & Baz) | (Foo & Bar & Spam) | (Foo & Eggs) | (Foo & Ham) | (Foo & Mushrooms) @@ -484,10 +516,12 @@ a covariant generic, this is equivalent to using the upper bound of the type par ```py from typing import Self + class Covariant[T]: def get(self) -> T: raise NotImplementedError + def _(x: object): if isinstance(x, Covariant): reveal_type(x) # revealed: Covariant[object] @@ -500,6 +534,7 @@ Similarly, contravariant type parameters use their lower bound of `Never`: class Contravariant[T]: def push(self, x: T) -> None: ... + def _(x: object): if isinstance(x, Contravariant): reveal_type(x) # revealed: Contravariant[Never] @@ -517,6 +552,7 @@ class Invariant[T]: def get(self) -> T: raise NotImplementedError + def _(x: object): if isinstance(x, Invariant): reveal_type(x) # revealed: Top[Invariant[Unknown]] @@ -566,14 +602,18 @@ during type ordering of normalized intersection types. Regression test for ```py from typing import Any, TypedDict, cast + class A(TypedDict): x: str + class B(TypedDict): y: str + T = int | A | B + def test(a: Any, items: list[T]) -> None: combined = a or items v = combined[0] @@ -590,6 +630,7 @@ narrowed. def get_value() -> int | str: return 1 + def f(): if isinstance(x := get_value(), int): reveal_type(x) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md index d07dee6a22..fcbd7a5e48 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -50,10 +50,17 @@ def _(flag1: bool, flag2: bool): ```py class Base: ... + + class Derived1(Base): ... + + class Derived2(Base): ... + + class Unrelated: ... + def _(flag1: bool, flag2: bool, flag3: bool): t1 = Derived1 if flag1 else Derived2 @@ -82,8 +89,11 @@ def _(flag1: bool, flag2: bool, flag3: bool): ```py class A: ... + + class B: ... + def _(t: type[object]): if issubclass(t, A): reveal_type(t) # revealed: type[A] @@ -105,6 +115,7 @@ python-version = "3.10" ```py from types import NoneType + def _(flag: bool): t = int if flag else NoneType @@ -122,6 +133,7 @@ def _(flag: bool): ```py class Unrelated: ... + def _(flag1: bool, flag2: bool): t = int if flag1 else str if flag2 else bytes @@ -211,6 +223,7 @@ IntOrStr = Union[int, str] reveal_type(IntOrStr) # revealed: + def f(x: type[int | str | bytes | range]): if issubclass(x, IntOrStr): reveal_type(x) # revealed: type[int] | type[str] @@ -233,10 +246,12 @@ always succeeds: ```py from typing import final + @final class GenericFinal[T]: x: T # invariant + def f(x: type[GenericFinal]): reveal_type(x) # revealed: @@ -253,6 +268,7 @@ This also works if the typevar has an upper bound: class BoundedGenericFinal[T: int]: x: T # invariant + def g(x: type[BoundedGenericFinal]): reveal_type(x) # revealed: @@ -274,6 +290,7 @@ to `issubclass`: ```py class A: ... + t = object() # error: [invalid-argument-type] @@ -301,9 +318,11 @@ if issubclass(t, int): def issubclass(c, ci): return True + def flag() -> bool: return True + t = int if flag() else str if issubclass(t, int): reveal_type(t) # revealed: | @@ -314,9 +333,11 @@ if issubclass(t, int): ```py issubclass_alias = issubclass + def flag() -> bool: return True + t = int if flag() else str if issubclass_alias(t, int): reveal_type(t) # revealed: @@ -327,9 +348,11 @@ if issubclass_alias(t, int): ```py from builtins import issubclass as imported_issubclass + def flag() -> bool: return True + t = int if flag() else str if imported_issubclass(t, int): reveal_type(t) # revealed: @@ -340,9 +363,11 @@ if imported_issubclass(t, int): ```py from typing import Any + def flag() -> bool: return True + t = int if flag() else str # error: [invalid-argument-type] "Argument to function `issubclass` is incorrect: Expected `type | UnionType | tuple[Divergent, ...]`, found `Literal["str"]" @@ -366,6 +391,7 @@ if issubclass(t, Any): def flag() -> bool: return True + t = int if flag() else str # error: [unknown-argument] @@ -390,13 +416,20 @@ known to be impossible due to the fact that `Meta1` is marked as `@final`. ```py from typing import final + @final class Meta1(type): ... + class Meta2(type): ... + + class UsesMeta1(metaclass=Meta1): ... + + class UsesMeta2(metaclass=Meta2): ... + def _(x: type[UsesMeta1], y: type[UsesMeta2]): if issubclass(x, y): reveal_type(x) # revealed: Never @@ -424,14 +457,18 @@ python-version = "3.12" from typing import Any, ClassVar from ty_extensions import Intersection + class Foo: ... + class Bar: attribute: ClassVar[int] + class Baz: attribute: ClassVar[str] + def f(x: type[Foo], y: Intersection[type[Bar], type[Baz]], z: type[Any]): if issubclass(x, y): reveal_type(x) # revealed: type[Foo] & type[Bar] & type[Baz] @@ -455,6 +492,7 @@ from typing import TypeVar T = TypeVar("T", bound=type[Bar]) + def h_old_syntax(x: type[Foo], y: T) -> T: if issubclass(x, y): reveal_type(x) # revealed: type[Foo] & type[Bar] @@ -462,6 +500,7 @@ def h_old_syntax(x: type[Foo], y: T) -> T: return y + def h[U: type[Bar | Baz]](x: type[Foo], y: U) -> U: if issubclass(x, y): reveal_type(x) # revealed: (type[Foo] & type[Bar]) | (type[Foo] & type[Baz]) @@ -475,11 +514,19 @@ Or even a tuple of tuple of typevars that have intersection bounds... ```py from ty_extensions import Intersection + class Spam: ... + + class Eggs: ... + + class Ham: ... + + class Mushrooms: ... + def i[T: Intersection[type[Bar], type[Baz | Spam]], U: (type[Eggs], type[Ham])](x: type[Foo], y: T, z: U) -> tuple[T, U]: if issubclass(x, (y, (z, Mushrooms))): # revealed: (type[Foo] & type[Bar] & type[Baz]) | (type[Foo] & type[Bar] & type[Spam]) | (type[Foo] & type[Eggs]) | (type[Foo] & type[Ham]) | (type[Foo] & type[Mushrooms]) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/len.md b/crates/ty_python_semantic/resources/mdtest/narrow/len.md index 92f92fcdda..7f949bee94 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/len.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/len.md @@ -14,6 +14,7 @@ The intersection with `~AlwaysFalsy` simplifies to just the non-empty literal. ```py from typing import Literal + def _(x: Literal["foo", ""]): if len(x): reveal_type(x) # revealed: Literal["foo"] @@ -26,6 +27,7 @@ def _(x: Literal["foo", ""]): ```py from typing import Literal + def _(x: Literal[b"foo", b""]): if len(x): reveal_type(x) # revealed: Literal[b"foo"] @@ -43,6 +45,7 @@ python-version = "3.11" ```py from typing import LiteralString + def _(x: LiteralString): if len(x): reveal_type(x) # revealed: LiteralString & ~Literal[""] @@ -68,6 +71,7 @@ def _(x: tuple[int, ...]): ```py from typing import Literal + def _(x: Literal["foo", ""] | tuple[int, ...]): if len(x): reveal_type(x) # revealed: Literal["foo"] | (tuple[int, ...] & ~AlwaysFalsy) @@ -84,6 +88,7 @@ types, and the truthiness of the `__len__` return type is consistent with the tr ```py from typing import Literal + class Foo: def __bool__(self) -> Literal[True]: return True @@ -91,6 +96,7 @@ class Foo: def __len__(self) -> Literal[42]: return 42 + class Bar: def __bool__(self) -> Literal[False]: return False @@ -98,6 +104,7 @@ class Bar: def __len__(self) -> Literal[0]: return 0 + class Inconsistent1: def __bool__(self) -> Literal[True]: return True @@ -105,6 +112,7 @@ class Inconsistent1: def __len__(self) -> Literal[0]: return 0 + class Inconsistent2: def __bool__(self) -> Literal[False]: return False @@ -112,6 +120,7 @@ class Inconsistent2: def __len__(self) -> Literal[42]: return 42 + def f( a: Foo | list[int], b: Bar | list[int], @@ -169,6 +178,7 @@ def not_narrowed_str(x: str): # No narrowing because `str` could be subclassed with a custom `__bool__` reveal_type(x) # revealed: str + def not_narrowed_list(x: list[int]): if len(x): # No narrowing because `list` could be subclassed with a custom `__bool__` @@ -183,6 +193,7 @@ leaving the non-narrowable parts unchanged: ```py from typing import Literal + def _(x: Literal["foo", ""] | list[int]): if len(x): # `Literal[""]` is removed, `list[int]` is unchanged diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 357c20155e..70c95e0275 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -28,9 +28,13 @@ def _(flag: bool): def get_object() -> object: return object() + class A: ... + + class B: ... + x = get_object() reveal_type(x) # revealed: object @@ -50,12 +54,15 @@ reveal_type(x) # revealed: object def get_object() -> object: return object() + class A: def y() -> int: return 1 + class B: ... + x = get_object() reveal_type(x) # revealed: object @@ -79,10 +86,12 @@ python-version = "3.12" ```py from typing import assert_never + class Covariant[T]: def get(self) -> T: raise NotImplementedError + def f(x: Covariant[int]): match x: case Covariant(): @@ -104,11 +113,13 @@ python-version = "3.12" ```py from typing import assert_never, final + @final class Covariant[T]: def get(self) -> T: raise NotImplementedError + def f(x: Covariant[int]): match x: case Covariant(): @@ -129,6 +140,7 @@ from typing import Any X = Any + def f(obj: object): match obj: case int(): @@ -136,6 +148,7 @@ def f(obj: object): case X(): reveal_type(obj) # revealed: Any & ~int + def g(obj: object, Y: Any): match obj: case int(): @@ -154,6 +167,7 @@ Consider the following example. ```py from typing import Literal + def _(x: Literal["foo"] | int): match x: case "foo": @@ -176,9 +190,11 @@ More examples follow. ```py from typing import Literal + class C: pass + def _(x: Literal["foo", "bar", 42, b"foo"] | bool | complex): match x: case "foo": @@ -200,9 +216,11 @@ def _(x: Literal["foo", "bar", 42, b"foo"] | bool | complex): ```py from typing import Literal + class C: pass + def _(x: Literal["foo", b"bar"] | int): match x: case "foo" if reveal_type(x): # revealed: Literal["foo"] | int @@ -219,11 +237,13 @@ def _(x: Literal["foo", b"bar"] | int): from typing import Literal from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 + def _(color: Color): match color: case Color.RED | Color.GREEN: @@ -241,10 +261,16 @@ def _(color: Color): case _: reveal_type(color) # revealed: Literal[Color.GREEN, Color.BLUE] + class A: ... + + class B: ... + + class C: ... + def _(x: A | B | C): match x: case A() | B(): @@ -272,6 +298,7 @@ def _(x: A | B | C): ```py from typing import Literal + def _(x: Literal["foo", b"bar"] | int): match x: case "foo" | 42 if reveal_type(x): # revealed: Literal["foo"] | int @@ -288,6 +315,7 @@ def _(x: Literal["foo", b"bar"] | int): def get_object() -> object: return object() + x = get_object() reveal_type(x) # revealed: object @@ -311,6 +339,7 @@ reveal_type(x) # revealed: object def get_object() -> object: return object() + x = get_object() reveal_type(x) # revealed: object @@ -339,6 +368,7 @@ to return `self` in the `assert_yes` method below: from enum import Enum from typing_extensions import Self, assert_never + class Answer(Enum): NO = 0 YES = 1 @@ -368,6 +398,7 @@ class Answer(Enum): reveal_type(self) # revealed: Self@assert_yes & ~Literal[Answer.YES] raise ValueError("Answer is not YES") + Answer.YES.is_yes() try: @@ -383,10 +414,16 @@ Narrow unions of tuples based on literal tag elements in `match` statements: ```py from typing import Literal + class A: ... + + class B: ... + + class C: ... + def _(x: tuple[Literal["tag1"], A] | tuple[Literal["tag2"], B, C]): match x[0]: case "tag1": @@ -399,6 +436,7 @@ def _(x: tuple[Literal["tag1"], A] | tuple[Literal["tag2"], B, C]): case _: reveal_type(x) # revealed: Never + # With int literals def _(x: tuple[Literal[1], A] | tuple[Literal[2], B]): match x[0]: @@ -409,6 +447,7 @@ def _(x: tuple[Literal[1], A] | tuple[Literal[2], B]): case _: reveal_type(x) # revealed: Never + # With bytes literals def _(x: tuple[Literal[b"a"], A] | tuple[Literal[b"b"], B]): match x[0]: @@ -419,6 +458,7 @@ def _(x: tuple[Literal[b"a"], A] | tuple[Literal[b"b"], B]): case _: reveal_type(x) # revealed: Never + # Using index 1 instead of 0 def _(x: tuple[A, Literal["tag1"]] | tuple[B, Literal["tag2"]]): match x[1]: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md index a7666dcf53..dd9881dc8d 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -5,9 +5,11 @@ ```py from typing import Literal + def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[()]: return 0 + x = foo() if x: @@ -54,12 +56,15 @@ Basically functions are always truthy. def flag() -> bool: return True + def foo(hello: int) -> bytes: return b"" + def bar(world: str, *args, **kwargs) -> float: return 0.0 + x = foo if flag() else bar if x: @@ -78,8 +83,11 @@ in the list. Therefore, these types should not be narrowed by `if x` or `if not ```py class A: ... + + class B: ... + def f(x: A | B): if x: reveal_type(x) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy) @@ -107,6 +115,7 @@ more accurately. def flag() -> bool: return True + x = int if flag() else str reveal_type(x) # revealed: | @@ -127,14 +136,17 @@ These types can always be fully narrowed in boolean contexts, as shown below: ```py from typing import Literal + class T: def __bool__(self) -> Literal[True]: return True + class F: def __bool__(self) -> Literal[False]: return False + t = T() if t: @@ -155,18 +167,25 @@ else: ```py from typing import Literal + class A: ... + + class B: ... + def flag() -> bool: return True + def instance() -> A | B: return A() + def literals() -> Literal[0, 42, "", "hello"]: return 42 + x = instance() y = literals() @@ -189,6 +208,7 @@ if isinstance(x, str) and not isinstance(x, B): ```py from typing import Literal + def f(x: Literal[0, 1], y: Literal["", "hello"]): if x and y and not x and not y: reveal_type(x) # revealed: Never @@ -215,6 +235,7 @@ should return to the original state. ```py class A: ... + x = A() if x and not x: @@ -232,27 +253,39 @@ reveal_type(y) # revealed: A ```py from typing import Literal + class MetaAmbiguous(type): def __bool__(self) -> bool: return True + class MetaFalsy(type): def __bool__(self) -> Literal[False]: return False + class MetaTruthy(type): def __bool__(self) -> Literal[True]: return True + class MetaDeferred(type): def __bool__(self) -> MetaAmbiguous: raise NotImplementedError + class AmbiguousClass(metaclass=MetaAmbiguous): ... + + class FalsyClass(metaclass=MetaFalsy): ... + + class TruthyClass(metaclass=MetaTruthy): ... + + class DeferredClass(metaclass=MetaDeferred): ... + def _( a: type[AmbiguousClass], t: type[TruthyClass], @@ -289,43 +322,56 @@ def _( ```py from typing import Literal + class A: ... + def _(x: Literal[0, 1]): reveal_type(x or A()) # revealed: Literal[1] | A reveal_type(x and A()) # revealed: Literal[0] | A + def _(x: str): reveal_type(x or A()) # revealed: (str & ~AlwaysFalsy) | A reveal_type(x and A()) # revealed: (str & ~AlwaysTruthy) | A + def _(x: bool | str): reveal_type(x or A()) # revealed: Literal[True] | (str & ~AlwaysFalsy) | A reveal_type(x and A()) # revealed: Literal[False] | (str & ~AlwaysTruthy) | A + class Falsy: def __bool__(self) -> Literal[False]: return False + class Truthy: def __bool__(self) -> Literal[True]: return True + def _(x: Falsy | Truthy): reveal_type(x or A()) # revealed: Truthy | A reveal_type(x and A()) # revealed: Falsy | A + class MetaFalsy(type): def __bool__(self) -> Literal[False]: return False + class MetaTruthy(type): def __bool__(self) -> Literal[True]: return True + class FalsyClass(metaclass=MetaFalsy): ... + + class TruthyClass(metaclass=MetaTruthy): ... + def _(x: type[FalsyClass] | type[TruthyClass]): reveal_type(x or A()) # revealed: type[TruthyClass] | A reveal_type(x and A()) # revealed: type[FalsyClass] | A @@ -336,6 +382,7 @@ def _(x: type[FalsyClass] | type[TruthyClass]): ```py from typing_extensions import LiteralString + def _(x: LiteralString): if x: reveal_type(x) # revealed: LiteralString & ~Literal[""] @@ -357,6 +404,7 @@ be narrowed. def get_value() -> str | None: return "hello" + def f(): if x := get_value(): reveal_type(x) # revealed: str & ~AlwaysFalsy diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type.md b/crates/ty_python_semantic/resources/mdtest/narrow/type.md index 68db43a4c8..c3c433eadf 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type.md @@ -5,12 +5,17 @@ ```py from typing import final + class A: ... + + class B: ... + @final class C: ... + def _(x: A | B, y: A | C): if type(x) is A: reveal_type(x) # revealed: A @@ -43,12 +48,17 @@ def _(x: A | B, y: A | C): ```py from typing import final + class A: ... + + class B: ... + @final class C: ... + def _(x: A | B, y: A | C): if type(x) is not A: # Same reasoning as above: no narrowing should occur here. @@ -85,6 +95,7 @@ def f(x: list[int] | None): else: reveal_type(x) # revealed: list[int] + # frozenset is covariant def g(x: frozenset[bytes] | None): if type(x) is frozenset: @@ -97,6 +108,7 @@ def g(x: frozenset[bytes] | None): else: reveal_type(x) # revealed: frozenset[bytes] + def h(x: object): if type(x) is list: reveal_type(x) # revealed: Top[list[Unknown]] @@ -123,8 +135,11 @@ python-version = "3.12" ```py class A[T]: ... + + class B: ... + def f(x: A[int] | B): if type(x) is A[int]: # this branch is actually unreachable -- we *could* reveal `Never` here! @@ -172,9 +187,13 @@ class IsEqualToEverything(type): def __eq__(cls, other): return True + class A(metaclass=IsEqualToEverything): ... + + class B(metaclass=IsEqualToEverything): ... + def _(x: A | B, y: object): if type(x) == A: reveal_type(x) # revealed: A | B @@ -192,11 +211,15 @@ def _(x: A | B, y: object): ```py class A: ... + + class B: ... + def type(x): return int + def _(x: A | B): if type(x) is A: reveal_type(x) # revealed: A | B @@ -235,8 +258,11 @@ def _(x: str | int): ```py class A: ... + + class B: ... + def _(x: A | B): alias_for_type = type @@ -258,8 +284,11 @@ class. ```py class A[T = int]: ... + + class B: ... + def _[T](x: A | B): if type(x) is A[str]: # TODO: `type()` never returns a generic alias, so `type(x)` cannot be `A[str]` @@ -282,8 +311,11 @@ def _(val): ```py class Base: ... + + class Derived(Base): ... + def _(x: Base): if type(x) is Base: # Ideally, this could be narrower, but there is now way to diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index 1036f61df2..647d3cbe90 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -9,6 +9,7 @@ User-defined type guards are functions of which the return type is either `TypeG from ty_extensions import Intersection, Not, TypeOf from typing_extensions import TypeGuard, TypeIs + def _( a: TypeGuard[str], b: TypeIs[str | int], @@ -24,17 +25,21 @@ def _( reveal_type(e) # revealed: Unknown reveal_type(f) # revealed: Unknown + # error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeGuard[str]`" def _(a) -> TypeGuard[str]: ... + # error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeIs[str]`" def _(a) -> TypeIs[str]: ... def f(a) -> TypeGuard[str]: return True + def g(a) -> TypeIs[str]: return True + def _(a: object): reveal_type(f(a)) # revealed: TypeGuard[str @ a] reveal_type(g(a)) # revealed: TypeIs[str @ a] @@ -111,6 +116,7 @@ Methods narrow the first positional argument after `self` or `cls` ```py from typing import TypeGuard + class C: def f(self, x: object) -> TypeGuard[str]: return True @@ -128,6 +134,7 @@ class C: def j(cls) -> TypeGuard[int]: # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow" return True + def _(x: object): if C().f(x): reveal_type(x) # revealed: str @@ -147,9 +154,11 @@ def _(x: object): ```py from typing_extensions import TypeIs + def is_int(val: object) -> TypeIs[int]: return isinstance(val, int) + class A: def is_int(self, val: object) -> TypeIs[int]: return isinstance(val, int) @@ -158,6 +167,7 @@ class A: def is_int2(cls, val: object) -> TypeIs[int]: return isinstance(val, int) + def _(x: object): if is_int(x): reveal_type(x) # revealed: int @@ -181,13 +191,16 @@ from typing_extensions import TypeGuard, TypeIs a = 123 + # error: [invalid-type-form] "Special form `typing.TypeGuard` expected exactly one type parameter" def f(_) -> TypeGuard[int, str]: ... + # error: [invalid-type-form] "Special form `typing.TypeIs` expected exactly one type parameter" # error: [invalid-type-form] "Variable of type `Literal[123]` is not allowed in a type expression" def g(_) -> TypeIs[a, str]: ... + reveal_type(f(0)) # revealed: Unknown reveal_type(g(0)) # revealed: Unknown ``` @@ -199,6 +212,7 @@ All code paths in a type guard function must return booleans. ```py from typing_extensions import Literal, TypeGuard, TypeIs, assert_never + def _(a: object, flag: bool) -> TypeGuard[str]: if flag: # error: [invalid-return-type] "Return type does not match returned value: expected `TypeGuard[str]`, found `Literal[0]`" @@ -207,12 +221,14 @@ def _(a: object, flag: bool) -> TypeGuard[str]: # error: [invalid-return-type] "Return type does not match returned value: expected `TypeGuard[str]`, found `Literal["foo"]`" return "foo" + # error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`" def f(a: object, flag: bool) -> TypeIs[str]: if flag: # error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `float`" return 1.2 + def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]: if a == "foo": # Logically wrong, but allowed regardless @@ -227,12 +243,15 @@ def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]: from typing import Any from typing_extensions import TypeGuard, TypeIs + def f(a: object) -> TypeGuard[str]: return True + def g(a: object) -> TypeIs[int]: return True + def _(d: Any): if f(): # error: [missing-argument] "No argument provided for required parameter `a` of function `f`" ... @@ -258,15 +277,21 @@ python-version = "3.12" from typing import Any from typing_extensions import TypeGuard, TypeIs + class Foo: ... + + class Bar: ... + def guard_foo(a: object) -> TypeGuard[Foo]: return True + def is_bar(a: object) -> TypeIs[Bar]: return True + def _(a: Foo | Bar): if guard_foo(a): reveal_type(a) # revealed: Foo @@ -282,18 +307,23 @@ def _(a: Foo | Bar): ```py from typing import TypeGuard, reveal_type + class P: pass + class A: pass + class B: pass + def is_b(val: object) -> TypeGuard[B]: return isinstance(val, B) + def _(x: P): if isinstance(x, A) or is_b(x): reveal_type(x) # revealed: B | (P & A) @@ -306,9 +336,11 @@ from typing_extensions import Any, Generic, Protocol, TypeVar T = TypeVar("T") + class C(Generic[T]): v: T + def _(a: tuple[Foo, Bar] | tuple[Bar, Foo], c: C[Any]): if reveal_type(guard_foo(a[1])): # revealed: TypeGuard[Foo @ a[1]] reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo] @@ -373,12 +405,15 @@ from typing_extensions import TypeVar T = TypeVar("T") + def f(v: object) -> TypeIs[Bar]: return True + def g(v: T) -> T: return v + def _(a: Foo): # `reveal_type()` has the type `[T]() -> T` if reveal_type(f(a)): # revealed: TypeIs[Bar @ a] @@ -399,16 +434,20 @@ transformation from `TypeIs[SomeCovariantGeneric[Any]]` to `TypeIs[Top[SomeCovar ```py class Unrelated: ... + class Covariant[T]: def get(self) -> T: raise NotImplementedError + def is_instance_of_covariant(arg: object) -> TypeIs[Covariant[Any]]: return isinstance(arg, Covariant) + def needs_instance_of_unrelated(arg: Unrelated): pass + def _(x: Unrelated | Covariant[int]): if is_instance_of_covariant(x): raise RuntimeError("oh no") @@ -426,25 +465,35 @@ def _(x: Unrelated | Covariant[int]): from typing import Any from typing_extensions import TypeGuard, TypeIs + class Foo: ... + + class Bar: ... + + class Baz(Bar): ... + def guard_foo(a: object) -> TypeGuard[Foo]: return True + def guard_bar(a: object) -> TypeGuard[Bar]: return True + def is_bar(a: object) -> TypeIs[Bar]: return True + def does_not_narrow_in_negative_case(a: Foo | Bar): if not guard_foo(a): reveal_type(a) # revealed: Foo | Bar else: reveal_type(a) # revealed: Foo + def narrowed_type_must_be_exact(a: object, b: Baz): if guard_foo(b): reveal_type(b) # revealed: Foo @@ -466,19 +515,28 @@ added on to TypeGuard constraints. ```py from typing_extensions import TypeGuard, TypeIs + class A: ... + + class B: ... + + class C: ... + def f(x: object) -> TypeGuard[A]: return True + def g(x: object) -> TypeGuard[B]: return True + def h(x: object) -> TypeIs[C]: return True + def _(x: object): if f(x) and g(x) and h(x): reveal_type(x) # revealed: B & C @@ -491,19 +549,28 @@ TypeGuard constraints need to properly distribute through boolean operations. ```py from typing_extensions import TypeGuard, TypeIs + class A: ... + + class B: ... + + class C: ... + def f(x: object) -> TypeIs[A]: return True + def g(x: object) -> TypeGuard[B]: return True + def h(x: object) -> TypeIs[C]: return True + def _(x: object): # g(x) or h(x) should give B | C # Then f(x) and (...) should distribute: (f(x) and g(x)) or (f(x) and h(x)) @@ -521,15 +588,19 @@ narrowed. ```py from typing_extensions import TypeGuard, TypeIs + def is_str(x: object) -> TypeIs[str]: return isinstance(x, str) + def guard_str(x: object) -> TypeGuard[str]: return isinstance(x, str) + def get_value() -> int | str: return 1 + def f(): if is_str(x := get_value()): reveal_type(x) # revealed: str diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/while.md b/crates/ty_python_semantic/resources/mdtest/narrow/while.md index deae318666..53ccc6a0f9 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/while.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/while.md @@ -12,6 +12,7 @@ is retained after the loop. def next_item() -> int | None: return 1 + x = next_item() while x is not None: @@ -27,6 +28,7 @@ reveal_type(x) # revealed: None def next_item() -> int | None: return 1 + x = next_item() while x is not None: @@ -43,9 +45,11 @@ reveal_type(x) # revealed: None ```py from typing import Literal + def next_item() -> Literal[1, 2, 3]: raise NotImplementedError + x = next_item() while x != 1: @@ -66,6 +70,7 @@ while x != 1: def next_item() -> int | None: return 1 + while True: x = next_item() if x is not None: diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index d688a2042c..da5d1636db 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -9,9 +9,11 @@ The definition of `typing.overload` in typeshed is an identity function. ```py from typing import overload + def foo(x: int) -> int: return x + reveal_type(foo) # revealed: def foo(x: int) -> int bar = overload(foo) reveal_type(bar) # revealed: def foo(x: int) -> int @@ -22,6 +24,7 @@ reveal_type(bar) # revealed: def foo(x: int) -> int ```py from typing import overload + @overload def add() -> None: ... @overload @@ -31,6 +34,7 @@ def add(x: int, y: int) -> int: ... def add(x: int | None = None, y: int | None = None) -> int | None: return (x or 0) + (y or 0) + reveal_type(add) # revealed: Overload[() -> None, (x: int) -> int, (x: int, y: int) -> int] reveal_type(add()) # revealed: None reveal_type(add(1)) # revealed: int @@ -47,6 +51,7 @@ An overloaded function is overriding another overloaded function: ```py from typing import overload + @overload def foo() -> None: ... @overload @@ -54,10 +59,12 @@ def foo(x: int) -> int: ... def foo(x: int | None = None) -> int | None: return x + reveal_type(foo) # revealed: Overload[() -> None, (x: int) -> int] reveal_type(foo()) # revealed: None reveal_type(foo(1)) # revealed: int + @overload def foo() -> None: ... @overload @@ -65,6 +72,7 @@ def foo(x: str) -> str: ... def foo(x: str | None = None) -> str | None: return x + reveal_type(foo) # revealed: Overload[() -> None, (x: str) -> str] reveal_type(foo()) # revealed: None reveal_type(foo("")) # revealed: str @@ -76,6 +84,7 @@ A non-overloaded function is overriding an overloaded function: def foo(x: int) -> int: return x + reveal_type(foo) # revealed: def foo(x: int) -> int ``` @@ -84,6 +93,7 @@ An overloaded function is overriding a non-overloaded function: ```py reveal_type(foo) # revealed: def foo(x: int) -> int + @overload def foo() -> None: ... @overload @@ -91,6 +101,7 @@ def foo(x: bytes) -> bytes: ... def foo(x: bytes | None = None) -> bytes | None: return x + reveal_type(foo) # revealed: Overload[() -> None, (x: bytes) -> bytes] reveal_type(foo()) # revealed: None reveal_type(foo(b"")) # revealed: bytes @@ -101,6 +112,7 @@ reveal_type(foo(b"")) # revealed: bytes ```py from typing_extensions import Self, overload + class Foo1: @overload def method(self) -> None: ... @@ -109,11 +121,13 @@ class Foo1: def method(self, x: int | None = None) -> int | None: return x + foo1 = Foo1() reveal_type(foo1.method) # revealed: Overload[() -> None, (x: int) -> int] reveal_type(foo1.method()) # revealed: None reveal_type(foo1.method(1)) # revealed: int + class Foo2: @overload def method(self) -> None: ... @@ -122,11 +136,13 @@ class Foo2: def method(self, x: str | None = None) -> str | None: return x + foo2 = Foo2() reveal_type(foo2.method) # revealed: Overload[() -> None, (x: str) -> str] reveal_type(foo2.method()) # revealed: None reveal_type(foo2.method("")) # revealed: str + class Foo3: @overload def takes_self_or_int(self: Self, x: Self) -> Self: ... @@ -135,6 +151,7 @@ class Foo3: def takes_self_or_int(self: Self, x: Self | int) -> Self | int: return x + foo3 = Foo3() reveal_type(foo3.takes_self_or_int(foo3)) # revealed: Foo3 reveal_type(foo3.takes_self_or_int(1)) # revealed: int @@ -145,6 +162,7 @@ reveal_type(foo3.takes_self_or_int(1)) # revealed: int ```py from typing import overload + class Foo: @overload def __init__(self) -> None: ... @@ -153,6 +171,7 @@ class Foo: def __init__(self, x: int | None = None) -> None: self.x = x + foo = Foo() reveal_type(foo) # revealed: Foo reveal_type(foo.x) # revealed: Unknown | int | None @@ -180,10 +199,12 @@ import sys from typing import overload if sys.version_info < (3, 10): + def func(x: int) -> int: return x elif sys.version_info <= (3, 12): + @overload def func() -> None: ... @overload @@ -191,6 +212,7 @@ elif sys.version_info <= (3, 12): def func(x: int | None = None) -> int | None: return x + reveal_type(func) # revealed: def func(x: int) -> int func() # error: [missing-argument] ``` @@ -207,10 +229,12 @@ import sys from typing import overload if sys.version_info < (3, 10): + def func(x: int) -> int: return x elif sys.version_info <= (3, 12): + @overload def func() -> None: ... @overload @@ -218,6 +242,7 @@ elif sys.version_info <= (3, 12): def func(x: int | None = None) -> int | None: return x + reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int] reveal_type(func()) # revealed: None reveal_type(func(1)) # revealed: int @@ -304,6 +329,7 @@ For an overloaded generic function, it's not necessary for all overloads to be g ```py from typing import overload + @overload def func() -> None: ... @overload @@ -311,6 +337,7 @@ def func[T](x: T) -> T: ... def func[T](x: T | None = None) -> T | None: return x + reveal_type(func) # revealed: Overload[() -> None, [T](x: T) -> T] reveal_type(func()) # revealed: None reveal_type(func(1)) # revealed: Literal[1] @@ -328,9 +355,11 @@ At least two `@overload`-decorated definitions must be present. ```py from typing import overload + @overload def func(x: int) -> int: ... + # error: [invalid-overload] def func(x: int | str) -> int | str: return x @@ -356,12 +385,14 @@ non-`@overload`-decorated definition (for the same function/method). ```py from typing import overload + @overload def func(x: int) -> int: ... @overload # error: [invalid-overload] "Overloads for function `func` must be followed by a non-`@overload`-decorated implementation function" def func(x: str) -> str: ... + class Foo: @overload def method(self, x: int) -> int: ... @@ -390,6 +421,7 @@ Overload definitions within protocols are exempt from this check. ```py from typing import Protocol, overload + class Foo(Protocol): @overload def f(self, x: int) -> int: ... @@ -405,6 +437,7 @@ Overload definitions within abstract base classes are exempt from this check. from abc import ABC, abstractmethod from typing import overload + class AbstractFoo(ABC): @overload @abstractmethod @@ -420,8 +453,10 @@ from it. ```py from abc import ABCMeta + class CustomAbstractMetaclass(ABCMeta): ... + class Fine(metaclass=CustomAbstractMetaclass): @overload @abstractmethod @@ -430,6 +465,7 @@ class Fine(metaclass=CustomAbstractMetaclass): @abstractmethod def f(self, x: str) -> str: ... + class Foo: @overload @abstractmethod @@ -451,6 +487,7 @@ class PartialFoo1(ABC): # error: [invalid-overload] def f(self, x: str) -> str: ... + class PartialFoo(ABC): @overload def f(self, x: int) -> int: ... @@ -470,6 +507,7 @@ an `if TYPE_CHECKING` block: from typing import overload, TYPE_CHECKING if TYPE_CHECKING: + @overload def a() -> str: ... @overload @@ -481,25 +519,34 @@ if TYPE_CHECKING: @overload def method(self, x: int) -> int: ... + class G: if TYPE_CHECKING: + @overload def method(self) -> None: ... @overload def method(self, x: int) -> int: ... + if TYPE_CHECKING: + @overload def b() -> str: ... + if TYPE_CHECKING: + @overload def b(x: int) -> int: ... + if TYPE_CHECKING: + @overload def c() -> None: ... + # not all overloads are in a `TYPE_CHECKING` block, so this is an error @overload # error: [invalid-overload] @@ -518,22 +565,26 @@ on the part of the user. We emit a warning-level diagnostic to alert them of thi ```py from typing import overload + @overload def x(y: int) -> int: ... @overload def x(y: str) -> str: """Docstring""" + @overload def x(y: bytes) -> bytes: pass + @overload def x(y: memoryview) -> memoryview: """More docs""" pass ... + def x(y): return y ``` @@ -545,12 +596,14 @@ Anything else, however, will trigger the lint: def foo(x: int) -> int: return x # error: [useless-overload-body] + @overload def foo(x: str) -> None: """Docstring""" pass print("oh no, a string") # error: [useless-overload-body] + def foo(x): return x ``` @@ -567,6 +620,7 @@ from __future__ import annotations from typing import overload + class CheckStaticMethod: @overload def method1(x: int) -> int: ... @@ -593,6 +647,7 @@ class CheckStaticMethod: @overload @staticmethod def method3(x: str) -> str: ... + # error: [invalid-overload] def method3(x: int | str) -> int | str: return x @@ -619,6 +674,7 @@ from __future__ import annotations from typing import overload + class CheckClassMethod: def __init__(self, x: int) -> None: self.x = x @@ -653,6 +709,7 @@ class CheckClassMethod: @overload @classmethod def try_from3(cls, x: str) -> None: ... + # error: [invalid-overload] def try_from3(cls, x: int | str) -> CheckClassMethod | None: if isinstance(x, int): @@ -683,6 +740,7 @@ only to the overload implementation if it is present. ```py from typing_extensions import final, overload + class Foo: @overload def method1(self, x: int) -> int: ... @@ -697,6 +755,7 @@ class Foo: def method2(self, x: int) -> int: ... @overload def method2(self, x: str) -> str: ... + # error: [invalid-overload] def method2(self, x: int | str) -> int | str: return x @@ -706,6 +765,7 @@ class Foo: @overload @final def method3(self, x: str) -> str: ... + # error: [invalid-overload] def method3(self, x: int | str) -> int | str: return x @@ -741,6 +801,7 @@ The same rules apply for `@override` as for [`@final`](#final). ```py from typing_extensions import overload, override + class Base: @overload def method(self, x: int) -> int: ... @@ -749,6 +810,7 @@ class Base: def method(self, x: int | str) -> int | str: return x + class Sub1(Base): @overload def method(self, x: int) -> int: ... @@ -758,22 +820,26 @@ class Sub1(Base): def method(self, x: int | str) -> int | str: return x + class Sub2(Base): @overload def method(self, x: int) -> int: ... @overload @override def method(self, x: str) -> str: ... + # error: [invalid-overload] def method(self, x: int | str) -> int | str: return x + class Sub3(Base): @overload @override def method(self, x: int) -> int: ... @overload def method(self, x: str) -> str: ... + # error: [invalid-overload] def method(self, x: int | str) -> int | str: return x diff --git a/crates/ty_python_semantic/resources/mdtest/override.md b/crates/ty_python_semantic/resources/mdtest/override.md index 0cf810c418..66d45a5621 100644 --- a/crates/ty_python_semantic/resources/mdtest/override.md +++ b/crates/ty_python_semantic/resources/mdtest/override.md @@ -228,24 +228,31 @@ class Foo: ```py from typing_extensions import override + def coinflip() -> bool: return False + class Parent: if coinflip(): + def method1(self) -> None: ... def method2(self) -> None: ... if coinflip(): + def method3(self) -> None: ... def method4(self) -> None: ... + else: + def method3(self) -> None: ... def method4(self) -> None: ... def method5(self) -> None: ... def method6(self) -> None: ... + class Child(Parent): @override def method1(self) -> None: ... @@ -253,35 +260,47 @@ class Child(Parent): def method2(self) -> None: ... if coinflip(): + @override def method3(self) -> None: ... if coinflip(): + @override def method4(self) -> None: ... + else: + @override def method4(self) -> None: ... if coinflip(): + @override def method5(self) -> None: ... if coinflip(): + @override def method6(self) -> None: ... + else: + @override def method6(self) -> None: ... if coinflip(): + @override def method7(self) -> None: ... # error: [invalid-explicit-override] if coinflip(): + @override def method8(self) -> None: ... # error: [invalid-explicit-override] + else: + @override def method8(self) -> None: ... ``` @@ -296,13 +315,18 @@ necessarily be the first definition of the symbol overall: ```py from typing_extensions import override, overload + def coinflip() -> bool: return True + class Foo: if coinflip(): + def method(self, x): ... + elif coinflip(): + @overload def method(self, x: str) -> str: ... @overload @@ -310,7 +334,9 @@ class Foo: @override def method(self, x: str | int) -> str | int: # error: [invalid-explicit-override] return x + elif coinflip(): + @override def method(self, x): ... ``` @@ -361,15 +387,20 @@ python-version = "3.10" import sys from typing_extensions import override, overload + class Parent: if sys.version_info >= (3, 10): + def foo(self) -> None: ... def foooo(self) -> None: ... + else: + def bar(self) -> None: ... def baz(self) -> None: ... def spam(self) -> None: ... + class Child(Parent): @override def foo(self) -> None: ... @@ -380,10 +411,12 @@ class Child(Parent): def bar(self) -> None: ... # error: [invalid-explicit-override] if sys.version_info >= (3, 10): + @override def foooo(self) -> None: ... @override def baz(self) -> None: ... # error: [invalid-explicit-override] + else: # This doesn't override any reachable definitions, # but the subclass definition also isn't a reachable definition @@ -404,6 +437,7 @@ though we also emit `invalid-overload` on these methods. ```py from typing_extensions import override, overload + class Spam: @overload def foo(self, x: str) -> str: ... @@ -508,13 +542,18 @@ class Foo: from typing_extensions import Any, override from does_not_exist import SomethingUnknown # error: [unresolved-import] + class Parent1(Any): ... + + class Parent2(SomethingUnknown): ... + class Child1(Parent1): @override def bar(self): ... # fine + class Child2(Parent2): @override def bar(self): ... # fine diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index 29b13ee4b3..63b9c39fbc 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -14,6 +14,7 @@ from typing import TypeAlias IntOrStr: TypeAlias = int | str + def _(x: IntOrStr): reveal_type(x) # revealed: int | str ``` @@ -25,6 +26,7 @@ import typing IntOrStr: typing.TypeAlias = int | str + def _(x: IntOrStr): reveal_type(x) # revealed: int | str ``` @@ -54,8 +56,10 @@ from ty_extensions import is_subtype_of, static_assert MyList: TypeAlias = list["int"] + class Foo(MyList): ... + static_assert(is_subtype_of(Foo, list[int])) ``` @@ -66,6 +70,7 @@ from typing import TypeAlias MyList: TypeAlias = "list[int]" + # error: [invalid-base] "Invalid class base with type `str`" class Foo(MyList): ... ``` @@ -81,6 +86,7 @@ from nonexistent import unknown_type # error: [unresolved-import] MyAlias: TypeAlias = int | unknown_type | str + def _(x: MyAlias): reveal_type(x) # revealed: int | Unknown | str ``` @@ -92,6 +98,7 @@ from typing import TypeAlias, Callable MyAlias: TypeAlias = int | Callable[[str], int] + def _(x: MyAlias): reveal_type(x) # revealed: int | ((str, /) -> int) ``` @@ -115,6 +122,7 @@ ListOrSet: TypeAlias = list[T] | set[T] reveal_type(MyList) # revealed: reveal_type(ListOrSet) # revealed: + def _(list_of_int: MyList[int], list_or_set_of_str: ListOrSet[str]): reveal_type(list_of_int) # revealed: list[int] reveal_type(list_or_set_of_str) # revealed: list[str] | set[str] @@ -131,6 +139,7 @@ U = TypeVar("U") TotallyStringifiedPEP613: TypeAlias = "dict[T, U]" TotallyStringifiedPartiallySpecialized: TypeAlias = "TotallyStringifiedPEP613[U, int]" + def f(x: "TotallyStringifiedPartiallySpecialized[str]"): reveal_type(x) # revealed: @Todo(Generic stringified PEP-613 type alias) ``` @@ -145,6 +154,7 @@ T = TypeVar("T") Alias1: TypeAlias = list[T] | set[T] MyAlias: TypeAlias = int | Alias1[str] + def _(x: MyAlias): reveal_type(x) # revealed: int | list[str] | set[str] ``` @@ -163,6 +173,7 @@ T = TypeVar("T") MyAlias1: TypeAlias = UnknownClass[T] | None + def _(a: MyAlias1[int]): reveal_type(a) # revealed: Unknown | None ``` @@ -175,6 +186,7 @@ V = TypeVar("V") MyAlias2: TypeAlias = UnknownClass[T, U, V] | int + def _(a: MyAlias2[int, str, bytes]): reveal_type(a) # revealed: Unknown | int ``` @@ -195,6 +207,7 @@ We can also reference these type aliases from other type aliases: ```py MyAlias3: TypeAlias = MyAlias1[str] | MyAlias2[int, str, bytes] + def _(c: MyAlias3): reveal_type(c) # revealed: Unknown | None | int ``` @@ -206,20 +219,24 @@ from typing_extensions import Callable, Concatenate, TypeAliasType MyAlias4: TypeAlias = Callable[Concatenate[dict[str, T], ...], list[U]] + def _(c: MyAlias4[int, str]): # TODO: should be (int, / ...) -> str reveal_type(c) # revealed: Unknown + T = TypeVar("T") MyList = TypeAliasType("MyList", list[T], type_params=(T,)) MyAlias5 = Callable[[MyList[T]], int] + def _(c: MyAlias5[int]): # TODO: should be (list[int], /) -> int reveal_type(c) # revealed: (Unknown, /) -> int + K = TypeVar("K") V = TypeVar("V") @@ -227,18 +244,23 @@ MyDict = TypeAliasType("MyDict", dict[K, V], type_params=(K, V)) MyAlias6 = Callable[[MyDict[K, V]], int] + def _(c: MyAlias6[str, bytes]): # TODO: should be (dict[str, bytes], /) -> int reveal_type(c) # revealed: (Unknown, /) -> int + ListOrDict: TypeAlias = MyList[T] | dict[str, T] + def _(x: ListOrDict[int]): # TODO: should be list[int] | dict[str, int] reveal_type(x) # revealed: Unknown | dict[str, int] + MyAlias7: TypeAlias = Callable[Concatenate[T, ...], None] + def _(c: MyAlias7[int]): # TODO: should be (int, / ...) -> None reveal_type(c) # revealed: Unknown @@ -259,6 +281,7 @@ MyAlias: TypeAlias = int | str ```py from alias import MyAlias + def _(x: MyAlias): reveal_type(x) # revealed: int | str ``` @@ -270,6 +293,7 @@ from typing import TypeAlias IntOrStr: TypeAlias = "int | str" + def _(x: IntOrStr): reveal_type(x) # revealed: int | str ``` @@ -282,32 +306,40 @@ from types import UnionType RecursiveTuple: TypeAlias = tuple[int | "RecursiveTuple", str] + def _(rec: RecursiveTuple): # TODO should be `tuple[int | RecursiveTuple, str]` reveal_type(rec) # revealed: tuple[Divergent, str] + RecursiveHomogeneousTuple: TypeAlias = tuple[int | "RecursiveHomogeneousTuple", ...] + def _(rec: RecursiveHomogeneousTuple): # TODO should be `tuple[int | RecursiveHomogeneousTuple, ...]` reveal_type(rec) # revealed: tuple[Divergent, ...] + ClassInfo: TypeAlias = type | UnionType | tuple["ClassInfo", ...] reveal_type(ClassInfo) # revealed: + def my_isinstance(obj: object, classinfo: ClassInfo) -> bool: # TODO should be `type | UnionType | tuple[ClassInfo, ...]` reveal_type(classinfo) # revealed: type | UnionType | tuple[Divergent, ...] return isinstance(obj, classinfo) + K = TypeVar("K") V = TypeVar("V") NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]] + def _(nested: NestedDict[str, int]): # TODO should be `dict[str, int | NestedDict[str, int]]` reveal_type(nested) # revealed: dict[@Todo(specialized recursive generic type alias), Divergent] + my_isinstance(1, int) my_isinstance(1, int | str) my_isinstance(1, (int, str)) @@ -335,6 +367,7 @@ except ImportError: MyAlias: TypeAlias = int + def _(x: MyAlias): reveal_type(x) # revealed: int ``` @@ -360,13 +393,17 @@ class B: ... ```py import stub + def f(x: stub.MyAlias): ... + f(stub.A()) f(stub.B()) + class Unrelated: ... + # error: [invalid-argument-type] f(Unrelated()) ``` @@ -379,10 +416,12 @@ context is an error. ```py from typing import TypeAlias + # error: [invalid-type-form] def _(x: TypeAlias): reveal_type(x) # revealed: Unknown + # error: [invalid-type-form] y: list[TypeAlias] = [] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 0b39f45bb4..fa83408228 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -19,6 +19,7 @@ x: IntOrStr = 1 reveal_type(x) # revealed: Literal[1] + def f() -> None: reveal_type(x) # revealed: int | str ``` @@ -48,6 +49,7 @@ type IntOrStrOrBytes = IntOrStr | bytes x: IntOrStrOrBytes = 1 + def f() -> None: reveal_type(x) # revealed: int | str | bytes ``` @@ -69,6 +71,7 @@ y: MyIntOrStr = None ```py type T = tuple[int, str] + def f(x: T): a, b = x reveal_type(a) # revealed: int @@ -83,9 +86,11 @@ eager) nested scope. ```py type Alias = Foo | str + def f(x: Alias): reveal_type(x) # revealed: Foo | str + class Foo: pass ``` @@ -97,6 +102,7 @@ def _(flag: bool): t = int if flag else None if t is not None: type Alias = t | str + def f(x: Alias): reveal_type(x) # revealed: int | str ``` @@ -108,25 +114,32 @@ type ListOrSet[T] = list[T] | set[T] reveal_type(ListOrSet.__type_params__) # revealed: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] type Tuple1[T] = tuple[T] + def _(cond: bool): Generic = ListOrSet if cond else Tuple1 def _(x: Generic[int]): reveal_type(x) # revealed: list[int] | set[int] | tuple[int] + try: + class Foo[T]: x: T + def foo(self) -> T: return self.x ... except Exception: + class Foo[T]: x: T + def foo(self) -> T: return self.x + def f(x: Foo[int]): reveal_type(x.foo()) # revealed: int ``` @@ -138,6 +151,7 @@ We can "break apart" a type alias by e.g. adding it to a union: ```py type IntOrStr = int | str + def f(x: IntOrStr, y: str | bytes): z = x or y reveal_type(z) # revealed: (int & ~AlwaysFalsy) | str | bytes @@ -147,10 +161,17 @@ def f(x: IntOrStr, y: str | bytes): ```py class A: ... + + class B: ... + + class C: ... + + class D: ... + type W = A | B type X = C | D type Y = W | X @@ -167,6 +188,7 @@ from typing import Literal type X = tuple[Literal[1], Literal[2]] + def _(x: X, y: tuple[Literal[1], Literal[3]]): reveal_type(x == y) # revealed: Literal[False] reveal_type(x < y) # revealed: Literal[True] @@ -209,6 +231,7 @@ reveal_type(IntOrStr) # revealed: TypeAliasType reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"] + def f(x: IntOrStr) -> None: reveal_type(x) # revealed: int | str ``` @@ -222,6 +245,7 @@ T = TypeVar("T") IntAndT = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,)) + def f(x: IntAndT[str]) -> None: # TODO: This should be `tuple[int, str]` reveal_type(x) # revealed: Unknown @@ -234,9 +258,11 @@ def f(x: IntAndT[str]) -> None: ```py from typing_extensions import TypeAliasType + def get_name() -> str: return "IntOrStr" + # error: [invalid-type-alias-type] "The name of a `typing.TypeAlias` must be a string literal" IntOrStr = TypeAliasType(get_name(), int | str) ``` @@ -248,6 +274,7 @@ IntOrStr = TypeAliasType(get_name(), int | str) ```py type OptNestedInt = int | tuple[OptNestedInt, ...] | None + def f(x: OptNestedInt) -> None: reveal_type(x) # revealed: int | tuple[OptNestedInt, ...] | None if x is not None: @@ -261,6 +288,7 @@ def f(x: OptNestedInt) -> None: type IntOr = int | IntOr type OrInt = OrInt | int + def f(x: IntOr, y: OrInt): reveal_type(x) # revealed: int reveal_type(y) # revealed: int @@ -269,9 +297,11 @@ def f(x: IntOr, y: OrInt): if not isinstance(y, int): reveal_type(y) # revealed: Never + # error: [cyclic-type-alias-definition] "Cyclic definition of `Itself`" type Itself = Itself + def foo( # this is a very strange thing to do, but this is a regression test to ensure it doesn't panic Itself: Itself, @@ -279,6 +309,7 @@ def foo( x: Itself reveal_type(Itself) # revealed: Divergent + # A type alias defined with invalid recursion behaves as a dynamic type. foo(42) foo("hello") @@ -288,10 +319,12 @@ type A = B # error: [cyclic-type-alias-definition] "Cyclic definition of `B`" type B = A + def bar(B: B): x: B reveal_type(B) # revealed: Divergent + # error: [cyclic-type-alias-definition] "Cyclic definition of `G`" type G[T] = G[T] # error: [cyclic-type-alias-definition] "Cyclic definition of `H`" @@ -306,6 +339,7 @@ type DirectRecursiveList[T] = list[DirectRecursiveList[T]] type Foo[T] = list[T] | Bar[T] type Bar[T] = int | Foo[T] + def _(x: Bar[int]): # TODO: should be `int | list[int]` reveal_type(x) # revealed: int | list[int] | Any @@ -320,12 +354,15 @@ T = TypeVar("T") type Alias = list["Alias"] | int + class A(Generic[T]): attr: T + class B(A[Alias]): pass + def f(b: B): reveal_type(b) # revealed: B reveal_type(b.attr) # revealed: list[Alias] | int @@ -337,6 +374,7 @@ def f(b: B): type A = tuple[B] | None type B = tuple[A] | None + def f(x: A): if x is not None: reveal_type(x) # revealed: tuple[B] @@ -344,11 +382,14 @@ def f(x: A): if y is not None: reveal_type(y) # revealed: tuple[A] + def g(x: A | B): reveal_type(x) # revealed: tuple[B] | None + from ty_extensions import Intersection + def h(x: Intersection[A, B]): reveal_type(x) # revealed: tuple[B] | None ``` @@ -360,6 +401,7 @@ from typing import Callable type C = Callable[[], C | None] + def _(x: C): reveal_type(x) # revealed: () -> C | None ``` @@ -385,12 +427,15 @@ from typing_extensions import Protocol, TypeVar T = TypeVar("T", default="C", covariant=True) + class P(Protocol[T]): pass + class C(P[T]): pass + reveal_type(C[int]()) # revealed: C[int] reveal_type(C()) # revealed: C[C[Divergent]] ``` @@ -404,6 +449,7 @@ from typing import Union type A = list[Union["A", str]] + def f(x: A): reveal_type(x) # revealed: list[A | str] for item in x: @@ -415,6 +461,7 @@ def f(x: A): ```py type A = list["A" | str] + def f(x: A): reveal_type(x) # revealed: list[A | str] for item in x: @@ -428,6 +475,7 @@ from typing import Optional, Union type A = list[Optional[Union["A", str]]] + def f(x: A): reveal_type(x) # revealed: list[A | str | None] for item in x: @@ -439,6 +487,7 @@ def f(x: A): ```py type X = tuple[X, int] + def _(x: X): reveal_type(x is x) # revealed: bool ``` @@ -449,6 +498,7 @@ def _(x: X): type X = dict[str, X] type Y = X | str | dict[str, Y] + def _(y: Y): if isinstance(y, dict): reveal_type(y) # revealed: dict[str, X] | dict[str, Y] @@ -462,6 +512,7 @@ This test case used to cause a stack overflow. The returned type `list[int]` is ```py type RecursiveT = int | tuple[RecursiveT, ...] + def foo(a: int, b: int) -> RecursiveT: some_intermediate_var = (a, b) # error: [invalid-return-type] "Return type does not match returned value: expected `RecursiveT`, found `list[int]`" diff --git a/crates/ty_python_semantic/resources/mdtest/properties.md b/crates/ty_python_semantic/resources/mdtest/properties.md index f0f88ae050..a74a63bfb5 100644 --- a/crates/ty_python_semantic/resources/mdtest/properties.md +++ b/crates/ty_python_semantic/resources/mdtest/properties.md @@ -15,6 +15,7 @@ class C: def my_property(self) -> int: return 1 + reveal_type(C().my_property) # revealed: int ``` @@ -41,6 +42,7 @@ class C: def my_property(self, value: int) -> None: pass + c = C() reveal_type(c.my_property) # revealed: int c.my_property = 2 @@ -56,11 +58,13 @@ A property that returns `Self` refers to an instance of the class: ```py from typing_extensions import Self + class Path: @property def parent(self) -> Self: raise NotImplementedError + reveal_type(Path().parent) # revealed: Path ``` @@ -76,6 +80,7 @@ class Node: def parent(self, value: Self) -> None: pass + root = Node() child = Node() child.parent = root @@ -102,6 +107,7 @@ class C: def my_property(self) -> str: return "a" + c = C() reveal_type(c.my_property) # revealed: str c.my_property = 2 @@ -129,6 +135,7 @@ class C: def my_property(self) -> None: pass + c = C() reveal_type(c.my_property) # revealed: int c.my_property = 2 @@ -148,6 +155,7 @@ class C: def attr(self) -> int: return 1 + c = C() # error: [invalid-assignment] @@ -162,8 +170,10 @@ When attempting to read a write-only property, we emit an error: class C: def attr_setter(self, value: int) -> None: pass + attr = property(fset=attr_setter) + c = C() c.attr = 1 @@ -179,6 +189,7 @@ class C: @property def attr(self) -> int: return 1 + # error: [invalid-argument-type] "Argument to bound method `setter` is incorrect: Expected `(Any, Any, /) -> None`, found `def attr(self) -> None`" @attr.setter def attr(self) -> None: @@ -205,8 +216,10 @@ Properties can also be constructed manually using the `property` class. We parti class C: def attr_getter(self) -> int: return 1 + attr = property(attr_getter) + c = C() reveal_type(c.attr) # revealed: Unknown | int ``` @@ -221,8 +234,10 @@ the getter). class C: def attr_getter(self) -> int: return 1 + attr: property = property(attr_getter) + c = C() reveal_type(c.attr) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 40b180fb35..e556c4e880 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -27,8 +27,10 @@ A protocol is defined by inheriting from the `Protocol` class, which is annotate from typing import Protocol from ty_extensions import reveal_mro + class MyProtocol(Protocol): ... + reveal_mro(MyProtocol) # revealed: (, typing.Protocol, typing.Generic, ) ``` @@ -38,6 +40,7 @@ class's bases: ```py class Foo(Protocol, Protocol): ... # error: [duplicate-base] + reveal_mro(Foo) # revealed: (, Unknown, ) ``` @@ -50,19 +53,24 @@ from typing import TypeVar, Generic T = TypeVar("T") + class Bar0(Protocol[T]): x: T + class Bar1(Protocol[T], Generic[T]): x: T + class Bar2[T](Protocol): x: T + # error: [invalid-generic-class] "Cannot both inherit from subscripted `Protocol` and use PEP 695 type variables" class Bar3[T](Protocol[T]): x: T + # Note that this class definition *will* actually succeed at runtime, # unlike classes that combine PEP-695 type parameters with inheritance from `Generic[]` reveal_mro(Bar3) # revealed: (, typing.Protocol, typing.Generic, ) @@ -75,6 +83,7 @@ simultaneously: class DuplicateBases(Protocol, Protocol[T]): # error: [duplicate-base] x: T + # revealed: (, Unknown, ) reveal_mro(DuplicateBases) ``` @@ -91,8 +100,10 @@ reveal_type(is_protocol(Bar1)) # revealed: Literal[True] reveal_type(is_protocol(Bar2)) # revealed: Literal[True] reveal_type(is_protocol(Bar3)) # revealed: Literal[True] + class NotAProtocol: ... + reveal_type(is_protocol(NotAProtocol)) # revealed: Literal[False] ``` @@ -103,6 +114,7 @@ protocols. We still consider these to be "protocol classes" internally, regardle class MyGenericProtocol[T](Protocol): x: T + reveal_type(is_protocol(MyGenericProtocol)) # revealed: Literal[True] # We still consider this a protocol class internally, @@ -125,6 +137,7 @@ it is not sufficient for it to have `Protocol` in its MRO. ```py class SubclassOfMyProtocol(MyProtocol): ... + # revealed: (, , typing.Protocol, typing.Generic, ) reveal_mro(SubclassOfMyProtocol) @@ -137,13 +150,17 @@ A protocol class may inherit from other protocols, however, as long as it re-inh ```py class SubProtocol(MyProtocol, Protocol): ... + reveal_type(is_protocol(SubProtocol)) # revealed: Literal[True] + class OtherProtocol(Protocol): some_attribute: str + class ComplexInheritance(SubProtocol, OtherProtocol, Protocol): ... + # revealed: (, , , , typing.Protocol, typing.Generic, ) reveal_mro(ComplexInheritance) @@ -157,20 +174,26 @@ or `TypeError` is raised at runtime when the class is created. # error: [invalid-protocol] "Protocol class `Invalid` cannot inherit from non-protocol class `NotAProtocol`" class Invalid(NotAProtocol, Protocol): ... + # revealed: (, , typing.Protocol, typing.Generic, ) reveal_mro(Invalid) + # error: [invalid-protocol] "Protocol class `AlsoInvalid` cannot inherit from non-protocol class `NotAProtocol`" class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ... + # revealed: (, , , , typing.Protocol, typing.Generic, ) reveal_mro(AlsoInvalid) + class NotAGenericProtocol[T]: ... + # error: [invalid-protocol] "Protocol class `StillInvalid` cannot inherit from non-protocol class `NotAGenericProtocol`" class StillInvalid(NotAGenericProtocol[int], Protocol): ... + # revealed: (, , typing.Protocol, typing.Generic, ) reveal_mro(StillInvalid) ``` @@ -182,6 +205,7 @@ from typing import TypeVar, Generic T = TypeVar("T") + # Note: pyright and pyrefly do not consider this to be a valid `Protocol` class, # but mypy does (and has an explicit test for this behavior). Mypy was the # reference implementation for PEP-544, and its behavior also matches the CPython @@ -189,11 +213,19 @@ T = TypeVar("T") # type checkers. class Fine(Protocol, object): ... + reveal_mro(Fine) # revealed: (, typing.Protocol, typing.Generic, ) + class StillFine(Protocol, Generic[T], object): ... + + class EvenThis[T](Protocol, object): ... + + class OrThis(Protocol[T], Generic[T]): ... + + class AndThis(Protocol[T], Generic[T], object): ... ``` @@ -203,6 +235,7 @@ And multiple inheritance from a mix of protocol and non-protocol classes is fine ```py class FineAndDandy(MyProtocol, OtherProtocol, NotAProtocol): ... + # revealed: (, , , typing.Protocol, typing.Generic, , ) reveal_mro(FineAndDandy) ``` @@ -294,12 +327,15 @@ from ty_extensions import static_assert, is_equivalent_to, TypeOf static_assert(is_equivalent_to(TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol])) static_assert(is_equivalent_to(int | str | TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol] | str | int)) + class Foo(typing.Protocol): x: int + class Bar(typing_extensions.Protocol): x: int + static_assert(typing_extensions.is_protocol(Foo)) static_assert(typing_extensions.is_protocol(Bar)) static_assert(is_equivalent_to(Foo, Bar)) @@ -312,10 +348,12 @@ The same goes for `typing.runtime_checkable` and `typing_extensions.runtime_chec class RuntimeCheckableFoo(typing.Protocol): x: int + @typing.runtime_checkable class RuntimeCheckableBar(typing_extensions.Protocol): x: int + static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo)) static_assert(typing_extensions.is_protocol(RuntimeCheckableBar)) static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar)) @@ -351,15 +389,19 @@ from typing_extensions import Protocol # error: [call-non-callable] reveal_type(Protocol()) # revealed: Unknown + class MyProtocol(Protocol): x: int + # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" reveal_type(MyProtocol()) # revealed: MyProtocol + class GenericProtocol[T](Protocol): x: T + # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`" reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] ``` @@ -369,10 +411,13 @@ But a non-protocol class can be instantiated, even if it has `Protocol` in its M ```py class SubclassOfMyProtocol(MyProtocol): ... + reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol + class SubclassOfGenericProtocol[T](GenericProtocol[T]): ... + reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] ``` @@ -396,6 +441,7 @@ via `typing_extensions`. ```py from typing_extensions import Protocol, get_protocol_members + class Foo(Protocol): x: int @@ -412,6 +458,7 @@ class Foo(Protocol): def method_member(self) -> bytes: return b"foo" + reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["method_member", "x", "y", "z"]] ``` @@ -453,27 +500,34 @@ reveal_protocol_interface(SupportsAbs[int]) # revealed: {"__iter__": MethodMember(`(self, /) -> Iterator[int]`), "__next__": MethodMember(`(self, /) -> int`)} reveal_protocol_interface(Iterator[int]) + class BaseProto(Protocol): def member(self) -> int: ... + class SubProto(BaseProto, Protocol): def member(self) -> bool: ... + # revealed: {"member": MethodMember(`(self, /) -> int`)} reveal_protocol_interface(BaseProto) # revealed: {"member": MethodMember(`(self, /) -> bool`)} reveal_protocol_interface(SubProto) + class ProtoWithClassVar(Protocol): x: ClassVar[int] + # revealed: {"x": AttributeMember(`int`; ClassVar)} reveal_protocol_interface(ProtoWithClassVar) + class ProtocolWithDefault(Protocol): x: int = 0 + # We used to incorrectly report this as having an `x: Literal[0]` member; # declared types should take priority over inferred types for protocol interfaces! # @@ -497,6 +551,7 @@ class Lumberjack(Protocol): def __init__(self, x: int) -> None: self.x = x + reveal_type(get_protocol_members(Lumberjack)) # revealed: frozenset[Literal["x"]] ``` @@ -506,13 +561,17 @@ A sub-protocol inherits and extends the members of its superclass protocol(s): class Bar(Protocol): spam: str + class Baz(Bar, Protocol): ham: memoryview + reveal_type(get_protocol_members(Baz)) # revealed: frozenset[Literal["ham", "spam"]] + class Baz2(Bar, Foo, Protocol): ... + # revealed: frozenset[Literal["method_member", "spam", "x", "y", "z"]] reveal_type(get_protocol_members(Baz2)) ``` @@ -533,16 +592,21 @@ python-version = "3.9" import sys from typing_extensions import Protocol, get_protocol_members + class Foo(Protocol): if sys.version_info >= (3, 10): a: int b = 42 + def c(self) -> None: ... + else: d: int e = 56 # error: [ambiguous-protocol-member] + def f(self) -> None: ... + reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["d", "e", "f"]] ``` @@ -560,12 +624,16 @@ python-version = "3.12" ```py from typing_extensions import Protocol, get_protocol_members + class NotAProtocol: ... + get_protocol_members(NotAProtocol) # error: [invalid-argument-type] + class AlsoNotAProtocol(NotAProtocol, object): ... + get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] ``` @@ -575,6 +643,7 @@ does not suffice: ```py class GenericProtocol[T](Protocol): ... + get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549) ``` @@ -594,21 +663,27 @@ from typing import Protocol, Any, ClassVar from collections.abc import Sequence from ty_extensions import static_assert, is_assignable_to, is_subtype_of + class HasX(Protocol): x: int + class HasXY(Protocol): x: int y: int + class Foo: x: int + class IntSub(int): ... + class HasXIntSub(Protocol): x: IntSub + static_assert(is_subtype_of(Foo, HasX)) static_assert(is_assignable_to(Foo, HasX)) static_assert(not is_subtype_of(Foo, HasXY)) @@ -619,30 +694,39 @@ static_assert(not is_assignable_to(HasXIntSub, HasX)) static_assert(not is_subtype_of(HasX, HasXIntSub)) static_assert(not is_assignable_to(HasX, HasXIntSub)) + class FooSub(Foo): ... + static_assert(is_subtype_of(FooSub, HasX)) static_assert(is_assignable_to(FooSub, HasX)) static_assert(not is_subtype_of(FooSub, HasXY)) static_assert(not is_assignable_to(FooSub, HasXY)) + class FooBool: x: bool + static_assert(not is_subtype_of(FooBool, HasX)) static_assert(not is_assignable_to(FooBool, HasX)) + class FooAny: x: Any + static_assert(not is_subtype_of(FooAny, HasX)) static_assert(is_assignable_to(FooAny, HasX)) + class SubclassOfAny(Any): ... + class FooSubclassOfAny: x: SubclassOfAny + static_assert(not is_subtype_of(FooSubclassOfAny, HasX)) # `FooSubclassOfAny` is assignable to `HasX` for the following reason. The `x` attribute on `FooSubclassOfAny` @@ -652,53 +736,68 @@ static_assert(not is_subtype_of(FooSubclassOfAny, HasX)) # yields `Any`, which is assignable to `int` and vice versa. static_assert(is_assignable_to(FooSubclassOfAny, HasX)) + class FooWithY(Foo): y: int + assert is_subtype_of(FooWithY, HasXY) static_assert(is_assignable_to(FooWithY, HasXY)) + class Bar: x: str + static_assert(not is_subtype_of(Bar, HasX)) static_assert(not is_assignable_to(Bar, HasX)) + class Baz: y: int + static_assert(not is_subtype_of(Baz, HasX)) static_assert(not is_assignable_to(Baz, HasX)) + class Qux: def __init__(self, x: int) -> None: self.x: int = x + static_assert(is_subtype_of(Qux, HasX)) static_assert(is_assignable_to(Qux, HasX)) + class HalfUnknownQux: def __init__(self, x: int) -> None: self.x = x + reveal_type(HalfUnknownQux(1).x) # revealed: Unknown | int static_assert(not is_subtype_of(HalfUnknownQux, HasX)) static_assert(is_assignable_to(HalfUnknownQux, HasX)) + class FullyUnknownQux: def __init__(self, x) -> None: self.x = x + static_assert(not is_subtype_of(FullyUnknownQux, HasX)) static_assert(is_assignable_to(FullyUnknownQux, HasX)) + class HasXWithDefault(Protocol): x: int = 0 + class FooWithZero: x: int = 0 + static_assert(is_subtype_of(FooWithZero, HasXWithDefault)) static_assert(is_assignable_to(FooWithZero, HasXWithDefault)) @@ -720,9 +819,11 @@ static_assert(is_assignable_to(Foo, HasXWithDefault)) static_assert(is_subtype_of(Qux, HasXWithDefault)) static_assert(is_assignable_to(Qux, HasXWithDefault)) + class HasClassVarX(Protocol): x: ClassVar[int] + static_assert(is_subtype_of(FooWithZero, HasClassVarX)) static_assert(is_assignable_to(FooWithZero, HasClassVarX)) # TODO: these should pass @@ -745,27 +846,34 @@ attribute's mutability: ```py from typing import Final + class A: @property def x(self) -> int: return 42 + # TODO: these should pass static_assert(not is_subtype_of(A, HasX)) # error: [static-assert-error] static_assert(not is_assignable_to(A, HasX)) # error: [static-assert-error] + class B: x: Final = 42 + # TODO: these should pass static_assert(not is_subtype_of(A, HasX)) # error: [static-assert-error] static_assert(not is_assignable_to(A, HasX)) # error: [static-assert-error] + class IntSub(int): ... + class C: x: IntSub + # due to invariance, a type is only a subtype of `HasX` # if its `x` attribute is of type *exactly* `int`: # a subclass of `int` does not satisfy the interface @@ -780,24 +888,30 @@ can never be considered to inhabit a protocol that declares a mutable-attribute from dataclasses import dataclass from typing import NamedTuple + @dataclass class MutableDataclass: x: int + static_assert(is_subtype_of(MutableDataclass, HasX)) static_assert(is_assignable_to(MutableDataclass, HasX)) + @dataclass(frozen=True) class ImmutableDataclass: x: int + # TODO: these should pass static_assert(not is_subtype_of(ImmutableDataclass, HasX)) # error: [static-assert-error] static_assert(not is_assignable_to(ImmutableDataclass, HasX)) # error: [static-assert-error] + class NamedTupleWithX(NamedTuple): x: int + # TODO: these should pass static_assert(not is_subtype_of(NamedTupleWithX, HasX)) # error: [static-assert-error] static_assert(not is_assignable_to(NamedTupleWithX, HasX)) # error: [static-assert-error] @@ -819,6 +933,7 @@ class XProperty: def x(self, x: int) -> None: self._x = x**2 + static_assert(is_subtype_of(XProperty, HasX)) static_assert(is_assignable_to(XProperty, HasX)) ``` @@ -834,12 +949,16 @@ provided in its class body: class HasXWithDefault(Protocol): x: int = 42 + reveal_type(HasXWithDefault.x) # revealed: int + class ExplicitSubclass(HasXWithDefault): ... + reveal_type(ExplicitSubclass.x) # revealed: int + def f(arg: HasXWithDefault): # TODO: should emit `[unresolved-reference]` and reveal `Unknown` reveal_type(type(arg).x) # revealed: int @@ -856,12 +975,14 @@ an ambiguous interface being declared by the protocol. ```py from typing_extensions import TypeAlias, get_protocol_members + class MyContext: def __enter__(self) -> int: return 42 def __exit__(self, *args) -> None: ... + class LotsOfBindings(Protocol): a: int a = 42 # this is fine, since `a` is declared in the class body @@ -871,7 +992,9 @@ class LotsOfBindings(Protocol): d: TypeAlias = bytes # same here class Nested: ... # also weird, but we should also probably allow it + class NestedProtocol(Protocol): ... # same here... + e = 72 # error: [ambiguous-protocol-member] # error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `f: int = ...`" @@ -892,15 +1015,19 @@ class LotsOfBindings(Protocol): # error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `m: int | str = ...`" m = 1 if 1.2 > 3.4 else "a" + # revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"]] reveal_type(get_protocol_members(LotsOfBindings)) + class Foo(Protocol): a: int + class Bar(Foo, Protocol): a = 42 # fine, because it's declared in the superclass + reveal_type(get_protocol_members(Bar)) # revealed: frozenset[Literal["a"]] ``` @@ -911,6 +1038,7 @@ if all definitions for the variable are in unreachable blocks: ```py import sys + class Protocol694(Protocol): if sys.version_info > (3, 694): x = 42 # no error! @@ -970,6 +1098,7 @@ class Foo(Protocol): self.c = 72 # TODO: should emit diagnostic + # Note: the list of members does not include `a`, `b` or `c`, # as none of these attributes is declared in the class body. reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["non_init_method", "x", "y"]] @@ -982,9 +1111,11 @@ the sub-protocol class without a redeclaration: class Super(Protocol): x: int + class Sub(Super, Protocol): x = 42 # no error here, since it's declared in the superclass + reveal_type(get_protocol_members(Super)) # revealed: frozenset[Literal["x"]] reveal_type(get_protocol_members(Sub)) # revealed: frozenset[Literal["x"]] ``` @@ -995,8 +1126,10 @@ are subtypes of it: ```py from typing import Protocol + class UniversalSet(Protocol): ... + static_assert(is_assignable_to(object, UniversalSet)) static_assert(is_subtype_of(object, UniversalSet)) ``` @@ -1024,6 +1157,7 @@ means that these protocols are also equivalent to `UniversalSet` and `object`: class SupportsStr(Protocol): def __str__(self) -> str: ... + static_assert(is_equivalent_to(SupportsStr, UniversalSet)) static_assert(is_equivalent_to(SupportsStr, object)) static_assert(is_subtype_of(SupportsStr, UniversalSet)) @@ -1031,10 +1165,12 @@ static_assert(is_subtype_of(UniversalSet, SupportsStr)) static_assert(is_assignable_to(UniversalSet, SupportsStr)) static_assert(is_assignable_to(SupportsStr, UniversalSet)) + class SupportsClass(Protocol): @property def __class__(self) -> type: ... + static_assert(is_equivalent_to(SupportsClass, UniversalSet)) static_assert(is_equivalent_to(SupportsClass, SupportsStr)) static_assert(is_equivalent_to(SupportsClass, object)) @@ -1083,8 +1219,10 @@ to be assignable to `Hashable`. This avoids false positives on code like this: from typing import Sequence from ty_extensions import is_disjoint_from + def takes_hashable_or_sequence(x: Hashable | list[Hashable]): ... + takes_hashable_or_sequence(["foo"]) # fine takes_hashable_or_sequence(None) # fine @@ -1102,6 +1240,7 @@ checkers: def needs_something_hashable(x: Hashable): hash(x) + needs_something_hashable([]) ``` @@ -1118,9 +1257,11 @@ it's a large section). ```py from typing import Protocol + def coinflip() -> bool: return True + class A(Protocol): # The `x` and `y` members attempt to use Python-2-style type comments # to indicate that the type should be `int | None` and `str` respectively, @@ -1149,9 +1290,11 @@ here too: ```py from typing import Protocol + # Ensure the number of scopes in `b.py` is greater than the number of scopes in `c.py`: class SomethingUnrelated: ... + class A(Protocol): x: int ``` @@ -1162,6 +1305,7 @@ class A(Protocol): from b import A from typing import Protocol + class C(A, Protocol): x = 42 # fine, due to declaration in the base class ``` @@ -1180,12 +1324,15 @@ different names: from typing import Protocol from ty_extensions import is_equivalent_to, static_assert + class HasX(Protocol): x: int + class AlsoHasX(Protocol): x: int + static_assert(is_equivalent_to(HasX, AlsoHasX)) ``` @@ -1196,12 +1343,17 @@ identical: class HasY(Protocol): y: str + class AlsoHasY(Protocol): y: str + class A: ... + + class B: ... + static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A)) ``` @@ -1211,12 +1363,15 @@ differently ordered unions: ```py class C: ... + class UnionProto1(Protocol): x: A | B | C + class UnionProto2(Protocol): x: C | A | B + static_assert(is_equivalent_to(UnionProto1, UnionProto2)) static_assert(is_equivalent_to(UnionProto1 | A | B, B | UnionProto2 | A)) ``` @@ -1229,23 +1384,31 @@ from typing import TypeVar S = TypeVar("S") + class NonGenericProto1(Protocol): x: int y: str + class NonGenericProto2(Protocol): y: str x: int + class Nominal1: ... + + class Nominal2: ... + class GenericProto[T](Protocol): x: T + class LegacyGenericProto(Protocol[S]): x: S + static_assert(is_equivalent_to(GenericProto[int], LegacyGenericProto[int])) static_assert(is_equivalent_to(GenericProto[NonGenericProto1], LegacyGenericProto[NonGenericProto2])) @@ -1270,14 +1433,18 @@ from both `X` and `Y`: from typing import Protocol from ty_extensions import Intersection, static_assert, is_equivalent_to + class HasX(Protocol): x: int + class HasY(Protocol): y: str + class HasXAndYProto(HasX, HasY, Protocol): ... + # TODO: this should pass static_assert(is_equivalent_to(HasXAndYProto, Intersection[HasX, HasY])) # error: [static-assert-error] ``` @@ -1288,6 +1455,7 @@ nominal type rather than a structural type): ```py class HasXAndYNominal(HasX, HasY): ... + static_assert(not is_equivalent_to(HasXAndYNominal, Intersection[HasX, HasY])) ``` @@ -1300,14 +1468,18 @@ that would lead to it satisfying `X`'s interface: from typing import final from ty_extensions import is_disjoint_from + class NotFinalNominal: ... + @final class FinalNominal: ... + static_assert(not is_disjoint_from(NotFinalNominal, HasX)) static_assert(is_disjoint_from(FinalNominal, HasX)) + def _(arg1: Intersection[HasX, NotFinalNominal], arg2: Intersection[HasX, FinalNominal]): reveal_type(arg1) # revealed: HasX & NotFinalNominal reveal_type(arg2) # revealed: Never @@ -1323,19 +1495,23 @@ class Proto(Protocol): y: str z: bytes + class Foo: x: int y: str z: None + static_assert(is_disjoint_from(Proto, Foo)) + @final class FinalFoo: x: int y: str z: None + static_assert(is_disjoint_from(Proto, FinalFoo)) ``` @@ -1351,18 +1527,22 @@ but will also not be disjoint from the protocol: from typing import final, ClassVar, Protocol from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to + def who_knows() -> bool: return False + @final class Foo: if who_knows(): x: ClassVar[int] = 42 + class HasReadOnlyX(Protocol): @property def x(self) -> int: ... + static_assert(not is_subtype_of(Foo, HasReadOnlyX)) static_assert(not is_assignable_to(Foo, HasReadOnlyX)) static_assert(not is_disjoint_from(Foo, HasReadOnlyX)) @@ -1384,6 +1564,7 @@ A similar principle applies to module-literal types that have possibly unbound a def who_knows() -> bool: return False + if who_knows(): x: int = 42 ``` @@ -1410,20 +1591,24 @@ from a import HasReadOnlyX, who_knows from typing import final, ClassVar, Protocol from ty_extensions import static_assert, is_disjoint_from, TypeOf + class Proto(Protocol): x: int + class Foo: def __init__(self): if who_knows(): self.x: None = None + @final class FinalFoo: def __init__(self): if who_knows(): self.x: None = None + static_assert(is_disjoint_from(Foo, Proto)) static_assert(is_disjoint_from(FinalFoo, Proto)) ``` @@ -1448,30 +1633,39 @@ import module from typing import Protocol from ty_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf + class HasX(Protocol): x: int + static_assert(is_subtype_of(TypeOf[module], HasX)) static_assert(is_assignable_to(TypeOf[module], HasX)) + class ExplicitProtocolSubtype(HasX, Protocol): y: int + static_assert(is_subtype_of(ExplicitProtocolSubtype, HasX)) static_assert(is_assignable_to(ExplicitProtocolSubtype, HasX)) + class ImplicitProtocolSubtype(Protocol): x: int y: str + static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX)) static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX)) + class Meta(type): x: int + class UsesMeta(metaclass=Meta): ... + # TODO: these should pass static_assert(is_subtype_of(UsesMeta, HasX)) # error: [static-assert-error] static_assert(is_assignable_to(UsesMeta, HasX)) # error: [static-assert-error] @@ -1489,33 +1683,41 @@ a readable `x` attribute must be accessible on any inhabitant of `ClassVarX`, an from typing import ClassVar, Protocol from ty_extensions import is_subtype_of, is_assignable_to, static_assert + class ClassVarXProto(Protocol): x: ClassVar[int] + def f(obj: ClassVarXProto): reveal_type(obj.x) # revealed: int reveal_type(type(obj).x) # revealed: int obj.x = 42 # error: [invalid-attribute-access] "Cannot assign to ClassVar `x` from an instance of type `ClassVarXProto`" + class InstanceAttrX: x: int + # TODO: these should pass static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto)) # error: [static-assert-error] static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto)) # error: [static-assert-error] + class PropertyX: @property def x(self) -> int: return 42 + # TODO: these should pass static_assert(not is_assignable_to(PropertyX, ClassVarXProto)) # error: [static-assert-error] static_assert(not is_subtype_of(PropertyX, ClassVarXProto)) # error: [static-assert-error] + class ClassVarX: x: ClassVar[int] = 42 + static_assert(is_assignable_to(ClassVarX, ClassVarXProto)) static_assert(is_subtype_of(ClassVarX, ClassVarXProto)) ``` @@ -1537,24 +1739,30 @@ read/write property, a `Final` attribute, or a `ClassVar` attribute: from typing import ClassVar, Final, Protocol from ty_extensions import is_subtype_of, is_assignable_to, static_assert + class HasXProperty(Protocol): @property def x(self) -> int: ... + class XAttr: x: int + static_assert(is_subtype_of(XAttr, HasXProperty)) static_assert(is_assignable_to(XAttr, HasXProperty)) + class XReadProperty: @property def x(self) -> int: return 42 + static_assert(is_subtype_of(XReadProperty, HasXProperty)) static_assert(is_assignable_to(XReadProperty, HasXProperty)) + class XReadWriteProperty: @property def x(self) -> int: @@ -1563,24 +1771,31 @@ class XReadWriteProperty: @x.setter def x(self, val: int) -> None: ... + static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) + class XClassVar: x: ClassVar[int] = 42 + static_assert(is_subtype_of(XClassVar, HasXProperty)) static_assert(is_assignable_to(XClassVar, HasXProperty)) + class XFinal: x: Final[int] = 42 + static_assert(is_subtype_of(XFinal, HasXProperty)) static_assert(is_assignable_to(XFinal, HasXProperty)) + class XImplicitFinal: x: Final = 42 + static_assert(is_subtype_of(XImplicitFinal, HasXProperty)) static_assert(is_assignable_to(XImplicitFinal, HasXProperty)) ``` @@ -1591,10 +1806,12 @@ But only if it has the correct type: class XAttrBad: x: str + class HasStrXProperty(Protocol): @property def x(self) -> str: ... + # TODO: these should pass static_assert(not is_assignable_to(XAttrBad, HasXProperty)) # error: [static-assert-error] static_assert(not is_assignable_to(HasStrXProperty, HasXProperty)) # error: [static-assert-error] @@ -1608,16 +1825,20 @@ is a subtype of `int` rather than being exactly `int`. ```py class MyInt(int): ... + class XSub: x: MyInt + static_assert(is_subtype_of(XSub, HasXProperty)) static_assert(is_assignable_to(XSub, HasXProperty)) + class XSubProto(Protocol): @property def x(self) -> XSub: ... + static_assert(is_subtype_of(XSubProto, HasXProperty)) static_assert(is_assignable_to(XSubProto, HasXProperty)) ``` @@ -1632,21 +1853,26 @@ class HasMutableXProperty(Protocol): @x.setter def x(self, val: int) -> None: ... + class XAttr: x: int + static_assert(is_subtype_of(XAttr, HasXProperty)) static_assert(is_assignable_to(XAttr, HasXProperty)) + class XReadProperty: @property def x(self) -> int: return 42 + # TODO: these should pass static_assert(not is_subtype_of(XReadProperty, HasMutableXProperty)) # error: [static-assert-error] static_assert(not is_assignable_to(XReadProperty, HasMutableXProperty)) # error: [static-assert-error] + class XReadWriteProperty: @property def x(self) -> int: @@ -1655,12 +1881,15 @@ class XReadWriteProperty: @x.setter def x(self, val: int) -> None: ... + static_assert(is_subtype_of(XReadWriteProperty, HasMutableXProperty)) static_assert(is_assignable_to(XReadWriteProperty, HasMutableXProperty)) + class XSub: x: MyInt + # TODO: these should pass static_assert(not is_subtype_of(XSub, HasMutableXProperty)) # error: [static-assert-error] static_assert(not is_assignable_to(XSub, HasMutableXProperty)) # error: [static-assert-error] @@ -1672,9 +1901,11 @@ attribute `x`. Both are subtypes of a protocol with a read-only property `x`: ```py from ty_extensions import is_equivalent_to + class HasMutableXAttr(Protocol): x: int + # TODO: should pass static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error: [static-assert-error] @@ -1690,9 +1921,11 @@ static_assert(is_assignable_to(HasMutableXProperty, HasXProperty)) static_assert(is_subtype_of(HasMutableXProperty, HasMutableXAttr)) static_assert(is_assignable_to(HasMutableXProperty, HasMutableXAttr)) + class HasMutableXAttrWrongType(Protocol): x: str + # TODO: these should pass static_assert(not is_assignable_to(HasMutableXAttrWrongType, HasXProperty)) # error: [static-assert-error] static_assert(not is_assignable_to(HasMutableXAttrWrongType, HasMutableXProperty)) # error: [static-assert-error] @@ -1720,9 +1953,11 @@ class HasAsymmetricXProperty(Protocol): @x.setter def x(self, val: MyInt) -> None: ... + class XAttr: x: int + static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty)) static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty)) ``` @@ -1735,15 +1970,19 @@ regular mutable attribute, where the implied getter-returned and setter-accepted class XAttrSub: x: MyInt + static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty)) static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty)) + class MyIntSub(MyInt): pass + class XAttrSubSub: x: MyIntSub + # TODO: should pass static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty)) # error: [static-assert-error] static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty)) # error: [static-assert-error] @@ -1762,6 +2001,7 @@ class XAsymmetricProperty: @x.setter def x(self, x: int) -> None: ... + static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty)) static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty)) ``` @@ -1775,9 +2015,11 @@ class Descriptor: def __set__(self, instance, value: int) -> None: ... + class XCustomDescriptor: x: Descriptor = Descriptor() + static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty)) static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty)) ``` @@ -1792,6 +2034,7 @@ class HasGetAttr: def __getattr__(self, attr: str) -> int: return 42 + static_assert(is_subtype_of(HasGetAttr, HasXProperty)) static_assert(is_assignable_to(HasGetAttr, HasXProperty)) @@ -1799,20 +2042,24 @@ static_assert(is_assignable_to(HasGetAttr, HasXProperty)) static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) # error: [static-assert-error] static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) # error: [static-assert-error] + class HasGetAttrWithUnsuitableReturn: def __getattr__(self, attr: str) -> tuple[int, int]: return (1, 2) + # TODO: these should pass static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty)) # error: [static-assert-error] static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty)) # error: [static-assert-error] + class HasGetAttrAndSetAttr: def __getattr__(self, attr: str) -> MyInt: return MyInt(0) def __setattr__(self, attr: str, value: int) -> None: ... + static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty)) static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty)) @@ -1820,12 +2067,14 @@ static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty)) static_assert(is_subtype_of(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error] static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error] + class HasSetAttrWithUnsuitableInput: def __getattr__(self, attr: str) -> int: return 1 def __setattr__(self, attr: str, value: str) -> None: ... + # TODO: these should pass static_assert(not is_subtype_of(HasSetAttrWithUnsuitableInput, HasMutableXProperty)) # error: [static-assert-error] static_assert(not is_assignable_to(HasSetAttrWithUnsuitableInput, HasMutableXProperty)) # error: [static-assert-error] @@ -1840,30 +2089,38 @@ class `T` has a method `m` which is assignable to the `Callable` supertype of th from typing import Protocol from ty_extensions import is_subtype_of, is_assignable_to, static_assert + class P(Protocol): def m(self, x: int, /) -> None: ... + class NominalSubtype: def m(self, y: int) -> None: ... + class NominalSubtype2: def m(self, *args: object) -> None: ... + class NotSubtype: def m(self, x: int) -> int: return 42 + class NominalWithClassMethod: @classmethod def m(cls, x: int) -> None: ... + class NominalWithStaticMethod: @staticmethod def m(_, x: int) -> None: ... + class DefinitelyNotSubtype: m = None + static_assert(is_subtype_of(NominalSubtype, P)) static_assert(is_subtype_of(NominalSubtype2, P)) static_assert(is_subtype_of(NominalSubtype | NominalSubtype2, P)) @@ -1893,16 +2150,20 @@ be a subtype of `P`: from typing import Callable, Protocol from ty_extensions import static_assert, is_assignable_to + class SupportsFooMethod(Protocol): def foo(self): ... + class SupportsFooAttr(Protocol): foo: Callable[..., object] + class Foo: def __init__(self): self.foo: Callable[..., object] = lambda *args, **kwargs: None + static_assert(not is_assignable_to(Foo, SupportsFooMethod)) static_assert(is_assignable_to(Foo, SupportsFooAttr)) ``` @@ -1920,10 +2181,12 @@ class object, not the instance. (Protocols with non-method members cannot be pas from typing import Iterable, Any from ty_extensions import static_assert, is_assignable_to + class Foo: def __init__(self): self.__iter__: Callable[..., object] = lambda *args, **kwargs: None + static_assert(not is_assignable_to(Foo, Iterable[Any])) ``` @@ -1935,14 +2198,17 @@ and subtyping, we understand that `IterableClass` here is a subtype of `Iterable from typing import Iterator, Iterable from ty_extensions import static_assert, is_subtype_of, TypeOf + class Meta(type): def __iter__(self) -> Iterator[int]: yield from range(42) + class IterableClass(metaclass=Meta): def __iter__(self) -> Iterator[str]: yield from "abc" + static_assert(is_subtype_of(TypeOf[IterableClass], Iterable[int])) ``` @@ -1953,16 +2219,20 @@ method on `type[C]` where `C` is a nominal class: ```py from typing import Protocol + class Foo(Protocol): def method(self) -> str: ... + def f(x: Foo): reveal_type(type(x).method) # revealed: def method(self, /) -> str + class Bar: def __init__(self): self.method = lambda: "foo" + f(Bar()) # error: [invalid-argument-type] ``` @@ -1977,14 +2247,17 @@ class HasPosOnlyDunders: def __lt__(self, other, /) -> bool: return True + class SupportsLessThan(Protocol): def __lt__(self, __other) -> bool: ... + class Invertable(Protocol): # `self` and `cls` are always implicitly positional-only for methods defined in `Protocol` # classes, even if no parameters in the method use the PEP-484 convention. def __invert__(self) -> object: ... + static_assert(is_assignable_to(HasPosOnlyDunders, SupportsLessThan)) static_assert(is_assignable_to(HasPosOnlyDunders, Invertable)) static_assert(is_assignable_to(str, SupportsLessThan)) @@ -2005,21 +2278,27 @@ from typing import final from typing_extensions import TypeVar, Self, Protocol from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of + class NewStyleClassScoped[T](Protocol): def method(self, input: T) -> None: ... + S = TypeVar("S") + class LegacyClassScoped(Protocol[S]): def method(self, input: S) -> None: ... + # TODO: these should pass static_assert(is_equivalent_to(NewStyleClassScoped, LegacyClassScoped)) # error: [static-assert-error] static_assert(is_equivalent_to(NewStyleClassScoped[int], LegacyClassScoped[int])) # error: [static-assert-error] + class NominalGeneric[T]: def method(self, input: T) -> None: ... + def _[T](x: T) -> T: # TODO: should pass static_assert(is_equivalent_to(NewStyleClassScoped[T], LegacyClassScoped[T])) # error: [static-assert-error] @@ -2027,9 +2306,11 @@ def _[T](x: T) -> T: static_assert(is_subtype_of(NominalGeneric[T], LegacyClassScoped[T])) return x + class NominalConcrete: def method(self, input: int) -> None: ... + static_assert(is_assignable_to(NominalConcrete, NewStyleClassScoped)) static_assert(is_assignable_to(NominalConcrete, LegacyClassScoped)) static_assert(is_assignable_to(NominalGeneric[int], NewStyleClassScoped)) @@ -2065,41 +2346,52 @@ And they can also have generic contexts scoped to the method: class NewStyleFunctionScoped(Protocol): def f[T](self, input: T) -> T: ... + S = TypeVar("S") + class LegacyFunctionScoped(Protocol): def f(self, input: S) -> S: ... + class UsesSelf(Protocol): def g(self: Self) -> Self: ... + class NominalNewStyle: def f[T](self, input: T) -> T: return input + class NominalLegacy: def f(self, input: S) -> S: return input + class NominalWithSelf: def g(self: Self) -> Self: return self + class NominalNotGeneric: def f(self, input: int) -> int: return input + class NominalReturningSelfNotGeneric: def g(self) -> "NominalReturningSelfNotGeneric": return self + @final class Other: ... + class NominalReturningOtherClass: def g(self) -> Other: raise NotImplementedError + # TODO: should pass static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error] @@ -2133,17 +2425,21 @@ static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, UsesSelf)) # static_assert(not is_assignable_to(NominalReturningOtherClass, UsesSelf)) + # These test cases are taken from the typing conformance suite: class ShapeProtocolImplicitSelf(Protocol): def set_scale(self, scale: float) -> Self: ... + class ShapeProtocolExplicitSelf(Protocol): def set_scale(self: Self, scale: float) -> Self: ... + class BadReturnType: def set_scale(self, scale: float) -> int: return 42 + static_assert(not is_assignable_to(BadReturnType, ShapeProtocolImplicitSelf)) static_assert(not is_assignable_to(BadReturnType, ShapeProtocolExplicitSelf)) ``` @@ -2165,41 +2461,50 @@ of `N` or inhabitants of `type[N]`, *and* the signature of `N.x` is equivalent t from typing import Protocol from ty_extensions import static_assert, is_subtype_of, is_assignable_to, is_equivalent_to, is_disjoint_from + class PClassMethod(Protocol): @classmethod def x(cls, val: int) -> str: ... + class PStaticMethod(Protocol): @staticmethod def x(val: int) -> str: ... + class NNotCallable: x = None + class NInstanceMethod: def x(self, val: int) -> str: return "foo" + class NClassMethodGood: @classmethod def x(cls, val: int) -> str: return "foo" + class NClassMethodBad: @classmethod def x(cls, val: str) -> int: return 42 + class NStaticMethodGood: @staticmethod def x(val: int) -> str: return "foo" + class NStaticMethodBad: @staticmethod def x(cls, val: int) -> str: return "foo" + # `PClassMethod.x` and `PStaticMethod.x` evaluate to callable types with equivalent signatures # whether you access them on the protocol class or instances of the protocol. # That means that they are equivalent protocols! @@ -2253,12 +2558,15 @@ for property members. from typing import Protocol from ty_extensions import is_equivalent_to, static_assert + class P1(Protocol): def x(self, y: int) -> None: ... + class P2(Protocol): def x(self, y: int) -> None: ... + class P3(Protocol): @property def y(self) -> str: ... @@ -2267,6 +2575,7 @@ class P3(Protocol): @z.setter def z(self, value: int) -> None: ... + class P4(Protocol): @property def y(self) -> str: ... @@ -2275,6 +2584,7 @@ class P4(Protocol): @z.setter def z(self, value: int) -> None: ... + static_assert(is_equivalent_to(P1, P2)) # TODO: should pass @@ -2286,8 +2596,11 @@ differently ordered unions: ```py class A: ... + + class B: ... + static_assert(is_equivalent_to(A | B | P1, P2 | B | A)) # TODO: should pass @@ -2304,19 +2617,28 @@ on `PSuper`: from typing import Protocol from ty_extensions import static_assert, is_subtype_of, is_assignable_to + class Super: ... + + class Sub(Super): ... + + class Unrelated: ... + class MethodPSuper(Protocol): def f(self) -> Super: ... + class MethodPSub(Protocol): def f(self) -> Sub: ... + class MethodPUnrelated(Protocol): def f(self) -> Unrelated: ... + static_assert(is_subtype_of(MethodPSub, MethodPSuper)) static_assert(not is_assignable_to(MethodPUnrelated, MethodPSuper)) @@ -2333,25 +2655,31 @@ A protocol with a method member can be considered a subtype of a protocol with a from typing import Protocol, Callable from ty_extensions import static_assert, is_subtype_of, is_assignable_to + class PropertyInt(Protocol): @property def f(self) -> Callable[[], int]: ... + class PropertyBool(Protocol): @property def f(self) -> Callable[[], bool]: ... + class PropertyNotReturningCallable(Protocol): @property def f(self) -> int: ... + class PropertyWithIncorrectSignature(Protocol): @property def f(self) -> Callable[[object], int]: ... + class Method(Protocol): def f(self) -> bool: ... + static_assert(is_subtype_of(Method, PropertyInt)) static_assert(is_subtype_of(Method, PropertyBool)) @@ -2370,6 +2698,7 @@ class ReadWriteProperty(Protocol): @f.setter def f(self, val: Callable[[], bool]): ... + # TODO: should pass static_assert(not is_assignable_to(Method, ReadWriteProperty)) # error: [static-assert-error] ``` @@ -2380,6 +2709,7 @@ And for the same reason, they are never assignable to attribute members (which a class Attribute(Protocol): f: Callable[[], bool] + static_assert(not is_assignable_to(Method, Attribute)) ``` @@ -2400,15 +2730,19 @@ the protocol: ```py from typing import ClassVar + class ClassVarAttribute(Protocol): f: ClassVar[Callable[[], bool]] + static_assert(is_subtype_of(ClassVarAttribute, Method)) static_assert(is_assignable_to(ClassVarAttribute, Method)) + class ClassVarAttributeBad(Protocol): f: ClassVar[Callable[[], str]] + static_assert(not is_subtype_of(ClassVarAttributeBad, Method)) static_assert(not is_assignable_to(ClassVarAttributeBad, Method)) ``` @@ -2424,9 +2758,11 @@ type inside these branches (this matches the behavior of other type checkers): ```py from typing_extensions import Protocol + class HasX(Protocol): x: int + def f(arg: object, arg2: type): if isinstance(arg, HasX): # error: [invalid-argument-type] reveal_type(arg) # revealed: HasX @@ -2445,10 +2781,12 @@ argument to `isisinstance()` at runtime: ```py from typing import runtime_checkable + @runtime_checkable class RuntimeCheckableHasX(Protocol): x: int + def f(arg: object): if isinstance(arg, RuntimeCheckableHasX): # no error! reveal_type(arg) # revealed: RuntimeCheckableHasX @@ -2467,6 +2805,7 @@ satisfy two conditions: class OnlyMethodMembers(Protocol): def method(self) -> None: ... + def f(arg1: type, arg2: type): if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members) reveal_type(arg1) # revealed: type[RuntimeCheckableHasX] @@ -2486,9 +2825,11 @@ An instance of a protocol type generally has ambiguous truthiness: ```py from typing import Protocol + class Foo(Protocol): x: int + def f(foo: Foo): reveal_type(bool(foo)) # revealed: bool ``` @@ -2499,15 +2840,19 @@ or `Literal[False]`: ```py from typing import Literal + class Truthy(Protocol): def __bool__(self) -> Literal[True]: ... + class FalsyFoo(Foo, Protocol): def __bool__(self) -> Literal[False]: ... + class FalsyFooSubclass(FalsyFoo, Protocol): y: str + def g(a: Truthy, b: FalsyFoo, c: FalsyFooSubclass): reveal_type(bool(a)) # revealed: Literal[True] reveal_type(bool(b)) # revealed: Literal[False] @@ -2519,9 +2864,11 @@ The same works with a class-level declaration of `__bool__`: ```py from typing import Callable + class InstanceAttrBool(Protocol): __bool__: Callable[[], Literal[True]] + def h(obj: InstanceAttrBool): reveal_type(bool(obj)) # revealed: Literal[True] ``` @@ -2533,9 +2880,11 @@ An instance of a protocol type is callable if the protocol defines a `__call__` ```py from typing import Protocol + class CallMeMaybe(Protocol): def __call__(self, x: int) -> str: ... + def f(obj: CallMeMaybe): reveal_type(obj(42)) # revealed: str obj("bar") # error: [invalid-argument-type] @@ -2555,6 +2904,7 @@ static_assert(not is_assignable_to(CallMeMaybe, Callable[[str], str])) static_assert(not is_subtype_of(CallMeMaybe, Callable[[CallMeMaybe, int], str])) static_assert(not is_assignable_to(CallMeMaybe, Callable[[CallMeMaybe, int], str])) + def g(obj: Callable[[int], str], obj2: CallMeMaybe, obj3: Callable[[str], str]): obj = obj2 obj3 = obj2 # error: [invalid-assignment] @@ -2567,9 +2917,11 @@ specified by the protocol: ```py from ty_extensions import TypeOf + class Foo(Protocol): def __call__(self, x: int, /) -> str: ... + static_assert(is_subtype_of(Callable[[int], str], Foo)) static_assert(is_assignable_to(Callable[[int], str], Foo)) @@ -2578,21 +2930,26 @@ static_assert(not is_assignable_to(Callable[[str], str], Foo)) static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo)) static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo)) + def h(obj: Callable[[int], str], obj2: Foo, obj3: Callable[[str], str]): obj2 = obj # error: [invalid-assignment] "Object of type `(str, /) -> str` is not assignable to `Foo`" obj2 = obj3 + def satisfies_foo(x: int) -> str: return "foo" + static_assert(is_assignable_to(TypeOf[satisfies_foo], Foo)) static_assert(is_subtype_of(TypeOf[satisfies_foo], Foo)) + def doesnt_satisfy_foo(x: str) -> int: return 42 + static_assert(not is_assignable_to(TypeOf[doesnt_satisfy_foo], Foo)) static_assert(not is_subtype_of(TypeOf[doesnt_satisfy_foo], Foo)) ``` @@ -2606,9 +2963,11 @@ static_assert(is_subtype_of(TypeOf[str], Foo)) T = TypeVar("T") + class SequenceMaker(Protocol[T]): def __call__(self, arg: Sequence[T], /) -> Sequence[T]: ... + static_assert(is_subtype_of(TypeOf[list[int]], SequenceMaker[int])) # TODO: these should pass @@ -2627,16 +2986,20 @@ Principle in some way. from typing import Protocol, final from ty_extensions import static_assert, is_subtype_of, is_disjoint_from + class X(Protocol): x: int + class YProto(X, Protocol): x: None = None # TODO: we should emit an error here due to the Liskov violation + @final class YNominal(X): x: None = None # TODO: we should emit an error here due to the Liskov violation + static_assert(is_subtype_of(YProto, X)) static_assert(is_subtype_of(YNominal, X)) static_assert(not is_disjoint_from(YProto, X)) @@ -2665,11 +3028,14 @@ violates the Liskov principle (this also matches the behaviour of other type che ```py from typing import Iterable + class Foo(Iterable[int]): __iter__ = None + static_assert(is_subtype_of(Foo, Iterable[int])) + def _(x: Foo): for item in x: # error: [not-iterable] pass @@ -2687,10 +3053,12 @@ worth it. Such cases should anyway be exceedingly rare and/or contrived. from typing import Protocol, Callable from ty_extensions import is_singleton, is_single_valued + class WeirdAndWacky(Protocol): @property def __class__(self) -> Callable[[], None]: ... + reveal_type(is_singleton(WeirdAndWacky)) # revealed: Literal[False] reveal_type(is_single_valued(WeirdAndWacky)) # revealed: Literal[False] ``` @@ -2702,11 +3070,13 @@ reveal_type(is_single_valued(WeirdAndWacky)) # revealed: Literal[False] ```py from typing import SupportsIndex, Sized, Literal + def one(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex): a: SupportsIndex = some_int b: SupportsIndex = some_literal_int c: SupportsIndex = some_indexable + def two(some_list: list, some_tuple: tuple[int, str], some_sized: Sized): a: Sized = some_list b: Sized = some_tuple @@ -2723,14 +3093,17 @@ from __future__ import annotations from typing import Protocol, Any, TypeVar from ty_extensions import static_assert, is_assignable_to, is_subtype_of, is_equivalent_to + class RecursiveFullyStatic(Protocol): parent: RecursiveFullyStatic x: int + class RecursiveNonFullyStatic(Protocol): parent: RecursiveNonFullyStatic x: Any + static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic)) static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic)) @@ -2738,24 +3111,30 @@ static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveNonFullyStatic) static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic)) static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveFullyStatic)) + class AlsoRecursiveFullyStatic(Protocol): parent: AlsoRecursiveFullyStatic x: int + static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic)) + class RecursiveOptionalParent(Protocol): parent: RecursiveOptionalParent | None + static_assert(is_assignable_to(RecursiveOptionalParent, RecursiveOptionalParent)) # Due to invariance of mutable attribute members, neither is assignable to the other static_assert(not is_assignable_to(RecursiveNonFullyStatic, RecursiveOptionalParent)) static_assert(not is_assignable_to(RecursiveOptionalParent, RecursiveNonFullyStatic)) + class Other(Protocol): z: str + def _(rec: RecursiveFullyStatic, other: Other): reveal_type(rec.parent.parent.parent) # revealed: RecursiveFullyStatic @@ -2765,25 +3144,30 @@ def _(rec: RecursiveFullyStatic, other: Other): rec.parent.parent.parent = other # error: [invalid-assignment] other = rec.parent.parent.parent # error: [invalid-assignment] + class Foo(Protocol): @property def x(self) -> "Foo": ... + class Bar(Protocol): @property def x(self) -> "Bar": ... + # TODO: this should pass # error: [static-assert-error] static_assert(is_equivalent_to(Foo, Bar)) T = TypeVar("T", bound="TypeVarRecursive") + class TypeVarRecursive(Protocol): # TODO: commenting this out will cause a stack overflow. # x: T y: "TypeVarRecursive" + def _(t: TypeVarRecursive): # reveal_type(t.x) # revealed: T reveal_type(t.y) # revealed: TypeVarRecursive @@ -2805,11 +3189,14 @@ from __future__ import annotations from typing import Protocol, Callable from ty_extensions import Intersection, Not, is_assignable_to, is_equivalent_to, static_assert + class C: ... + class GenericC[T](Protocol): pass + class Recursive(Protocol): direct: Recursive @@ -2831,9 +3218,11 @@ class Recursive(Protocol): nested: Recursive | Callable[[Recursive | Recursive, tuple[Recursive, Recursive]], Recursive | Recursive] + static_assert(is_equivalent_to(Recursive, Recursive)) static_assert(is_assignable_to(Recursive, Recursive)) + def _(r: Recursive): reveal_type(r.direct) # revealed: Recursive reveal_type(r.union) # revealed: None | Recursive @@ -2856,12 +3245,15 @@ def _(r: Recursive): from typing import Protocol from ty_extensions import is_equivalent_to, static_assert + class Foo(Protocol): x: "Bar" + class Bar(Protocol): x: Foo + static_assert(is_equivalent_to(Foo, Bar)) ``` @@ -2871,12 +3263,15 @@ static_assert(is_equivalent_to(Foo, Bar)) from typing import Protocol from ty_extensions import is_disjoint_from, static_assert + class Proto(Protocol): x: "Proto" + class Nominal: x: "Nominal" + static_assert(not is_disjoint_from(Proto, Nominal)) ``` @@ -2887,12 +3282,15 @@ This snippet caused us to panic on an early version of the implementation for pr ```py from typing import Protocol + class A(Protocol): def x(self) -> "B | A": ... + class B(Protocol): def y(self): ... + obj = something_unresolvable # error: [unresolved-reference] reveal_type(obj) # revealed: Unknown if isinstance(obj, (B, A)): @@ -2914,11 +3312,14 @@ python-version = "3.12" from typing_extensions import Protocol, Self from ty_extensions import static_assert + class _HashObject(Protocol): def copy(self) -> Self: ... + class Foo: ... + # Attempting to build this union caused us to overflow on an early version of # x: Foo | _HashObject @@ -2931,12 +3332,15 @@ Some other similar cases that caused issues in our early `Protocol` implementati ```py from typing_extensions import Protocol, Self + class PGconn(Protocol): def connect(self) -> Self: ... + class Connection: pgconn: PGconn + def is_crdb(conn: PGconn) -> bool: return isinstance(conn, Connection) ``` @@ -2948,12 +3352,15 @@ and: ```py from typing_extensions import Protocol + class PGconn(Protocol): def connect[T: PGconn](self: T) -> T: ... + class Connection: pgconn: PGconn + def f(x: PGconn): isinstance(x, Connection) ``` @@ -2973,9 +3380,11 @@ python-version = "3.12" from __future__ import annotations from typing import cast, Protocol + class Iterator[T](Protocol): def __iter__(self) -> Iterator[T]: ... + def f(value: Iterator): cast(Iterator, value) # error: [redundant-cast] ``` @@ -2993,20 +3402,25 @@ python-version = "3.12" ```py from typing import Protocol, TypeVar + class A: ... + class Foo[T](Protocol): def x(self) -> "T | Foo[T]": ... + y: A | Foo[A] # The same thing, but using the legacy syntax: S = TypeVar("S") + class Bar(Protocol[S]): def x(self) -> "S | Bar[S]": ... + z: S | Bar[S] ``` @@ -3024,9 +3438,11 @@ python-version = "3.12" ```py from typing import Protocol + class C[T](Protocol): a: "C[set[T]]" + def takes_c(c: C[set[int]]) -> None: ... def f(c: C[int]) -> None: # The key thing is that we don't stack overflow while checking this. @@ -3042,15 +3458,19 @@ from typing import Generic, TypeVar, Protocol T = TypeVar("T") + class P(Protocol[T]): attr: "P[T] | T" + class A(Generic[T]): attr: T + class B(A[P[int]]): pass + def f(b: B): reveal_type(b) # revealed: B reveal_type(b.attr) # revealed: P[int] @@ -3070,23 +3490,30 @@ python-version = "3.12" ```py from typing import Protocol + class Foo[T]: ... + class A(Protocol): @property def _(self: "A") -> Foo: ... + class B(Protocol): @property def b(self) -> Foo[A]: ... + class C(Undefined): ... # error: "Name `Undefined` used when not defined" + class D: b: Foo[C] + class E[T: B](Protocol): ... + x: E[D] ``` @@ -3100,19 +3527,24 @@ without violating the Liskov Substitution Principle, since all protocols are als from typing import Protocol from ty_extensions import static_assert, is_subtype_of, is_equivalent_to, is_disjoint_from + class HasRepr(Protocol): # error: [invalid-method-override] def __repr__(self) -> object: ... + class HasReprRecursive(Protocol): # error: [invalid-method-override] def __repr__(self) -> "HasReprRecursive": ... + class HasReprRecursiveAndFoo(Protocol): # error: [invalid-method-override] def __repr__(self) -> "HasReprRecursiveAndFoo": ... + foo: int + static_assert(is_subtype_of(object, HasRepr)) static_assert(is_subtype_of(HasRepr, object)) static_assert(is_equivalent_to(object, HasRepr)) @@ -3144,11 +3576,14 @@ minimum in the meantime. from typing import Protocol, ClassVar from ty_extensions import static_assert, is_assignable_to, TypeOf, is_subtype_of + class Foo(Protocol): x: int y: ClassVar[str] + def method(self) -> bytes: ... + def _(f: type[Foo]): reveal_type(f) # revealed: type[@Todo(type[T] for protocols)] @@ -3165,18 +3600,23 @@ def _(f: type[Foo]): # TODO: should be `Callable[[Foo], bytes]` reveal_type(f.method) # revealed: @Todo(type[T] for protocols) + class Bar: ... + # TODO: these should pass static_assert(not is_assignable_to(type[Bar], type[Foo])) # error: [static-assert-error] static_assert(not is_assignable_to(TypeOf[Bar], type[Foo])) # error: [static-assert-error] + class Baz: x: int y: ClassVar[str] = "foo" + def method(self) -> bytes: return b"foo" + static_assert(is_assignable_to(type[Baz], type[Foo])) static_assert(is_assignable_to(TypeOf[Baz], type[Foo])) @@ -3221,24 +3661,33 @@ T2 = TypeVar("T2", bound="A1[Any]") T3 = TypeVar("T3", bound="B2[Any]") T4 = TypeVar("T4", bound="B1[Any]") + class A1(Protocol[T1]): def get_x(self): ... + class A2(Protocol[T2]): def get_y(self): ... + class B1(A1[T3], Protocol[T3]): ... + + class B2(A2[T4], Protocol[T4]): ... + # TODO should just be `B2[Any]` reveal_type(T3.__bound__) # revealed: B2[Any] | @Todo(specialized non-generic class) + # TODO error: [invalid-type-arguments] def f(x: B1[int]): pass + reveal_type(T4.__bound__) # revealed: B1[Any] + # error: [invalid-type-arguments] def g(x: B2[int]): pass diff --git a/crates/ty_python_semantic/resources/mdtest/public_types.md b/crates/ty_python_semantic/resources/mdtest/public_types.md index f8e522e767..048bfff6c6 100644 --- a/crates/ty_python_semantic/resources/mdtest/public_types.md +++ b/crates/ty_python_semantic/resources/mdtest/public_types.md @@ -13,14 +13,20 @@ or `B`: ```py class A: ... + + class B: ... + + class C: ... + def outer() -> None: x = A() def inner() -> None: reveal_type(x) # revealed: A | B + # This call would observe `x` as `A`. inner() @@ -38,6 +44,7 @@ def outer(flag: bool) -> None: def inner() -> None: reveal_type(x) # revealed: A | B | C + inner() if flag: @@ -60,6 +67,7 @@ def outer() -> None: def inner() -> None: reveal_type(x) # revealed: A | C + inner() if False: @@ -69,11 +77,13 @@ def outer() -> None: x = C() inner() + def outer(flag: bool) -> None: x = A() def inner() -> None: reveal_type(x) # revealed: A | C + inner() if flag: @@ -84,6 +94,7 @@ def outer(flag: bool) -> None: x = C() inner() + def outer(flag: bool) -> None: if flag: x = A() @@ -93,6 +104,7 @@ def outer(flag: bool) -> None: def inner() -> None: reveal_type(x) # revealed: A | C + x = C() inner() ``` @@ -106,6 +118,7 @@ def outer(flag: bool) -> None: def inner() -> None: reveal_type(x) # revealed: A + inner() ``` @@ -117,6 +130,7 @@ def outer(flag: bool) -> None: def inner() -> None: # TODO: Ideally, we would emit a possibly-unresolved-reference error here. reveal_type(x) # revealed: A + inner() ``` @@ -130,16 +144,19 @@ def outer() -> None: def inner() -> None: reveal_type(x) # revealed: A + inner() return # unreachable + def outer(flag: bool) -> None: x = A() def inner() -> None: reveal_type(x) # revealed: A | B + if flag: x = B() inner() @@ -148,9 +165,11 @@ def outer(flag: bool) -> None: inner() + def outer(x: A) -> None: def inner() -> None: reveal_type(x) # revealed: A + raise ``` @@ -165,9 +184,13 @@ def f0() -> None: def f3() -> None: def f4() -> None: reveal_type(x) # revealed: A | B + f4() + f3() + f2() + f1() x = B() @@ -184,17 +207,23 @@ evaluated), but they can be applied if there is no reassignment of the symbol. ```py class A: ... -def outer(x: A | None): - if x is not None: - def inner() -> None: - reveal_type(x) # revealed: A | None - inner() - x = None def outer(x: A | None): if x is not None: + + def inner() -> None: + reveal_type(x) # revealed: A | None + + inner() + x = None + + +def outer(x: A | None): + if x is not None: + def inner() -> None: reveal_type(x) # revealed: A + inner() ``` @@ -209,6 +238,7 @@ def outer() -> None: def inner() -> None: # In this scope, `x` may refer to `x = None` or `x = 1`. reveal_type(x) # revealed: None | Literal[1] + inner() x = 1 @@ -218,8 +248,10 @@ def outer() -> None: def inner2() -> None: # In this scope, `x = None` appears as being shadowed by `x = 1`. reveal_type(x) # revealed: Literal[1] + inner2() + def outer() -> None: x = None @@ -227,10 +259,12 @@ def outer() -> None: def inner() -> None: reveal_type(x) # revealed: Literal[1, 2] + inner() x = 2 + def outer(x: A | None): if x is None: x = A() @@ -239,8 +273,10 @@ def outer(x: A | None): def inner() -> None: reveal_type(x) # revealed: A + inner() + def outer(x: A | None): x = x or A() @@ -248,6 +284,7 @@ def outer(x: A | None): def inner() -> None: reveal_type(x) # revealed: A + inner() ``` @@ -259,11 +296,13 @@ The behavior is the same if the outer scope is the global scope of a module: def flag() -> bool: return True + if flag(): x = 1 def f() -> None: reveal_type(x) # revealed: Literal[1, 2] + # Function only used inside this branch f() @@ -282,6 +321,7 @@ in other branches: def flag() -> bool: return True + if flag(): A: str = "" else: @@ -289,6 +329,7 @@ else: reveal_type(A) # revealed: Literal[""] | None + def _(): reveal_type(A) # revealed: str | None ``` @@ -305,6 +346,7 @@ except ImportError: reveal_type(optional_dependency) # revealed: Unknown | None + def _(): reveal_type(optional_dependency) # revealed: Unknown | None ``` @@ -321,6 +363,7 @@ def outer() -> None: def inner() -> None: # TODO: this should ideally be `Literal[1]`, but no other type checker supports this either reveal_type(x) # revealed: None | Literal[1] + x = None # [additional code here] @@ -335,8 +378,11 @@ modules) cannot be recognized from lazy scopes. ```py class A: ... + + class A: ... + def f(x: A): # TODO: no error # error: [invalid-assignment] "Object of type `mdtest_snippet.A @ src/mdtest_snippet.py:12:7 | mdtest_snippet.A @ src/mdtest_snippet.py:13:7` is not assignable to `mdtest_snippet.A @ src/mdtest_snippet.py:13:7`" @@ -354,11 +400,13 @@ def outer() -> None: def set_x() -> None: nonlocal x x = 1 + set_x() def inner() -> None: # TODO: this should ideally be `None | Literal[1]`. Mypy and pyright support this. reveal_type(x) # revealed: None + inner() ``` @@ -372,6 +420,7 @@ definitions of `f`. This would otherwise result in a union of all three definiti ```py from typing import overload + @overload def f(x: int) -> int: ... @overload @@ -379,8 +428,10 @@ def f(x: str) -> str: ... def f(x: int | str) -> int | str: raise NotImplementedError + reveal_type(f) # revealed: Overload[(x: int) -> int, (x: str) -> str] + def _(): reveal_type(f) # revealed: Overload[(x: int) -> int, (x: str) -> str] ``` @@ -391,7 +442,9 @@ This also works if there are conflicting declarations: def flag() -> bool: return True + if flag(): + @overload def g(x: int) -> int: ... @overload @@ -402,9 +455,11 @@ if flag(): else: g: str = "" + def _(): reveal_type(g) # revealed: (Overload[(x: int) -> int, (x: str) -> str]) | str + # error: [conflicting-declarations] g = "test" ``` @@ -450,10 +505,13 @@ def _(): ```py from typing import overload + def flag() -> bool: return True + if flag(): + @overload def f(x: int) -> int: ... @overload diff --git a/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md b/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md index 309a41422c..7a7529f10a 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md @@ -44,15 +44,20 @@ if TYPE_CHECKING: UserT = TypeVar("UserT", covariant=True) MessageT = TypeVar("MessageT", bound="Message", default="Message", covariant=True) + class Messageable(Protocol[MessageT]): ... + ClanT = TypeVar("ClanT", bound="Clan | None", default="Clan | None", covariant=True) GroupT = TypeVar("GroupT", bound="Group | None", default="Group | None", covariant=True) + class Channel(Messageable[MessageT], Generic[MessageT, ClanT, GroupT]): ... + ChannelT = TypeVar("ChannelT", bound=Channel, default=Channel, covariant=True) + class Message(Generic[UserT, ChannelT]): ... ``` @@ -76,14 +81,19 @@ MemberT = TypeVar("MemberT", covariant=True) AuthorT = TypeVar("AuthorT", covariant=True) + class ChatMessage(Message[AuthorT, ChatT], Generic[AuthorT, MemberT, ChatT]): ... + ChatMessageT = TypeVar("ChatMessageT", bound="GroupMessage | ClanMessage", default="GroupMessage | ClanMessage", covariant=True) + class Chat(Channel[ChatMessageT, ClanT, GroupT]): ... + ChatGroupTypeT = TypeVar("ChatGroupTypeT", covariant=True) + class ChatGroup(Generic[MemberT, ChatT, ChatGroupTypeT]): ... ``` @@ -99,6 +109,7 @@ from .chat import Chat if TYPE_CHECKING: from .clan import Clan + class ClanChannel(Chat["Clan", None]): ... ``` @@ -113,6 +124,7 @@ from typing_extensions import Self from .chat import ChatGroup + class Clan(ChatGroup[str], str): ... ``` @@ -123,6 +135,7 @@ from __future__ import annotations from .chat import ChatGroup + class Group(ChatGroup[str]): ... ``` @@ -141,6 +154,9 @@ from .chat import ChatMessage if TYPE_CHECKING: from .channel import ClanChannel + class GroupMessage(ChatMessage["str"]): ... + + class ClanMessage(ChatMessage["ClanChannel"]): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/regression/2236_tuple_is_disjoint.md b/crates/ty_python_semantic/resources/mdtest/regression/2236_tuple_is_disjoint.md index 6fab7cf645..5af38916f2 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/2236_tuple_is_disjoint.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/2236_tuple_is_disjoint.md @@ -11,9 +11,13 @@ python-version = "3.11" from types import FunctionType from ty_extensions import Not, AlwaysTruthy, is_subtype_of, static_assert, is_disjoint_from + class Meta(type): ... + + class F(metaclass=Meta): ... + static_assert(not is_subtype_of(tuple[FunctionType, type[F]], Not[tuple[*tuple[AlwaysTruthy, ...], Meta]])) static_assert(not is_subtype_of(Not[tuple[*tuple[AlwaysTruthy, ...], Meta]], tuple[FunctionType, type[F]])) static_assert(is_disjoint_from(tuple[FunctionType, type[F]], Not[tuple[*tuple[AlwaysTruthy, ...], Meta]])) diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md index 169931352f..a3631258e7 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md @@ -24,10 +24,12 @@ builtin type with the conditionally-defined type: def flag() -> bool: return True + if flag(): abs = 1 chr: int = 1 + def _(): # TODO: Should ideally be `Literal[1] | (def abs(x: SupportsAbs[_T], /) -> _T)` reveal_type(abs) # revealed: Literal[1] diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md index 7130538acf..913da41e15 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md @@ -62,11 +62,13 @@ within the class body: __qualname__ = 42 __module__ = 42 + class Foo: # Inside the class body, these are the implicit class attributes reveal_type(__qualname__) # revealed: str reveal_type(__module__) # revealed: str + # Outside the class, the globals are visible reveal_type(__qualname__) # revealed: Literal[42] reveal_type(__module__) # revealed: Literal[42] @@ -84,9 +86,11 @@ python-version = "3.13" ```py __firstlineno__ = "not an int" + class Foo: reveal_type(__firstlineno__) # revealed: int + reveal_type(__firstlineno__) # revealed: Literal["not an int"] ``` @@ -114,6 +118,7 @@ A common use case is defining a logger with the class name: ```py import logging + class MyClass: logger = logging.getLogger(__qualname__) reveal_type(logger) # revealed: Logger diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/eager.md b/crates/ty_python_semantic/resources/mdtest/scopes/eager.md index 4dafacfb96..6b4b9e49e3 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/eager.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/eager.md @@ -11,9 +11,11 @@ Function definitions are evaluated lazily. ```py x = 1 + def f(): reveal_type(x) # revealed: Literal[1, 2] + x = 2 ``` @@ -135,11 +137,13 @@ scope. ```py x = 1 + class A: reveal_type(x) # revealed: Literal[1] y = x + x = 2 reveal_type(A.y) # revealed: Unknown | Literal[1] @@ -279,8 +283,10 @@ def _(): x = 2 + x = 1 + def _(): class C: # revealed: Literal[1] @@ -301,6 +307,7 @@ def _(): def f(): # revealed: Literal[1, 2] [reveal_type(x) for a in range(1)] + x = 2 ``` @@ -335,6 +342,7 @@ def _(): def g(): # revealed: Literal[1, 2] reveal_type(x) + x = 2 ``` @@ -369,9 +377,11 @@ from typing import ClassVar x = int + class C: var: ClassVar[x] + reveal_type(C.var) # revealed: int x = str @@ -386,9 +396,11 @@ from typing import ClassVar x = int + class C: var: ClassVar[x] + reveal_type(C.var) # revealed: int | str x = str @@ -422,9 +434,11 @@ python-version = "3.12" ```py type Foo = Bar + class Bar: pass + def _(x: Foo): if isinstance(x, Bar): reveal_type(x) # revealed: Bar @@ -439,16 +453,20 @@ def _(x: Foo): class D[T](Bar): pass + class E[T: Bar]: pass + # error: [unresolved-reference] def g[T](x: Bar): pass + def h[T: Bar](x: T): pass + class Bar: pass ``` diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/global-constants.md b/crates/ty_python_semantic/resources/mdtest/scopes/global-constants.md index 7163aa6980..3d29b4f430 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/global-constants.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/global-constants.md @@ -7,6 +7,7 @@ The [`__debug__` constant] should be globally available: ```py reveal_type(__debug__) # revealed: bool + def foo(): reveal_type(__debug__) # revealed: bool ``` diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/global.md b/crates/ty_python_semantic/resources/mdtest/scopes/global.md index 5924852432..97a769d4a4 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/global.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/global.md @@ -7,6 +7,7 @@ A name reference to a never-defined symbol in a function is implicitly a global ```py x = 1 + def f(): reveal_type(x) # revealed: Literal[1] ``` @@ -16,6 +17,7 @@ def f(): ```py x = 1 + def f(): global x reveal_type(x) # revealed: Literal[1] @@ -26,6 +28,7 @@ def f(): ```py x: int = 1 + def f(): y: int = 1 # error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`" @@ -39,6 +42,7 @@ def f(): # error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`" z = "" + z: int ``` @@ -49,6 +53,7 @@ A `global` statement causes lookup to skip any bindings in intervening scopes: ```py x: int = 1 + def outer(): x: str = "" @@ -65,6 +70,7 @@ assignment. ```py x: int | None + def f(): global x x = 1 @@ -76,6 +82,7 @@ Same for an `if` statement: ```py x: int | None + def f(): # The `global` keyword isn't necessary here, but this is testing that it doesn't get in the way # of narrowing. @@ -92,8 +99,10 @@ marks the `nonlocal` line, while `mypy`, `pyright`, and `ruff` (`PLE0115`) mark ```py x = 1 + def f(): x = 1 + def g() -> None: nonlocal x global x # error: [invalid-syntax] "name `x` is nonlocal and global" @@ -108,6 +117,7 @@ def f(): y = x x = 1 # No error. + x = 2 ``` @@ -119,76 +129,90 @@ Using a name prior to its `global` declaration in the same scope is a syntax err x = 1 y = 2 + def f(): print(x) global x # error: [invalid-syntax] "name `x` is used prior to global declaration" print(x) + def f(): global x print(x) global x # error: [invalid-syntax] "name `x` is used prior to global declaration" print(x) + def f(): print(x) global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" print(x) + def f(): global x, y print(x) global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" print(x) + def f(): x = 1 global x # error: [invalid-syntax] "name `x` is used prior to global declaration" x = 1 + def f(): global x x = 1 global x # error: [invalid-syntax] "name `x` is used prior to global declaration" x = 1 + def f(): del x global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" del x + def f(): global x, y del x global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" del x + def f(): del x global x # error: [invalid-syntax] "name `x` is used prior to global declaration" del x + def f(): global x del x global x # error: [invalid-syntax] "name `x` is used prior to global declaration" del x + def f(): del x global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" del x + def f(): global x, y del x global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" del x + def f(): print(f"{x=}") global x # error: [invalid-syntax] "name `x` is used prior to global declaration" + # still an error in module scope x = None global x # error: [invalid-syntax] "name `x` is used prior to global declaration" @@ -199,6 +223,7 @@ global x # error: [invalid-syntax] "name `x` is used prior to global declaratio ```py x = 42 + def f(): global x reveal_type(x) # revealed: Literal[42] @@ -211,6 +236,7 @@ def f(): ```py x = 42 + def f(): # error: [unresolved-reference] "Name `x` used when not defined" reveal_type(x) # revealed: Unknown @@ -223,6 +249,7 @@ def f(): ```py x: int = 1 + def f(): global x x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be global" @@ -235,9 +262,11 @@ Even if the `global` declaration isn't used in an assignment, we conservatively ```py x = 1 + def f(): global x + # TODO: reveal_type(x) # revealed: Unknown | Literal["1"] ``` @@ -251,6 +280,7 @@ x = 1 y: int # z is neither bound nor declared in the global scope + def f(): global x, y, z # error: [unresolved-global] "Invalid global declaration of `z`: `z` has no declarations or bindings in the global scope" ``` @@ -273,6 +303,7 @@ import secrets x: str = "a" + def f(x: int, y: int): class C: reveal_type(x) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md index 2b1419f19d..cfcbee2e1b 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -27,9 +27,11 @@ from builtins import __builtins__ as __bi__ reveal_type(__bi__) # revealed: Any + class X: reveal_type(__name__) # revealed: str + def foo(): reveal_type(__name__) # revealed: str ``` @@ -80,6 +82,7 @@ reveal_type(module.__spec__) # revealed: ModuleSpec | None # error: [unresolved-attribute] reveal_type(module.__warningregistry__) # revealed: Unknown + def nested_scope(): global __loader__ reveal_type(__loader__) # revealed: LoaderProtocol | None @@ -128,6 +131,7 @@ import types reveal_type(types.ModuleType.__getattr__) # revealed: def __getattr__(self, name: str) -> Any + def f(module: types.ModuleType): reveal_type(module.__getattr__) # revealed: bound method ModuleType.__getattr__(name: str) -> Any @@ -168,9 +172,11 @@ conditionally defined type: ```py __file__ = "foo" + def returns_bool() -> bool: return True + if returns_bool(): __name__ = 1 # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `str`" @@ -186,9 +192,11 @@ The same is true if the name is annotated: # error: [invalid-declaration] "Cannot shadow implicit global attribute `__file__` with declaration of type `int`" __file__: int = 42 + def returns_bool() -> bool: return True + if returns_bool(): # error: [invalid-declaration] "Cannot shadow implicit global attribute `__name__` with declaration of type `int`" __name__: int = 1 diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md b/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md index 9c1816ed49..8881e29ce6 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md @@ -5,6 +5,7 @@ ```py def f(): x = 1 + def g(): reveal_type(x) # revealed: Literal[1] ``` @@ -14,6 +15,7 @@ def f(): ```py def f(): x = 1 + def g(): def h(): reveal_type(x) # revealed: Literal[1] @@ -27,6 +29,7 @@ def f(): class C: x = 2 + def g(): reveal_type(x) # revealed: Literal[1] ``` @@ -36,10 +39,12 @@ def f(): ```py def f(): x: int = 1 + def g(): # TODO: This example should actually be an unbound variable error. However to avoid false # positives, we'd need to analyze `nonlocal x` statements in other inner functions. x: str + def h(): reveal_type(x) # revealed: str ``` @@ -53,10 +58,13 @@ whether the variable is bound in that scope: ```py x: int = 1 + def f(): x: str = "hello" + def g(): global x + def h(): # allowed: this loads the global `x` variable due to the `global` declaration in the immediate enclosing scope y: int = x @@ -70,6 +78,7 @@ enclosing scopes. This example isn't a type error, because the inner `x` shadows ```py def f(): x: int = 1 + def g(): x = "hello" # allowed ``` @@ -79,6 +88,7 @@ With `nonlocal` it is a type error, because `x` refers to the same place in both ```py def f(): x: int = 1 + def g(): nonlocal x x = "hello" # error: [invalid-assignment] "Object of type `Literal["hello"]` is not assignable to `int`" @@ -118,6 +128,7 @@ sibling scopes, child scopes, second-cousin-once-removed scopes, etc: ```py def a(): x = 1 + def b(): nonlocal x x = 2 @@ -126,6 +137,7 @@ def a(): def d(): nonlocal x x = 3 + # TODO: This should include 2 and 3. reveal_type(x) # revealed: Literal[1] ``` @@ -138,6 +150,7 @@ binding, rather than to `x = 1` in `f`'s scope: ```py def f(): x = 1 + def g(): if x == 1: # error: [unresolved-reference] "Name `x` used when not defined" x = 2 @@ -149,6 +162,7 @@ scope): ```py def f(): x = 1 + def g(): nonlocal x if x == 1: @@ -161,17 +175,22 @@ For the same reason, using the `+=` operator in an inner scope is an error witho ```py def f(): x = 1 + def g(): x += 1 # error: [unresolved-reference] "Name `x` used when not defined" + def f(): x = 1 + def g(): x = 1 x += 1 # allowed, but doesn't affect the outer scope + def f(): x = 1 + def g(): nonlocal x x += 1 # allowed, and affects the outer scope @@ -186,8 +205,10 @@ def f(): def g(): nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found" + def f(): x = 1 + def g(): nonlocal x, y # error: [invalid-syntax] "no binding for nonlocal `y` found" ``` @@ -197,18 +218,23 @@ A global `x` doesn't work. The target must be in a function-like scope: ```py x = 1 + def f(): def g(): nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found" + def f(): global x + def g(): nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found" + def f(): # A *use* of `x` in an enclosing scope isn't good enough. There needs to be a binding. print(x) + def g(): nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found" ``` @@ -218,6 +244,7 @@ A class-scoped `x` also doesn't work: ```py class Foo: x = 1 + @staticmethod def f(): nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found" @@ -245,8 +272,10 @@ def f(): ```py def f(): x = 1 + def g(): x = 2 + def h(): nonlocal x reveal_type(x) # revealed: Literal[2] @@ -259,8 +288,10 @@ Multiple `nonlocal` statements can "chain" through nested scopes: ```py def f(): x = 1 + def g(): nonlocal x + def h(): nonlocal x reveal_type(x) # revealed: Literal[1] @@ -271,8 +302,10 @@ And the `nonlocal` chain can skip over a scope that doesn't bind the variable: ```py def f1(): x = 1 + def f2(): nonlocal x + def f3(): # No binding; this scope gets skipped. def f4(): @@ -285,10 +318,13 @@ But a `global` statement breaks the chain: ```py x = 1 + def f(): x = 2 + def g(): global x + def h(): nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found" ``` @@ -298,6 +334,7 @@ def f(): ```py def f(): x: int + def g(): nonlocal x x = "string" # error: [invalid-assignment] "Object of type `Literal["string"]` is not assignable to `int`" @@ -311,6 +348,7 @@ x: bool = True y: bool = True z: bool = True + def f1(): # Local definitions of `x`, `y`, and `z`. x: int = 1 @@ -352,8 +390,10 @@ affected by `g`: ```py def f(): x = 1 + def g(): reveal_type(x) # revealed: Literal[1] + reveal_type(x) # revealed: Literal[1] ``` @@ -363,11 +403,13 @@ regardless of whether `g` actually writes to `x`. With a write: ```py def f(): x = 1 + def g(): nonlocal x reveal_type(x) # revealed: Literal[1] x += 1 reveal_type(x) # revealed: Literal[2] + # TODO: should be `Unknown | Literal[1]` reveal_type(x) # revealed: Literal[1] ``` @@ -377,9 +419,11 @@ Without a write: ```py def f(): x = 1 + def g(): nonlocal x reveal_type(x) # revealed: Literal[1] + # TODO: should be `Unknown | Literal[1]` reveal_type(x) # revealed: Literal[1] ``` @@ -389,6 +433,7 @@ def f(): ```py def f(): x: int = 1 + def g(): nonlocal x x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be nonlocal" @@ -401,6 +446,7 @@ Using a name prior to its `nonlocal` declaration in the same scope is a syntax e ```py def f(): x = 1 + def g(): x = 2 nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration" @@ -412,13 +458,16 @@ of them come after the usage: ```py def f(): x = 1 + def g(): nonlocal x x = 2 nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration" + def f(): x = 1 + def g(): nonlocal x nonlocal x @@ -434,6 +483,7 @@ def f(): def g(): # This is allowed, because of the subsequent definition of `x`. nonlocal x + x = 1 ``` @@ -442,6 +492,7 @@ def f(): ```py def foo(): x: int = 1 + def bar(): if isinstance(x, str): reveal_type(x) # revealed: Never diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md index bf1f81d5c2..44753f6a83 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md @@ -8,14 +8,17 @@ Name lookups within a class scope fall back to globals, but lookups of class att def coinflip() -> bool: return True + flag = coinflip() x = 1 + class C: y = x if flag: x = 2 + # error: [possibly-missing-attribute] "Attribute `x` may be missing on class `C`" reveal_type(C.x) # revealed: Unknown | Literal[2] reveal_type(C.y) # revealed: Unknown | Literal[1] @@ -27,9 +30,11 @@ reveal_type(C.y) # revealed: Unknown | Literal[1] def coinflip() -> bool: return True + if coinflip(): x = "abc" + class C: if coinflip(): x = 1 @@ -37,6 +42,7 @@ class C: # Possibly unbound variables in enclosing scopes are considered bound. y = x + reveal_type(C.y) # revealed: Unknown | Literal[1, "abc"] ``` @@ -46,12 +52,14 @@ reveal_type(C.y) # revealed: Unknown | Literal[1, "abc"] def coinflip() -> bool: return True + class C: if coinflip(): x: int = 1 elif coinflip(): x: str = "abc" + # error: [possibly-missing-attribute] reveal_type(C.x) # revealed: int | str ``` @@ -63,6 +71,7 @@ An unbound function local that has definitions in the scope does not fall back t ```py x = 1 + def f(): # error: [unresolved-reference] # revealed: Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/shadowing/class.md b/crates/ty_python_semantic/resources/mdtest/shadowing/class.md index 2f2cae26e7..a53083bfc4 100644 --- a/crates/ty_python_semantic/resources/mdtest/shadowing/class.md +++ b/crates/ty_python_semantic/resources/mdtest/shadowing/class.md @@ -5,6 +5,7 @@ ```py class C: ... + C = 1 # error: [invalid-assignment] ``` @@ -15,5 +16,6 @@ No diagnostic is raised in the case of explicit shadowing: ```py class C: ... + C: int = 1 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/shadowing/function.md b/crates/ty_python_semantic/resources/mdtest/shadowing/function.md index cf5f07f4ad..b1cbc8358a 100644 --- a/crates/ty_python_semantic/resources/mdtest/shadowing/function.md +++ b/crates/ty_python_semantic/resources/mdtest/shadowing/function.md @@ -15,6 +15,7 @@ def f(x: str): ```py def f(): ... + f = 1 # error: [invalid-assignment] ``` @@ -23,6 +24,7 @@ f = 1 # error: [invalid-assignment] ```py def f(): ... + f: int = 1 ``` @@ -35,19 +37,25 @@ non-`def` declaration, without error. f = 1 reveal_type(f) # revealed: Literal[1] + def f(): ... + reveal_type(f) # revealed: def f() -> Unknown + def f(x: int) -> int: raise NotImplementedError + reveal_type(f) # revealed: def f(x: int) -> int f: int = 1 reveal_type(f) # revealed: Literal[1] + def f(): ... + reveal_type(f) # revealed: def f() -> Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md index 4b7e73498c..9a8249051f 100644 --- a/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md +++ b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md @@ -13,10 +13,12 @@ If we can statically determine that the condition is always true, then we can al ```py import sys + class C: if sys.version_info >= (3, 9): SomeFeature: str = "available" + # C.SomeFeature is unconditionally available here, because we are on Python 3.9 or newer: reveal_type(C.SomeFeature) # revealed: str ``` @@ -38,6 +40,7 @@ import typing if typing.TYPE_CHECKING: from module import SomeType + # `SomeType` is unconditionally available here for type checkers: def f(s: SomeType) -> None: ... ``` @@ -173,6 +176,7 @@ demonstrate this, since semantic index building is inherently single-module: ```py from typing import Literal + class AlwaysTrue: def __bool__(self) -> Literal[True]: return True @@ -258,6 +262,7 @@ Just for comparison, we still infer the combined type if the condition is not st def flag() -> bool: return True + x = 1 if flag(): @@ -287,6 +292,7 @@ reveal_type(x) # revealed: Literal[2] def flag() -> bool: return True + x = 1 if flag(): @@ -305,6 +311,7 @@ reveal_type(x) # revealed: Literal[2, 4] def flag() -> bool: return True + x = 1 if flag(): @@ -323,6 +330,7 @@ reveal_type(x) # revealed: Literal[2, 3] def flag() -> bool: return True + x = 1 if flag(): @@ -343,6 +351,7 @@ Make sure that we include bindings from all non-`False` branches: def flag() -> bool: return True + x = 1 if flag(): @@ -371,6 +380,7 @@ Make sure that we only include the binding from the first `elif True` branch: def flag() -> bool: return True + x = 1 if flag(): @@ -395,6 +405,7 @@ reveal_type(x) # revealed: Literal[2, 3, 4] def flag() -> bool: return True + x = 1 if flag(): @@ -411,6 +422,7 @@ reveal_type(x) # revealed: Literal[2, 3] def flag() -> bool: return True + x = 1 if flag(): @@ -457,6 +469,7 @@ reveal_type(x) # revealed: Literal[1] def flag() -> bool: return True + x = 1 if True: @@ -474,6 +487,7 @@ reveal_type(x) # revealed: Literal[1, 2] def flag() -> bool: return True + x = 1 if flag(): @@ -519,6 +533,7 @@ reveal_type(x) # revealed: Literal[1] def flag() -> bool: return True + x = 1 if False: @@ -570,6 +585,7 @@ reveal_type(x) # revealed: Literal[3] def flag() -> bool: return True + x = 1 if True: @@ -589,6 +605,7 @@ reveal_type(x) # revealed: Literal[2, 3] def flag() -> bool: return True + x = 1 if flag(): @@ -640,6 +657,7 @@ reveal_type(x) # revealed: Literal[4] def flag() -> bool: return True + x = 1 if False: @@ -662,6 +680,7 @@ reveal_type(x) # revealed: Literal[3, 4] ```py def may_raise() -> None: ... + x = 1 try: @@ -681,6 +700,7 @@ reveal_type(x) # revealed: Literal[2, 4] ```py def may_raise() -> None: ... + x = 1 if True: @@ -702,6 +722,7 @@ reveal_type(x) # revealed: Literal[2, 3, 4] ```py def may_raise() -> None: ... + x = 1 if True: @@ -723,6 +744,7 @@ reveal_type(x) # revealed: Literal[3, 4] ```py def may_raise() -> None: ... + x = 1 if True: @@ -749,6 +771,7 @@ reveal_type(x) # revealed: Literal[5] def iterable() -> list[object]: return [1, ""] + x = 1 for _ in iterable(): @@ -765,6 +788,7 @@ reveal_type(x) # revealed: Literal[1, 3] def iterable() -> list[object]: return [1, ""] + x = 1 for _ in iterable(): @@ -784,6 +808,7 @@ reveal_type(x) # revealed: Literal[3] def iterable() -> list[object]: return [1, ""] + x = 1 if True: @@ -801,6 +826,7 @@ reveal_type(x) # revealed: Literal[1, 2] def iterable() -> list[object]: return [1, ""] + x = 1 if True: @@ -820,6 +846,7 @@ reveal_type(x) # revealed: Literal[3] def iterable() -> list[object]: return [1, ""] + x = 1 if True: @@ -947,6 +974,7 @@ Make sure that we still infer the combined type if the condition is not statical def flag() -> bool: return True + x = 1 while flag(): @@ -1003,6 +1031,7 @@ do not panic in the original scenario: def flag() -> bool: return True + while True: if flag(): break @@ -1072,6 +1101,7 @@ Make sure we don't infer a static truthiness in case there is a case guard: def flag() -> bool: return True + x = 1 match "a": @@ -1123,6 +1153,7 @@ For definitely-false cases, the presence of a guard has no influence: def flag() -> bool: return True + x = 1 match "something else": @@ -1220,6 +1251,7 @@ x: str if False: x: int + def f() -> None: reveal_type(x) # revealed: str ``` @@ -1234,6 +1266,7 @@ if True: else: x: int + def f() -> None: reveal_type(x) # revealed: str ``` @@ -1288,6 +1321,7 @@ reveal_type(x) # revealed: int def flag() -> bool: return True + x: str if flag(): @@ -1308,17 +1342,22 @@ reveal_type(x) # revealed: str | int def f() -> int: return 1 + def g() -> int: return 1 + if True: + def f() -> str: return "" else: + def g() -> str: return "" + reveal_type(f()) # revealed: str reveal_type(g()) # revealed: int ``` @@ -1327,13 +1366,16 @@ reveal_type(g()) # revealed: int ```py if True: + class C: x: int = 1 else: + class C: x: str = "a" + reveal_type(C.x) # revealed: int ``` @@ -1346,6 +1388,7 @@ class C: else: x: str = "a" + reveal_type(C.x) # revealed: int ``` @@ -1404,6 +1447,7 @@ unbound: def flag() -> bool: return True + if flag(): x = 1 @@ -1417,6 +1461,7 @@ x def flag() -> bool: return True + if False: if True: unbound1 = 1 @@ -1480,6 +1525,7 @@ z if True: x = 1 + def f(): # x is always bound, no error x @@ -1525,6 +1571,7 @@ from module import symbol def flag() -> bool: return True + if flag(): symbol = 1 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md b/crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md index a8077eba1f..bc053f93fd 100644 --- a/crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md +++ b/crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md @@ -62,6 +62,7 @@ be assigned if `EllipsisType` is actually assignable to the annotated type. # error: [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`" def f(x: int = ...) -> None: ... + # error: [invalid-assignment] "Object of type `EllipsisType` is not assignable to `int`" a: int = ... b = ... diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md b/crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md index ac76a95211..e8f7dadac7 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md @@ -39,9 +39,11 @@ config[0] = 3 # error: [invalid-assignment] ```py from typing import TypedDict + class Config(TypedDict): retries: int + def _(config: Config) -> None: config["retries"] = "three" # error: [invalid-assignment] ``` @@ -51,9 +53,11 @@ def _(config: Config) -> None: ```py from typing import TypedDict + class Config(TypedDict): retries: int + def _(config: Config) -> None: config[0] = 3 # error: [invalid-key] ``` @@ -63,9 +67,11 @@ def _(config: Config) -> None: ```py from typing import TypedDict + class Config(TypedDict): retries: int + def _(config: Config) -> None: config["Retries"] = 30.0 # error: [invalid-key] ``` @@ -77,6 +83,7 @@ class ReadOnlyDict: def __getitem__(self, key: str) -> int: return 42 + config = ReadOnlyDict() config["retries"] = 3 # error: [invalid-assignment] ``` @@ -93,14 +100,17 @@ def _(config: dict[str, int] | None) -> None: ```py from typing import TypedDict + class Person(TypedDict): name: str phone_number: str + class Animal(TypedDict): name: str legs: int + def _(being: Person | Animal) -> None: being["legs"] = 4 # error: [invalid-key] ``` @@ -110,14 +120,17 @@ def _(being: Person | Animal) -> None: ```py from typing import TypedDict + class Person(TypedDict): name: str phone_number: str + class Animal(TypedDict): name: str legs: int + def _(being: Person | Animal) -> None: # error: [invalid-key] # error: [invalid-key] diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/bytes.md b/crates/ty_python_semantic/resources/mdtest/subscript/bytes.md index 1939318c20..56541ad4ae 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/bytes.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/bytes.md @@ -22,6 +22,7 @@ reveal_type(x) # revealed: Unknown y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5" reveal_type(y) # revealed: Unknown + def _(n: int): a = b"abcde"[n] reveal_type(a) # revealed: int @@ -40,10 +41,12 @@ b[:4:0] # error: [zero-stepsize-in-slice] b[0::0] # error: [zero-stepsize-in-slice] b[::0] # error: [zero-stepsize-in-slice] + def _(m: int, n: int): byte_slice1 = b[m:n] reveal_type(byte_slice1) # revealed: bytes + def _(s: bytes) -> bytes: byte_slice2 = s[0:5] return reveal_type(byte_slice2) # revealed: bytes diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/class.md b/crates/ty_python_semantic/resources/mdtest/subscript/class.md index 1c14dd23a5..1f733de3d7 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/class.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/class.md @@ -5,6 +5,7 @@ ```py class NotSubscriptable: ... + # error: "Cannot subscript object of type `` with no `__class_getitem__` method" a = NotSubscriptable[0] ``` @@ -16,6 +17,7 @@ class Identity: def __class_getitem__(cls, item: int) -> str: return str(item) + reveal_type(Identity[0]) # revealed: str ``` @@ -31,9 +33,12 @@ reveal_type(Identity.__class_getitem__(0)) # revealed: str def _(flag: bool): class UnionClassGetItem: if flag: + def __class_getitem__(cls, item: int) -> str: return str(item) + else: + def __class_getitem__(cls, item: int) -> int: return item @@ -63,12 +68,15 @@ def _(flag: bool): ```py def _(flag: bool): if flag: + class Spam: def __class_getitem__(self, x: int) -> str: return "foo" else: + class Spam: ... + # error: [not-subscriptable] "Cannot subscript object of type `` with no `__class_getitem__` method" # revealed: str | Unknown reveal_type(Spam[42]) @@ -79,6 +87,7 @@ def _(flag: bool): ```py def _(flag: bool): if flag: + class Eggs: def __class_getitem__(self, x: int) -> str: return "foo" @@ -99,10 +108,12 @@ should be reported even if it would not succeed for some other element of the in ```py class Foo: ... + class Bar: def __getitem__(self, key: str) -> int: return 42 + def f(x: Foo): if isinstance(x, Bar): # TODO: should be `int` diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/instance.md b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md index e9691f255a..31018a9fc1 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/instance.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md @@ -7,6 +7,7 @@ ```py class NotSubscriptable: ... + a = NotSubscriptable()[0] # error: [not-subscriptable] ``` @@ -16,6 +17,7 @@ a = NotSubscriptable()[0] # error: [not-subscriptable] class NotSubscriptable: __getitem__ = None + # TODO: this would be more user-friendly if the `call-non-callable` diagnostic was # transformed into a `not-subscriptable` diagnostic with a subdiagnostic explaining # that this was because `__getitem__` was possibly not callable @@ -31,6 +33,7 @@ class Identity: def __getitem__(self, index: int) -> int: return index + reveal_type(Identity()[0]) # revealed: int ``` @@ -40,9 +43,12 @@ reveal_type(Identity()[0]) # revealed: int def _(flag: bool): class Identity: if flag: + def __getitem__(self, index: int) -> int: return index + else: + def __getitem__(self, index: int) -> str: return str(index) @@ -56,6 +62,7 @@ class Identity: def __getitem__(self, index: int) -> int: return index + a = Identity() # error: [invalid-argument-type] "Method `__getitem__` of type `bound method Identity.__getitem__(index: int) -> int` cannot be called with key of type `Literal["a"]` on object of type `Identity`" a["a"] @@ -68,6 +75,7 @@ class NoGetitem: def __setitem__(self, index: int, value: int) -> None: pass + a = NoGetitem() a[0] = 0 ``` @@ -77,6 +85,7 @@ a[0] = 0 ```py class NoSetitem: ... + a = NoSetitem() a[0] = 0 # error: "Cannot assign to a subscript on an object of type `NoSetitem`" ``` @@ -87,6 +96,7 @@ a[0] = 0 # error: "Cannot assign to a subscript on an object of type `NoSetitem class NoSetitem: __setitem__ = None + a = NoSetitem() a[0] = 0 # error: "Method `__setitem__` of type `Unknown | None` may not be callable on object of type `NoSetitem`" ``` @@ -98,6 +108,7 @@ class Identity: def __setitem__(self, index: int, value: int) -> None: pass + a = Identity() a[0] = 0 ``` @@ -109,6 +120,7 @@ class Identity: def __setitem__(self, index: int, value: int) -> None: pass + a = Identity() # error: [invalid-assignment] "Invalid subscript assignment with key of type `Literal["a"]` and value of type `Literal[0]` on object of type `Identity`" a["a"] = 0 diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/stepsize_zero.md b/crates/ty_python_semantic/resources/mdtest/subscript/stepsize_zero.md index 2d574a6aac..34ecf8212c 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/stepsize_zero.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/stepsize_zero.md @@ -9,5 +9,6 @@ class MySequence: def __getitem__(self, s: slice) -> int: return 0 + MySequence()[0:1:0] # No error ``` diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/string.md b/crates/ty_python_semantic/resources/mdtest/subscript/string.md index 469300c0b4..b0c6cdc615 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/string.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/string.md @@ -19,6 +19,7 @@ reveal_type(a) # revealed: Unknown b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5" reveal_type(b) # revealed: Unknown + def _(n: int): a = "abcde"[n] reveal_type(a) # revealed: LiteralString diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md index 1ec85b3203..922bd45b30 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md @@ -29,15 +29,27 @@ Precise types for index operations are also inferred for tuple subclasses: ```py class I0: ... + + class I1: ... + + class I2: ... + + class I3: ... + + class I5: ... + + class HeterogeneousSubclass0(tuple[()]): ... + # revealed: Overload[(self, index: SupportsIndex, /) -> Never, (self, index: slice[Any, Any, Any], /) -> tuple[()]] reveal_type(HeterogeneousSubclass0.__getitem__) + def f0(h0: HeterogeneousSubclass0, i: int): # error: [index-out-of-bounds] reveal_type(h0[0]) # revealed: Unknown @@ -48,11 +60,14 @@ def f0(h0: HeterogeneousSubclass0, i: int): reveal_type(h0[i]) # revealed: Never + class HeterogeneousSubclass1(tuple[I0]): ... + # revealed: Overload[(self, index: SupportsIndex, /) -> I0, (self, index: slice[Any, Any, Any], /) -> tuple[I0, ...]] reveal_type(HeterogeneousSubclass1.__getitem__) + def f0(h1: HeterogeneousSubclass1, i: int): reveal_type(h1[0]) # revealed: I0 # error: [index-out-of-bounds] @@ -60,13 +75,16 @@ def f0(h1: HeterogeneousSubclass1, i: int): reveal_type(h1[-1]) # revealed: I0 reveal_type(h1[i]) # revealed: I0 + # Element at index 2 is deliberately the same as the element at index 1, # to illustrate that the `__getitem__` overloads for these two indices are combined class HeterogeneousSubclass4(tuple[I0, I1, I0, I3]): ... + # revealed: Overload[(self, index: Literal[-4, -2, 0, 2], /) -> I0, (self, index: Literal[-3, 1], /) -> I1, (self, index: Literal[-1, 3], /) -> I3, (self, index: SupportsIndex, /) -> I0 | I1 | I3, (self, index: slice[Any, Any, Any], /) -> tuple[I0 | I1 | I3, ...]] reveal_type(HeterogeneousSubclass4.__getitem__) + def f(h4: HeterogeneousSubclass4, i: int): reveal_type(h4[0]) # revealed: I0 reveal_type(h4[1]) # revealed: I1 @@ -78,11 +96,14 @@ def f(h4: HeterogeneousSubclass4, i: int): reveal_type(h4[-4]) # revealed: I0 reveal_type(h4[i]) # revealed: I0 | I1 | I3 + class MixedSubclass(tuple[I0, *tuple[I1, ...], I2, I3, I2, I5]): ... + # revealed: Overload[(self, index: Literal[0], /) -> I0, (self, index: Literal[-5], /) -> I1 | I0, (self, index: Literal[-1], /) -> I5, (self, index: Literal[1], /) -> I1 | I2, (self, index: Literal[-4, -2], /) -> I2, (self, index: Literal[2, 3], /) -> I1 | I2 | I3, (self, index: Literal[-3], /) -> I3, (self, index: Literal[4], /) -> I1 | I2 | I3 | I5, (self, index: SupportsIndex, /) -> I0 | I1 | I2 | I3 | I5, (self, index: slice[Any, Any, Any], /) -> tuple[I0 | I1 | I2 | I3 | I5, ...]] reveal_type(MixedSubclass.__getitem__) + def g(m: MixedSubclass, i: int): reveal_type(m[0]) # revealed: I0 reveal_type(m[1]) # revealed: I1 | I2 @@ -102,11 +123,14 @@ def g(m: MixedSubclass, i: int): reveal_type(m[i]) # revealed: I0 | I1 | I2 | I3 | I5 + class MixedSubclass2(tuple[I0, I1, *tuple[I2, ...], I3]): ... + # revealed: Overload[(self, index: Literal[0], /) -> I0, (self, index: Literal[-2], /) -> I2 | I1, (self, index: Literal[1], /) -> I1, (self, index: Literal[-3], /) -> I2 | I1 | I0, (self, index: Literal[-1], /) -> I3, (self, index: Literal[2], /) -> I2 | I3, (self, index: SupportsIndex, /) -> I0 | I1 | I2 | I3, (self, index: slice[Any, Any, Any], /) -> tuple[I0 | I1 | I2 | I3, ...]] reveal_type(MixedSubclass2.__getitem__) + def g(m: MixedSubclass2, i: int): reveal_type(m[0]) # revealed: I0 reveal_type(m[1]) # revealed: I1 @@ -160,13 +184,17 @@ tuples are naturally understood as being subtypes of protocols that have precise from typing import Protocol, Literal from ty_extensions import static_assert, is_subtype_of + class IntFromZeroSubscript(Protocol): def __getitem__(self, index: Literal[0], /) -> int: ... + static_assert(is_subtype_of(tuple[int, str], IntFromZeroSubscript)) + class TupleSubclass(tuple[int, str]): ... + static_assert(is_subtype_of(TupleSubclass, IntFromZeroSubscript)) ``` @@ -221,12 +249,22 @@ def _(m: int, n: int): tuple_slice = t[m:n] reveal_type(tuple_slice) # revealed: tuple[Literal[1, "a", b"b"] | None, ...] + class I0: ... + + class I1: ... + + class I2: ... + + class I3: ... + + class HeterogeneousTupleSubclass(tuple[I0, I1, I2, I3]): ... + def __(t: HeterogeneousTupleSubclass, m: int, n: int): reveal_type(t[0:0]) # revealed: tuple[()] reveal_type(t[0:1]) # revealed: tuple[I0] @@ -284,6 +322,7 @@ python-version = "3.11" ```py from typing import Literal + def homogeneous(t: tuple[str, ...]) -> None: reveal_type(t[0]) # revealed: str reveal_type(t[1]) # revealed: str @@ -295,6 +334,7 @@ def homogeneous(t: tuple[str, ...]) -> None: reveal_type(t[-3]) # revealed: str reveal_type(t[-4]) # revealed: str + def mixed(t: tuple[Literal[1], Literal[2], Literal[3], *tuple[str, ...], Literal[8], Literal[9], Literal[10]]) -> None: reveal_type(t[0]) # revealed: Literal[1] reveal_type(t[1]) # revealed: Literal[2] @@ -323,6 +363,7 @@ def _(a: tuple, b: tuple[int], c: tuple[int, str], d: tuple[int, ...]) -> None: reveal_type(c) # revealed: tuple[int, str] reveal_type(d) # revealed: tuple[int, ...] + reveal_type(tuple) # revealed: reveal_type(tuple[int]) # revealed: reveal_type(tuple[int, str]) # revealed: @@ -332,6 +373,7 @@ reveal_type(tuple[int, ...]) # revealed: ```py from typing import Any + def _(a: type[tuple], b: type[tuple[int]], c: type[tuple[int, ...]], d: type[tuple[Any, ...]]) -> None: reveal_type(a) # revealed: type[tuple[Unknown, ...]] reveal_type(b) # revealed: type[tuple[int]] @@ -349,13 +391,17 @@ python-version = "3.9" ```py from ty_extensions import reveal_mro + class A(tuple[int, str]): ... + # revealed: (, , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(A) + class C(tuple): ... + # revealed: (, , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(C) ``` @@ -369,8 +415,10 @@ reveal_mro(C) ```py from typing import Any, Tuple + class A: ... + def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]): reveal_type(c) # revealed: tuple[Unknown, ...] reveal_type(d) # revealed: tuple[int, A] @@ -391,13 +439,17 @@ python-version = "3.9" from typing import Tuple from ty_extensions import reveal_mro + class A(Tuple[int, str]): ... + # revealed: (, , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(A) + class C(Tuple): ... + # revealed: (, , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(C) ``` @@ -408,6 +460,7 @@ reveal_mro(C) def test(val: tuple[str] | tuple[int]): reveal_type(val[0]) # revealed: str | int + def test2(val: tuple[str, None] | list[int | float]): reveal_type(val[0]) # revealed: str | int | float ``` @@ -425,9 +478,13 @@ def test3(val: tuple[str] | tuple[int] | int): ```py from ty_extensions import Intersection + class Foo: ... + + class Bar: ... + def test4(val: Intersection[tuple[Foo], tuple[Bar]]): # TODO: should be `Foo & Bar` reveal_type(val[0]) # revealed: @Todo(Subscript expressions on intersections) diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/no_type_check.md b/crates/ty_python_semantic/resources/mdtest/suppressions/no_type_check.md index 262d287f97..e5817b5495 100644 --- a/crates/ty_python_semantic/resources/mdtest/suppressions/no_type_check.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/no_type_check.md @@ -10,6 +10,7 @@ ```py from typing import no_type_check + @no_type_check def test() -> int: return a + 5 @@ -20,6 +21,7 @@ def test() -> int: ```py from typing import no_type_check + @no_type_check def test() -> int: def nested(): @@ -31,6 +33,7 @@ def test() -> int: ```py from typing import no_type_check + @no_type_check def test() -> int: class Nested: @@ -45,6 +48,7 @@ Don't suppress diagnostics for decorators appearing before the `no_type_check` d ```py from typing import no_type_check + @unknown_decorator # error: [unresolved-reference] @no_type_check def test() -> int: @@ -63,6 +67,7 @@ the discussion on the ```py from typing import no_type_check + @no_type_check @unknown_decorator def test() -> int: @@ -76,6 +81,7 @@ def test() -> int: ```py from typing import no_type_check + @no_type_check def test(a: int = "test"): return x + 5 @@ -86,6 +92,7 @@ def test(a: int = "test"): ```py from typing import no_type_check + @no_type_check def test() -> Undefined: return x + 5 @@ -104,6 +111,7 @@ class. ```py from typing import no_type_check + @no_type_check class Test: def test(self): @@ -115,6 +123,7 @@ class Test: ```py from typing import no_type_check + @no_type_check def test(): # error: [unused-ignore-comment] "Unused `ty: ignore` directive" diff --git a/crates/ty_python_semantic/resources/mdtest/terminal_statements.md b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md index 25d458ae67..afcde95edf 100644 --- a/crates/ty_python_semantic/resources/mdtest/terminal_statements.md +++ b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md @@ -14,6 +14,7 @@ def f(cond: bool) -> str: raise ValueError return x + def g(cond: bool): if cond: x = "test" @@ -44,6 +45,7 @@ def resolved_reference(cond: bool) -> str: return "early" return x # no possibly-unresolved-reference diagnostic! + def return_in_then_branch(cond: bool): if cond: x = "terminal" @@ -54,6 +56,7 @@ def return_in_then_branch(cond: bool): reveal_type(x) # revealed: Literal["test"] reveal_type(x) # revealed: Literal["test"] + def return_in_else_branch(cond: bool): if cond: x = "test" @@ -64,6 +67,7 @@ def return_in_else_branch(cond: bool): return reveal_type(x) # revealed: Literal["test"] + def return_in_both_branches(cond: bool): if cond: x = "terminal1" @@ -74,6 +78,7 @@ def return_in_both_branches(cond: bool): reveal_type(x) # revealed: Literal["terminal2"] return + def return_in_try(cond: bool): x = "before" try: @@ -89,6 +94,7 @@ def return_in_try(cond: bool): reveal_type(x) # revealed: Literal["before", "test"] reveal_type(x) # revealed: Literal["before", "test"] + def return_in_nested_then_branch(cond1: bool, cond2: bool): if cond1: x = "test1" @@ -104,6 +110,7 @@ def return_in_nested_then_branch(cond1: bool, cond2: bool): reveal_type(x) # revealed: Literal["test2"] reveal_type(x) # revealed: Literal["test1", "test2"] + def return_in_nested_else_branch(cond1: bool, cond2: bool): if cond1: x = "test1" @@ -119,6 +126,7 @@ def return_in_nested_else_branch(cond1: bool, cond2: bool): reveal_type(x) # revealed: Literal["test2"] reveal_type(x) # revealed: Literal["test1", "test2"] + def return_in_both_nested_branches(cond1: bool, cond2: bool): if cond1: x = "test" @@ -157,6 +165,7 @@ def resolved_reference(cond: bool) -> str: continue return x + def continue_in_then_branch(cond: bool, i: int): x = "before" for _ in range(i): @@ -171,6 +180,7 @@ def continue_in_then_branch(cond: bool, i: int): # TODO: Should be Literal["before", "loop", "continue"] reveal_type(x) # revealed: Literal["before", "loop"] + def continue_in_else_branch(cond: bool, i: int): x = "before" for _ in range(i): @@ -185,6 +195,7 @@ def continue_in_else_branch(cond: bool, i: int): # TODO: Should be Literal["before", "loop", "continue"] reveal_type(x) # revealed: Literal["before", "loop"] + def continue_in_both_branches(cond: bool, i: int): x = "before" for _ in range(i): @@ -199,6 +210,7 @@ def continue_in_both_branches(cond: bool, i: int): # TODO: Should be Literal["before", "continue1", "continue2"] reveal_type(x) # revealed: Literal["before"] + def continue_in_nested_then_branch(cond1: bool, cond2: bool, i: int): x = "before" for _ in range(i): @@ -218,6 +230,7 @@ def continue_in_nested_then_branch(cond1: bool, cond2: bool, i: int): # TODO: Should be Literal["before", "loop1", "loop2", "continue"] reveal_type(x) # revealed: Literal["before", "loop1", "loop2"] + def continue_in_nested_else_branch(cond1: bool, cond2: bool, i: int): x = "before" for _ in range(i): @@ -237,6 +250,7 @@ def continue_in_nested_else_branch(cond1: bool, cond2: bool, i: int): # TODO: Should be Literal["before", "loop1", "loop2", "continue"] reveal_type(x) # revealed: Literal["before", "loop1", "loop2"] + def continue_in_both_nested_branches(cond1: bool, cond2: bool, i: int): x = "before" for _ in range(i): @@ -275,6 +289,7 @@ def resolved_reference(cond: bool) -> str: return x return x # error: [unresolved-reference] + def break_in_then_branch(cond: bool, i: int): x = "before" for _ in range(i): @@ -288,6 +303,7 @@ def break_in_then_branch(cond: bool, i: int): reveal_type(x) # revealed: Literal["loop"] reveal_type(x) # revealed: Literal["before", "break", "loop"] + def break_in_else_branch(cond: bool, i: int): x = "before" for _ in range(i): @@ -301,6 +317,7 @@ def break_in_else_branch(cond: bool, i: int): reveal_type(x) # revealed: Literal["loop"] reveal_type(x) # revealed: Literal["before", "loop", "break"] + def break_in_both_branches(cond: bool, i: int): x = "before" for _ in range(i): @@ -314,6 +331,7 @@ def break_in_both_branches(cond: bool, i: int): break reveal_type(x) # revealed: Literal["before", "break1", "break2"] + def break_in_nested_then_branch(cond1: bool, cond2: bool, i: int): x = "before" for _ in range(i): @@ -332,6 +350,7 @@ def break_in_nested_then_branch(cond1: bool, cond2: bool, i: int): reveal_type(x) # revealed: Literal["loop1", "loop2"] reveal_type(x) # revealed: Literal["before", "loop1", "break", "loop2"] + def break_in_nested_else_branch(cond1: bool, cond2: bool, i: int): x = "before" for _ in range(i): @@ -350,6 +369,7 @@ def break_in_nested_else_branch(cond1: bool, cond2: bool, i: int): reveal_type(x) # revealed: Literal["loop1", "loop2"] reveal_type(x) # revealed: Literal["before", "loop1", "loop2", "break"] + def break_in_both_nested_branches(cond1: bool, cond2: bool, i: int): x = "before" for _ in range(i): @@ -409,6 +429,7 @@ def raise_in_then_branch(cond: bool): # Exceptions can occur anywhere, so "before" and "raise" are valid possibilities reveal_type(x) # revealed: Literal["before", "raise", "else"] + def raise_in_else_branch(cond: bool): x = "before" try: @@ -434,6 +455,7 @@ def raise_in_else_branch(cond: bool): # Exceptions can occur anywhere, so "before" and "raise" are valid possibilities reveal_type(x) # revealed: Literal["before", "else", "raise"] + def raise_in_both_branches(cond: bool): x = "before" try: @@ -462,6 +484,7 @@ def raise_in_both_branches(cond: bool): # Exceptions can occur anywhere, so "before" and "raise" are valid possibilities reveal_type(x) # revealed: Literal["before", "raise1", "raise2"] + def raise_in_nested_then_branch(cond1: bool, cond2: bool): x = "before" try: @@ -492,6 +515,7 @@ def raise_in_nested_then_branch(cond1: bool, cond2: bool): # Exceptions can occur anywhere, so "before" and "raise" are valid possibilities reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"] + def raise_in_nested_else_branch(cond1: bool, cond2: bool): x = "before" try: @@ -522,6 +546,7 @@ def raise_in_nested_else_branch(cond1: bool, cond2: bool): # Exceptions can occur anywhere, so "before" and "raise" are valid possibilities reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"] + def raise_in_both_nested_branches(cond1: bool, cond2: bool): x = "before" try: @@ -584,6 +609,7 @@ invalid return type. from typing import NoReturn import sys + def f() -> NoReturn: sys.exit(1) ``` @@ -594,11 +620,13 @@ Let's try cases where the function annotated with `NoReturn` is some sub-express from typing import NoReturn import sys + # TODO: this is currently not yet supported # error: [invalid-return-type] def _() -> NoReturn: 3 + sys.exit(1) + # TODO: this is currently not yet supported # error: [invalid-return-type] def _() -> NoReturn: @@ -614,6 +642,7 @@ If a variable's type is a union, and some types in the union result in a functio from typing import NoReturn import sys + def g(x: int | None): if x is None: sys.exit(1) @@ -631,6 +660,7 @@ should not give any diagnostics. ```py import sys + def _(flag: bool): if flag: x = 3 @@ -646,6 +676,7 @@ a call with `NoReturn`. ```py import sys + def _(): try: x = 3 @@ -664,6 +695,7 @@ similar to the ones for `return` above. ```py import sys + def call_in_then_branch(cond: bool): if cond: x = "terminal" @@ -674,6 +706,7 @@ def call_in_then_branch(cond: bool): reveal_type(x) # revealed: Literal["test"] reveal_type(x) # revealed: Literal["test"] + def call_in_else_branch(cond: bool): if cond: x = "test" @@ -684,6 +717,7 @@ def call_in_else_branch(cond: bool): sys.exit() reveal_type(x) # revealed: Literal["test"] + def call_in_both_branches(cond: bool): if cond: x = "terminal1" @@ -696,6 +730,7 @@ def call_in_both_branches(cond: bool): reveal_type(x) # revealed: Never + def call_in_nested_then_branch(cond1: bool, cond2: bool): if cond1: x = "test1" @@ -711,6 +746,7 @@ def call_in_nested_then_branch(cond1: bool, cond2: bool): reveal_type(x) # revealed: Literal["test2"] reveal_type(x) # revealed: Literal["test1", "test2"] + def call_in_nested_else_branch(cond1: bool, cond2: bool): if cond1: x = "test1" @@ -726,6 +762,7 @@ def call_in_nested_else_branch(cond1: bool, cond2: bool): reveal_type(x) # revealed: Literal["test2"] reveal_type(x) # revealed: Literal["test1", "test2"] + def call_in_both_nested_branches(cond1: bool, cond2: bool): if cond1: x = "test" @@ -751,16 +788,19 @@ evaluation algorithm when evaluating the constraints. ```py from typing import NoReturn, overload + @overload def f(x: int) -> NoReturn: ... @overload def f(x: str) -> int: ... def f(x): ... + # No errors def _() -> NoReturn: f(3) + # This should be an error because of implicitly returning `None` # error: [invalid-return-type] def _() -> NoReturn: @@ -777,6 +817,7 @@ import sys from typing import NoReturn + class C: def __call__(self) -> NoReturn: sys.exit() @@ -784,10 +825,12 @@ class C: def die(self) -> NoReturn: sys.exit() + # No "implicitly returns `None`" diagnostic def _() -> NoReturn: C()() + # No "implicitly returns `None`" diagnostic def _() -> NoReturn: C().die() @@ -807,6 +850,7 @@ def top_level_return(cond1: bool, cond2: bool): def g(): reveal_type(x) # revealed: Literal[1, 2, 3] + if cond1: if cond2: x = 2 @@ -814,11 +858,13 @@ def top_level_return(cond1: bool, cond2: bool): x = 3 return + def return_from_if(cond1: bool, cond2: bool): x = 1 def g(): reveal_type(x) # revealed: Literal[1, 2, 3] + if cond1: if cond2: x = 2 @@ -826,11 +872,13 @@ def return_from_if(cond1: bool, cond2: bool): x = 3 return + def return_from_nested_if(cond1: bool, cond2: bool): x = 1 def g(): reveal_type(x) # revealed: Literal[1, 2, 3] + if cond1: if cond2: x = 2 diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index 09315697da..febfc0b35a 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -18,11 +18,13 @@ directly. from typing import Literal from ty_extensions import Not, static_assert + def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None: reveal_type(n1) # revealed: ~int reveal_type(n2) # revealed: int reveal_type(n3) # revealed: ~int + # error: "Special form `ty_extensions.Not` expected exactly 1 type argument, got 2" n: Not[int, str] # error: [invalid-type-form] "Special form `ty_extensions.Not` expected exactly 1 type argument, got 0" @@ -30,6 +32,7 @@ o: Not[()] p: Not[(int,)] + def static_truthiness(not_one: Not[Literal[1]]) -> None: # TODO: `bool` is not incorrect, but these would ideally be `Literal[True]` and `Literal[False]` # respectively, since all possible runtime objects that are created by the literal syntax `1` @@ -55,33 +58,47 @@ python-version = "3.12" from ty_extensions import Intersection, Not, is_subtype_of, static_assert from typing_extensions import Literal, Never + class S: ... + + class T: ... + def x(x1: Intersection[S, T], x2: Intersection[S, Not[T]]) -> None: reveal_type(x1) # revealed: S & T reveal_type(x2) # revealed: S & ~T + def y(y1: Intersection[int, object], y2: Intersection[int, bool], y3: Intersection[int, Never]) -> None: reveal_type(y1) # revealed: int reveal_type(y2) # revealed: bool reveal_type(y3) # revealed: Never + def z(z1: Intersection[int, Not[Literal[1]], Not[Literal[2]]]) -> None: reveal_type(z1) # revealed: int & ~Literal[1] & ~Literal[2] + class A: ... + + class B: ... + + class C: ... + type ABC = Intersection[A, B, C] static_assert(is_subtype_of(ABC, A)) static_assert(is_subtype_of(ABC, B)) static_assert(is_subtype_of(ABC, C)) + class D: ... + static_assert(not is_subtype_of(ABC, D)) ``` @@ -96,6 +113,7 @@ from ty_extensions import Unknown, static_assert, is_assignable_to, reveal_mro static_assert(is_assignable_to(Unknown, int)) static_assert(is_assignable_to(int, Unknown)) + def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None: reveal_type(x) # revealed: Unknown reveal_type(y) # revealed: tuple[str, Unknown] @@ -107,6 +125,7 @@ def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None ```py class C(Unknown): ... + # revealed: (, Unknown, ) reveal_mro(C) @@ -132,10 +151,12 @@ static_assert(is_subtype_of(Literal[False], AlwaysFalsy)) static_assert(not is_subtype_of(int, AlwaysFalsy)) static_assert(not is_subtype_of(str, AlwaysFalsy)) + def _(t: AlwaysTruthy, f: AlwaysFalsy): reveal_type(t) # revealed: AlwaysTruthy reveal_type(f) # revealed: AlwaysFalsy + def f( a: AlwaysTruthy[int], # error: [invalid-type-form] b: AlwaysFalsy[str], # error: [invalid-type-form] @@ -186,6 +207,7 @@ Static assertions can be used to enforce narrowing constraints: ```py from ty_extensions import static_assert + def f(x: int | None) -> None: if x is not None: static_assert(x is not None) @@ -233,10 +255,12 @@ static_assert(2 * 3 == 7) # error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness" static_assert(int(2.0 * 3.0) == 6) + class InvalidBoolDunder: def __bool__(self) -> int: return 1 + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `InvalidBoolDunder`" static_assert(InvalidBoolDunder()) ``` @@ -326,10 +350,16 @@ static_assert(is_subtype_of(bool, int | str)) static_assert(is_subtype_of(str, int | str)) static_assert(not is_subtype_of(bytes, int | str)) + class Base: ... + + class Derived(Base): ... + + class Unrelated: ... + static_assert(is_subtype_of(Derived, Base)) static_assert(not is_subtype_of(Base, Derived)) static_assert(is_subtype_of(Base, Base)) @@ -404,7 +434,10 @@ static_assert(is_subtype_of(str, type[str])) # Correct, returns True: static_assert(is_subtype_of(TypeOf[str], type[str])) + class Base: ... + + class Derived(Base): ... ``` @@ -419,9 +452,11 @@ def type_of_annotation() -> None: s1: type[Base] = Base s2: type[Base] = Derived # no error here + # error: "Special form `ty_extensions.TypeOf` expected exactly 1 type argument, got 3" t: TypeOf[int, str, bytes] + # error: [invalid-type-form] "`ty_extensions.TypeOf` requires exactly one argument when used in a type expression" def f(x: TypeOf) -> None: reveal_type(x) # revealed: Unknown @@ -438,15 +473,19 @@ It accepts a single type parameter which is expected to be a callable object. ```py from ty_extensions import CallableTypeOf + def f1(): return + def f2() -> int: return 1 + def f3(x: int, y: str) -> None: return + # error: [invalid-type-form] "Special form `ty_extensions.CallableTypeOf` expected exactly 1 type argument, got 2" c1: CallableTypeOf[f1, f2] @@ -456,10 +495,12 @@ c2: CallableTypeOf["foo"] # error: [invalid-type-form] "Expected the first argument to `ty_extensions.CallableTypeOf` to be a callable object, but got an object of type `Literal["foo"]`" c20: CallableTypeOf[("foo",)] + # error: [invalid-type-form] "`ty_extensions.CallableTypeOf` requires exactly one argument when used in a type expression" def f(x: CallableTypeOf) -> None: reveal_type(x) # revealed: Unknown + c3: CallableTypeOf[(f3,)] # error: [invalid-type-form] "Special form `ty_extensions.CallableTypeOf` expected exactly 1 type argument, got 0" @@ -471,6 +512,7 @@ Using it in annotation to reveal the signature of the callable object: ```py from typing_extensions import Self + class Foo: def __init__(self, x: int) -> None: pass @@ -485,6 +527,7 @@ class Foo: def class_method(cls, x: int) -> Self: return cls(x) + def _( c1: CallableTypeOf[f1], c2: CallableTypeOf[f2], diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/always_truthy_falsy.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/always_truthy_falsy.md index ede8b40a30..99ecf7cb69 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/always_truthy_falsy.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/always_truthy_falsy.md @@ -19,14 +19,17 @@ Here, we give a few examples of values that belong to these types: from ty_extensions import AlwaysTruthy, AlwaysFalsy from typing_extensions import Literal + class CustomAlwaysTruthyType: def __bool__(self) -> Literal[True]: return True + class CustomAlwaysFalsyType: def __bool__(self) -> Literal[False]: return False + at: AlwaysTruthy at = True at = 1 @@ -79,13 +82,16 @@ static_assert(is_disjoint_from(AlwaysTruthy, AmbiguousTruthiness)) static_assert(is_disjoint_from(AlwaysFalsy, AmbiguousTruthiness)) static_assert(not is_disjoint_from(Truthy, Falsy)) + class CustomAmbiguousTruthinessType: def __bool__(self) -> bool: return choice((True, False)) + def maybe_empty_list() -> list[int]: return choice(([], [1, 2, 3])) + reveal_type(bool(maybe_empty_list())) # revealed: bool reveal_type(bool(CustomAmbiguousTruthinessType())) # revealed: bool diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md index 255e744af9..403dd75f25 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md @@ -11,8 +11,10 @@ type, which means that it represents an *unknown* set of runtime values. from ty_extensions import static_assert, is_assignable_to from typing_extensions import Never, Any + class C: ... + static_assert(is_assignable_to(C, Any)) static_assert(is_assignable_to(Any, C)) @@ -47,10 +49,16 @@ from typing_extensions import Any # A class hierarchy Small <: Medium <: Big + class Big: ... + + class Medium(Big): ... + + class Small(Medium): ... + static_assert(is_assignable_to(Any | Medium, Big)) static_assert(is_assignable_to(Any | Medium, Medium)) @@ -76,10 +84,16 @@ type with *upper bound* `T`: from ty_extensions import static_assert, is_assignable_to, Intersection, is_equivalent_to from typing import Any + class Big: ... + + class Medium(Big): ... + + class Small(Medium): ... + static_assert(is_assignable_to(Small, Intersection[Any, Medium])) static_assert(is_assignable_to(Medium, Intersection[Any, Medium])) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md index 66b759b9ac..0ae5b1e8c1 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md @@ -110,6 +110,7 @@ Also, `int` can be subclassed, and instances of that subclass are also subtypes class CustomInt(int): pass + static_assert(is_subtype_of(CustomInt, int)) ``` @@ -196,10 +197,12 @@ from typing import Literal, assert_type type Nat = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + def pythagorean_triples(a: Nat, b: Nat, c: Nat): # Answer is `bool`, because solutions do exist (3² + 4² = 5²) assert_type(a**2 + b**2 == c**2, bool) + def fermats_last_theorem(a: Nat, b: Nat, c: Nat): # Answer is `Literal[False]`, because no solutions exist assert_type(a**3 + b**3 == c**3, Literal[False]) @@ -224,6 +227,7 @@ This can be used for type-narrowing: ```py from typing_extensions import Literal, assert_type + def f(x: Literal[0, 1, 54365]): if x: assert_type(x, Literal[1, 54365]) diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/never.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/never.md index f3d9d74407..d9857f3570 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/never.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/never.md @@ -11,8 +11,10 @@ type is a subtype of `Never`, except for `Never` itself or type variables with u from ty_extensions import static_assert, is_subtype_of from typing_extensions import Never, TypeVar + class C: ... + static_assert(is_subtype_of(Never, int)) static_assert(is_subtype_of(Never, object)) static_assert(is_subtype_of(Never, C)) @@ -22,6 +24,7 @@ static_assert(not is_subtype_of(int, Never)) T = TypeVar("T", bound=Never) + def _(t: T): static_assert(is_subtype_of(T, Never)) ``` @@ -41,9 +44,11 @@ static_assert(is_assignable_to(Never, object)) static_assert(is_assignable_to(Never, Any)) static_assert(is_assignable_to(Never, Never)) + def raise_error() -> Never: raise Exception("...") + def f(divisor: int) -> None: x: float = (1 / divisor) if divisor != 0 else raise_error() ``` @@ -57,18 +62,22 @@ it calls itself recursively. All of these functions "Never" return control back ```py from typing_extensions import Never + def raises_unconditionally() -> Never: raise Exception("This function always raises an exception") + def exits_unconditionally() -> Never: import sys return sys.exit(1) + def loops_forever() -> Never: while True: pass + def recursive_never() -> Never: return recursive_never() ``` @@ -90,8 +99,10 @@ it is disjoint from every other type: from ty_extensions import static_assert, is_disjoint_from from typing_extensions import Never + class C: ... + static_assert(is_disjoint_from(Never, int)) static_assert(is_disjoint_from(Never, object)) static_assert(is_disjoint_from(Never, C)) @@ -106,9 +117,13 @@ static_assert(is_disjoint_from(Never, Never)) from ty_extensions import static_assert, is_equivalent_to from typing_extensions import Never + class P: ... + + class Q: ... + static_assert(is_equivalent_to(P | Never | Q | None, P | Q | None)) ``` @@ -120,9 +135,13 @@ Intersecting with `Never` results in `Never`: from ty_extensions import static_assert, is_equivalent_to, Intersection from typing_extensions import Never + class P: ... + + class Q: ... + static_assert(is_equivalent_to(Intersection[P, Never, Q], Never)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/none.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/none.md index 08fcd7905b..5b4c57ad52 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/none.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/none.md @@ -31,8 +31,10 @@ The type `Optional[T]` is an alias for `T | None` (or `Union[T, None]`): from ty_extensions import static_assert, is_equivalent_to from typing import Optional, Union + class T: ... + static_assert(is_equivalent_to(Optional[T], T | None)) static_assert(is_equivalent_to(Optional[T], Union[T, None])) ``` @@ -44,8 +46,10 @@ Just like for other singleton types, we support type narrowing using `is` or `is ```py from typing_extensions import assert_type + class T: ... + def f(x: T | None): if x is None: assert_type(x, None) diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md index 261452e000..6744d2730a 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md @@ -10,9 +10,13 @@ The type `Not[T]` is the complement of the type `T`. It describes the set of all ```py from ty_extensions import Not, static_assert, is_disjoint_from + class T: ... + + class S(T): ... + static_assert(is_disjoint_from(Not[T], T)) static_assert(is_disjoint_from(Not[T], S)) ``` @@ -25,8 +29,10 @@ to `object`: ```py from ty_extensions import Not, static_assert, is_equivalent_to + class T: ... + static_assert(is_equivalent_to(T | Not[T], object)) ``` @@ -37,9 +43,13 @@ If `S <: T`, then `Not[T] <: Not[S]`:, similar to how negation in logic reverses ```py from ty_extensions import Not, static_assert, is_subtype_of + class T: ... + + class S(T): ... + static_assert(is_subtype_of(S, T)) static_assert(is_subtype_of(Not[T], Not[S])) ``` @@ -52,9 +62,13 @@ Assignability relationships are similarly reversed: from ty_extensions import Not, Intersection, static_assert, is_assignable_to from typing import Any + class T: ... + + class S(T): ... + static_assert(is_assignable_to(S, T)) static_assert(is_assignable_to(Not[T], Not[S])) @@ -71,12 +85,15 @@ If two types `P` and `Q` are disjoint, then `P` must be a subtype of `Not[Q]`, a from ty_extensions import Not, static_assert, is_subtype_of, is_disjoint_from from typing import final + @final class P: ... + @final class Q: ... + static_assert(is_disjoint_from(P, Q)) static_assert(is_subtype_of(P, Not[Q])) @@ -91,7 +108,10 @@ set-theoretic types: ```py from ty_extensions import Not, static_assert, is_equivalent_to, Intersection + class P: ... + + class Q: ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/object.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/object.md index f8c9e4fec2..a2a9dbd0ba 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/object.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/object.md @@ -67,9 +67,13 @@ Intersecting with `object` is equivalent to the original type: ```py from ty_extensions import static_assert, is_equivalent_to, Intersection + class P: ... + + class Q: ... + static_assert(is_equivalent_to(Intersection[P, object, Q], Intersection[P, Q])) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md index 928e67293a..eed39f753e 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -9,9 +9,13 @@ Cartesian product of sets. ```py from typing_extensions import assert_type + class P: ... + + class Q: ... + def _(p: P, q: Q): assert_type((p, q), tuple[P, Q]) ``` @@ -36,10 +40,12 @@ reveal_type(tuple[int, *tuple[str, ...]]((1,))) # revealed: tuple[int, *tuple[s reveal_type(().__class__()) # revealed: tuple[()] reveal_type((1, 2).__class__((1, 2))) # revealed: tuple[Literal[1], Literal[2]] + class LiskovUncompliantIterable(Iterable[int]): # TODO we should emit an error here about the Liskov violation __iter__ = None + def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never], bb: LiskovUncompliantIterable): reveal_type(tuple(x)) # revealed: tuple[int, ...] reveal_type(tuple(y)) # revealed: tuple[str, ...] @@ -53,6 +59,7 @@ def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never], bb: LiskovUncom # violated, though -- this test is really just to make sure we don't crash in this situation. reveal_type(tuple(bb)) # revealed: tuple[Unknown, ...] + reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]] reveal_type(tuple([1])) # revealed: tuple[Unknown | int, ...] @@ -72,6 +79,7 @@ reveal_type((1,).__class__()) # revealed: tuple[Literal[1]] # error: [missing-argument] "No argument provided for required parameter `iterable`" reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]] + def g(x: tuple[int, str] | tuple[bytes, bool], y: tuple[int, str] | tuple[bytes, bool, bytes]): reveal_type(tuple(x)) # revealed: tuple[int, str] | tuple[bytes, bool] reveal_type(tuple(y)) # revealed: tuple[int, str] | tuple[bytes, bool, bytes] @@ -89,12 +97,22 @@ python-version = "3.11" ```py from typing_extensions import Iterable, Never + class UnspecializedTupleSubclass(tuple): ... + + class EmptyTupleSubclass(tuple[()]): ... + + class SingleElementTupleSubclass(tuple[int]): ... + + class VariadicTupleSubclass(tuple[int, ...]): ... + + class MixedTupleSubclass(tuple[int, *tuple[str, ...]]): ... + reveal_type(UnspecializedTupleSubclass()) # revealed: UnspecializedTupleSubclass reveal_type(UnspecializedTupleSubclass(())) # revealed: UnspecializedTupleSubclass reveal_type(UnspecializedTupleSubclass((1, 2, "foo"))) # revealed: UnspecializedTupleSubclass @@ -125,6 +143,7 @@ reveal_type(MixedTupleSubclass((1, b"foo"))) # revealed: MixedTupleSubclass # error: [missing-argument] "No argument provided for required parameter `iterable`" reveal_type(MixedTupleSubclass()) # revealed: MixedTupleSubclass + def _(empty: EmptyTupleSubclass, single_element: SingleElementTupleSubclass, mixed: MixedTupleSubclass, x: tuple[int, int]): # error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[()]`, found `tuple[Literal[1], Literal[2]]`" empty.__class__((1, 2)) @@ -164,11 +183,19 @@ and `S2` is a subtype of `T2`, and similar for other lengths of tuples: ```py from ty_extensions import static_assert, is_subtype_of + class T1: ... + + class S1(T1): ... + + class T2: ... + + class S2(T2): ... + static_assert(is_subtype_of(tuple[S1], tuple[T1])) static_assert(not is_subtype_of(tuple[T1], tuple[S1])) @@ -196,8 +223,10 @@ from ty_extensions import static_assert, is_singleton, is_subtype_of, is_equival static_assert(not is_singleton(tuple[()])) + class AnotherEmptyTuple(tuple[()]): ... + static_assert(not is_equivalent_to(AnotherEmptyTuple, tuple[()])) static_assert(is_subtype_of(AnotherEmptyTuple, tuple[()])) @@ -265,6 +294,7 @@ def takes_zero_or_more(t: tuple[int, ...]) -> None: ... def takes_one_or_more(t: tuple[int, *tuple[int, ...]]) -> None: ... def takes_two_or_more(t: tuple[int, int, *tuple[int, ...]]) -> None: ... + takes_zero_or_more(()) takes_zero_or_more((1,)) takes_zero_or_more((1, 2)) @@ -285,6 +315,7 @@ def takes_one_or_more_suffix(t: tuple[*tuple[int, ...], int]) -> None: ... def takes_two_or_more_suffix(t: tuple[*tuple[int, ...], int, int]) -> None: ... def takes_two_or_more_mixed(t: tuple[int, *tuple[int, ...], int]) -> None: ... + takes_one_or_more_suffix(()) # error: [invalid-argument-type] takes_one_or_more_suffix((1,)) takes_one_or_more_suffix((1, 2)) @@ -351,15 +382,21 @@ contain elements `Q1, Q2` if either `P1` is disjoint from `Q1` or if `P2` is dis ```py from typing import final + @final class F1: ... + @final class F2: ... + class N1: ... + + class N2: ... + static_assert(is_disjoint_from(F1, F2)) static_assert(not is_disjoint_from(N1, N2)) @@ -394,8 +431,10 @@ for the possibility of `tuple` to be subclassed ```py class C: ... + static_assert(not is_disjoint_from(tuple[int, str], C)) + class CommonSubtype(tuple[int, str], C): ... ``` @@ -404,8 +443,11 @@ other heterogeneous tuples above: ```py class I1(tuple[F1, F2]): ... + + class I2(tuple[F2, F1]): ... + # TODO # This is a subtype of both `tuple[F1, F2]` and `tuple[F2, F1]`, so those two heterogeneous tuples # should not be disjoint from each other (see conflicting test above). @@ -489,6 +531,7 @@ class NotAlwaysTruthyTuple(tuple[int]): def __bool__(self) -> bool: return False + t: tuple[int] = NotAlwaysTruthyTuple((1,)) ``` @@ -502,6 +545,7 @@ from ty_extensions import Unknown, is_equivalent_to, static_assert static_assert(is_equivalent_to(tuple[Any, ...], tuple[Unknown, ...])) + def f(x: tuple, y: tuple[Unknown, ...]): reveal_type(x) # revealed: tuple[Unknown, ...] assert_type(x, tuple[Any, ...]) @@ -560,6 +604,7 @@ tup: Sequence[str] = (*{"foo": 42, "bar": 56},) # TODO: `tuple[str, str]` would be better, given the type annotation reveal_type(tup) # revealed: tuple[Unknown | str, Unknown | str] + def f(x: list[int]): reveal_type((42, 56, *x, 97)) # revealed: tuple[Literal[42], Literal[56], *tuple[int, ...], Literal[97]] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_display/callable.md b/crates/ty_python_semantic/resources/mdtest/type_display/callable.md index d90911213d..c0b961720c 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_display/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/type_display/callable.md @@ -9,6 +9,7 @@ We parenthesize callable types when they appear inside more complex types, to di ```py from typing import Callable + def f(x: Callable[[], str] | Callable[[int], str]): reveal_type(x) # revealed: (() -> str) | ((int, /) -> str) ``` @@ -22,6 +23,7 @@ We don't parenthesize display of an overloaded callable, since it is already wra from typing import overload, Callable from ty_extensions import CallableTypeOf + @overload def f(x: int) -> bool: ... @overload @@ -29,6 +31,7 @@ def f(x: str) -> str: ... def f(x: int | str) -> bool | str: return bool(x) if isinstance(x, int) else str(x) + def _(flag: bool, c: CallableTypeOf[f]): x = c if flag else True reveal_type(x) # revealed: Overload[(x: int) -> bool, (x: str) -> str] | Literal[True] @@ -42,6 +45,7 @@ And we don't parenthesize the top callable, since it is wrapped in `Top[...]`: from typing import Callable from ty_extensions import Top + def f(x: Top[Callable[..., str]] | Callable[[int], int]): reveal_type(x) # revealed: Top[(...) -> str] | ((int, /) -> int) ``` @@ -58,10 +62,12 @@ We wrap the signature of a top ParamSpec with `Top[...]`: ```py from typing import Callable + class C[**P]: def __init__(self, f: Callable[P, object]) -> None: self.f = f + def _(x: object): if callable(x): c = C(x) @@ -79,41 +85,53 @@ python-version = "3.12" type Scalar = int | float type Array1d = list[Scalar] | tuple[Scalar] + def f(x: Scalar | Array1d) -> None: pass + reveal_type(f) # revealed: def f(x: Scalar | Array1d) -> None + class Foo: def f(self, x: Scalar | Array1d) -> None: pass + reveal_type(Foo().f) # revealed: bound method Foo.f(x: Scalar | Array1d) -> None type ArrayNd = Scalar | list[ArrayNd] | tuple[ArrayNd] + def g(x: Scalar | ArrayNd) -> None: pass + reveal_type(g) # revealed: def g(x: Scalar | ArrayNd) -> None + class Bar: def g(self, x: Scalar | ArrayNd) -> None: pass + # TODO: should be `bound method Bar.g(x: Scalar | ArrayNd) -> None` reveal_type(Bar().g) # revealed: bound method Bar.g(x: Scalar | list[Any] | tuple[Any]) -> None type GenericArray1d[T] = list[T] | tuple[T] + def h(x: Scalar | GenericArray1d[Scalar]) -> None: pass + reveal_type(h) # revealed: def h(x: Scalar | GenericArray1d[Scalar]) -> None + class Baz: def h(self, x: Scalar | GenericArray1d[Scalar]) -> None: pass + reveal_type(Baz().h) # revealed: bound method Baz.h(x: Scalar | GenericArray1d[Scalar]) -> None ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md index 6d747cb6f1..4487f66649 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -5,6 +5,7 @@ ```py class A: ... + def _(c: type[A]): reveal_type(c) # revealed: type[A] ``` @@ -15,6 +16,7 @@ def _(c: type[A]): class A: class B: ... + def f(c: type[A.B]): reveal_type(c) # revealed: type[B] ``` @@ -26,6 +28,7 @@ class A: class B: class C: ... + def f(c: type[A.B.C]): reveal_type(c) # revealed: type[C] ``` @@ -35,6 +38,7 @@ def f(c: type[A.B.C]): ```py from a import A + def f(c: type[A]): reveal_type(c) # revealed: type[A] ``` @@ -50,6 +54,7 @@ class A: ... ```py import a + def f(c: type[a.B]): reveal_type(c) # revealed: type[B] ``` @@ -67,6 +72,7 @@ class B: ... ```py import a.b + def f(c: type[a.b.C]): reveal_type(c) # revealed: type[C] ``` @@ -86,12 +92,16 @@ class C: ... ```py class BasicUser: ... + + class ProUser: ... + class A: class B: class C: ... + def _(u: type[BasicUser | ProUser | A.B.C]): # revealed: type[BasicUser] | type[ProUser] | type[C] reveal_type(u) @@ -102,13 +112,18 @@ def _(u: type[BasicUser | ProUser | A.B.C]): ```py from typing import Union + class BasicUser: ... + + class ProUser: ... + class A: class B: class C: ... + def f(a: type[Union[BasicUser, ProUser, A.B.C]], b: type[Union[str]], c: type[Union[BasicUser, Union[ProUser, A.B.C]]]): reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C] reveal_type(b) # revealed: type[str] @@ -120,13 +135,18 @@ def f(a: type[Union[BasicUser, ProUser, A.B.C]], b: type[Union[str]], c: type[Un ```py from typing import Union + class BasicUser: ... + + class ProUser: ... + class A: class B: class C: ... + def f(a: type[BasicUser | Union[ProUser, A.B.C]], b: type[Union[BasicUser | Union[ProUser, A.B.C | str]]]): reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C] reveal_type(b) # revealed: type[BasicUser] | type[ProUser] | type[C] | type[str] @@ -136,8 +156,11 @@ def f(a: type[BasicUser | Union[ProUser, A.B.C]], b: type[Union[BasicUser | Unio ```py class A: ... + + class B: ... + # error: [invalid-type-form] _: type[A, B] ``` @@ -147,8 +170,10 @@ _: type[A, B] ```py from ty_extensions import reveal_mro + class Foo(type[int]): ... + reveal_mro(Foo) # revealed: (, , ) ``` @@ -168,13 +193,16 @@ from types import EllipsisType from typing import final from enum import Enum + @final class Foo: ... + class Answer(Enum): NO = 0 YES = 1 + def _(x: type[Foo], y: type[EllipsisType], z: type[Answer]): reveal_type(x) # revealed: reveal_type(y) # revealed: @@ -192,31 +220,40 @@ python-version = "3.12" from typing import final, Any from ty_extensions import is_assignable_to, is_subtype_of, is_disjoint_from, static_assert + class Biv[T]: ... + class Cov[T]: def pop(self) -> T: raise NotImplementedError + class Contra[T]: def push(self, value: T) -> None: pass + class Inv[T]: x: T + @final class BivSub[T](Biv[T]): ... + @final class CovSub[T](Cov[T]): ... + @final class ContraSub[T](Contra[T]): ... + @final class InvSub[T](Inv[T]): ... + def _[T, U](): static_assert(is_subtype_of(type[BivSub[T]], type[BivSub[U]])) static_assert(not is_disjoint_from(type[BivSub[U]], type[BivSub[T]])) @@ -231,6 +268,7 @@ def _[T, U](): static_assert(not is_subtype_of(type[InvSub[T]], type[InvSub[U]])) static_assert(not is_disjoint_from(type[InvSub[U]], type[InvSub[T]])) + def _(): static_assert(is_subtype_of(type[BivSub[bool]], type[BivSub[int]])) static_assert(is_subtype_of(type[BivSub[int]], type[BivSub[bool]])) @@ -256,6 +294,7 @@ def _(): # TODO: These are disjoint. static_assert(not is_disjoint_from(type[InvSub[bool]], type[InvSub[int]])) + def _[T](): static_assert(is_subtype_of(type[BivSub[T]], type[BivSub[Any]])) static_assert(is_subtype_of(type[BivSub[Any]], type[BivSub[T]])) @@ -281,6 +320,7 @@ def _[T](): static_assert(is_assignable_to(type[InvSub[Any]], type[InvSub[T]])) static_assert(not is_disjoint_from(type[InvSub[T]], type[InvSub[Any]])) + def _[T, U](): static_assert(is_subtype_of(type[BivSub[T]], type[Biv[T]])) static_assert(not is_subtype_of(type[Biv[T]], type[BivSub[T]])) @@ -306,6 +346,7 @@ def _[T, U](): static_assert(not is_disjoint_from(type[InvSub[U]], type[Inv[T]])) static_assert(not is_disjoint_from(type[InvSub[U]], type[Inv[U]])) + def _(): static_assert(is_subtype_of(type[BivSub[bool]], type[Biv[int]])) static_assert(is_subtype_of(type[BivSub[int]], type[Biv[bool]])) @@ -329,6 +370,7 @@ def _(): # TODO: These are disjoint. static_assert(not is_disjoint_from(type[InvSub[int]], type[Inv[bool]])) + def _[T](): static_assert(is_subtype_of(type[BivSub[T]], type[Biv[Any]])) static_assert(is_subtype_of(type[BivSub[Any]], type[Biv[T]])) diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/dynamic.md b/crates/ty_python_semantic/resources/mdtest/type_of/dynamic.md index 4e59d97eae..620640c407 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/dynamic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/dynamic.md @@ -8,6 +8,7 @@ This file contains tests for non-fully-static `type[]` types, such as `type[Any] ```py from typing import Any + def f(x: type[Any], y: type[str]): reveal_type(x) # revealed: type[Any] # TODO: could be ` & Any` @@ -17,8 +18,10 @@ def f(x: type[Any], y: type[str]): a: type[str] = x b: type[Any] = y + class A: ... + x: type[Any] = object x: type[Any] = type x: type[Any] = A @@ -37,8 +40,10 @@ def f(x: type): reveal_type(x) # revealed: type reveal_type(x.__repr__) # revealed: bound method type.__repr__() -> str + class A: ... + x: type = object x: type = type x: type = A @@ -52,8 +57,10 @@ def f(x: type[object]): reveal_type(x) # revealed: type reveal_type(x.__repr__) # revealed: bound method type.__repr__() -> str + class A: ... + x: type[object] = object x: type[object] = type x: type[object] = A @@ -72,6 +79,7 @@ from does_not_exist import SomethingUnknown # error: [unresolved-import] reveal_type(SomethingUnknown) # revealed: Unknown + def test(x: Any, y: SomethingUnknown): reveal_type(x.__class__) # revealed: type[Any] reveal_type(x.__class__.__class__.__class__.__class__) # revealed: type[Any] @@ -89,6 +97,7 @@ from does_not_exist import SomethingUnknown # error: [unresolved-import] has_unknown_type = SomethingUnknown.__class__ reveal_type(has_unknown_type) # revealed: type[Unknown] + def test(x: type[str], y: type[Any]): """Both `type[Any]` and `type[Unknown]` are assignable to all `type[]` types""" a: type[Any] = x @@ -96,6 +105,7 @@ def test(x: type[str], y: type[Any]): c: type[Any] = has_unknown_type d: type[str] = has_unknown_type + def test2(a: type[Any]): """`type[Any]` and `type[Unknown]` are also assignable to all instances of `type` subclasses""" b: abc.ABCMeta = a diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md index 12d8d0a0e7..639ed37200 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -37,9 +37,13 @@ class A: def __init__(self, value: str): ... + class B(A): ... + + class C: ... + def upper_bound[T: A](x: type[T]) -> T: reveal_type(x) # revealed: type[T@upper_bound] reveal_type(x.__qualname__) # revealed: str @@ -47,6 +51,7 @@ def upper_bound[T: A](x: type[T]) -> T: return x("hello") + reveal_type(upper_bound(A)) # revealed: A reveal_type(upper_bound(B)) # revealed: B @@ -64,6 +69,7 @@ def constrained[T: (int, str)](x: type[T]) -> T: return x("hello") + reveal_type(constrained(int)) # revealed: int reveal_type(constrained(str)) # revealed: str @@ -79,8 +85,11 @@ still be accessible: ```py class Replace: ... + + class Multiply: ... + def union_bound[T: Replace | Multiply](x: type[T]) -> T: reveal_type(x) # revealed: type[T@union_bound] # All classes have __name__ and __qualname__ from type's metaclass @@ -90,6 +99,7 @@ def union_bound[T: Replace | Multiply](x: type[T]) -> T: return x() + reveal_type(union_bound(Replace)) # revealed: Replace reveal_type(union_bound(Multiply)) # revealed: Multiply ``` @@ -99,12 +109,15 @@ reveal_type(union_bound(Multiply)) # revealed: Multiply ```py from ty_extensions import Intersection, Unknown + def _[T: int](x: type | type[T]): reveal_type(x()) # revealed: Any + def _[T: int](x: type[int] | type[T]): reveal_type(x()) # revealed: int + def _[T](x: type[int] | type[T]): reveal_type(x()) # revealed: int | T@_ ``` @@ -114,8 +127,10 @@ def _[T](x: type[int] | type[T]): ```py from typing import TypeVar + class A: ... + def narrow_a[B: A](a: A, b: B): type_of_a = type(a) @@ -134,6 +149,7 @@ def narrow_a[B: A](a: A, b: B): ```py from typing import Self + class A: def copy(self: Self) -> Self: reveal_type(self.__class__) # revealed: type[Self@copy] @@ -149,9 +165,11 @@ A class `A` is a subtype of `type[T]` if any instance of `A` is a subtype of `T` from typing import Any, Callable, Protocol from ty_extensions import is_assignable_to, is_subtype_of, is_disjoint_from, static_assert + class Callback[T](Protocol): def __call__(self, *args, **kwargs) -> T: ... + def _[T](_: T): static_assert(not is_subtype_of(type[T], T)) static_assert(not is_subtype_of(T, type[T])) @@ -170,6 +188,7 @@ def _[T](_: T): static_assert(not is_assignable_to(type[T], Callback[int])) static_assert(not is_disjoint_from(type[T], Callback[int])) + def _[T: int](_: T): static_assert(not is_subtype_of(type[T], T)) static_assert(not is_subtype_of(T, type[T])) @@ -207,6 +226,7 @@ def _[T: int](_: T): static_assert(is_subtype_of(type[T], type[T] | type[float])) static_assert(not is_disjoint_from(type[T], type[T] | type[float])) + def _[T: (int, str)](_: T): static_assert(not is_subtype_of(type[T], T)) static_assert(not is_subtype_of(T, type[T])) @@ -245,6 +265,7 @@ def _[T: (int, str)](_: T): static_assert(not is_disjoint_from(type[T], type[int | str])) static_assert(not is_disjoint_from(type[T], type[int] | type[str])) + def _[T: (int | str, int)](_: T): static_assert(is_subtype_of(type[int], type[T])) static_assert(not is_disjoint_from(type[int], type[T])) @@ -257,6 +278,7 @@ class X[T]: def get(self) -> T: return self.value + def _[T](x: X[type[T]]): reveal_type(x.get()) # revealed: type[T@_] ``` @@ -267,21 +289,26 @@ def _[T](x: X[type[T]]): def f1[T](x: type[T]) -> type[T]: return x + reveal_type(f1(int)) # revealed: type[int] reveal_type(f1(object)) # revealed: type + def f2[T](x: T) -> type[T]: return type(x) + reveal_type(f2(int(1))) # revealed: type[int] reveal_type(f2(object())) # revealed: type # TODO: This should reveal `type[Literal[1]]`. reveal_type(f2(1)) # revealed: type[Unknown] + def f3[T](x: type[T]) -> T: return x() + reveal_type(f3(int)) # revealed: int reveal_type(f3(object)) # revealed: object ``` @@ -291,8 +318,10 @@ reveal_type(f3(object)) # revealed: object ```py from typing import Any + class Foo[T]: ... + # TODO: This should not error. # error: [invalid-parameter-default] "Default value of type `` is not assignable to annotated parameter type `type[T@f]`" def f[T: Foo[Any]](x: type[T] = Foo): ... @@ -308,12 +337,16 @@ python-version = "3.12" ```py from typing import Generic, TypeVar + class Foo[T]: ... + S = TypeVar("S") + class Bar(Generic[S]): ... + def _(x: Foo[int], y: Bar[str], z: list[bytes]): reveal_type(type(x)) # revealed: type[Foo[int]] reveal_type(type(y)) # revealed: type[Bar[str]] @@ -331,9 +364,11 @@ python-version = "3.12" class C[T]: pass + class D[T]: pass + var: type[C[int]] = C[int] var: type[C[int]] = D[int] # error: [invalid-assignment] "Object of type `` is not assignable to `type[C[int]]`" ``` @@ -343,12 +378,15 @@ However, generic `Protocol` classes are still TODO: ```py from typing import Protocol + class Proto[U](Protocol): def some_method(self): ... + # TODO: should be error: [invalid-assignment] var: type[Proto[int]] = C[int] + def _(p: type[Proto[int]]): reveal_type(p) # revealed: type[@Todo(type[T] for protocols)] ``` @@ -366,16 +404,20 @@ An unspecialized generic final class object is assignable to its default-special ```py from typing import final + @final class P[T]: x: T + def expects_type_p(x: type[P]): pass + def expects_type_p_of_int(x: type[P[int]]): pass + # OK, the default specialization of `P` is assignable to `type[P[Unknown]]` expects_type_p(P) @@ -399,15 +441,19 @@ because the default-specialization is no longer a forgiving `Unknown` type: class P[T = str]: x: T + def expects_type_p(x: type[P]): pass + def expects_type_p_of_int(x: type[P[int]]): pass + def expects_type_p_of_str(x: type[P[str]]): pass + # OK, the default specialization is now `P[str]`, but we have the default specialization on both # sides, so it is assignable. expects_type_p(P) @@ -430,9 +476,11 @@ This also works with `ParamSpec`: @final class C[**P]: ... + def expects_type_c(f: type[C]): ... def expects_type_c_of_int_and_str(x: type[C[int, str]]): ... + # OK, the unspecialized `C` is assignable to `type[C[...]]` expects_type_c(C) @@ -458,10 +506,12 @@ And with a `ParamSpec` that has a default: @final class C[**P = [int, str]]: ... + def expects_type_c_default(f: type[C]): ... def expects_type_c_default_of_int(f: type[C[int]]): ... def expects_type_c_default_of_int_str(f: type[C[int, str]]): ... + expects_type_c_default(C) expects_type_c_default(C[int, str]) expects_type_c_default_of_int(C) diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md b/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md index 33ea650090..4fb0f90f09 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md @@ -7,8 +7,10 @@ ```py from typing import Type + class A: ... + def _(c: Type, d: Type[A]): reveal_type(c) # revealed: type reveal_type(d) # revealed: type[A] @@ -25,8 +27,10 @@ not a class. from typing import Type from ty_extensions import reveal_mro + class C(Type): ... + # Runtime value: `(C, type, typing.Generic, object)` # TODO: Add `Generic` to the MRO reveal_mro(C) # revealed: (, , ) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index ec4a31a711..14695cfee3 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -36,13 +36,20 @@ upper bound. from typing import Any, final, Never, Sequence from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + @final class Unrelated: ... + def _[T]() -> None: # (Sub ≤ T@_ ≤ Super) ConstraintSet.range(Sub, T, Super) @@ -128,13 +135,20 @@ strict subtype of the lower bound, a strict supertype of the upper bound, or inc from typing import Any, final, Never, Sequence from ty_extensions import ConstraintSet, Not, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + @final class Unrelated: ... + def _[T]() -> None: # ¬(Sub ≤ T@_ ≤ Super) ~ConstraintSet.range(Sub, T, Super) @@ -226,8 +240,13 @@ cases, we can simplify the result of an intersection. ```py from ty_extensions import ConstraintSet + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... ``` @@ -249,14 +268,23 @@ The intersection of two ranges is where the ranges "overlap". from typing import final from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + + class SubSub(Sub): ... + @final class Unrelated: ... + def _[T]() -> None: constraints = ConstraintSet.range(SubSub, T, Base) & ConstraintSet.range(Sub, T, Super) expected = ConstraintSet.range(Sub, T, Base) @@ -291,9 +319,11 @@ satisfy their intersection `T ≤ Base & Other`, and vice versa. from typing import Never from ty_extensions import Intersection + # This is not final, so it's possible for a subclass to inherit from both Base and Other. class Other: ... + def upper_bounds[T](): # (T@upper_bounds ≤ Base & Other) intersection_type = ConstraintSet.range(Never, T, Intersection[Base, Other]) @@ -325,11 +355,19 @@ the intersection as removing the hole from the range constraint. from typing import final, Never from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + + class SubSub(Sub): ... + @final class Unrelated: ... ``` @@ -379,14 +417,23 @@ smaller constraint. For negated ranges, the smaller constraint is the one with t from typing import final from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + + class SubSub(Sub): ... + @final class Unrelated: ... + def _[T]() -> None: constraints = ~ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base) expected = ~ConstraintSet.range(SubSub, T, Super) @@ -437,8 +484,13 @@ can simplify the result of an union. ```py from ty_extensions import ConstraintSet + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... ``` @@ -461,14 +513,23 @@ bounds. from typing import final from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + + class SubSub(Sub): ... + @final class Unrelated: ... + def _[T]() -> None: constraints = ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base) expected = ConstraintSet.range(SubSub, T, Super) @@ -515,9 +576,11 @@ that satisfies the union constraint satisfies the union type. ```py from typing import Never + # This is not final, so it's possible for a subclass to inherit from both Base and Other. class Other: ... + def union[T](): # (T@union ≤ Base | Other) union_type = ConstraintSet.range(Never, T, Base | Other) @@ -565,11 +628,19 @@ the union as filling part of the hole with the types from the range constraint. from typing import final, Never from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + + class SubSub(Sub): ... + @final class Unrelated: ... ``` @@ -618,14 +689,23 @@ The union of two negated ranges has a hole where the ranges "overlap". from typing import final from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + + class SubSub(Sub): ... + @final class Unrelated: ... + def _[T]() -> None: constraints = ~ConstraintSet.range(SubSub, T, Base) | ~ConstraintSet.range(Sub, T, Super) expected = ~ConstraintSet.range(Sub, T, Base) @@ -660,10 +740,16 @@ def _[T]() -> None: from typing import Never from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + def _[T]() -> None: # ¬(Sub ≤ T@_ ≤ Base) ~ConstraintSet.range(Sub, T, Base) @@ -689,11 +775,14 @@ def _[T]() -> None: from typing import final, Never from ty_extensions import ConstraintSet, static_assert + class Base: ... + @final class Unrelated: ... + def _[T, U]() -> None: # ¬(T@_ ≤ Base) ∨ ¬(U@_ ≤ Base) ~(ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base)) @@ -727,12 +816,14 @@ enforce an arbitrary ordering on typevars, and always place the constraint on th from typing import Never from ty_extensions import ConstraintSet, static_assert + def f[S, T](): # (S@f ≤ T@f) c1 = ConstraintSet.range(Never, S, T) c2 = ConstraintSet.range(S, T, object) static_assert(c1 == c2) + def f[T, S](): # (S@f ≤ T@f) c1 = ConstraintSet.range(Never, S, T) @@ -750,6 +841,7 @@ def f[S, T](): c2 = ConstraintSet.range(S, T, S) static_assert(c1 == c2) + def f[T, S](): # (S@f = T@f) c1 = ConstraintSet.range(T, S, T) @@ -781,6 +873,7 @@ set. from typing import Never from ty_extensions import ConstraintSet, Intersection, static_assert + def f[T](): c1 = ConstraintSet.range(Never, T, str | int) c2 = ConstraintSet.range(Never, T, int | str) @@ -802,6 +895,7 @@ static types.) from typing import Never from ty_extensions import ConstraintSet, static_assert + def same_typevar[T](): constraints = ConstraintSet.range(Never, T, T) expected = ConstraintSet.range(Never, T, object) @@ -823,6 +917,7 @@ as shown above.) ```py from ty_extensions import Intersection + def same_typevar[T](): constraints = ConstraintSet.range(Never, T, T | None) expected = ConstraintSet.range(Never, T, object) @@ -843,6 +938,7 @@ constraint set can never be satisfied, since every type is disjoint with its neg ```py from ty_extensions import Not + def same_typevar[T](): constraints = ConstraintSet.range(Intersection[Not[T], None], T, object) expected = ~ConstraintSet.range(Never, T, object) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md index 1bfcedb906..f250012ae4 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md @@ -17,6 +17,7 @@ fully static type that does not contain a typevar.) ```py from ty_extensions import ConstraintSet, is_subtype_of, static_assert + def equivalent_to_other_relationships[T](): static_assert(is_subtype_of(bool, int)) static_assert(ConstraintSet.always().implies_subtype_of(bool, int)) @@ -33,11 +34,13 @@ there isn't a valid specialization for the typevars we are considering. from typing import Never from ty_extensions import ConstraintSet + def even_given_constraints[T](): constraints = ConstraintSet.range(Never, T, int) static_assert(constraints.implies_subtype_of(bool, int)) static_assert(not constraints.implies_subtype_of(bool, str)) + def even_given_unsatisfiable_constraints(): static_assert(ConstraintSet.never().implies_subtype_of(bool, int)) static_assert(not ConstraintSet.never().implies_subtype_of(bool, str)) @@ -52,6 +55,7 @@ question when considering a typevar, by translating the desired relationship int from typing import Any from ty_extensions import ConstraintSet, is_assignable_to, is_subtype_of, static_assert + def assignability[T](): constraints = is_assignable_to(T, bool) # TODO: expected = ConstraintSet.range(Never, T, bool) @@ -67,6 +71,7 @@ def assignability[T](): expected = ConstraintSet.always() static_assert(constraints == expected) + def subtyping[T](): constraints = is_subtype_of(T, bool) # TODO: expected = ConstraintSet.range(Never, T, bool) @@ -93,10 +98,12 @@ class Covariant[T]: def get(self) -> T: raise ValueError + class Contravariant[T]: def set(self, value: T): pass + def assignability[T](): constraints = is_assignable_to(T, Any) expected = ConstraintSet.range(Never, T, object) @@ -126,6 +133,7 @@ def assignability[T](): expected = ConstraintSet.never() static_assert(constraints == expected) + def subtyping[T](): constraints = is_subtype_of(T, Any) # TODO: expected = ConstraintSet.range(Never, T, Never) @@ -166,6 +174,7 @@ considering. from typing import Never from ty_extensions import ConstraintSet, static_assert + def given_constraints[T](): static_assert(not ConstraintSet.always().implies_subtype_of(T, int)) static_assert(not ConstraintSet.always().implies_subtype_of(T, bool)) @@ -216,6 +225,7 @@ def mutually_constrained[T, U](): static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) + def mutually_constrained[U, T](): # If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well. given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) @@ -238,10 +248,12 @@ All of the relationships in the above section also apply when a typevar appears from typing import Never from ty_extensions import ConstraintSet, static_assert + class Covariant[T]: def get(self) -> T: raise ValueError + def given_constraints[T](): static_assert(not ConstraintSet.always().implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not ConstraintSet.always().implies_subtype_of(Covariant[T], Covariant[bool])) @@ -268,6 +280,7 @@ def given_constraints[T](): static_assert(given_bool_int.implies_subtype_of(Covariant[bool], Covariant[T])) static_assert(not given_bool_int.implies_subtype_of(Covariant[str], Covariant[T])) + def mutually_constrained[T, U](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Covariant[T] ≤ Covariant[int]). @@ -283,6 +296,7 @@ def mutually_constrained[T, U](): static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) + # Repeat the test with a different typevar ordering def mutually_constrained[U, T](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore @@ -307,6 +321,7 @@ class Contravariant[T]: def set(self, value: T): pass + def given_constraints[T](): static_assert(not ConstraintSet.always().implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not ConstraintSet.always().implies_subtype_of(Contravariant[bool], Contravariant[T])) @@ -329,6 +344,7 @@ def given_constraints[T](): static_assert(not given_bool.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_bool.implies_subtype_of(Contravariant[str], Contravariant[T])) + def mutually_constrained[T, U](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Contravariant[int] ≤ Contravariant[T]). @@ -344,6 +360,7 @@ def mutually_constrained[T, U](): static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) + # Repeat the test with a different typevar ordering def mutually_constrained[U, T](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore @@ -372,6 +389,7 @@ class Invariant[T]: def set(self, value: T): pass + def given_constraints[T](): static_assert(not ConstraintSet.always().implies_subtype_of(Invariant[T], Invariant[int])) static_assert(not ConstraintSet.always().implies_subtype_of(Invariant[T], Invariant[bool])) @@ -402,6 +420,7 @@ def given_constraints[T](): static_assert(not given_int.implies_subtype_of(Invariant[str], Invariant[T])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str])) + def mutually_constrained[T, U](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well. But because T is invariant, that # does _not_ imply that (Invariant[T] ≤ Invariant[int]). @@ -420,6 +439,7 @@ def mutually_constrained[T, U](): static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str])) static_assert(not given_int.implies_subtype_of(Invariant[str], Invariant[T])) + # Repeat the test with a different typevar ordering def mutually_constrained[U, T](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well. But because T is invariant, that @@ -451,9 +471,11 @@ the generic callable.) from typing import Callable from ty_extensions import CallableTypeOf, ConstraintSet, TypeOf, is_subtype_of, static_assert + def identity[T](t: T) -> T: return t + type GenericIdentity[T] = Callable[[T], T] constraints = ConstraintSet.always() @@ -545,6 +567,7 @@ def identity2[T](t: T) -> T: from typing import Never from ty_extensions import ConstraintSet, static_assert + def concrete_pivot[T, U](): # If [int ≤ T ∧ T ≤ U], then [int ≤ U] must be true as well. constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(T, U, object) @@ -557,6 +580,7 @@ def concrete_pivot[T, U](): from typing import Never from ty_extensions import ConstraintSet, static_assert + def concrete_pivot[T, U](): # If [T ≤ int ∧ int ≤ U], then [T ≤ U] must be true as well. constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(int, U, object) @@ -569,6 +593,7 @@ def concrete_pivot[T, U](): from typing import Any, Never from ty_extensions import ConstraintSet, static_assert + def concrete_pivot[T, U](): # If [T ≤ Any ∧ Any ≤ U], then the two `Any`s might materialize to different types. That means # [T ≤ U] is NOT necessarily true. diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index c64e659ed1..c447cc378a 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -20,12 +20,22 @@ assignable to `T`. Two equivalent types are subtypes of each other: ```py from ty_extensions import static_assert, is_assignable_to + class Parent: ... + + class Child1(Parent): ... + + class Child2(Parent): ... + + class Grandchild(Child1, Child2): ... + + class Unrelated: ... + static_assert(is_assignable_to(int, int)) static_assert(is_assignable_to(Parent, Parent)) static_assert(is_assignable_to(Child1, Parent)) @@ -136,10 +146,12 @@ from ty_extensions import static_assert, is_assignable_to from typing_extensions import Literal from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + static_assert(is_assignable_to(Literal[Answer.YES], Literal[Answer.YES])) static_assert(is_assignable_to(Literal[Answer.YES], Answer)) static_assert(is_assignable_to(Literal[Answer.YES, Answer.NO], Answer)) @@ -147,9 +159,11 @@ static_assert(is_assignable_to(Answer, Literal[Answer.YES, Answer.NO])) static_assert(not is_assignable_to(Literal[Answer.YES], Literal[Answer.NO])) + class Single(Enum): VALUE = 1 + static_assert(is_assignable_to(Literal[Single.VALUE], Single)) static_assert(is_assignable_to(Single, Literal[Single.VALUE])) ``` @@ -219,26 +233,35 @@ static_assert(is_assignable_to(type[Any], type[Unknown])) static_assert(not is_assignable_to(object, type[Any])) static_assert(not is_assignable_to(str, type[Any])) + class Meta(type): ... + static_assert(is_assignable_to(type[Any], Meta)) static_assert(is_assignable_to(type[Unknown], Meta)) static_assert(is_assignable_to(Meta, type[Any])) static_assert(is_assignable_to(Meta, type[Unknown])) + def _(x: Any): class AnyMeta(metaclass=x): ... + static_assert(is_assignable_to(type[AnyMeta], type)) static_assert(is_assignable_to(type[AnyMeta], type[object])) static_assert(is_assignable_to(type[AnyMeta], type[Any])) + from typing import TypeVar, Generic, Any T_co = TypeVar("T_co", covariant=True) + class Foo(Generic[T_co]): ... + + class Bar(Foo[T_co], Generic[T_co]): ... + static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]])) @@ -272,9 +295,12 @@ to `type`: from typing import Any from ty_extensions import is_assignable_to, static_assert, TypeOf + def test(x: Any): class Foo(x): ... + class Bar(Any): ... + static_assert(is_assignable_to(TypeOf[Foo], Any)) static_assert(is_assignable_to(TypeOf[Foo], type)) static_assert(is_assignable_to(TypeOf[Foo], type[int])) @@ -299,16 +325,20 @@ Instances of classes that inherit `Any` are assignable to any non-final type. from ty_extensions import is_assignable_to, static_assert from typing_extensions import Any, final + class InheritsAny(Any): pass + class Arbitrary: pass + @final class FinalClass: pass + static_assert(is_assignable_to(InheritsAny, Arbitrary)) static_assert(is_assignable_to(InheritsAny, Any)) static_assert(is_assignable_to(InheritsAny, object)) @@ -660,12 +690,22 @@ static_assert(not is_assignable_to(Literal[True] | AlwaysFalsy, Literal[False] | from ty_extensions import static_assert, is_assignable_to, Intersection, Not, AlwaysTruthy, AlwaysFalsy from typing_extensions import Any, Literal, final, LiteralString + class Parent: ... + + class Child1(Parent): ... + + class Child2(Parent): ... + + class Grandchild(Child1, Child2): ... + + class Unrelated: ... + static_assert(is_assignable_to(Intersection[Child1, Child2], Child1)) static_assert(is_assignable_to(Intersection[Child1, Child2], Child2)) static_assert(is_assignable_to(Intersection[Child1, Child2], Parent)) @@ -752,9 +792,11 @@ from typing import Callable # `Callable[..., Unknown]` has explicit Unknown return type static_assert(is_assignable_to(Intersection[Not[type], Not[Callable[..., Unknown]]], Not[type])) + # Function with no return annotation (has implicit Unknown return type internally) def no_return_annotation(*args, **kwargs): ... + # `CallableTypeOf[no_return_annotation]` has `returns: None` internally (no annotation) static_assert(is_assignable_to(Intersection[Not[type], Not[CallableTypeOf[no_return_annotation]]], Not[type])) ``` @@ -958,6 +1000,7 @@ def keyword_only(*, a: int, b: int) -> None: ... def keyword_variadic(**kwargs: int) -> None: ... def mixed(a: int, /, b: int, *args: int, c: int, **kwargs: int) -> None: ... + static_assert(is_assignable_to(CallableTypeOf[positional_only], Callable[..., None])) static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None])) static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None])) @@ -976,6 +1019,7 @@ def keyword_only(*, a, b) -> None: ... def keyword_variadic(**kwargs) -> None: ... def mixed(a, /, b, *args, c, **kwargs) -> None: ... + static_assert(is_assignable_to(CallableTypeOf[positional_only], Callable[..., None])) static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None])) static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None])) @@ -989,12 +1033,15 @@ static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None])) ```py from typing import Any, Callable + def f(x: Any) -> str: return "" + def g(x: Any) -> int: return 1 + c: Callable[[Any], str] = f # error: [invalid-assignment] "Object of type `def g(x: Any) -> int` is not assignable to `(Any, /) -> str`" @@ -1008,6 +1055,7 @@ A function with no explicit return type should be assignable to a callable with def h(): return + c: Callable[[], Any] = h ``` @@ -1017,6 +1065,7 @@ And, similarly for parameters with no annotations: def i(a, b, /) -> None: return + c: Callable[[Any, Any], None] = i ``` @@ -1028,9 +1077,11 @@ parameter type. def variadic_without_annotation(*args, **kwargs): return + def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any: return + c: Callable[..., Any] = variadic_without_annotation c: Callable[..., Any] = variadic_with_annotation ``` @@ -1040,6 +1091,7 @@ c: Callable[..., Any] = variadic_with_annotation ```py from typing import Any, Callable + class A: def f(self, x: Any) -> str: return "" @@ -1047,6 +1099,7 @@ class A: def g(self, x: Any) -> int: return 1 + c: Callable[[Any], str] = A().f # error: [invalid-assignment] "Object of type `bound method A.g(x: Any) -> int` is not assignable to `(Any, /) -> str`" @@ -1066,25 +1119,33 @@ c: Callable[[str], Any] = int # error: [invalid-assignment] c: Callable[[str], Any] = object + class A: def __init__(self, x: int) -> None: ... + a: Callable[[int], A] = A + class C: def __new__(cls, *args, **kwargs) -> "C": return super().__new__(cls) def __init__(self, x: int) -> None: ... + c: Callable[[int], C] = C + def f(a: Callable[..., Any], b: Callable[[Any], Any]): ... + f(tuple, tuple) + def g(a: Callable[[Any, Any], Any]): ... + # error: [invalid-argument-type] "Argument to function `g` is incorrect: Expected `(Any, Any, /) -> Any`, found ``" g(tuple) ``` @@ -1099,17 +1160,21 @@ python-version = "3.12" ```py from typing import Callable + class B[T]: def __init__(self, x: T) -> None: ... + b: Callable[[int], B[int]] = B[int] + class C[T]: def __new__(cls, *args, **kwargs) -> "C[T]": return super().__new__(cls) def __init__(self, x: T) -> None: ... + c: Callable[[int], C[int]] = C[int] ``` @@ -1150,13 +1215,16 @@ c: Callable[[int], str] = overloaded from typing import Callable, Any from ty_extensions import static_assert, is_assignable_to + class TakesAny: def __call__(self, a: Any) -> str: return "" + class ReturnsAny: def __call__(self, a: str) -> Any: ... + static_assert(is_assignable_to(TakesAny, Callable[[int], str])) static_assert(not is_assignable_to(TakesAny, Callable[[int], int])) @@ -1165,8 +1233,10 @@ static_assert(not is_assignable_to(ReturnsAny, Callable[[int], int])) from functools import partial + def f(x: int, y: str) -> None: ... + c1: Callable[[int], None] = partial(f, y="a") ``` @@ -1184,28 +1254,41 @@ from ty_extensions import static_assert, is_assignable_to T = TypeVar("T") P = ParamSpec("P") + class Foo[T]: def __call__(self): ... + class FooLegacy(Generic[T]): def __call__(self): ... + class Bar[T, **P]: def __call__(self): ... + class BarLegacy(Generic[T, P]): def __call__(self): ... + static_assert(is_assignable_to(Foo, Callable[..., Any])) static_assert(is_assignable_to(FooLegacy, Callable[..., Any])) static_assert(is_assignable_to(Bar, Callable[..., Any])) static_assert(is_assignable_to(BarLegacy, Callable[..., Any])) + class Spam[T]: ... + + class SpamLegacy(Generic[T]): ... + + class Eggs[T, **P]: ... + + class EggsLegacy(Generic[T, P]): ... + static_assert(not is_assignable_to(Spam, Callable[..., Any])) static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any])) static_assert(not is_assignable_to(Eggs, Callable[..., Any])) @@ -1223,12 +1306,15 @@ from __future__ import annotations from typing import Callable from ty_extensions import static_assert, is_assignable_to + def call_impl(a: A, x: int) -> str: return "" + class A: __call__: Callable[[A, int], str] = call_impl + static_assert(is_assignable_to(A, Callable[[int], str])) static_assert(not is_assignable_to(A, Callable[[int], int])) reveal_type(A()(1)) # revealed: str @@ -1242,13 +1328,16 @@ reveal_type(A()(1)) # revealed: str from typing import Callable from ty_extensions import static_assert, is_assignable_to + class A: def __init__(self, x: int) -> None: ... + class B: def __new__(cls, x: str) -> "B": return super().__new__(cls) + static_assert(is_assignable_to(type[A], Callable[[int], A])) static_assert(not is_assignable_to(type[A], Callable[[str], A])) @@ -1276,9 +1365,11 @@ the generic callable.) from typing import Callable from ty_extensions import CallableTypeOf, TypeOf, is_assignable_to, static_assert + def identity[T](t: T) -> T: return t + static_assert(is_assignable_to(TypeOf[identity], Callable[[int], int])) static_assert(is_assignable_to(TypeOf[identity], Callable[[str], str])) # TODO: no error @@ -1328,13 +1419,20 @@ from ty_extensions import static_assert, is_assignable_to InvariantTypeVar = TypeVar("InvariantTypeVar") + class Foo(Generic[InvariantTypeVar]): x: InvariantTypeVar + class A: ... + + class B(A): ... + + class C: ... + static_assert(is_assignable_to(Foo[A], Foo[B | Any])) static_assert(is_assignable_to(Foo[B | Any], Foo[A])) static_assert(is_assignable_to(Foo[Foo[Any]], Foo[Foo[A | C]])) @@ -1342,21 +1440,27 @@ static_assert(is_assignable_to(Foo[Foo[A | C]], Foo[Foo[Any]])) static_assert(is_assignable_to(Foo[tuple[A]], Foo[tuple[Any] | tuple[B]])) static_assert(is_assignable_to(Foo[tuple[Any] | tuple[B]], Foo[tuple[A]])) + def f(obj: Foo[A]): g(obj) + def g(obj: Foo[B | Any]): f(obj) + def f2(obj: Foo[Foo[Any]]): g2(obj) + def g2(obj: Foo[Foo[A | C]]): f2(obj) + def f3(obj: Foo[tuple[Any] | tuple[B]]): g3(obj) + def g3(obj: Foo[tuple[A]]): f3(obj) ``` @@ -1367,27 +1471,33 @@ def g3(obj: Foo[tuple[A]]): from typing import final from ty_extensions import static_assert, is_assignable_to, TypeOf + class GenericClass[T]: x: T # invariant + static_assert(is_assignable_to(TypeOf[GenericClass], type[GenericClass])) static_assert(is_assignable_to(TypeOf[GenericClass[int]], type[GenericClass])) static_assert(is_assignable_to(TypeOf[GenericClass], type[GenericClass[int]])) static_assert(is_assignable_to(TypeOf[GenericClass[int]], type[GenericClass[int]])) static_assert(not is_assignable_to(TypeOf[GenericClass[str]], type[GenericClass[int]])) + class GenericClassIntBound[T: int]: x: T # invariant + static_assert(is_assignable_to(TypeOf[GenericClassIntBound], type[GenericClassIntBound])) static_assert(is_assignable_to(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound])) static_assert(is_assignable_to(TypeOf[GenericClassIntBound], type[GenericClassIntBound[int]])) static_assert(is_assignable_to(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound[int]])) + @final class GenericFinalClass[T]: x: T # invariant + static_assert(is_assignable_to(TypeOf[GenericFinalClass], type[GenericFinalClass])) static_assert(is_assignable_to(TypeOf[GenericFinalClass[int]], type[GenericFinalClass])) static_assert(is_assignable_to(TypeOf[GenericFinalClass], type[GenericFinalClass[int]])) @@ -1418,6 +1528,7 @@ from typing import ParamSpec, Mapping, Callable, Any P = ParamSpec("P") + def f(func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: static_assert(is_assignable_to(TypeOf[args], tuple[Any, ...])) static_assert(is_assignable_to(TypeOf[args], tuple[object, ...])) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index 0b2d95842f..485298aa24 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -28,10 +28,16 @@ static_assert(not is_disjoint_from(str, LiteralString)) from ty_extensions import is_disjoint_from, static_assert, Intersection, is_subtype_of from typing import final + class A: ... + + class B1(A): ... + + class B2(A): ... + # B1 and B2 are subclasses of A, so they are not disjoint from A: static_assert(not is_disjoint_from(A, B1)) static_assert(not is_disjoint_from(A, B2)) @@ -39,33 +45,43 @@ static_assert(not is_disjoint_from(A, B2)) # The two subclasses B1 and B2 are also not disjoint ... static_assert(not is_disjoint_from(B1, B2)) + # ... because they could share a common subclass ... class C(B1, B2): ... + # ... which lies in their intersection: static_assert(is_subtype_of(C, Intersection[B1, B2])) + # However, if a class is marked final, it cannot be subclassed ... @final class FinalSubclass(A): ... + static_assert(not is_disjoint_from(FinalSubclass, A)) # ... which makes it disjoint from B1, B2: static_assert(is_disjoint_from(B1, FinalSubclass)) static_assert(is_disjoint_from(B2, FinalSubclass)) + # Instance types can also be disjoint if they have disjoint metaclasses. # No possible subclass of `Meta1` and `Meta2` could exist, therefore # no possible subclass of `UsesMeta1` and `UsesMeta2` can exist: class Meta1(type): ... + + class UsesMeta1(metaclass=Meta1): ... + @final class Meta2(type): ... + class UsesMeta2(metaclass=Meta2): ... + static_assert(is_disjoint_from(UsesMeta1, UsesMeta2)) ``` @@ -76,8 +92,10 @@ Some builtins types are declared as `@final`: ```py from ty_extensions import static_assert, is_disjoint_from + class Foo: ... + # `range`, `slice` and `memoryview` are all declared as `@final`: static_assert(is_disjoint_from(range, Foo)) static_assert(is_disjoint_from(type[range], type[Foo])) @@ -98,14 +116,19 @@ python-version = "3.12" from typing import Any, final from ty_extensions import static_assert, is_disjoint_from + @final class Foo[T]: def get(self) -> T: raise NotImplementedError + class A: ... + + class B: ... + static_assert(not is_disjoint_from(A, B)) static_assert(not is_disjoint_from(Foo[A], Foo[B])) static_assert(not is_disjoint_from(Foo[A], Foo[Any])) @@ -133,8 +156,10 @@ from typing import Any from typing_extensions import disjoint_base from ty_extensions import static_assert, is_disjoint_from + class Foo: ... + static_assert(is_disjoint_from(list, dict)) static_assert(is_disjoint_from(list[Foo], dict)) static_assert(is_disjoint_from(list[Any], dict)) @@ -148,12 +173,15 @@ static_assert(is_disjoint_from(type[list], type[dict])) static_assert(is_disjoint_from(asyncio.Task, dict)) + @disjoint_base class A: ... + @disjoint_base class B: ... + static_assert(is_disjoint_from(A, B)) ``` @@ -166,15 +194,19 @@ ty: ```py from ty_extensions import static_assert, is_disjoint_from + class A: __slots__ = ("a",) + class B: __slots__ = ("a",) + class C: __slots__ = () + static_assert(is_disjoint_from(A, B)) static_assert(is_disjoint_from(type[A], type[B])) static_assert(not is_disjoint_from(A, C)) @@ -189,6 +221,7 @@ Two disjoint bases are not disjoint if one inherits from the other, however: class D(A): __slots__ = ("d",) + static_assert(is_disjoint_from(D, B)) static_assert(not is_disjoint_from(D, A)) ``` @@ -199,20 +232,25 @@ static_assert(not is_disjoint_from(D, A)) from dataclasses import dataclass from ty_extensions import is_disjoint_from, static_assert + @dataclass(slots=True) class F: ... + @dataclass(slots=True) class G: ... + @dataclass(slots=True) class I: x: int + @dataclass(slots=True) class J: y: int + # A dataclass with empty `__slots__` is not disjoint from another dataclass with `__slots__` static_assert(not is_disjoint_from(F, G)) static_assert(not is_disjoint_from(F, I)) @@ -269,15 +307,19 @@ static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3])) from typing_extensions import Literal, final, Any, LiteralString from ty_extensions import Intersection, is_disjoint_from, static_assert, Not, AlwaysFalsy + @final class P: ... + @final class Q: ... + @final class R: ... + # For three pairwise disjoint classes ... static_assert(is_disjoint_from(P, Q)) static_assert(is_disjoint_from(P, R)) @@ -288,11 +330,17 @@ static_assert(is_disjoint_from(Intersection[P, Q], R)) static_assert(is_disjoint_from(Intersection[P, R], Q)) static_assert(is_disjoint_from(Intersection[Q, R], P)) + # On the other hand, for non-disjoint classes ... class X: ... + + class Y: ... + + class Z: ... + static_assert(not is_disjoint_from(X, Y)) static_assert(not is_disjoint_from(X, Z)) static_assert(not is_disjoint_from(Y, Z)) @@ -307,9 +355,13 @@ static_assert(is_disjoint_from(int, Not[int])) static_assert(is_disjoint_from(Intersection[X, Y, Not[Z]], Intersection[X, Z])) static_assert(is_disjoint_from(Intersection[X, Not[Literal[1]]], Literal[1])) + class Parent: ... + + class Child(Parent): ... + static_assert(not is_disjoint_from(Parent, Child)) static_assert(not is_disjoint_from(Parent, Not[Child])) static_assert(not is_disjoint_from(Not[Parent], Not[Child])) @@ -372,10 +424,12 @@ from typing_extensions import Literal, LiteralString from ty_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert, AlwaysFalsy, AlwaysTruthy from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + static_assert(is_disjoint_from(Literal[True], Literal[False])) static_assert(is_disjoint_from(Literal[True], Literal[1])) static_assert(is_disjoint_from(Literal[False], Literal[0])) @@ -437,9 +491,13 @@ python-version = "3.12" from types import ModuleType, FunctionType from ty_extensions import TypeOf, is_disjoint_from, static_assert + class A: ... + + class B: ... + type LiteralA = TypeOf[A] type LiteralB = TypeOf[B] @@ -464,9 +522,11 @@ static_assert(is_disjoint_from(TypeOf[random], TypeOf[math])) static_assert(not is_disjoint_from(TypeOf[random], ModuleType)) static_assert(not is_disjoint_from(TypeOf[random], object)) + def f(): ... def g(): ... + static_assert(is_disjoint_from(TypeOf[f], TypeOf[g])) static_assert(not is_disjoint_from(TypeOf[f], FunctionType)) static_assert(not is_disjoint_from(TypeOf[f], object)) @@ -498,9 +558,11 @@ the instance type is not a subclass of `T`'s metaclass. from typing import final from ty_extensions import is_disjoint_from, static_assert + @final class Foo: ... + static_assert(is_disjoint_from(Foo, type[int])) static_assert(is_disjoint_from(type[object], Foo)) static_assert(is_disjoint_from(type[dict], Foo)) @@ -508,16 +570,23 @@ static_assert(is_disjoint_from(type[dict], Foo)) # Instance types can be disjoint from `type[]` types # even if the instance type is a subtype of `type` + @final class Meta1(type): ... + class UsesMeta1(metaclass=Meta1): ... + static_assert(not is_disjoint_from(Meta1, type[UsesMeta1])) + class Meta2(type): ... + + class UsesMeta2(metaclass=Meta2): ... + static_assert(not is_disjoint_from(Meta2, type[UsesMeta2])) static_assert(is_disjoint_from(Meta1, type[UsesMeta2])) ``` @@ -531,16 +600,23 @@ metaclass of `T` is disjoint from the metaclass of `S`. from typing import final from ty_extensions import static_assert, is_disjoint_from + @final class Meta1(type): ... + class Meta2(type): ... + static_assert(is_disjoint_from(type[Meta1], type[Meta2])) + class UsesMeta1(metaclass=Meta1): ... + + class UsesMeta2(metaclass=Meta2): ... + static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2])) ``` @@ -550,19 +626,24 @@ static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2])) from ty_extensions import is_disjoint_from, static_assert, TypeOf from typing import final + class C: @property def prop(self) -> int: return 1 + reveal_type(C.prop) # revealed: property + @final class D: pass + class Whatever: ... + static_assert(not is_disjoint_from(Whatever, TypeOf[C.prop])) static_assert(not is_disjoint_from(TypeOf[C.prop], Whatever)) static_assert(is_disjoint_from(TypeOf[C.prop], D)) @@ -592,33 +673,42 @@ type of the protocol's member. from typing_extensions import Protocol, Literal, final, ClassVar from ty_extensions import is_disjoint_from, static_assert + class HasAttrA(Protocol): attr: Literal["a"] + class SupportsInt(Protocol): def __int__(self) -> int: ... + class A: attr: Literal["a"] + class B: attr: Literal["b"] + class C: foo: int + class D: attr: int + @final class E: pass + @final class F: def __int__(self) -> int: return 1 + static_assert(not is_disjoint_from(HasAttrA, A)) static_assert(is_disjoint_from(HasAttrA, B)) # A subclass of E may satisfy HasAttrA @@ -629,17 +719,22 @@ static_assert(is_disjoint_from(HasAttrA, E)) static_assert(is_disjoint_from(SupportsInt, E)) static_assert(not is_disjoint_from(SupportsInt, F)) + class NotIterable(Protocol): __iter__: ClassVar[None] + static_assert(is_disjoint_from(tuple[int, int], NotIterable)) + class Foo: BAR: ClassVar[int] + class BarNone(Protocol): BAR: None + static_assert(is_disjoint_from(type[Foo], BarNone)) ``` @@ -651,16 +746,19 @@ from __future__ import annotations from typing import NamedTuple, final from ty_extensions import is_disjoint_from, static_assert + @final class Path(NamedTuple): prev: Path | None key: str + @final class Path2(NamedTuple): prev: Path2 | None key: str + static_assert(not is_disjoint_from(Path, Path)) static_assert(not is_disjoint_from(Path, tuple[Path | None, str])) static_assert(is_disjoint_from(Path, tuple[Path | None])) @@ -679,27 +777,33 @@ python-version = "3.12" from typing import final from ty_extensions import static_assert, is_disjoint_from, TypeOf + class GenericClass[T]: x: T # invariant + static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass])) static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass])) static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass[int]])) static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass[int]])) static_assert(is_disjoint_from(TypeOf[GenericClass[str]], type[GenericClass[int]])) + class GenericClassIntBound[T: int]: x: T # invariant + static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound])) static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound])) static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound[int]])) static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound[int]])) + @final class GenericFinalClass[T]: x: T # invariant + static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass])) static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass])) static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass[int]])) @@ -718,8 +822,10 @@ would inhabit both types simultaneously. from ty_extensions import CallableTypeOf, is_disjoint_from, static_assert from typing_extensions import Callable, Literal, Never + def mixed(a: int, /, b: str, *args: int, c: int = 2, **kwargs: int) -> None: ... + static_assert(not is_disjoint_from(Callable[[], Never], CallableTypeOf[mixed])) static_assert(not is_disjoint_from(Callable[[int, str], float], CallableTypeOf[mixed])) @@ -752,9 +858,11 @@ A callable type is disjoint from nominal instance types where the classes are fi from ty_extensions import CallableTypeOf, is_disjoint_from, static_assert from typing_extensions import Any, Callable, final + @final class C: ... + static_assert(is_disjoint_from(bool, Callable[..., Any])) static_assert(is_disjoint_from(C, Callable[..., Any])) static_assert(is_disjoint_from(bool | C, Callable[..., Any])) @@ -769,6 +877,7 @@ static_assert(not is_disjoint_from(bool | str, Callable[..., Any])) static_assert(not is_disjoint_from(Callable[..., Any], str)) static_assert(not is_disjoint_from(Callable[..., Any], bool | str)) + def bound_with_valid_type(): @final class D: @@ -777,15 +886,18 @@ def bound_with_valid_type(): static_assert(not is_disjoint_from(D, Callable[..., Any])) static_assert(not is_disjoint_from(Callable[..., Any], D)) + def possibly_unbound_with_valid_type(flag: bool): @final class E: if flag: + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... static_assert(not is_disjoint_from(E, Callable[..., Any])) static_assert(not is_disjoint_from(Callable[..., Any], E)) + def bound_with_invalid_type(): @final class F: @@ -794,6 +906,7 @@ def bound_with_invalid_type(): static_assert(is_disjoint_from(F, Callable[..., Any])) static_assert(is_disjoint_from(Callable[..., Any], F)) + def possibly_unbound_with_invalid_type(flag: bool): @final class G: @@ -858,17 +971,21 @@ from enum import Enum from ty_extensions import is_disjoint_from, static_assert from typing_extensions import Literal + class MyEnum(Enum): def special_method(self): pass + class MyAnswer(MyEnum): NO = 0 YES = 1 + class UnrelatedClass: pass + static_assert(is_disjoint_from(Literal[MyAnswer.NO], Literal[MyAnswer.YES])) static_assert(is_disjoint_from(Literal[MyAnswer.NO], UnrelatedClass)) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index 71d53f44a3..47061b466b 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -17,13 +17,16 @@ from typing_extensions import Literal, LiteralString, Protocol, Never from ty_extensions import Unknown, is_equivalent_to, static_assert, TypeOf, AlwaysTruthy, AlwaysFalsy from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + class Single(Enum): VALUE = 1 + static_assert(is_equivalent_to(Literal[1, 2], Literal[1, 2])) static_assert(is_equivalent_to(type[object], type)) static_assert(is_equivalent_to(type, type[object])) @@ -45,12 +48,15 @@ static_assert(is_equivalent_to(Literal[Single.VALUE], Literal[Single.VALUE])) static_assert(is_equivalent_to(tuple[Single] | int | str, str | int | tuple[Literal[Single.VALUE]])) + class Protocol1(Protocol): a: Single + class Protocol2(Protocol): a: Literal[Single.VALUE] + static_assert(is_equivalent_to(Protocol1, Protocol2)) static_assert(is_equivalent_to(Never, Never)) @@ -101,11 +107,19 @@ static_assert(not is_equivalent_to(str | int | bytes, int | str | dict)) static_assert(is_equivalent_to(Unknown, Unknown | Any)) static_assert(is_equivalent_to(Unknown, Intersection[Unknown, Any])) + class P: ... + + class Q: ... + + class R: ... + + class S: ... + static_assert(is_equivalent_to(P | Q | R, P | R | Q)) # 1 static_assert(is_equivalent_to(P | Q | R, Q | P | R)) # 2 static_assert(is_equivalent_to(P | Q | R, Q | R | P)) # 3 @@ -129,9 +143,11 @@ static_assert(is_equivalent_to(Intersection[Q, Not[P]], Intersection[Not[P], Q]) static_assert(is_equivalent_to(Intersection[Q, R, Not[P]], Intersection[Not[P], R, Q])) static_assert(is_equivalent_to(Intersection[Q | R, Not[P | S]], Intersection[Not[S | P], R | Q])) + class Single(Enum): VALUE = 1 + static_assert(is_equivalent_to(P | Q | Single, Literal[Single.VALUE] | Q | P)) static_assert(is_equivalent_to(Any, Any | Intersection[Any, str])) @@ -163,11 +179,19 @@ static_assert(not is_equivalent_to(tuple[str, int], tuple[int, str])) from ty_extensions import is_equivalent_to, TypeOf, static_assert, Intersection, Not from typing import Literal + class P: ... + + class Q: ... + + class R: ... + + class S: ... + static_assert(is_equivalent_to(tuple[P | Q], tuple[Q | P])) static_assert(is_equivalent_to(tuple[P | None], tuple[None | P])) static_assert( @@ -180,9 +204,13 @@ static_assert( ```py from ty_extensions import is_equivalent_to, static_assert, Intersection + class P: ... + + class Q: ... + static_assert( is_equivalent_to( tuple[tuple[tuple[P | Q]]] | P, @@ -202,10 +230,16 @@ static_assert( ```py from ty_extensions import is_equivalent_to, static_assert, Intersection + class P: ... + + class Q: ... + + class R: ... + static_assert(is_equivalent_to(Intersection[tuple[P | Q], R], Intersection[tuple[Q | P], R])) ``` @@ -219,10 +253,16 @@ python-version = "3.12" ```py from ty_extensions import is_equivalent_to, static_assert + class A: ... + + class B: ... + + class Foo[T]: ... + static_assert(is_equivalent_to(A | Foo[A | B], Foo[B | A] | A)) ``` @@ -238,9 +278,11 @@ other callable should also have a default value. from ty_extensions import CallableTypeOf, is_equivalent_to, static_assert from typing import Callable + def f1(a: int = 1) -> None: ... def f2(a: int = 2) -> None: ... + static_assert(is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) static_assert(is_equivalent_to(CallableTypeOf[f1] | bool | CallableTypeOf[f2], CallableTypeOf[f2] | bool | CallableTypeOf[f1])) ``` @@ -252,6 +294,7 @@ same. def f3(a1: int, /, *args1: int, **kwargs2: int) -> None: ... def f4(a2: int, /, *args2: int, **kwargs1: int) -> None: ... + static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3])) ``` @@ -262,6 +305,7 @@ Putting it all together, the following two callables are equivalent: def f5(a1: int, /, b: float, c: bool = False, *args1: int, d: int = 1, e: str, **kwargs1: float) -> None: ... def f6(a2: int, /, b: float, c: bool = True, *args2: int, d: int = 2, e: str, **kwargs2: float) -> None: ... + static_assert(is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6])) static_assert(is_equivalent_to(CallableTypeOf[f5] | bool | CallableTypeOf[f6], CallableTypeOf[f6] | bool | CallableTypeOf[f5])) ``` @@ -281,6 +325,7 @@ When the number of parameters is different: def f1(a: int) -> None: ... def f2(a: int, b: int) -> None: ... + static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) ``` @@ -290,6 +335,7 @@ When the return types are not equivalent in one or both of the callable types: def f3(): ... def f4() -> None: ... + static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None])) static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3])) static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) @@ -302,6 +348,7 @@ When the parameter names are different: def f5(a: int) -> None: ... def f6(b: int) -> None: ... + static_assert(not is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6])) ``` @@ -317,6 +364,7 @@ When the parameter kinds are different: def f7(a: int, /) -> None: ... def f8(a: int) -> None: ... + static_assert(not is_equivalent_to(CallableTypeOf[f7], CallableTypeOf[f8])) ``` @@ -328,6 +376,7 @@ def f9(a: int) -> None: ... def f10(a: str) -> None: ... def f11(a) -> None: ... + static_assert(not is_equivalent_to(CallableTypeOf[f9], CallableTypeOf[f10])) static_assert(not is_equivalent_to(CallableTypeOf[f10], CallableTypeOf[f11])) static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f10])) @@ -340,6 +389,7 @@ When the default value for a parameter is present only in one of the callable ty def f12(a: int) -> None: ... def f13(a: int = 2) -> None: ... + static_assert(not is_equivalent_to(CallableTypeOf[f12], CallableTypeOf[f13])) static_assert(not is_equivalent_to(CallableTypeOf[f13], CallableTypeOf[f12])) ``` @@ -352,9 +402,11 @@ ordered: ```py from ty_extensions import CallableTypeOf, Unknown, is_equivalent_to, static_assert + def f(x): ... def g(x: Unknown): ... + static_assert(is_equivalent_to(CallableTypeOf[f] | int | str, str | int | CallableTypeOf[g])) ``` @@ -394,8 +446,10 @@ def overloaded(a: Grandparent) -> None: ... from ty_extensions import CallableTypeOf, is_equivalent_to, static_assert from overloaded import Grandparent, Parent, Child, overloaded + def grandparent(a: Grandparent) -> None: ... + static_assert(is_equivalent_to(CallableTypeOf[grandparent], CallableTypeOf[overloaded])) static_assert(is_equivalent_to(CallableTypeOf[overloaded], CallableTypeOf[grandparent])) ``` @@ -444,14 +498,18 @@ python-version = "3.12" ```py from ty_extensions import is_equivalent_to, TypeOf, static_assert + def f(): ... + static_assert(is_equivalent_to(TypeOf[f], TypeOf[f])) + class A: def method(self) -> int: return 42 + static_assert(is_equivalent_to(TypeOf[A.method], TypeOf[A.method])) type X = TypeOf[A.method] static_assert(is_equivalent_to(X, X)) @@ -483,9 +541,11 @@ with a return type of `Any`. def f1(): return + def f1_equivalent() -> Any: return + static_assert(is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f1_equivalent])) ``` @@ -495,9 +555,11 @@ And, similarly for parameters with no annotations. def f2(a, b, /) -> None: return + def f2_equivalent(a: Any, b: Any, /) -> None: return + static_assert(is_equivalent_to(CallableTypeOf[f2], CallableTypeOf[f2_equivalent])) ``` @@ -509,9 +571,11 @@ type. def variadic_without_annotation(*args, **kwargs): return + def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any: return + def _( signature_variadic_without_annotation: CallableTypeOf[variadic_without_annotation], signature_variadic_with_annotation: CallableTypeOf[variadic_with_annotation], @@ -538,9 +602,11 @@ A function with either `*args` or `**kwargs` (and not both) is is not equivalent def variadic_args(*args): return + def variadic_kwargs(**kwargs): return + def _( signature_variadic_args: CallableTypeOf[variadic_args], signature_variadic_kwargs: CallableTypeOf[variadic_kwargs], @@ -550,6 +616,7 @@ def _( # revealed: (**kwargs) -> Unknown reveal_type(signature_variadic_kwargs) + static_assert(not is_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any])) static_assert(not is_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any])) ``` @@ -561,18 +628,23 @@ equivalence. def f1(a): ... def f2(b): ... + static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) + def f3(a=1): ... def f4(a=2): ... def f5(a): ... + static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3])) static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5])) + def f6(a, /): ... + static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6])) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md index 0692250475..1809f57004 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md @@ -16,9 +16,13 @@ static_assert(is_single_valued(Literal[b"abc"])) static_assert(is_single_valued(tuple[()])) static_assert(is_single_valued(tuple[Literal[True], Literal[1]])) + class EmptyTupleSubclass(tuple[()]): ... + + class HeterogeneousTupleSubclass(tuple[Literal[True], Literal[1]]): ... + # N.B. this follows from the fact that `EmptyTupleSubclass` is a subtype of `tuple[()]`, # and any property recognised for `tuple[()]` should therefore also be recognised for # `EmptyTupleSubclass` since an `EmptyTupleSubclass` instance can be used anywhere where @@ -37,16 +41,20 @@ static_assert(not is_single_valued(Literal[1, 2])) static_assert(not is_single_valued(tuple[None, int])) + class MultiValuedHeterogeneousTupleSubclass(tuple[None, int]): ... + static_assert(not is_single_valued(MultiValuedHeterogeneousTupleSubclass)) static_assert(not is_single_valued(Callable[..., None])) static_assert(not is_single_valued(Callable[[int, str], None])) + class A: def method(self): ... + static_assert(is_single_valued(TypeOf[A().method])) static_assert(is_single_valued(TypeOf[types.FunctionType.__get__])) static_assert(is_single_valued(TypeOf[A.method.__get__])) @@ -60,13 +68,16 @@ literal type might not compare equal to itself. from ty_extensions import is_single_valued, static_assert, TypeOf from enum import Enum + class NormalEnum(Enum): NO = 0 YES = 1 + class SingleValuedEnum(Enum): VALUE = 1 + class ComparesEqualEnum(Enum): NO = 0 YES = 1 @@ -74,6 +85,7 @@ class ComparesEqualEnum(Enum): def __eq__(self, other: object) -> Literal[True]: return True + class CustomEqEnum(Enum): NO = 0 YES = 1 @@ -81,6 +93,7 @@ class CustomEqEnum(Enum): def __eq__(self, other: object) -> bool: return False + class CustomNeEnum(Enum): NO = 0 YES = 1 @@ -88,14 +101,17 @@ class CustomNeEnum(Enum): def __ne__(self, other: object) -> bool: return False + class StrEnum(str, Enum): A = "a" B = "b" + class IntEnum(int, Enum): A = 1 B = 2 + static_assert(is_single_valued(Literal[NormalEnum.NO])) static_assert(is_single_valued(Literal[NormalEnum.YES])) static_assert(not is_single_valued(NormalEnum)) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_singleton.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_singleton.md index 25a55199ba..0b647a367b 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_singleton.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_singleton.md @@ -9,13 +9,16 @@ from typing_extensions import Literal, Never, Callable from ty_extensions import is_singleton, static_assert from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + class Single(Enum): VALUE = 1 + static_assert(is_singleton(None)) static_assert(is_singleton(Literal[True])) static_assert(is_singleton(Literal[False])) @@ -159,9 +162,11 @@ import types from typing import Callable from ty_extensions import static_assert, is_singleton, TypeOf + class A: def method(self): ... + static_assert(is_singleton(TypeOf[types.FunctionType.__get__])) static_assert(not is_singleton(Callable[[], None])) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 9877c14a86..7fdaca24d0 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -53,11 +53,19 @@ static_assert(is_subtype_of(FloatingPointError, Exception)) from ty_extensions import is_subtype_of, static_assert from typing_extensions import Never + class A: ... + + class B1(A): ... + + class B2(A): ... + + class C(B1, B2): ... + static_assert(is_subtype_of(B1, A)) static_assert(not is_subtype_of(A, B1)) @@ -92,13 +100,16 @@ from typing_extensions import Literal, LiteralString from ty_extensions import is_subtype_of, static_assert, TypeOf, JustFloat from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + class Single(Enum): VALUE = 1 + # Boolean literals static_assert(is_subtype_of(Literal[True], bool)) static_assert(is_subtype_of(Literal[True], int)) @@ -141,12 +152,22 @@ static_assert(is_subtype_of(Single, Literal[Single.VALUE])) ```py from ty_extensions import is_subtype_of, static_assert + class A1: ... + + class B1(A1): ... + + class A2: ... + + class B2(A2): ... + + class Unrelated: ... + static_assert(is_subtype_of(B1, A1)) static_assert(is_subtype_of(B2, A2)) @@ -435,12 +456,22 @@ static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, in from ty_extensions import is_subtype_of, static_assert from typing import Literal + class A: ... + + class B1(A): ... + + class B2(A): ... + + class Unrelated1: ... + + class Unrelated2: ... + static_assert(is_subtype_of(B1, A)) static_assert(is_subtype_of(B2, A)) @@ -475,12 +506,22 @@ static_assert(not is_subtype_of(Literal[1, "two", 3], int)) from typing_extensions import Literal, LiteralString from ty_extensions import Intersection, Not, is_subtype_of, static_assert + class A: ... + + class B1(A): ... + + class B2(A): ... + + class C(B1, B2): ... + + class Unrelated: ... + static_assert(is_subtype_of(B1, A)) static_assert(is_subtype_of(B2, A)) static_assert(is_subtype_of(C, A)) @@ -637,28 +678,38 @@ static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], Not[A # error: [static-assert-error] static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy])) + class Length2TupleSubclass(tuple[int, str]): ... + static_assert(is_subtype_of(Length2TupleSubclass, AlwaysTruthy)) + class EmptyTupleSubclass(tuple[()]): ... + static_assert(is_subtype_of(EmptyTupleSubclass, AlwaysFalsy)) + class TupleSubclassWithAtLeastLength2(tuple[int, *tuple[str, ...], bytes]): ... + static_assert(is_subtype_of(TupleSubclassWithAtLeastLength2, AlwaysTruthy)) + class UnknownLength(tuple[int, ...]): ... + static_assert(not is_subtype_of(UnknownLength, AlwaysTruthy)) static_assert(not is_subtype_of(UnknownLength, AlwaysFalsy)) + class Invalid(tuple[int, str]): # TODO: we should emit an error here (Liskov violation) def __bool__(self) -> Literal[False]: return False + static_assert(is_subtype_of(Invalid, AlwaysFalsy)) ``` @@ -739,9 +790,13 @@ from typing import _SpecialForm, Any from typing_extensions import Literal, assert_type from ty_extensions import TypeOf, is_subtype_of, static_assert + class Meta(type): ... + + class HasCustomMetaclass(metaclass=Meta): ... + type LiteralBool = TypeOf[bool] type LiteralInt = TypeOf[int] type LiteralStr = TypeOf[str] @@ -823,10 +878,16 @@ static_assert(not is_subtype_of(LiteralListOfInt, type[Any])) from typing_extensions import assert_type from ty_extensions import TypeOf, is_subtype_of, static_assert + class Base: ... + + class Derived(Base): ... + + class Unrelated: ... + type LiteralBase = TypeOf[Base] type LiteralDerived = TypeOf[Derived] type LiteralUnrelated = TypeOf[Unrelated] @@ -869,36 +930,44 @@ static_assert(is_subtype_of(int, Any | int)) static_assert(is_subtype_of(Intersection[Any, int], int)) static_assert(not is_subtype_of(tuple[int, int], tuple[int, Any])) + class Covariant[T]: def get(self) -> T: raise NotImplementedError + static_assert(not is_subtype_of(Covariant[Any], Covariant[Any])) static_assert(not is_subtype_of(Covariant[Any], Covariant[int])) static_assert(not is_subtype_of(Covariant[int], Covariant[Any])) static_assert(is_subtype_of(Covariant[Any], Covariant[object])) static_assert(not is_subtype_of(Covariant[object], Covariant[Any])) + class Contravariant[T]: def receive(self, input: T): ... + static_assert(not is_subtype_of(Contravariant[Any], Contravariant[Any])) static_assert(not is_subtype_of(Contravariant[Any], Contravariant[int])) static_assert(not is_subtype_of(Contravariant[int], Contravariant[Any])) static_assert(not is_subtype_of(Contravariant[Any], Contravariant[object])) static_assert(is_subtype_of(Contravariant[object], Contravariant[Any])) + class Invariant[T]: mutable_attribute: T + static_assert(not is_subtype_of(Invariant[Any], Invariant[Any])) static_assert(not is_subtype_of(Invariant[Any], Invariant[int])) static_assert(not is_subtype_of(Invariant[int], Invariant[Any])) static_assert(not is_subtype_of(Invariant[Any], Invariant[object])) static_assert(not is_subtype_of(Invariant[object], Invariant[Any])) + class Bivariant[T]: ... + static_assert(is_subtype_of(Bivariant[Any], Bivariant[Any])) static_assert(is_subtype_of(Bivariant[Any], Bivariant[int])) static_assert(is_subtype_of(Bivariant[int], Bivariant[Any])) @@ -933,9 +1002,11 @@ They are subtypes of `object`. class InheritsAny(Any): pass + class Arbitrary: pass + static_assert(not is_subtype_of(InheritsAny, Arbitrary)) static_assert(not is_subtype_of(InheritsAny, Any)) static_assert(is_subtype_of(InheritsAny, object)) @@ -980,14 +1051,17 @@ from ty_extensions import is_subtype_of, static_assert, TypeOf flag: bool = True + def optional_return_type() -> int | None: if flag: return 1 return None + def required_return_type() -> int: return 1 + static_assert(not is_subtype_of(TypeOf[optional_return_type], TypeOf[required_return_type])) # TypeOf[some_function] is a singleton function-literal type, not a general callable type static_assert(not is_subtype_of(TypeOf[required_return_type], TypeOf[optional_return_type])) @@ -1004,9 +1078,11 @@ Parameter types are contravariant. from typing import Callable from ty_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf + def float_param(a: float, /) -> None: ... def int_param(a: int, /) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[float_param], CallableTypeOf[int_param])) static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[float_param])) @@ -1022,6 +1098,7 @@ Parameter name is not required to be the same for positional-only parameters at ```py def int_param_different_name(b: int, /) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[int_param_different_name])) static_assert(is_subtype_of(CallableTypeOf[int_param_different_name], CallableTypeOf[int_param])) ``` @@ -1032,6 +1109,7 @@ Multiple positional-only parameters are checked in order: def multi_param1(a: float, b: int, c: str, /) -> None: ... def multi_param2(b: int, c: bool, a: str, /) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[multi_param1], CallableTypeOf[multi_param2])) static_assert(not is_subtype_of(CallableTypeOf[multi_param2], CallableTypeOf[multi_param1])) @@ -1049,10 +1127,12 @@ corresponding position in the supertype does not need to have a default value. from typing import Callable from ty_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf + def float_with_default(a: float = 1, /) -> None: ... def int_with_default(a: int = 1, /) -> None: ... def int_without_default(a: int, /) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default])) static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default])) @@ -1072,6 +1152,7 @@ As the parameter itself is optional, it can be omitted in the supertype: ```py def empty() -> None: ... + static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty])) static_assert(not is_subtype_of(CallableTypeOf[int_without_default], CallableTypeOf[empty])) static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default])) @@ -1083,6 +1164,7 @@ value: ```py def multi_param(a: float = 1, b: int = 2, c: str = "3", /) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[multi_param], CallableTypeOf[empty])) static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[multi_param])) ``` @@ -1095,12 +1177,14 @@ cannot be any other parameter kind. ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def positional_only(a: int, /) -> None: ... def standard(a: int) -> None: ... def keyword_only(*, a: int) -> None: ... def variadic(*a: int) -> None: ... def keyword_variadic(**a: int) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[standard])) static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[keyword_only])) static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[variadic])) @@ -1116,9 +1200,11 @@ Unlike positional-only parameters, standard parameters should have the same name ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def int_param_a(a: int) -> None: ... def int_param_b(b: int) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[int_param_a], CallableTypeOf[int_param_b])) static_assert(not is_subtype_of(CallableTypeOf[int_param_b], CallableTypeOf[int_param_a])) ``` @@ -1129,6 +1215,7 @@ Apart from the name, it behaves the same as positional-only parameters. def float_param(a: float) -> None: ... def int_param(a: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[float_param], CallableTypeOf[int_param])) static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[float_param])) ``` @@ -1140,6 +1227,7 @@ def float_with_default(a: float = 1) -> None: ... def int_with_default(a: int = 1) -> None: ... def empty() -> None: ... + static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default])) static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default])) @@ -1156,6 +1244,7 @@ Multiple standard parameters are checked in order along with their names: def multi_param1(a: float, b: int, c: str) -> None: ... def multi_param2(a: int, b: bool, c: str) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[multi_param1], CallableTypeOf[multi_param2])) static_assert(not is_subtype_of(CallableTypeOf[multi_param2], CallableTypeOf[multi_param1])) ``` @@ -1165,6 +1254,7 @@ The subtype can include as many standard parameters as long as they have the def ```py def multi_param_default(a: float = 1, b: int = 2, c: str = "s") -> None: ... + static_assert(is_subtype_of(CallableTypeOf[multi_param_default], CallableTypeOf[empty])) static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[multi_param_default])) ``` @@ -1178,22 +1268,28 @@ than a keyword-only parameter. ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def standard_a(a: int) -> None: ... def keyword_b(*, b: int) -> None: ... + # The name of the parameters are different static_assert(not is_subtype_of(CallableTypeOf[standard_a], CallableTypeOf[keyword_b])) + def standard_float(a: float) -> None: ... def keyword_int(*, a: int) -> None: ... + # Here, the name of the parameters are the same static_assert(is_subtype_of(CallableTypeOf[standard_float], CallableTypeOf[keyword_int])) + def standard_with_default(a: int = 1) -> None: ... def keyword_with_default(*, a: int = 1) -> None: ... def empty() -> None: ... + static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[keyword_with_default])) static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[empty])) ``` @@ -1204,6 +1300,7 @@ The position of the keyword-only parameters does not matter: def multi_standard(a: float, b: int, c: str) -> None: ... def multi_keyword(*, b: bool, c: str, a: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_keyword])) ``` @@ -1216,21 +1313,27 @@ than a positional-only parameter. ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def standard_a(a: int) -> None: ... def positional_b(b: int, /) -> None: ... + # The names are not important in this context static_assert(is_subtype_of(CallableTypeOf[standard_a], CallableTypeOf[positional_b])) + def standard_float(a: float) -> None: ... def positional_int(a: int, /) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[standard_float], CallableTypeOf[positional_int])) + def standard_with_default(a: int = 1) -> None: ... def positional_with_default(a: int = 1, /) -> None: ... def empty() -> None: ... + static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[positional_with_default])) static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[empty])) ``` @@ -1241,9 +1344,11 @@ The position of the positional-only parameters matter: def multi_standard(a: float, b: int, c: str) -> None: ... def multi_positional1(b: int, c: bool, a: str, /) -> None: ... + # Here, the type of the parameter `a` makes the subtype relation invalid def multi_positional2(b: int, a: float, c: str, /) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_positional1])) static_assert(not is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_positional2])) ``` @@ -1256,10 +1361,12 @@ parameter in the subtype. ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def standard(a: int) -> None: ... def variadic(*a: int) -> None: ... def keyword_variadic(**a: int) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[variadic])) static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keyword_variadic])) ``` @@ -1271,9 +1378,11 @@ The name of the variadic parameter does not need to be the same in the subtype. ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def variadic_float(*args2: float) -> None: ... def variadic_int(*args1: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[variadic_float], CallableTypeOf[variadic_int])) static_assert(not is_subtype_of(CallableTypeOf[variadic_int], CallableTypeOf[variadic_float])) ``` @@ -1283,6 +1392,7 @@ The variadic parameter does not need to be present in the supertype: ```py def empty() -> None: ... + static_assert(is_subtype_of(CallableTypeOf[variadic_int], CallableTypeOf[empty])) static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[variadic_int])) ``` @@ -1295,14 +1405,18 @@ supertype should be checked against the variadic parameter. ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def variadic(a: int, /, *args: float) -> None: ... + # Here, the parameter `b` and `c` are unmatched def positional_only(a: int, b: float, c: int, /) -> None: ... + # Here, the parameter `b` is unmatched and there's also a variadic parameter def positional_variadic(a: int, b: float, /, *args: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[positional_only])) static_assert(is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[positional_variadic])) ``` @@ -1315,16 +1429,20 @@ parameters from the supertype, not any other parameter kind. ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def variadic(*args: int) -> None: ... + # Both positional-only parameters are unmatched so uses the variadic parameter but the other # parameter `c` remains and cannot be matched. def standard(a: int, b: float, /, c: int) -> None: ... + # Similarly, for other kinds def keyword_only(a: int, /, *, b: int) -> None: ... def keyword_variadic(a: int, /, **kwargs: int) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[standard])) static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_only])) static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_variadic])) @@ -1339,6 +1457,7 @@ def variadic_keyword(*args: int, **kwargs: int) -> None: ... def standard_int(a: int) -> None: ... def standard_float(a: float) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_int])) static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_float])) ``` @@ -1350,6 +1469,7 @@ parameter, then the subtyping relation is invalid. def variadic_bool(*args: bool, **kwargs: int) -> None: ... def keyword_variadic_bool(*args: int, **kwargs: bool) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[variadic_bool], CallableTypeOf[standard_int])) static_assert(not is_subtype_of(CallableTypeOf[keyword_variadic_bool], CallableTypeOf[standard_int])) ``` @@ -1360,6 +1480,7 @@ The standard parameter can follow a variadic parameter in the subtype. def standard_variadic_int(a: int, *args: int) -> None: ... def standard_variadic_float(a: int, *args: float) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_int])) static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_float])) ``` @@ -1371,6 +1492,7 @@ same name if the keyword-variadic parameter is absent. def variadic_a(*args: int, a: int) -> None: ... def variadic_b(*args: int, b: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[variadic_a], CallableTypeOf[standard_int])) # The parameter name is different static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[standard_int])) @@ -1383,10 +1505,12 @@ For keyword-only parameters, the name should be the same: ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def keyword_int(*, a: int) -> None: ... def keyword_float(*, a: float) -> None: ... def keyword_b(*, b: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[keyword_float], CallableTypeOf[keyword_int])) static_assert(not is_subtype_of(CallableTypeOf[keyword_int], CallableTypeOf[keyword_float])) static_assert(not is_subtype_of(CallableTypeOf[keyword_int], CallableTypeOf[keyword_b])) @@ -1398,6 +1522,7 @@ But, the order of the keyword-only parameters is not required to be the same: def keyword_ab(*, a: float, b: float) -> None: ... def keyword_ba(*, b: int, a: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[keyword_ab], CallableTypeOf[keyword_ba])) static_assert(not is_subtype_of(CallableTypeOf[keyword_ba], CallableTypeOf[keyword_ab])) ``` @@ -1407,11 +1532,13 @@ static_assert(not is_subtype_of(CallableTypeOf[keyword_ba], CallableTypeOf[keywo ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def float_with_default(*, a: float = 1) -> None: ... def int_with_default(*, a: int = 1) -> None: ... def int_keyword(*, a: int) -> None: ... def empty() -> None: ... + static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default])) static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default])) @@ -1429,6 +1556,7 @@ order: # A keyword-only parameter with a default value follows the one without a default value (it's valid) def mixed(*, b: int = 1, a: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[int_keyword])) static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[mixed])) ``` @@ -1438,9 +1566,11 @@ static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[mixe ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def keywords1(*, a: int, b: int) -> None: ... def standard(b: float, a: float) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[keywords1], CallableTypeOf[standard])) static_assert(is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keywords1])) ``` @@ -1451,6 +1581,7 @@ The subtype can include additional standard parameters as long as it has the def def standard_with_default(b: float, a: float, c: float = 1) -> None: ... def standard_without_default(b: float, a: float, c: float) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[standard_without_default], CallableTypeOf[keywords1])) static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[keywords1])) ``` @@ -1461,6 +1592,7 @@ Here, we mix keyword-only parameters with standard parameters: def keywords2(*, a: int, c: int, b: int) -> None: ... def mixed(b: float, a: float, *, c: float) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[keywords2], CallableTypeOf[mixed])) static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[keywords2])) ``` @@ -1470,6 +1602,7 @@ But, we shouldn't consider any unmatched positional-only parameters: ```py def mixed_positional(b: float, /, a: float, *, c: float) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[mixed_positional], CallableTypeOf[keywords2])) ``` @@ -1478,6 +1611,7 @@ But, an unmatched variadic parameter is still valid: ```py def mixed_variadic(*args: float, a: float, b: float, c: float, **kwargs: float) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[mixed_variadic], CallableTypeOf[keywords2])) ``` @@ -1488,9 +1622,11 @@ The name of the keyword-variadic parameter does not need to be the same in the s ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def kwargs_float(**kwargs2: float) -> None: ... def kwargs_int(**kwargs1: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[kwargs_float], CallableTypeOf[kwargs_int])) static_assert(not is_subtype_of(CallableTypeOf[kwargs_int], CallableTypeOf[kwargs_float])) ``` @@ -1500,6 +1636,7 @@ A variadic parameter can be omitted in the subtype: ```py def empty() -> None: ... + static_assert(is_subtype_of(CallableTypeOf[kwargs_int], CallableTypeOf[empty])) static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[kwargs_int])) ``` @@ -1512,10 +1649,12 @@ supertype should be checked against the keyword-variadic parameter. ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def kwargs(**kwargs: float) -> None: ... def keyword_only(*, a: int, b: float, c: bool) -> None: ... def keyword_variadic(*, a: int, **kwargs: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[keyword_only])) static_assert(is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[keyword_variadic])) ``` @@ -1525,9 +1664,11 @@ This is valid only for keyword-only parameters, not any other parameter kind: ```py def mixed1(a: int, *, b: int) -> None: ... + # Same as above but with the default value def mixed2(a: int = 1, *, b: int) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[mixed1])) static_assert(not is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[mixed2])) ``` @@ -1540,9 +1681,11 @@ as long as they contain the default values for non-variadic parameters. ```py from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def empty() -> None: ... def mixed(a: int = 1, /, b: int = 2, *args: int, c: int = 3, **kwargs: int) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[empty])) static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[mixed])) ``` @@ -1553,20 +1696,25 @@ static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[mixed])) from ty_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf from typing import Callable + def f1(a: int, b: str, /, *c: float, d: int = 1, **e: float) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[f1], object)) static_assert(not is_subtype_of(object, CallableTypeOf[f1])) + def _( f3: Callable[[int, str], None], ) -> None: static_assert(is_subtype_of(TypeOf[f3], object)) static_assert(not is_subtype_of(object, TypeOf[f3])) + class C: def foo(self) -> None: ... + static_assert(is_subtype_of(TypeOf[C.foo], object)) static_assert(not is_subtype_of(object, TypeOf[C.foo])) ``` @@ -1580,9 +1728,11 @@ any arguments of any type, but otherwise is not a subtype or supertype of any ca from typing import Callable, Never from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + def bottom(*args: object, **kwargs: object) -> Never: raise Exception() + type BottomCallable = CallableTypeOf[bottom] static_assert(is_subtype_of(BottomCallable, Callable[..., Never])) @@ -1601,8 +1751,10 @@ would not pass if we didn't handle this special case. from typing import Callable, Any from ty_extensions import is_subtype_of, static_assert, CallableTypeOf + def f(*args: Any, **kwargs: Any) -> Any: ... + static_assert(not is_subtype_of(CallableTypeOf[f], Callable[[], object])) ``` @@ -1612,10 +1764,12 @@ static_assert(not is_subtype_of(CallableTypeOf[f], Callable[[], object])) from typing import Callable, Any from ty_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to + class A: def __call__(self, a: int) -> int: return a + a = A() static_assert(is_subtype_of(A, Callable[[int], int])) @@ -1624,8 +1778,10 @@ static_assert(not is_subtype_of(Callable[[int], int], A)) static_assert(not is_subtype_of(A, Callable[[Any], int])) static_assert(not is_subtype_of(A, Callable[[int], Any])) + def f(fn: Callable[[int], int]) -> None: ... + f(a) ``` @@ -1640,12 +1796,15 @@ from __future__ import annotations from typing import Callable from ty_extensions import static_assert, is_subtype_of + def call_impl(a: A, x: int) -> str: return "" + class A: __call__: Callable[[A, int], str] = call_impl + static_assert(is_subtype_of(A, Callable[[int], str])) static_assert(not is_subtype_of(A, Callable[[int], int])) reveal_type(A()(1)) # revealed: str @@ -1669,34 +1828,43 @@ from typing import Callable, Protocol, overload from typing_extensions import Self from ty_extensions import TypeOf, static_assert, is_subtype_of + class MetaWithReturn(type): def __call__(cls) -> "A": return super().__call__() + class A(metaclass=MetaWithReturn): ... + class Returns[T](Protocol): def __call__(self) -> T: ... + class ReturnsWithArgument[T1, T2](Protocol): def __call__(self, arg: T1, /) -> T2: ... + static_assert(is_subtype_of(TypeOf[A], Callable[[], A])) static_assert(is_subtype_of(TypeOf[A], Returns[A])) static_assert(not is_subtype_of(TypeOf[A], Callable[[object], A])) static_assert(not is_subtype_of(TypeOf[A], ReturnsWithArgument[object, A])) + class MetaWithDifferentReturn(type): def __call__(cls) -> int: return super().__call__() + class B(metaclass=MetaWithDifferentReturn): ... + static_assert(is_subtype_of(TypeOf[B], Callable[[], int])) static_assert(is_subtype_of(TypeOf[B], Returns[int])) static_assert(not is_subtype_of(TypeOf[B], Callable[[], B])) static_assert(not is_subtype_of(TypeOf[B], Returns[B])) + class MetaWithOverloadReturn(type): @overload def __call__(cls, x: int) -> int: ... @@ -1705,8 +1873,10 @@ class MetaWithOverloadReturn(type): def __call__(cls, x: int | None = None) -> str | int: return super().__call__() + class C(metaclass=MetaWithOverloadReturn): ... + static_assert(is_subtype_of(TypeOf[C], Callable[[int], int])) static_assert(is_subtype_of(TypeOf[C], Callable[[], str])) static_assert(is_subtype_of(TypeOf[C], ReturnsWithArgument[int, int])) @@ -1719,32 +1889,42 @@ static_assert(is_subtype_of(TypeOf[C], Returns[str])) from typing import Callable, overload, Protocol from ty_extensions import TypeOf, static_assert, is_subtype_of + class A: def __new__(cls, a: int) -> int: return a + class Returns[T](Protocol): def __call__(self) -> T: ... + class ReturnsWithArgument[T1, T2](Protocol): def __call__(self, arg: T1, /) -> T2: ... + static_assert(is_subtype_of(TypeOf[A], Callable[[int], int])) static_assert(is_subtype_of(TypeOf[A], ReturnsWithArgument[int, int])) static_assert(not is_subtype_of(TypeOf[A], Callable[[], int])) static_assert(not is_subtype_of(TypeOf[A], Returns[int])) + class B: ... + + class C(B): ... + class D: def __new__(cls) -> B: return B() + class E(D): def __new__(cls) -> C: return C() + static_assert(is_subtype_of(TypeOf[E], Callable[[], C])) static_assert(is_subtype_of(TypeOf[E], Returns[C])) static_assert(is_subtype_of(TypeOf[E], Callable[[], B])) @@ -1754,6 +1934,7 @@ static_assert(not is_subtype_of(TypeOf[D], Returns[C])) static_assert(is_subtype_of(TypeOf[D], Callable[[], B])) static_assert(is_subtype_of(TypeOf[D], Returns[B])) + class F: @overload def __new__(cls) -> int: ... @@ -1764,6 +1945,7 @@ class F: def __init__(self, y: str) -> None: ... + static_assert(is_subtype_of(TypeOf[F], Callable[[int], F])) static_assert(is_subtype_of(TypeOf[F], Callable[[], int])) static_assert(not is_subtype_of(TypeOf[F], Callable[[str], F])) @@ -1777,17 +1959,21 @@ If `__call__` and `__new__` are both present, `__call__` takes precedence. from typing import Callable, Protocol from ty_extensions import TypeOf, static_assert, is_subtype_of + class MetaWithIntReturn(type): def __call__(cls) -> int: return super().__call__() + class F(metaclass=MetaWithIntReturn): def __new__(cls) -> str: return super().__new__(cls) + class Returns[T](Protocol): def __call__(self) -> T: ... + static_assert(is_subtype_of(TypeOf[F], Callable[[], int])) static_assert(is_subtype_of(TypeOf[F], Returns[int])) static_assert(not is_subtype_of(TypeOf[F], Callable[[], str])) @@ -1800,20 +1986,25 @@ static_assert(not is_subtype_of(TypeOf[F], Returns[str])) from typing import Callable, overload, Protocol from ty_extensions import TypeOf, static_assert, is_subtype_of + class Returns[T](Protocol): def __call__(self) -> T: ... + class ReturnsWithArgument[T1, T2](Protocol): def __call__(self, arg: T1, /) -> T2: ... + class A: def __init__(self, a: int) -> None: ... + static_assert(is_subtype_of(TypeOf[A], Callable[[int], A])) static_assert(is_subtype_of(TypeOf[A], ReturnsWithArgument[int, A])) static_assert(not is_subtype_of(TypeOf[A], Callable[[], A])) static_assert(not is_subtype_of(TypeOf[A], Returns[A])) + class B: @overload def __init__(self, a: int) -> None: ... @@ -1821,14 +2012,17 @@ class B: def __init__(self) -> None: ... def __init__(self, a: int | None = None) -> None: ... + static_assert(is_subtype_of(TypeOf[B], Callable[[int], B])) static_assert(is_subtype_of(TypeOf[B], ReturnsWithArgument[int, B])) static_assert(is_subtype_of(TypeOf[B], Callable[[], B])) static_assert(is_subtype_of(TypeOf[B], Returns[B])) + class D[T]: def __init__(self, x: T) -> None: ... + static_assert(is_subtype_of(TypeOf[D[int]], Callable[[int], D[int]])) static_assert(is_subtype_of(TypeOf[D[int]], ReturnsWithArgument[int, D[int]])) static_assert(not is_subtype_of(TypeOf[D[int]], Callable[[str], D[int]])) @@ -1841,48 +2035,58 @@ static_assert(not is_subtype_of(TypeOf[D[int]], ReturnsWithArgument[str, D[int]] from typing import Callable, overload, Self, Protocol from ty_extensions import TypeOf, static_assert, is_subtype_of + class Returns[T](Protocol): def __call__(self) -> T: ... + class ReturnsWithArgument[T1, T2](Protocol): def __call__(self, arg: T1, /) -> T2: ... + class A: def __new__(cls, a: int) -> Self: return super().__new__(cls) def __init__(self, a: int) -> None: ... + static_assert(is_subtype_of(TypeOf[A], Callable[[int], A])) static_assert(is_subtype_of(TypeOf[A], ReturnsWithArgument[int, A])) static_assert(not is_subtype_of(TypeOf[A], Callable[[], A])) static_assert(not is_subtype_of(TypeOf[A], Returns[A])) + class B: def __new__(cls, a: int) -> int: return super().__new__(cls) def __init__(self, a: str) -> None: ... + static_assert(is_subtype_of(TypeOf[B], Callable[[int], int])) static_assert(is_subtype_of(TypeOf[B], ReturnsWithArgument[int, int])) static_assert(not is_subtype_of(TypeOf[B], Callable[[str], B])) static_assert(not is_subtype_of(TypeOf[B], ReturnsWithArgument[str, B])) + class C: def __new__(cls, *args, **kwargs) -> "C": return super().__new__(cls) def __init__(self, x: int) -> None: ... + # Not subtype because __new__ signature is not fully static static_assert(not is_subtype_of(TypeOf[C], Callable[[int], C])) static_assert(not is_subtype_of(TypeOf[C], ReturnsWithArgument[int, C])) static_assert(not is_subtype_of(TypeOf[C], Callable[[], C])) static_assert(not is_subtype_of(TypeOf[C], Returns[C])) + class D: ... + class E: @overload def __new__(cls) -> int: ... @@ -1893,17 +2097,20 @@ class E: def __init__(self, y: str) -> None: ... + static_assert(is_subtype_of(TypeOf[E], Callable[[int], D])) static_assert(is_subtype_of(TypeOf[E], ReturnsWithArgument[int, D])) static_assert(is_subtype_of(TypeOf[E], Callable[[], int])) static_assert(is_subtype_of(TypeOf[E], Returns[int])) + class F[T]: def __new__(cls, x: T) -> "F[T]": return super().__new__(cls) def __init__(self, x: T) -> None: ... + static_assert(is_subtype_of(TypeOf[F[int]], Callable[[int], F[int]])) static_assert(is_subtype_of(TypeOf[F[int]], ReturnsWithArgument[int, F[int]])) static_assert(not is_subtype_of(TypeOf[F[int]], Callable[[str], F[int]])) @@ -1918,22 +2125,27 @@ If `__call__`, `__new__` and `__init__` are all present, `__call__` takes preced from typing import Callable, Protocol from ty_extensions import TypeOf, static_assert, is_subtype_of + class Returns[T](Protocol): def __call__(self) -> T: ... + class ReturnsWithArgument[T1, T2](Protocol): def __call__(self, arg: T1, /) -> T2: ... + class MetaWithIntReturn(type): def __call__(cls) -> int: return super().__call__() + class F(metaclass=MetaWithIntReturn): def __new__(cls) -> str: return super().__new__(cls) def __init__(self, x: int) -> None: ... + static_assert(is_subtype_of(TypeOf[F], Callable[[], int])) static_assert(is_subtype_of(TypeOf[F], Returns[int])) static_assert(not is_subtype_of(TypeOf[F], Callable[[], str])) @@ -1948,11 +2160,14 @@ static_assert(not is_subtype_of(TypeOf[F], ReturnsWithArgument[int, F])) from typing import Callable, Protocol from ty_extensions import TypeOf, static_assert, is_subtype_of + class Returns[T](Protocol): def __call__(self) -> T: ... + class A: ... + static_assert(is_subtype_of(TypeOf[A], Callable[[], A])) static_assert(is_subtype_of(TypeOf[A], Returns[A])) ``` @@ -1965,13 +2180,16 @@ static_assert(is_subtype_of(TypeOf[A], Returns[A])) from typing import Callable from ty_extensions import TypeOf, static_assert, is_subtype_of + class A: def __init__(self, x: int) -> None: ... + class B: def __new__(cls, x: str) -> "B": return super().__new__(cls) + static_assert(is_subtype_of(type[A], Callable[[int], A])) static_assert(not is_subtype_of(type[A], Callable[[str], A])) @@ -1988,10 +2206,12 @@ from typing import Callable from ty_extensions import TypeOf, static_assert, is_subtype_of from dataclasses import dataclass + @dataclass class A: x: "A" | None + static_assert(is_subtype_of(type[A], Callable[[A], A])) static_assert(is_subtype_of(type[A], Callable[[None], A])) static_assert(is_subtype_of(type[A], Callable[[A | None], A])) @@ -2004,6 +2224,7 @@ static_assert(not is_subtype_of(type[A], Callable[[int], A])) from typing import Callable from ty_extensions import TypeOf, static_assert, is_subtype_of + class A: def f(self, a: int) -> int: return a @@ -2012,6 +2233,7 @@ class A: def g(cls, a: int) -> int: return a + a = A() static_assert(is_subtype_of(TypeOf[a.f], Callable[[int], int])) @@ -2050,10 +2272,12 @@ def overloaded(x: B) -> None: ... from ty_extensions import CallableTypeOf, is_subtype_of, static_assert from overloaded import A, B, C, overloaded + def accepts_a(x: A) -> None: ... def accepts_b(x: B) -> None: ... def accepts_c(x: C) -> None: ... + static_assert(is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_a])) static_assert(is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_b])) static_assert(not is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_c])) @@ -2085,15 +2309,19 @@ def overloaded(a: Grandparent) -> None: ... from ty_extensions import CallableTypeOf, is_subtype_of, static_assert from overloaded import Grandparent, Parent, Child, overloaded + # This is a subtype of only the first overload def child(a: Child) -> None: ... + # This is a subtype of the first and second overload def parent(a: Parent) -> None: ... + # This is the only function that's a subtype of all overloads def grandparent(a: Grandparent) -> None: ... + static_assert(not is_subtype_of(CallableTypeOf[child], CallableTypeOf[overloaded])) static_assert(not is_subtype_of(CallableTypeOf[parent], CallableTypeOf[overloaded])) static_assert(is_subtype_of(CallableTypeOf[grandparent], CallableTypeOf[overloaded])) @@ -2217,9 +2445,11 @@ the generic callable.) from typing import Callable from ty_extensions import CallableTypeOf, TypeOf, is_subtype_of, static_assert + def identity[T](t: T) -> T: return t + # TODO: Confusingly, these are not the same results as the corresponding checks in # is_assignable_to.md, even though all of these types are fully static. We have some heuristics that # currently conflict with each other, that we are in the process of removing with the constraint set diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index 056c8ff900..265ab8f011 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -31,6 +31,7 @@ The dynamic type at the top-level is replaced with `object`. from typing import Any, Callable from ty_extensions import Unknown, Top + def _(top_any: Top[Any], top_unknown: Top[Unknown]): reveal_type(top_any) # revealed: object reveal_type(top_unknown) # revealed: object @@ -58,6 +59,7 @@ The dynamic type at the top-level is replaced with `Never`. from typing import Any, Callable from ty_extensions import Unknown, Bottom + def _(bottom_any: Bottom[Any], bottom_unknown: Bottom[Unknown]): reveal_type(bottom_any) # revealed: Never reveal_type(bottom_unknown) # revealed: Never @@ -92,10 +94,12 @@ from typing import Any, Literal from ty_extensions import TypeOf, Bottom, Top, is_equivalent_to, static_assert from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + static_assert(is_equivalent_to(Top[int], int)) static_assert(is_equivalent_to(Bottom[int], int)) @@ -121,9 +125,11 @@ signature might have `Any` in it. (TODO: this is probably not right.) ```py def function(x: Any) -> None: ... + class A: def method(self, x: Any) -> None: ... + def _( top_func: Top[TypeOf[function]], bottom_func: Bottom[TypeOf[function]], @@ -153,6 +159,7 @@ from ty_extensions import TypeOf, Unknown, Bottom, Top type C1 = Callable[[Any, Unknown], Any] + def _(top: Top[C1], bottom: Bottom[C1]) -> None: reveal_type(top) # revealed: (Never, Never, /) -> object reveal_type(bottom) # revealed: (object, object, /) -> Never @@ -163,6 +170,7 @@ The parameter types in a callable inherits the contravariant position. ```py type C2 = Callable[[int, tuple[int | Any]], tuple[Any]] + def _(top: Top[C2], bottom: Bottom[C2]) -> None: reveal_type(top) # revealed: (int, tuple[int], /) -> tuple[object] reveal_type(bottom) # revealed: (int, tuple[object], /) -> Never @@ -175,6 +183,7 @@ flipped to covariant, invariant remains invariant. ```py type C3 = Callable[[Any, Callable[[Unknown], Any]], Callable[[Any, int], Any]] + def _(top: Top[C3], bottom: Bottom[C3]) -> None: # revealed: (Never, (object, /) -> Never, /) -> (Never, int, /) -> object reveal_type(top) @@ -201,6 +210,7 @@ from ty_extensions import Bottom, Top, is_equivalent_to, is_subtype_of, static_a type GradualCallable = Callable[..., Any] + def _(top: Top[GradualCallable], bottom: Bottom[GradualCallable]) -> None: # The top materialization keeps the gradual parameters wrapped reveal_type(top) # revealed: Top[(...) -> object] @@ -208,11 +218,13 @@ def _(top: Top[GradualCallable], bottom: Bottom[GradualCallable]) -> None: # The bottom materialization simplifies to the fully static bottom callable reveal_type(bottom) # revealed: (*args: object, **kwargs: object) -> Never + # The bottom materialization of a gradual callable is a subtype of (and supertype of) # a protocol with `__call__(self, *args: object, **kwargs: object) -> Never` class EquivalentToBottom(Protocol): def __call__(self, *args: object, **kwargs: object) -> Never: ... + static_assert(is_subtype_of(EquivalentToBottom, Bottom[Callable[..., Never]])) static_assert(is_subtype_of(Bottom[Callable[..., Never]], EquivalentToBottom)) @@ -230,6 +242,7 @@ Gradual parameters can be top- and bottom-materialized even if the return type i ```py type GradualParams = Callable[..., int] + def _(top: Top[GradualParams], bottom: Bottom[GradualParams]) -> None: reveal_type(top) # revealed: Top[(...) -> int] @@ -242,6 +255,7 @@ Materializing an overloaded callable materializes each overload separately. from typing import overload from ty_extensions import CallableTypeOf + @overload def f(x: int) -> Any: ... @overload @@ -249,6 +263,7 @@ def f(*args: Any, **kwargs: Any) -> str: ... def f(*args: object, **kwargs: object) -> object: pass + def _(top: Top[CallableTypeOf[f]], bottom: Bottom[CallableTypeOf[f]]): reveal_type(top) # revealed: Overload[(x: int) -> object, Top[(...) -> str]] reveal_type(bottom) # revealed: Overload[(x: int) -> Never, (*args: object, **kwargs: object) -> str] @@ -260,6 +275,7 @@ The top callable can be represented in a `ParamSpec`: def takes_paramspec[**P](f: Callable[P, None]) -> Callable[P, None]: return f + def _(top: Top[Callable[..., None]]): revealed = takes_paramspec(top) reveal_type(revealed) # revealed: Top[(...) -> None] @@ -270,10 +286,12 @@ The top callable is not a subtype of `(*object, **object) -> object`: ```py type TopCallable = Top[Callable[..., Any]] + @staticmethod def takes_objects(*args: object, **kwargs: object) -> object: pass + static_assert(not is_subtype_of(TopCallable, CallableTypeOf[takes_objects])) ``` @@ -309,6 +327,7 @@ from ty_extensions import TypeOf type C = Callable[[tuple[Any, int], tuple[str, Unknown]], None] + def _(top: Top[C], bottom: Bottom[C]) -> None: reveal_type(top) # revealed: (Never, Never, /) -> None reveal_type(bottom) # revealed: (tuple[object, int], tuple[str, object], /) -> None @@ -321,6 +340,7 @@ type LTAnyInt = list[tuple[Any, int]] type LTStrUnknown = list[tuple[str, Unknown]] type LTAnyIntUnknown = list[tuple[Any, int, Unknown]] + def _( top_ai: Top[LTAnyInt], bottom_ai: Bottom[LTAnyInt], @@ -369,6 +389,7 @@ inherit the contravariant position. from typing import Callable from ty_extensions import TypeOf + def _(callable: Callable[[Any | int, str | Unknown], None]) -> None: static_assert(is_equivalent_to(Top[TypeOf[callable]], Callable[[int, str], None])) static_assert(is_equivalent_to(Bottom[TypeOf[callable]], Callable[[object, object], None])) @@ -411,10 +432,13 @@ static_assert(is_equivalent_to(Bottom[Intersection[Any, int]], Never)) static_assert(is_equivalent_to(Top[Intersection[Any | int, tuple[str, Unknown]]], tuple[str, object])) static_assert(is_equivalent_to(Bottom[Intersection[Any | int, tuple[str, Unknown]]], Never)) + class Foo: ... + static_assert(is_equivalent_to(Bottom[Intersection[Any | Foo, tuple[str]]], Intersection[Foo, tuple[str]])) + def _( top: Top[Intersection[list[Any], list[int]]], bottom: Bottom[Intersection[list[Any], list[int]]], @@ -465,6 +489,7 @@ static_assert(is_equivalent_to(Bottom[type[Unknown]], Never)) static_assert(is_equivalent_to(Top[type[int | Any]], type)) static_assert(is_equivalent_to(Bottom[type[int | Any]], type[int])) + # Here, `T` has an upper bound of `type` def _(top: Top[list[type[Any]]], bottom: Bottom[list[type[Any]]]): reveal_type(top) # revealed: Top[list[type[Any]]] @@ -482,12 +507,14 @@ python-version = "3.12" from typing import Any, Never, TypeVar from ty_extensions import Unknown, Bottom, Top, static_assert, is_subtype_of + def bounded_by_gradual[T: Any](t: T) -> None: # Top materialization of `T: Any` is `T: object` # Bottom materialization of `T: Any` is `T: Never` static_assert(is_subtype_of(Bottom[T], Never)) + def constrained_by_gradual[T: (int, Any)](t: T) -> None: # Top materialization of `T: (int, Any)` is `T: (int, object)` @@ -518,19 +545,24 @@ T = TypeVar("T") T_co = TypeVar("T_co", covariant=True) T_contra = TypeVar("T_contra", contravariant=True) + class GenericInvariant(Generic[T]): pass + class GenericCovariant(Generic[T_co]): pass + class GenericContravariant(Generic[T_contra]): pass + def _(top: Top[GenericInvariant[Any]], bottom: Bottom[GenericInvariant[Any]]): reveal_type(top) # revealed: Top[GenericInvariant[Any]] reveal_type(bottom) # revealed: Bottom[GenericInvariant[Any]] + static_assert(is_equivalent_to(Top[GenericCovariant[Any]], GenericCovariant[object])) static_assert(is_equivalent_to(Bottom[GenericCovariant[Any]], GenericCovariant[Never])) @@ -548,14 +580,17 @@ type InvariantCallable = Callable[[GenericInvariant[Any]], None] type CovariantCallable = Callable[[GenericCovariant[Any]], None] type ContravariantCallable = Callable[[GenericContravariant[Any]], None] + def invariant(top: Top[InvariantCallable], bottom: Bottom[InvariantCallable]) -> None: reveal_type(top) # revealed: (Bottom[GenericInvariant[Any]], /) -> None reveal_type(bottom) # revealed: (Top[GenericInvariant[Any]], /) -> None + def covariant(top: Top[CovariantCallable], bottom: Bottom[CovariantCallable]) -> None: reveal_type(top) # revealed: (GenericCovariant[Never], /) -> None reveal_type(bottom) # revealed: (GenericCovariant[object], /) -> None + def contravariant(top: Top[ContravariantCallable], bottom: Bottom[ContravariantCallable]) -> None: reveal_type(top) # revealed: (GenericContravariant[object], /) -> None reveal_type(bottom) # revealed: (GenericContravariant[Never], /) -> None @@ -570,6 +605,7 @@ It is invalid to use them without a type argument. ```py from ty_extensions import Bottom, Top + def _( just_top: Top, # error: [invalid-type-form] just_bottom: Bottom, # error: [invalid-type-form] @@ -772,22 +808,28 @@ number of other covariant ABCs, but we'll use a synthetic example. from typing import Generic, TypeVar, Any from ty_extensions import static_assert, is_assignable_to, is_equivalent_to, Top + class A: pass + class B(A): pass + T_co = TypeVar("T_co", covariant=True) T = TypeVar("T") + class CovariantBase(Generic[T_co]): def get(self) -> T_co: raise NotImplementedError + class InvariantChild(CovariantBase[T]): def push(self, obj: T) -> None: ... + static_assert(is_assignable_to(InvariantChild[A], CovariantBase[A])) static_assert(is_assignable_to(InvariantChild[B], CovariantBase[A])) static_assert(not is_assignable_to(InvariantChild[A], CovariantBase[B])) @@ -811,6 +853,7 @@ python-version = "3.12" from ty_extensions import Top, Bottom from typing import Any + class Invariant[T]: def get(self) -> T: raise NotImplementedError @@ -819,6 +862,7 @@ class Invariant[T]: attr: T + def capybara(top: Top[Invariant[Any]], bottom: Bottom[Invariant[Any]]) -> None: reveal_type(top.get) # revealed: bound method Top[Invariant[Any]].get() -> object reveal_type(top.push) # revealed: bound method Top[Invariant[Any]].push(obj: Never) -> None diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md b/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md index 865cfa8395..8d10f85e8e 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md @@ -30,6 +30,7 @@ def f[T](t: T) -> T: # satisfied for _all_ valid specializations of T. return t + # When invoking the function, T is inferable — we attempt to infer a specialization that is valid # for the particular arguments that are passed to the function. Assignability checks (in particular, # that the argument type is assignable to the parameter type) only need to succeed for _at least @@ -52,13 +53,20 @@ type. from typing import final, Never from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + @final class Unrelated: ... + def unbounded[T](): static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) static_assert(ConstraintSet.always().satisfied_by_all_typevars()) @@ -98,13 +106,20 @@ for every type that satisfies the upper bound. from typing import final, Never from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + @final class Unrelated: ... + def bounded[T: Base](): static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) static_assert(ConstraintSet.always().satisfied_by_all_typevars()) @@ -152,6 +167,7 @@ the constraint set. ```py from typing import Any + def bounded_by_gradual[T: Any](): static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) static_assert(ConstraintSet.always().satisfied_by_all_typevars()) @@ -243,13 +259,20 @@ constraint set to be satisfied by all of those constraints. from typing import final, Never from ty_extensions import ConstraintSet, static_assert + class Super: ... + + class Base(Super): ... + + class Sub(Base): ... + @final class Unrelated: ... + def constrained[T: (Base, Unrelated)](): static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) static_assert(ConstraintSet.always().satisfied_by_all_typevars()) @@ -321,6 +344,7 @@ satisfy the constraint set. ```py from typing import Any + def constrained_by_gradual[T: (Base, Any)](): static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) static_assert(ConstraintSet.always().satisfied_by_all_typevars()) @@ -349,6 +373,7 @@ def constrained_by_gradual[T: (Base, Any)](): # specializations, both of which satisfy (T ≤ Base). static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + def constrained_by_two_gradual[T: (Any, Any)](): static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) static_assert(ConstraintSet.always().satisfied_by_all_typevars()) @@ -429,6 +454,7 @@ def constrained_by_gradual[T: (list[Base], list[Any])](): # (T ≤ list[Unrelated] ∧ T ≠ Never). static_assert(not constraints.satisfied_by_all_typevars()) + def constrained_by_two_gradual[T: (list[Any], list[Any])](): static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) static_assert(ConstraintSet.always().satisfied_by_all_typevars()) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/str_repr.md b/crates/ty_python_semantic/resources/mdtest/type_properties/str_repr.md index e267601929..1f8c58cd02 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/str_repr.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/str_repr.md @@ -4,10 +4,12 @@ from typing_extensions import Literal, LiteralString from enum import Enum + class Answer(Enum): NO = 0 YES = 1 + def _( a: Literal[1], b: Literal[True], diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md index fc58c77482..331c43a2ad 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -6,6 +6,7 @@ from typing_extensions import Literal, LiteralString from ty_extensions import AlwaysFalsy, AlwaysTruthy + def _( a: Literal[1], b: Literal[-1], @@ -21,6 +22,7 @@ def _( reveal_type(bool(e)) # revealed: Literal[True] reveal_type(bool(f)) # revealed: Literal[True] + def _( a: tuple[()], b: Literal[0], @@ -36,6 +38,7 @@ def _( reveal_type(bool(e)) # revealed: Literal[False] reveal_type(bool(f)) # revealed: Literal[False] + def _( a: str, b: Literal[1, 0], @@ -58,6 +61,7 @@ Checks that we don't get into a cycle if someone sets their `__bool__` method to class BoolIsBool: __bool__ = bool + reveal_type(bool(BoolIsBool())) # revealed: bool ``` @@ -67,12 +71,14 @@ reveal_type(bool(BoolIsBool())) # revealed: bool def flag() -> bool: return True + class Boom: if flag(): __bool__ = bool else: __bool__ = int + reveal_type(bool(Boom())) # revealed: bool ``` @@ -81,14 +87,18 @@ reveal_type(bool(Boom())) # revealed: bool ```py from typing import Literal + def flag() -> bool: return True + class PossiblyUnboundTrue: if flag(): + def __bool__(self) -> Literal[True]: return True + reveal_type(bool(PossiblyUnboundTrue())) # revealed: bool ``` @@ -126,6 +136,7 @@ static_assert(is_subtype_of(types.WrapperDescriptorType, AlwaysTruthy)) ```py from typing import Callable + def f(x: Callable, y: Callable[[int], str]): reveal_type(bool(x)) # revealed: bool reveal_type(bool(y)) # revealed: bool @@ -136,9 +147,11 @@ But certain callable single-valued types are known to be always truthy: ```py from types import FunctionType + class A: def method(self): ... + reveal_type(bool(A().method)) # revealed: Literal[True] reveal_type(bool(f.__get__)) # revealed: Literal[True] reveal_type(bool(FunctionType.__get__)) # revealed: Literal[True] @@ -150,10 +163,12 @@ reveal_type(bool(FunctionType.__get__)) # revealed: Literal[True] from enum import Enum from typing import Literal + class NormalEnum(Enum): NO = 0 YES = 1 + class FalsyEnum(Enum): NO = 0 YES = 1 @@ -161,6 +176,7 @@ class FalsyEnum(Enum): def __bool__(self) -> Literal[False]: return False + class AmbiguousEnum(Enum): NO = 0 YES = 1 @@ -168,14 +184,17 @@ class AmbiguousEnum(Enum): def __bool__(self) -> bool: return self is AmbiguousEnum.YES + class AmbiguousBase(Enum): def __bool__(self) -> bool: return True + class AmbiguousEnum2(AmbiguousBase): NO = 0 YES = 1 + class CustomLenEnum(Enum): NO = 0 YES = 1 @@ -183,6 +202,7 @@ class CustomLenEnum(Enum): def __len__(self): return 0 + reveal_type(bool(NormalEnum.NO)) # revealed: Literal[True] reveal_type(bool(NormalEnum.YES)) # revealed: Literal[True] @@ -216,40 +236,50 @@ items, and `closed=True` is used. ```py from typing_extensions import TypedDict, Literal, NotRequired + class Normal(TypedDict): a: str b: int + def _(n: Normal) -> None: # Could be `Literal[True]` reveal_type(bool(n)) # revealed: bool + class OnlyFalsyItems(TypedDict): wrong: Literal[False] + def _(n: OnlyFalsyItems) -> None: # Could be `Literal[True]` (it does not matter if all items are falsy) reveal_type(bool(n)) # revealed: bool + class Empty(TypedDict): pass + def _(e: Empty) -> None: # This should be `bool`. `Literal[False]` would be wrong, as `Empty` can be subclassed. reveal_type(bool(e)) # revealed: bool + class AllKeysOptional(TypedDict, total=False): a: str b: int + def _(a: AllKeysOptional) -> None: # This should be `bool`. `Literal[True]` would be wrong as `{}` is a valid value. reveal_type(bool(a)) # revealed: bool + class AllKeysNotRequired(TypedDict): a: NotRequired[str] b: NotRequired[int] + def _(a: AllKeysNotRequired) -> None: # This should be `bool`. `Literal[True]` would be wrong as `{}` is a valid value. reveal_type(bool(a)) # revealed: bool diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md b/crates/ty_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md index 5536b84c98..334edf0c56 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md @@ -20,6 +20,7 @@ static_assert(is_equivalent_to(Never, tuple[int, Never, str])) static_assert(is_equivalent_to(Never, tuple[int, tuple[str, Never]])) static_assert(is_equivalent_to(Never, tuple[tuple[str, Never], int])) + def _(x: tuple[Never], y: tuple[int, Never], z: tuple[Never, int]): reveal_type(x) # revealed: Never reveal_type(y) # revealed: Never diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md index 5578332677..745425b54a 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md @@ -12,6 +12,7 @@ For more details on the semantics of pure class variables, see [this test](../at import typing from typing import ClassVar, Annotated + class C: a: ClassVar[int] = 1 b: Annotated[ClassVar[int], "the annotation for b"] = 1 @@ -20,6 +21,7 @@ class C: e: "ClassVar[int]" = 1 f: typing.ClassVar = 1 + reveal_type(C.a) # revealed: int reveal_type(C.b) # revealed: int reveal_type(C.c) # revealed: int @@ -74,15 +76,18 @@ intersecting them. This means that we consider `a` to be a `ClassVar` here: ```py from typing import ClassVar + def flag() -> bool: return True + class C: if flag(): a: ClassVar[int] = 1 else: a: str + reveal_type(C.a) # revealed: int | str c = C() @@ -96,6 +101,7 @@ c.a = 2 ```py from typing import ClassVar + class C: # error: [invalid-type-form] "Type qualifier `typing.ClassVar` expected exactly 1 argument, got 2" x: ClassVar[int, str] = 1 @@ -110,10 +116,12 @@ and emit a proper error rather than crashing (see ```py from typing import ClassVar + class C: # error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?" x: ClassVar[(),] + # error: [invalid-attribute-access] "Cannot assign to ClassVar `x` from an instance of type `C`" C().x = 42 reveal_type(C.x) # revealed: Unknown @@ -125,10 +133,12 @@ This also applies when the trailing comma is inside the brackets (see ```py from typing import ClassVar + class D: # A trailing comma here doesn't change the meaning; it's still one argument. a: ClassVar[int,] = 1 + reveal_type(D.a) # revealed: int ``` @@ -137,6 +147,7 @@ reveal_type(D.a) # revealed: int ```py from typing import ClassVar + class C: # error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)" x: ClassVar | int @@ -159,6 +170,7 @@ from ty_extensions import reveal_mro # error: [invalid-type-form] "`ClassVar` annotations are only allowed in class-body scopes" x: ClassVar[int] = 1 + class C: def __init__(self) -> None: # error: [invalid-type-form] "`ClassVar` annotations are not allowed for non-name targets" @@ -167,25 +179,31 @@ class C: # error: [invalid-type-form] "`ClassVar` annotations are only allowed in class-body scopes" y: ClassVar[int] = 1 + # error: [invalid-type-form] "`ClassVar` is not allowed in function parameter annotations" def f(x: ClassVar[int]) -> None: pass + # error: [invalid-type-form] "`ClassVar` is not allowed in function parameter annotations" def f[T](x: ClassVar[T]) -> T: return x + # error: [invalid-type-form] "`ClassVar` is not allowed in function return type annotations" def f() -> ClassVar[int]: return 1 + # error: [invalid-type-form] "`ClassVar` is not allowed in function return type annotations" def f[T](x: T) -> ClassVar[T]: return x + # TODO: this should be an error class Foo(ClassVar[tuple[int]]): ... + # TODO: Show `Unknown` instead of `@Todo` type in the MRO; or ignore `ClassVar` and show the MRO as if `ClassVar` was not there # revealed: (, @Todo(Inference of subscript on special form), ) reveal_mro(Foo) diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md index af468f6132..6f98e23578 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -28,6 +28,7 @@ reveal_type(FINAL_C) # revealed: Literal[1] reveal_type(FINAL_D) # revealed: Literal[1] reveal_type(FINAL_D) # revealed: Literal[1] + def nonlocal_uses(): reveal_type(FINAL_A) # revealed: int reveal_type(FINAL_B) # revealed: int @@ -63,6 +64,7 @@ FINAL_A: Final = 1 reveal_type(FINAL_A) # revealed: Literal[1] + def nonlocal_uses(): reveal_type(FINAL_A) # revealed: Literal[1] ``` @@ -80,6 +82,7 @@ reveal_type(FINAL_A) # revealed: Literal[1] ```py from typing import Final + class C: FINAL_A: Final[int] = 1 FINAL_B: Final = 1 @@ -90,6 +93,7 @@ class C: self.FINAL_E: Final self.FINAL_E = 1 + reveal_type(C.FINAL_A) # revealed: int reveal_type(C.FINAL_B) # revealed: Literal[1] @@ -127,6 +131,7 @@ FINAL_D = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINA FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed" FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed" + def global_use(): global FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E, FINAL_F FINAL_A = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_A` is not allowed" @@ -136,6 +141,7 @@ def global_use(): FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed" FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed" + def local_use(): # These are not errors, because they refer to local variables FINAL_A = 2 @@ -145,8 +151,10 @@ def local_use(): FINAL_E = 2 FINAL_F = 2 + def nonlocal_use(): X: Final[int] = 1 + def inner(): nonlocal X X = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `X` is not allowed: Reassignment of `Final` symbol" @@ -172,10 +180,12 @@ Assignments to attributes qualified with `Final` are also not allowed: ```py from typing import Final + class Meta(type): META_FINAL_A: Final[int] = 1 META_FINAL_B: Final = 1 + class C(metaclass=Meta): CLASS_FINAL_A: Final[int] = 1 CLASS_FINAL_B: Final = 1 @@ -186,6 +196,7 @@ class C(metaclass=Meta): self.INSTANCE_FINAL_C: Final[int] self.INSTANCE_FINAL_C = 1 + # error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type ``" C.META_FINAL_A = 2 # error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_B` on type ``" @@ -217,9 +228,11 @@ object, but that object itself may still be mutable: ```py from typing import Final + class C: x: int = 1 + FINAL_C_INSTANCE: Final[C] = C() FINAL_C_INSTANCE.x = 2 @@ -234,11 +247,13 @@ When a symbol is qualified with `Final` in a class, it cannot be overridden in s ```py from typing import Final + class Base: FINAL_A: Final[int] = 1 FINAL_B: Final[int] = 1 FINAL_C: Final = 1 + class Derived(Base): # TODO: This should be an error FINAL_A = 2 @@ -271,6 +286,7 @@ LEGAL_C = 1 LEGAL_D: Final LEGAL_D = 1 + class C: LEGAL_E: ClassVar[Final[int]] = 1 LEGAL_F: Final[ClassVar[int]] = 1 @@ -281,24 +297,30 @@ class C: self.LEGAL_I: Final[int] self.LEGAL_I = 1 + # error: [invalid-type-form] "`Final` is not allowed in function parameter annotations" def f(ILLEGAL: Final[int]) -> None: pass + # error: [invalid-type-form] "`Final` is not allowed in function parameter annotations" def f[T](ILLEGAL: Final[T]) -> T: return ILLEGAL + # error: [invalid-type-form] "`Final` is not allowed in function return type annotations" def f() -> Final[None]: ... + # error: [invalid-type-form] "`Final` is not allowed in function return type annotations" def f[T](x: T) -> Final[T]: return x + # TODO: This should be an error class Foo(Final[tuple[int]]): ... + # TODO: Show `Unknown` instead of `@Todo` type in the MRO; or ignore `Final` and show the MRO as if `Final` was not there # revealed: (, @Todo(Inference of subscript on special form), ) reveal_mro(Foo) @@ -312,6 +334,7 @@ attribute must be assigned only once, when the instance is created. ```py from typing import Final + class C: def some_method(self): # TODO: This should be an error @@ -335,6 +358,7 @@ for _ in range(10): ```py from typing import Final + class C: # error: [invalid-type-form] "Type qualifier `typing.Final` expected exactly 1 argument, got 2" x: Final[int, str] = 1 @@ -364,6 +388,7 @@ from typing import Final # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)" x: list[Final[int]] = [] # Error! + class C: # error: [invalid-type-form] x: Final | int @@ -397,6 +422,7 @@ NO_ASSIGNMENT_A: Final # TODO: This should be an error NO_ASSIGNMENT_B: Final[int] + class C: # TODO: This should be an error NO_ASSIGNMENT_A: Final @@ -422,6 +448,7 @@ python-version = "3.11" ```py from typing import Final, Self + class ClassA: ID4: Final[int] # OK because initialized in __init__ @@ -432,12 +459,14 @@ class ClassA: # error: [invalid-assignment] "Cannot assign to final attribute `ID4` on type `Self@other_method`" self.ID4 = 2 # Should still error outside __init__ + class ClassB: ID5: Final[int] def __init__(self): # Without Self annotation self.ID5 = 1 # Should also be OK + reveal_type(ClassA().ID4) # revealed: int reveal_type(ClassB().ID5) # revealed: int ``` @@ -451,6 +480,7 @@ assignment in `__init__` is not allowed if the attribute already has a value at ```py from typing import Final + # Case 1: Declared in class, assigned once in __init__ - ALLOWED class DeclaredAssignedInInit: attr1: Final[int] @@ -458,10 +488,12 @@ class DeclaredAssignedInInit: def __init__(self): self.attr1 = 1 # OK: First and only assignment + # Case 2: Declared and assigned in class body - ALLOWED (no __init__ assignment) class DeclaredAndAssignedInClass: attr2: Final[int] = 10 + # Case 3: Reassignment when already assigned in class body class ReassignmentFromClass: attr3: Final[int] = 10 @@ -470,6 +502,7 @@ class ReassignmentFromClass: # error: [invalid-assignment] self.attr3 = 20 # Error: already assigned in class body + # Case 4: Multiple assignments within __init__ itself # Per conformance suite and PEP 591, all assignments in __init__ are allowed class MultipleAssignmentsInInit: @@ -479,6 +512,7 @@ class MultipleAssignmentsInInit: self.attr4 = 1 # OK: Assignment in __init__ self.attr4 = 2 # OK: Multiple assignments in __init__ are allowed + class ConditionalAssignment: X: Final[int] @@ -488,11 +522,13 @@ class ConditionalAssignment: else: self.X = 56 # OK: Multiple assignments in __init__ are allowed + # Case 5: Declaration and assignment in __init__ - ALLOWED class DeclareAndAssignInInit: def __init__(self): self.attr5: Final[int] = 1 # OK: Declare and assign in __init__ + # Case 6: Assignment outside __init__ should still fail class AssignmentOutsideInit: attr6: Final[int] @@ -510,24 +546,29 @@ parameter (`self`). ```py from typing import Final + class C: x: Final[int] = 100 + # Assignment from standalone function (even named __init__) def _(c: C): # error: [invalid-assignment] "Cannot assign to final attribute `x`" c.x = 1 # Error: Not in C.__init__ + def __init__(c: C): # error: [invalid-assignment] "Cannot assign to final attribute `x`" c.x = 1 # Error: Not a method + # Assignment from another class's __init__ class A: def __init__(self, c: C): # error: [invalid-assignment] "Cannot assign to final attribute `x`" c.x = 1 # Error: Not C's __init__ + # Assignment to non-self parameter in __init__ class D: y: Final[int] diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md index 77478861b0..d2594b0ecf 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md @@ -14,8 +14,10 @@ Consider the following dataclass example where the `db` attribute is annotated w ```py from dataclasses import InitVar, dataclass + class Database: ... + @dataclass(order=True) class Person: db: InitVar[Database] @@ -60,6 +62,7 @@ and on instances: ```py from dataclasses import InitVar, dataclass + @dataclass class Person: name: str @@ -67,6 +70,7 @@ class Person: metadata: InitVar[str] = "default" + reveal_type(Person.__init__) # revealed: (self: Person, name: str, age: int, metadata: str = "default") -> None alice = Person("Alice", 30) @@ -85,6 +89,7 @@ case, we also allow the attribute to be accessed: ```py from dataclasses import InitVar, dataclass + @dataclass class Person: name: str @@ -93,6 +98,7 @@ class Person: def __post_init__(self, metadata: str) -> None: self.metadata = f"Person with name {self.name}" + alice = Person("Alice", "metadata that will be overwritten") reveal_type(alice.metadata) # revealed: str @@ -107,6 +113,7 @@ reveal_type(alice.metadata) # revealed: str ```py from dataclasses import InitVar, dataclass + @dataclass class Wrong: x: InitVar[int, str] # error: [invalid-type-form] "Type qualifier `InitVar` expected exactly 1 argument, got 2" @@ -119,11 +126,13 @@ and emit a proper error rather than crashing (see ```py from dataclasses import InitVar, dataclass + @dataclass class AlsoWrong: # error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?" x: InitVar[(),] + # revealed: (self: AlsoWrong, x: Unknown) -> None reveal_type(AlsoWrong.__init__) @@ -149,16 +158,20 @@ from dataclasses import InitVar, dataclass # error: [invalid-type-form] "`InitVar` annotations are only allowed in class-body scopes" x: InitVar[int] = 1 + def f(x: InitVar[int]) -> None: # error: [invalid-type-form] "`InitVar` is not allowed in function parameter annotations" pass + def g() -> InitVar[int]: # error: [invalid-type-form] "`InitVar` is not allowed in function return type annotations" return 1 + class C: # TODO: this would ideally be an error x: InitVar[int] + @dataclass class D: def __init__(self) -> None: diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 17bf0533a1..2d6a31de39 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -15,6 +15,7 @@ Here, we define a `TypedDict` using the class-based syntax: ```py from typing import TypedDict + class Person(TypedDict): name: str age: int | None @@ -60,12 +61,15 @@ from typing import Literal, Final NAME = "name" AGE = "age" + def non_literal() -> str: return "name" + def name_or_age() -> Literal["name", "age"]: return "name" + carol: Person = {NAME: "Carol", AGE: 20} reveal_type(carol[NAME]) # revealed: str @@ -76,18 +80,22 @@ reveal_type(carol[name_or_age()]) # revealed: str | int | None FINAL_NAME: Final = "name" FINAL_AGE: Final = "age" + def _(): carol: Person = {FINAL_NAME: "Carol", FINAL_AGE: 20} + CAPITALIZED_NAME = "Name" # error: [invalid-key] "Unknown key "Name" for TypedDict `Person` - did you mean "name"?" # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor" dave: Person = {CAPITALIZED_NAME: "Dave", "age": 20} + def age() -> Literal["age"] | None: return "age" + eve: Person = {"na" + "me": "Eve", age() or "age": 20} ``` @@ -129,11 +137,14 @@ class Plot(TypedDict): y: list[int | None] x: list[int | None] | None + plot1: Plot = {"y": [1, 2, 3], "x": None} + def homogeneous_list[T](*args: T) -> list[T]: return list(args) + reveal_type(homogeneous_list(1, 2, 3)) # revealed: list[int] plot2: Plot = {"y": homogeneous_list(1, 2, 3), "x": None} reveal_type(plot2["y"]) # revealed: list[int | None] @@ -148,9 +159,11 @@ X = "x" plot4: Plot = {Y: [1, 2, 3], X: None} plot5: Plot = {Y: homogeneous_list(1, 2, 3), X: None} + class Items(TypedDict): items: list[int | str] + items1: Items = {"items": homogeneous_list(1, 2, 3)} ITEMS = "items" items2: Items = {ITEMS: homogeneous_list(1, 2, 3)} @@ -183,10 +196,12 @@ Nested `TypedDict` fields are also supported. ```py from typing import TypedDict + class Inner(TypedDict): name: str age: int | None + class Person(TypedDict): inner: Inner ``` @@ -209,15 +224,19 @@ alice: Person = {"inner": {"name": "Alice", "age": 30, "extra": 1}} ```py from typing import TypedDict + class Person(TypedDict): name: str age: int | None + class House: owner: Person + house = House() + def accepts_person(p: Person) -> None: pass ``` @@ -323,15 +342,19 @@ literal: from typing import TypedDict from typing_extensions import NotRequired + class Foo(TypedDict): foo: int + x1: Foo | None = {"foo": 1} reveal_type(x1) # revealed: Foo + class Bar(TypedDict): bar: int + x2: Foo | Bar = {"foo": 1} reveal_type(x2) # revealed: Foo @@ -345,19 +368,23 @@ reveal_type(x4) # revealed: Bar x5: Foo | Bar = {"baz": 1} reveal_type(x5) # revealed: Foo | Bar + class FooBar1(TypedDict): foo: int bar: int + class FooBar2(TypedDict): foo: int bar: int + class FooBar3(TypedDict): foo: int bar: int baz: NotRequired[int] + x6: FooBar1 | FooBar2 = {"foo": 1, "bar": 1} reveal_type(x6) # revealed: FooBar1 | FooBar2 @@ -373,12 +400,15 @@ In doing so, may have to infer the same type with multiple distinct type context ```py from typing import TypedDict + class NestedFoo(TypedDict): foo: list[FooBar1] + class NestedBar(TypedDict): foo: list[FooBar2] + x1: NestedFoo | NestedBar = {"foo": [{"foo": 1, "bar": 1}]} reveal_type(x1) # revealed: NestedFoo | NestedBar ``` @@ -390,10 +420,12 @@ Users should be able to ignore TypedDict validation errors with `# type: ignore` ```py from typing import TypedDict + class Person(TypedDict): name: str age: int + alice_bad: Person = {"name": None} # type: ignore Person(name=None, age=30) # type: ignore Person(name="Alice", age=30, extra=True) # type: ignore @@ -407,10 +439,12 @@ correctly: ```py from typing import TypedDict + class User(TypedDict): name: str age: int + # Valid usage - all required fields provided user1 = User({"name": "Alice", "age": 30}) @@ -432,10 +466,12 @@ optional by setting `total=False`: ```py from typing import TypedDict + class OptionalPerson(TypedDict, total=False): name: str age: int | None + # All fields are optional with total=False charlie = OptionalPerson() david = OptionalPerson(name="David") @@ -470,18 +506,21 @@ all keys are required by default, `total=False` means that all keys are non-requ ```py from typing_extensions import TypedDict, Required, NotRequired, Final + # total=False by default, but id is explicitly Required class Message(TypedDict, total=False): id: Required[int] # Always required, even though total=False content: str # Optional due to total=False timestamp: NotRequired[str] # Explicitly optional (redundant here) + # total=True by default, but content is explicitly NotRequired class User(TypedDict): name: str # Required due to total=True (default) email: Required[str] # Explicitly required (redundant here) bio: NotRequired[str] # Optional despite total=True + ID: Final = "id" # Valid Message constructions @@ -491,9 +530,11 @@ msg3 = Message(id=3, timestamp="2024-01-01") # id required, timestamp optional msg4: Message = {"id": 4} # id required, content optional msg5: Message = {ID: 5} # id required, content optional + def msg() -> Message: return {ID: 1} + # Valid User constructions user1 = User(name="Alice", email="alice@example.com") # required fields user2 = User(name="Bob", email="bob@example.com", bio="Developer") # with optional bio @@ -532,16 +573,20 @@ from typing import TypedDict from typing_extensions import ReadOnly from ty_extensions import static_assert, is_assignable_to, is_subtype_of + class Person(TypedDict): name: str + class Employee(TypedDict): name: str employee_id: int + class Robot(TypedDict): name: int + static_assert(is_assignable_to(Employee, Person)) static_assert(not is_assignable_to(Person, Employee)) @@ -558,12 +603,15 @@ cover keys that are explicitly marked `NotRequired`, and also all the keys in a ```py from typing_extensions import NotRequired + class Spy1(TypedDict): name: NotRequired[str] + class Spy2(TypedDict, total=False): name: str + # invalid because `Spy1` and `Spy2` might be missing `name` static_assert(not is_assignable_to(Spy1, Person)) static_assert(not is_assignable_to(Spy2, Person)) @@ -572,12 +620,15 @@ static_assert(not is_assignable_to(Spy2, Person)) static_assert(not is_assignable_to(Person, Spy1)) static_assert(not is_assignable_to(Person, Spy2)) + class Amnesiac1(TypedDict): name: NotRequired[ReadOnly[str]] + class Amnesiac2(TypedDict, total=False): name: ReadOnly[str] + # invalid because `Amnesiac1` and `Amnesiac2` might be missing `name` static_assert(not is_assignable_to(Amnesiac1, Person)) static_assert(not is_assignable_to(Amnesiac2, Person)) @@ -597,42 +648,55 @@ test all the permutations: from typing import Any from typing_extensions import ReadOnly + class RequiredMutableInt(TypedDict): x: int + class RequiredReadOnlyInt(TypedDict): x: ReadOnly[int] + class NotRequiredMutableInt(TypedDict): x: NotRequired[int] + class NotRequiredReadOnlyInt(TypedDict): x: NotRequired[ReadOnly[int]] + class RequiredMutableBool(TypedDict): x: bool + class RequiredReadOnlyBool(TypedDict): x: ReadOnly[bool] + class NotRequiredMutableBool(TypedDict): x: NotRequired[bool] + class NotRequiredReadOnlyBool(TypedDict): x: NotRequired[ReadOnly[bool]] + class RequiredMutableAny(TypedDict): x: Any + class RequiredReadOnlyAny(TypedDict): x: ReadOnly[Any] + class NotRequiredMutableAny(TypedDict): x: NotRequired[Any] + class NotRequiredReadOnlyAny(TypedDict): x: NotRequired[ReadOnly[Any]] + # fmt: off static_assert( is_assignable_to( RequiredMutableInt, RequiredMutableInt)) static_assert( is_subtype_of( RequiredMutableInt, RequiredMutableInt)) @@ -741,10 +805,12 @@ All typed dictionaries can be assigned to `Mapping[str, object]`: ```py from typing import Mapping, TypedDict + class Person(TypedDict): name: str age: int | None + alice = Person(name="Alice", age=30) # Always assignable. _: Mapping[str, object] = alice @@ -767,12 +833,15 @@ ways: ```py from typing import TypedDict + def dangerous(d: dict[str, object]) -> None: d["name"] = 1 + class Person(TypedDict): name: str + alice: Person = {"name": "Alice"} # error: [invalid-argument-type] "Argument to function `dangerous` is incorrect: Expected `dict[str, object]`, found `Person`" @@ -810,24 +879,30 @@ only thing standing in the way of this unsound example: ```py from typing_extensions import TypedDict, NotRequired + class C(TypedDict): x: int y: str + class B(TypedDict): x: int + class A(TypedDict): x: int y: NotRequired[object] # incompatible with both C and (surprisingly!) B + def b_from_c(c: C) -> B: return c # allowed + def a_from_b(b: B) -> A: # error: [invalid-return-type] "Return type does not match returned value: expected `A`, found `B`" return b + # The [invalid-return-type] error above is the only thing that keeps us from corrupting the type of c['y']. c: C = {"x": 1, "y": "hello"} a: A = a_from_b(b_from_c(c)) @@ -841,17 +916,21 @@ target item must be assignable from `object`: ```py from typing_extensions import ReadOnly + class A2(TypedDict): x: int y: NotRequired[ReadOnly[object]] + def a2_from_b(b: B) -> A2: return b # allowed + class A3(TypedDict): x: int y: NotRequired[ReadOnly[int]] # not assignable from `object` + def a3_from_b(b: B) -> A3: return b # error: [invalid-return-type] ``` @@ -862,24 +941,29 @@ def a3_from_b(b: B) -> A3: from typing_extensions import TypedDict, ReadOnly, NotRequired from ty_extensions import static_assert, is_assignable_to, is_subtype_of + class Inner1(TypedDict): name: str + class Inner2(TypedDict): name: str + class Outer1(TypedDict): a: Inner1 b: ReadOnly[Inner1] c: NotRequired[Inner1] d: ReadOnly[NotRequired[Inner1]] + class Outer2(TypedDict): a: Inner2 b: ReadOnly[Inner2] c: NotRequired[Inner2] d: ReadOnly[NotRequired[Inner2]] + def _(o1: Outer1, o2: Outer2): static_assert(is_assignable_to(Outer1, Outer2)) static_assert(is_subtype_of(Outer1, Outer2)) @@ -892,21 +976,25 @@ This also extends to gradual types: ```py from typing import Any + class Inner3(TypedDict): name: Any + class Outer3(TypedDict): a: Inner3 b: ReadOnly[Inner3] c: NotRequired[Inner3] d: ReadOnly[NotRequired[Inner3]] + class Outer4(TypedDict): a: Any b: ReadOnly[Any] c: NotRequired[Any] d: ReadOnly[NotRequired[Any]] + def _(o1: Outer1, o2: Outer2, o3: Outer3, o4: Outer4): static_assert(is_assignable_to(Outer3, Outer1)) static_assert(not is_subtype_of(Outer3, Outer1)) @@ -946,20 +1034,24 @@ types: from typing_extensions import Any, TypedDict, ReadOnly, assert_type from ty_extensions import is_assignable_to, is_equivalent_to, static_assert + class Foo(TypedDict): x: int y: Any + # exactly the same fields class Bar(TypedDict): x: int y: Any + # the same fields but in a different order class Baz(TypedDict): y: Any x: int + static_assert(is_assignable_to(Foo, Bar)) static_assert(is_equivalent_to(Foo, Bar)) static_assert(is_assignable_to(Foo, Baz)) @@ -991,35 +1083,44 @@ equivalence: class FewerFields(TypedDict): x: int + static_assert(is_assignable_to(Foo, FewerFields)) static_assert(not is_equivalent_to(Foo, FewerFields)) + class DifferentMutability(TypedDict): x: int y: ReadOnly[Any] + static_assert(is_assignable_to(Foo, DifferentMutability)) static_assert(not is_equivalent_to(Foo, DifferentMutability)) + class MoreFields(TypedDict): x: int y: Any z: str + static_assert(not is_assignable_to(Foo, MoreFields)) static_assert(not is_equivalent_to(Foo, MoreFields)) + class DifferentFieldStaticType(TypedDict): x: str y: Any + static_assert(not is_assignable_to(Foo, DifferentFieldStaticType)) static_assert(not is_equivalent_to(Foo, DifferentFieldStaticType)) + class DifferentFieldGradualType(TypedDict): x: int y: Any | str + static_assert(is_assignable_to(Foo, DifferentFieldGradualType)) static_assert(not is_equivalent_to(Foo, DifferentFieldGradualType)) ``` @@ -1030,25 +1131,31 @@ static_assert(not is_equivalent_to(Foo, DifferentFieldGradualType)) from ty_extensions import static_assert, is_equivalent_to from typing_extensions import TypedDict, Required, NotRequired + class Foo1(TypedDict, total=False): x: int y: str + class Foo2(TypedDict): y: NotRequired[str] x: NotRequired[int] + static_assert(is_equivalent_to(Foo1, Foo2)) static_assert(is_equivalent_to(Foo1 | int, int | Foo2)) + class Bar1(TypedDict, total=False): x: int y: Required[str] + class Bar2(TypedDict): y: str x: NotRequired[int] + static_assert(is_equivalent_to(Bar1, Bar2)) static_assert(is_equivalent_to(Bar1 | int, int | Bar2)) ``` @@ -1059,25 +1166,31 @@ static_assert(is_equivalent_to(Bar1 | int, int | Bar2)) from typing_extensions import TypedDict from ty_extensions import static_assert, is_assignable_to, is_equivalent_to + class Node1(TypedDict): value: int next: "Node1" | None + class Node2(TypedDict): value: int next: "Node2" | None + static_assert(is_assignable_to(Node1, Node2)) static_assert(is_equivalent_to(Node1, Node2)) + class Person1(TypedDict): name: str friends: list["Person1"] + class Person2(TypedDict): name: str friends: list["Person2"] + static_assert(is_assignable_to(Person1, Person2)) static_assert(is_equivalent_to(Person1, Person2)) ``` @@ -1092,12 +1205,15 @@ names, the warning makes that clear: ```py from typing import TypedDict, cast + class Foo2(TypedDict): x: int + class Bar2(TypedDict): x: int + foo: Foo2 = {"x": 1} _ = cast(Foo2, foo) # error: [redundant-cast] _ = cast(Bar2, foo) # error: [redundant-cast] @@ -1110,16 +1226,20 @@ _ = cast(Bar2, foo) # error: [redundant-cast] ```py from typing import TypedDict, Final, Literal, Any + class Person(TypedDict): name: str age: int | None + class Animal(TypedDict): name: str + NAME_FINAL: Final = "name" AGE_FINAL: Final[Literal["age"]] = "age" + def _( person: Person, being: Person | Animal, @@ -1161,18 +1281,22 @@ def _( from typing_extensions import TypedDict, Final, Literal, LiteralString, Any from ty_extensions import Intersection + class Person(TypedDict): name: str surname: str age: int | None + class Animal(TypedDict): name: str legs: int + NAME_FINAL: Final = "name" AGE_FINAL: Final[Literal["age"]] = "age" + def _(person: Person): person["name"] = "Alice" person["age"] = 30 @@ -1180,13 +1304,16 @@ def _(person: Person): # error: [invalid-key] "Unknown key "naem" for TypedDict `Person` - did you mean "name"?" person["naem"] = "Alice" + def _(person: Person): person[NAME_FINAL] = "Alice" person[AGE_FINAL] = 30 + def _(person: Person, literal_key: Literal["age"]): person[literal_key] = 22 + def _(person: Person, union_of_keys: Literal["name", "surname"]): person[union_of_keys] = "unknown" @@ -1194,6 +1321,7 @@ def _(person: Person, union_of_keys: Literal["name", "surname"]): # error: [invalid-assignment] "Invalid assignment to key "surname" with declared type `str` on TypedDict `Person`: value of type `Literal[1]`" person[union_of_keys] = 1 + def _(being: Person | Animal): being["name"] = "Being" @@ -1204,6 +1332,7 @@ def _(being: Person | Animal): # error: [invalid-key] "Unknown key "surname" for TypedDict `Animal` - did you mean "name"?" being["surname"] = "unknown" + def _(centaur: Intersection[Person, Animal]): centaur["name"] = "Chiron" centaur["age"] = 100 @@ -1212,12 +1341,14 @@ def _(centaur: Intersection[Person, Animal]): # error: [invalid-key] "Unknown key "unknown" for TypedDict `Person`" centaur["unknown"] = "value" + def _(person: Person, union_of_keys: Literal["name", "age"], unknown_value: Any): person[union_of_keys] = unknown_value # error: [invalid-assignment] "Invalid assignment to key "name" with declared type `str` on TypedDict `Person`: value of type `None`" person[union_of_keys] = None + def _(person: Person, str_key: str, literalstr_key: LiteralString): # error: [invalid-key] "TypedDict `Person` can only be subscripted with a string literal key, got key of type `str`." person[str_key] = None @@ -1225,6 +1356,7 @@ def _(person: Person, str_key: str, literalstr_key: LiteralString): # error: [invalid-key] "TypedDict `Person` can only be subscripted with a string literal key, got key of type `LiteralString`." person[literalstr_key] = None + def _(person: Person, unknown_key: Any): # No error here: person[unknown_key] = "Eve" @@ -1237,11 +1369,13 @@ Assignments to keys that are marked `ReadOnly` will produce an error: ```py from typing_extensions import TypedDict, ReadOnly, Required + class Person(TypedDict, total=False): id: ReadOnly[Required[int]] name: str age: int | None + alice: Person = {"id": 1, "name": "Alice", "age": 30} alice["age"] = 31 # okay @@ -1257,6 +1391,7 @@ class Config(TypedDict): host: ReadOnly[str] port: ReadOnly[int] + config: Config = {"host": "localhost", "port": 8080} # error: [invalid-assignment] "Cannot assign to key "host" on TypedDict `Config`: key is marked read-only" @@ -1271,11 +1406,13 @@ config["port"] = 80 from typing import TypedDict from typing_extensions import NotRequired + class Person(TypedDict): name: str age: int | None extra: NotRequired[str] + def _(p: Person) -> None: reveal_type(p.keys()) # revealed: dict_keys[str, object] reveal_type(p.values()) # revealed: dict_values[str, object] @@ -1324,10 +1461,12 @@ of a `TypedDict` type will return `dict`: ```py from typing import TypedDict + class Person(TypedDict): name: str age: int | None + def _(p: Person) -> None: reveal_type(type(p)) # revealed: @@ -1341,10 +1480,12 @@ on inhabitants of the type defined by the class: # error: [unresolved-attribute] "Class `Person` has no attribute `name`" Person.name + def _(P: type[Person]): # error: [unresolved-attribute] "Object of type `type[Person]` has no attribute `name`" P.name + def _(p: Person) -> None: # error: [unresolved-attribute] "Object of type `Person` has no attribute `name`" p.name @@ -1359,10 +1500,12 @@ def _(p: Person) -> None: ```py from typing import TypedDict + class Person(TypedDict): name: str age: int | None + reveal_type(Person.__total__) # revealed: bool reveal_type(Person.__required_keys__) # revealed: frozenset[str] reveal_type(Person.__optional_keys__) # revealed: frozenset[str] @@ -1395,6 +1538,7 @@ def accepts_typed_dict_class(t_person: type[Person]) -> None: reveal_type(t_person.__required_keys__) # revealed: frozenset[str] reveal_type(t_person.__optional_keys__) # revealed: frozenset[str] + accepts_typed_dict_class(Person) ``` @@ -1405,17 +1549,21 @@ accepts_typed_dict_class(Person) ```py from typing import TypedDict + class Person(TypedDict): name: str + class Employee(Person): employee_id: int + alice: Employee = {"name": "Alice", "employee_id": 1} # error: [missing-typed-dict-key] "Missing required key 'employee_id' in TypedDict `Employee` constructor" eve: Employee = {"name": "Eve"} + def combine(p: Person, e: Employee): reveal_type(p.copy()) # revealed: Person reveal_type(e.copy()) # revealed: Employee @@ -1433,15 +1581,18 @@ original requirement status, while new fields follow the child class's `total` s ```py from typing import TypedDict + # Case 1: total=True parent, total=False child class PersonBase(TypedDict): id: int # required (from total=True) name: str # required (from total=True) + class PersonOptional(PersonBase, total=False): age: int # optional (from total=False) email: str # optional (from total=False) + # Inherited fields keep their original requirement status person1 = PersonOptional(id=1, name="Alice") # Valid - id/name still required person2 = PersonOptional(id=1, name="Alice", age=25) # Valid - age optional @@ -1454,14 +1605,17 @@ person_invalid1 = PersonOptional(name="Bob") # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `PersonOptional` constructor" person_invalid2 = PersonOptional(id=2) + # Case 2: total=False parent, total=True child class PersonBaseOptional(TypedDict, total=False): id: int # optional (from total=False) name: str # optional (from total=False) + class PersonRequired(PersonBaseOptional): # total=True by default age: int # required (from total=True) + # New fields in child are required, inherited fields stay optional person4 = PersonRequired(age=30) # Valid - only age required, id/name optional person5 = PersonRequired(id=1, name="Charlie", age=35) # Valid - all provided @@ -1476,14 +1630,17 @@ This also works with `Required` and `NotRequired`: ```py from typing_extensions import TypedDict, Required, NotRequired + # Case 3: Mixed inheritance with Required/NotRequired class PersonMixed(TypedDict, total=False): id: Required[int] # required despite total=False name: str # optional due to total=False + class Employee(PersonMixed): # total=True by default department: str # required due to total=True + # id stays required (Required override), name stays optional, department is required emp1 = Employee(id=1, department="Engineering") # Valid emp2 = Employee(id=2, name="Eve", department="Sales") # Valid @@ -1508,22 +1665,27 @@ from ty_extensions import static_assert, is_assignable_to, is_subtype_of T = TypeVar("T") + class TaggedData(TypedDict, Generic[T]): data: T tag: str + p1: TaggedData[int] = {"data": 42, "tag": "number"} p2: TaggedData[str] = {"data": "Hello", "tag": "text"} # error: [invalid-argument-type] "Invalid argument to key "data" with declared type `int` on TypedDict `TaggedData[int]`: value of type `Literal["not a number"]`" p3: TaggedData[int] = {"data": "not a number", "tag": "number"} + class Items(TypedDict, Generic[T]): items: list[T] + def homogeneous_list(*args: T) -> list[T]: return list(args) + items1: Items[int] = {"items": [1, 2, 3]} items2: Items[str] = {"items": ["a", "b", "c"]} items3: Items[int] = {"items": homogeneous_list(1, 2, 3)} @@ -1550,22 +1712,27 @@ python-version = "3.12" from typing import TypedDict, Any from ty_extensions import static_assert, is_assignable_to, is_subtype_of + class TaggedData[T](TypedDict): data: T tag: str + p1: TaggedData[int] = {"data": 42, "tag": "number"} p2: TaggedData[str] = {"data": "Hello", "tag": "text"} # error: [invalid-argument-type] "Invalid argument to key "data" with declared type `int` on TypedDict `TaggedData[int]`: value of type `Literal["not a number"]`" p3: TaggedData[int] = {"data": "not a number", "tag": "number"} + class Items[T](TypedDict): items: list[T] + def homogeneous_list[T](*args: T) -> list[T]: return list(args) + items1: Items[int] = {"items": [1, 2, 3]} items2: Items[str] = {"items": ["a", "b", "c"]} items3: Items[int] = {"items": homogeneous_list(1, 2, 3)} @@ -1589,10 +1756,12 @@ static_assert(not is_subtype_of(Items[Any], Items[int])) from __future__ import annotations from typing import TypedDict + class Node(TypedDict): name: str parent: Node | None + root: Node = {"name": "root", "parent": None} child: Node = {"name": "child", "parent": root} grandchild: Node = {"name": "grandchild", "parent": child} @@ -1610,10 +1779,12 @@ class Person(TypedDict): name: str parent: Person | None + def _(node: Node, person: Person): _: Person = node _: Node = person + _: Node = Person(name="Alice", parent=Node(name="Bob", parent=Person(name="Charlie", parent=None))) ``` @@ -1658,13 +1829,16 @@ Values that inhabit a `TypedDict` type must be instances of `dict` itself, not a ```py from typing import TypedDict + class MyDict(dict): pass + class Person(TypedDict): name: str age: int | None + # error: [invalid-assignment] "Object of type `MyDict` is not assignable to `Person`" x: Person = MyDict({"name": "Alice", "age": 30}) ``` @@ -1674,10 +1848,12 @@ x: Person = MyDict({"name": "Alice", "age": 30}) ```py from typing import TypedDict + class Person(TypedDict): name: str age: int | None + def _(obj: object) -> bool: # TODO: this should be an error return isinstance(obj, Person) @@ -1692,30 +1868,39 @@ Snapshot tests for diagnostic messages including suggestions: ```py from typing import TypedDict, Final + class Person(TypedDict): name: str age: int | None + def access_invalid_literal_string_key(person: Person): person["naem"] # error: [invalid-key] + NAME_KEY: Final = "naem" + def access_invalid_key(person: Person): person[NAME_KEY] # error: [invalid-key] + def access_with_str_key(person: Person, str_key: str): person[str_key] # error: [invalid-key] + def write_to_key_with_wrong_type(person: Person): person["age"] = "42" # error: [invalid-assignment] + def write_to_non_existing_key(person: Person): person["naem"] = "Alice" # error: [invalid-key] + def write_to_non_literal_string_key(person: Person, str_key: str): person[str_key] = "Alice" # error: [invalid-key] + def create_with_invalid_string_key(): # error: [invalid-key] alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} @@ -1729,10 +1914,12 @@ Assignment to `ReadOnly` keys: ```py from typing_extensions import ReadOnly + class Employee(TypedDict): id: ReadOnly[int] name: str + def write_to_readonly_key(employee: Employee): employee["id"] = 42 # error: [invalid-assignment] ``` @@ -1753,10 +1940,12 @@ def write_to_non_existing_key_single_quotes(person: Person): from typing import TypedDict as TD from typing_extensions import Required + class UserWithAlias(TD, total=False): name: Required[str] age: int + user_empty = UserWithAlias(name="Alice") # name is required user_partial = UserWithAlias(name="Alice", age=30) @@ -1775,16 +1964,20 @@ treated as a `TypedDict`: ```py from typing import TypedDict as TD + class TypedDict: def __init__(self): pass + class NotActualTypedDict(TypedDict, total=True): name: str + class ActualTypedDict(TD, total=True): name: str + not_td = NotActualTypedDict() reveal_type(not_td) # revealed: NotActualTypedDict @@ -1806,16 +1999,20 @@ from typing import TypedDict, final from typing_extensions import ReadOnly from ty_extensions import static_assert, is_disjoint_from + # Two simple disjoint types, to avoid relying on `@disjoint_base` special cases for built-ins like # `int` and `str`. @final class Final1: ... + @final class Final2: ... + static_assert(is_disjoint_from(Final1, Final2)) + class DisjointTD1(TypedDict): # Make this example `ReadOnly` because that actually ends up checking the field types for # disjointness in practice. Mutable fields are stricter. We'll get to that below. @@ -1825,11 +2022,13 @@ class DisjointTD1(TypedDict): common1: object common2: object + class DisjointTD2(TypedDict): disjoint: ReadOnly[Final2] common1: object common2: object + static_assert(is_disjoint_from(DisjointTD1, DisjointTD2)) ``` @@ -1840,30 +2039,40 @@ both. `TypedDict` disjointness takes this into account. For example: ```py from ty_extensions import is_assignable_to + class NonFinal1: ... + + class NonFinal2: ... + + class CommonSub(NonFinal1, NonFinal2): ... + static_assert(not is_disjoint_from(NonFinal1, NonFinal2)) static_assert(not is_assignable_to(NonFinal1, NonFinal2)) static_assert(is_assignable_to(CommonSub, NonFinal1)) static_assert(is_assignable_to(CommonSub, NonFinal2)) + class NonDisjointTD1(TypedDict): non_disjoint: ReadOnly[NonFinal1] # While we're here: It doesn't matter how many "extra" fields there are, or what order the # fields are in. Only shared field names can establish disjointness. extra1: int + class NonDisjointTD2(TypedDict): extra2: str non_disjoint: ReadOnly[NonFinal2] + class CommonSubTD(TypedDict): extra2: str extra1: int non_disjoint: ReadOnly[CommonSub] + # The first two TDs above are not assignable in either direction... static_assert(not is_assignable_to(NonDisjointTD1, NonDisjointTD2)) static_assert(not is_assignable_to(NonDisjointTD2, NonDisjointTD1)) @@ -1883,12 +2092,15 @@ common need to have *compatible* types (in the fully-static case, equivalent typ ```py from typing import Any, Generic, TypeVar + class IntTD(TypedDict): x: int + class BoolTD(TypedDict): x: bool + # `bool` is assignable to `int`, but `int` is not assignable to `bool`. If `x` was `ReadOnly` (even, # as we'll see below, only on the `int` side), then these two TDs would not be disjoint, but in this # mutable case they are. @@ -1901,12 +2113,15 @@ static_assert(is_disjoint_from(BoolTD, IntTD)) # other for the same reason.) However, `bool` is *not* compatible with `int | Any`, because there's # no materialization that's equivalent to `bool`. + class IntOrAnyTD(TypedDict): x: int | Any + class BoolOrAnyTD(TypedDict): x: bool | Any + static_assert(not is_disjoint_from(IntTD, IntOrAnyTD)) static_assert(not is_disjoint_from(IntOrAnyTD, IntTD)) static_assert(not is_disjoint_from(IntTD, BoolOrAnyTD)) @@ -1922,9 +2137,11 @@ static_assert(not is_disjoint_from(BoolOrAnyTD, BoolTD)) # `Any` is compatible with everything. + class AnyTD(TypedDict): x: Any + static_assert(not is_disjoint_from(IntTD, AnyTD)) static_assert(not is_disjoint_from(AnyTD, IntTD)) static_assert(not is_disjoint_from(BoolTD, AnyTD)) @@ -1937,24 +2154,30 @@ static_assert(not is_disjoint_from(AnyTD, AnyTD)) # This works with generic `TypedDict`s too. + class TwoIntsTD(TypedDict): x: int y: int + class TwoBoolsTD(TypedDict): x: bool y: bool + class IntBoolTD(TypedDict): x: int y: bool + T = TypeVar("T") + class TwoGenericTD(TypedDict, Generic[T]): x: T y: T + static_assert(not is_disjoint_from(TwoGenericTD[Any], TwoIntsTD)) static_assert(not is_disjoint_from(TwoGenericTD[int], TwoIntsTD)) static_assert(is_disjoint_from(TwoGenericTD[bool], TwoIntsTD)) @@ -1974,9 +2197,11 @@ isn't assignable to the immutable side: class ReadOnlyIntTD(TypedDict): x: ReadOnly[int] + class ReadOnlyBoolTD(TypedDict): x: ReadOnly[bool] + static_assert(not is_disjoint_from(ReadOnlyIntTD, ReadOnlyBoolTD)) static_assert(not is_disjoint_from(ReadOnlyBoolTD, ReadOnlyIntTD)) static_assert(not is_disjoint_from(BoolTD, ReadOnlyIntTD)) @@ -1997,12 +2222,15 @@ disjointness: ```py from typing_extensions import NotRequired + class NotRequiredIntTD(TypedDict): x: NotRequired[int] + class NotRequiredReadOnlyIntTD(TypedDict): x: NotRequired[ReadOnly[int]] + static_assert(is_disjoint_from(NotRequiredIntTD, IntTD)) static_assert(is_disjoint_from(IntTD, NotRequiredIntTD)) static_assert(is_disjoint_from(NotRequiredIntTD, ReadOnlyIntTD)) @@ -2019,9 +2247,11 @@ check: class NotRequiredBoolTD(TypedDict): x: NotRequired[bool] + class NotRequiredReadOnlyBoolTD(TypedDict): x: NotRequired[ReadOnly[bool]] + static_assert(not is_disjoint_from(IntTD, IntTD)) static_assert(is_disjoint_from(IntTD, BoolTD)) static_assert(not is_disjoint_from(IntTD, ReadOnlyIntTD)) @@ -2066,11 +2296,14 @@ static_assert(not is_disjoint_from(NotRequiredReadOnlyBoolTD, NotRequiredReadOnl from typing import TypedDict, Mapping from ty_extensions import static_assert, is_disjoint_from + class TD(TypedDict): x: int + class RegularNonTD: ... + static_assert(not is_disjoint_from(TD, object)) static_assert(not is_disjoint_from(TD, Mapping[str, object])) static_assert(is_disjoint_from(TD, Mapping[int, object])) @@ -2096,18 +2329,23 @@ given a distinct `Literal` type/value. We can narrow the union by constraining t ```py from typing import TypedDict, Literal + class Foo(TypedDict): tag: Literal["foo"] + class Bar(TypedDict): tag: Literal[42] + class Baz(TypedDict): tag: Literal[b"baz"] # `BytesLiteral` is supported. + class Bing(TypedDict): tag: Literal["bing"] + def _(u: Foo | Bar | Baz | Bing): if u["tag"] == "foo": reveal_type(u) # revealed: Foo @@ -2125,6 +2363,7 @@ We can descend into intersections to discover `TypedDict` types that need narrow from collections.abc import Mapping from ty_extensions import Intersection + def _(u: Foo | Intersection[Bar, Mapping[str, int]]): if u["tag"] == "foo": reveal_type(u) # revealed: Foo @@ -2148,9 +2387,11 @@ anything about the type of `x`. Here's an example where narrowing would be tempt ```py from ty_extensions import is_assignable_to, static_assert + class NonLiteralTD(TypedDict): tag: int + def _(u: Foo | NonLiteralTD): if u["tag"] == "foo": # We can't narrow the union here... @@ -2159,12 +2400,14 @@ def _(u: Foo | NonLiteralTD): # ...(even though we can here)... reveal_type(u) # revealed: NonLiteralTD + # ...because `NonLiteralTD["tag"]` could be assigned to with one of these, which would make the # first condition above true at runtime! class WackyInt(int): def __eq__(self, other): return True + _: NonLiteralTD = {"tag": WackyInt(99)} # allowed ``` @@ -2177,11 +2420,13 @@ def _(u: Foo | Bar | dict): # false negative in `is_disjoint_impl`. reveal_type(u) # revealed: Foo | (dict[Unknown, Unknown] & ~) + # The negation(s) will simplify out if we add something to the union that doesn't inherit from # `dict`. It just needs to support indexing with a string key. class NotADict: def __getitem__(self, key): ... + def _(u: Foo | Bar | NotADict): if u["tag"] == 42: reveal_type(u) # revealed: Bar | NotADict @@ -2195,12 +2440,15 @@ field, it could be *assigned to* with another `TypedDict` that does: ```py from typing_extensions import Literal + class Foo(TypedDict): foo: int + class Bar(TypedDict): bar: int + def disappointment(u: Foo | Bar, v: Literal["foo"]): if "foo" in u: # We can't narrow the union here... @@ -2214,11 +2462,13 @@ def disappointment(u: Foo | Bar, v: Literal["foo"]): else: reveal_type(u) # revealed: Bar + # ...because `u` could turn out to be one of these. class FooBar(TypedDict): foo: int bar: int + static_assert(is_assignable_to(FooBar, Foo)) static_assert(is_assignable_to(FooBar, Bar)) ``` @@ -2231,6 +2481,7 @@ that contain `TypedDict`s, and unions that contain intersections that contain `T from typing_extensions import Literal, Any from ty_extensions import Intersection, is_assignable_to, static_assert + def _(t: Bar, u: Foo | Intersection[Bar, Any], v: Intersection[Bar, Any], w: Literal["bar"]): reveal_type(u) # revealed: Foo | (Bar & Any) reveal_type(v) # revealed: Bar & Any @@ -2271,18 +2522,23 @@ python-version = "3.10" ```py from typing import TypedDict, Literal + class Foo(TypedDict): tag: Literal["foo"] + class Bar(TypedDict): tag: Literal[42] + class Baz(TypedDict): tag: Literal[b"baz"] + class Bing(TypedDict): tag: Literal["bing"] + def match_statements(u: Foo | Bar | Baz | Bing): match u["tag"]: case "foo": @@ -2311,9 +2567,11 @@ Narrowing is restricted to `Literal` tags: ```py from ty_extensions import is_assignable_to, static_assert + class NonLiteralTD(TypedDict): tag: int + def match_non_literal(u: Foo | NonLiteralTD): match u["tag"]: case "foo": @@ -2346,6 +2604,7 @@ not allowed to have a value. ```py from typing import TypedDict + class Foo(TypedDict): """docstring""" @@ -2357,12 +2616,14 @@ class Foo(TypedDict): # As a non-standard but common extension, we interpret `...` as equivalent to `pass`. ... + class Bar(TypedDict): a: int # error: [invalid-typed-dict-statement] "invalid statement in TypedDict class body" 42 # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value" b: str = "hello" + # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods" def bar(self): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/custom.md b/crates/ty_python_semantic/resources/mdtest/unary/custom.md index c471ff509f..93ba24c069 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/custom.md @@ -13,9 +13,13 @@ class Yes: def __invert__(self) -> int: return 17 + class Sub(Yes): ... + + class No: ... + reveal_type(+Yes()) # revealed: bool reveal_type(-Yes()) # revealed: str reveal_type(~Yes()) # revealed: int @@ -49,9 +53,13 @@ class Yes: def __invert__(self) -> int: return 17 + class Sub(Yes): ... + + class No: ... + # error: [unsupported-operator] "Unary operator `+` is not supported for object of type ``" reveal_type(+Yes) # revealed: Unknown # error: [unsupported-operator] "Unary operator `-` is not supported for object of type ``" @@ -80,6 +88,7 @@ reveal_type(~No) # revealed: Unknown def f(): pass + # error: [unsupported-operator] "Unary operator `+` is not supported for object of type `def f() -> Unknown`" reveal_type(+f) # revealed: Unknown # error: [unsupported-operator] "Unary operator `-` is not supported for object of type `def f() -> Unknown`" @@ -101,18 +110,25 @@ class Yes: def __invert__(self) -> int: return 17 + class Sub(Yes): ... + + class No: ... + def yes() -> type[Yes]: return Yes + def sub() -> type[Sub]: return Sub + def no() -> type[No]: return No + # error: [unsupported-operator] "Unary operator `+` is not supported for object of type `type[Yes]`" reveal_type(+yes()) # revealed: Unknown # error: [unsupported-operator] "Unary operator `-` is not supported for object of type `type[Yes]`" @@ -148,10 +164,16 @@ class Meta(type): def __invert__(self) -> int: return 17 + class Yes(metaclass=Meta): ... + + class Sub(Yes): ... + + class No: ... + reveal_type(+Yes) # revealed: bool reveal_type(-Yes) # revealed: str reveal_type(~Yes) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md b/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md index 53b4ca6366..030f849166 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md @@ -5,6 +5,7 @@ ```py from typing import Literal + class Number: def __init__(self, value: int): self.value = 1 @@ -18,14 +19,17 @@ class Number: def __invert__(self) -> Literal[True]: return True + a = Number(0) reveal_type(+a) # revealed: int reveal_type(-a) # revealed: int reveal_type(~a) # revealed: Literal[True] + class NoDunder: ... + b = NoDunder() +b # error: [unsupported-operator] "Unary operator `+` is not supported for object of type `NoDunder`" -b # error: [unsupported-operator] "Unary operator `-` is not supported for object of type `NoDunder`" diff --git a/crates/ty_python_semantic/resources/mdtest/unary/not.md b/crates/ty_python_semantic/resources/mdtest/unary/not.md index e0cb63d2b5..d053787f74 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/not.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/not.md @@ -13,6 +13,7 @@ reveal_type(not not None) # revealed: Literal[False] def f(): return 1 + reveal_type(not f) # revealed: Literal[False] # TODO Unknown should not be part of the type of typing.reveal_type # reveal_type(not reveal_type) revealed: Literal[False] @@ -127,42 +128,53 @@ truthiness. ```py from typing import Literal + class AlwaysTrue: def __bool__(self) -> Literal[True]: return True + # revealed: Literal[False] reveal_type(not AlwaysTrue()) + class AlwaysFalse: def __bool__(self) -> Literal[False]: return False + # revealed: Literal[True] reveal_type(not AlwaysFalse()) + # At runtime, no `__bool__` and no `__len__` means truthy, but we can't rely on that, because # a subclass could add a `__bool__` method. class NoBoolMethod: ... + # revealed: bool reveal_type(not NoBoolMethod()) + # And we can't rely on `__len__` for the same reason: a subclass could add `__bool__`. class LenZero: def __len__(self) -> Literal[0]: return 0 + # revealed: bool reveal_type(not LenZero()) + class LenNonZero: def __len__(self) -> Literal[1]: return 1 + # revealed: bool reveal_type(not LenNonZero()) + class WithBothLenAndBool1: def __bool__(self) -> Literal[False]: return False @@ -170,9 +182,11 @@ class WithBothLenAndBool1: def __len__(self) -> Literal[2]: return 2 + # revealed: Literal[True] reveal_type(not WithBothLenAndBool1()) + class WithBothLenAndBool2: def __bool__(self) -> Literal[True]: return True @@ -180,26 +194,33 @@ class WithBothLenAndBool2: def __len__(self) -> Literal[0]: return 0 + # revealed: Literal[False] reveal_type(not WithBothLenAndBool2()) + class MethodBoolInvalid: def __bool__(self) -> int: return 0 + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `MethodBoolInvalid`" # revealed: bool reveal_type(not MethodBoolInvalid()) + # Don't trust a possibly-unbound `__bool__` method: def get_flag() -> bool: return True + class PossiblyUnboundBool: if get_flag(): + def __bool__(self) -> Literal[False]: return False + # revealed: bool reveal_type(not PossiblyUnboundBool()) ``` @@ -212,6 +233,7 @@ reveal_type(not PossiblyUnboundBool()) class NotBoolable: __bool__: int = 3 + # error: [unsupported-bool-conversion] not NotBoolable() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md index dc4da435f5..9bec3c8631 100644 --- a/crates/ty_python_semantic/resources/mdtest/union_types.md +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -7,6 +7,7 @@ This test suite covers certain basic properties and simplification strategies fo ```py from typing import Literal + def _(u1: int | str, u2: Literal[0] | Literal[1]) -> None: reveal_type(u1) # revealed: int | str reveal_type(u2) # revealed: Literal[0, 1] @@ -28,10 +29,12 @@ and so we eagerly simplify it away. `NoReturn` is equivalent to `Never`. ```py from typing_extensions import Never, NoReturn + def never(u1: int | Never, u2: int | Never | str) -> None: reveal_type(u1) # revealed: int reveal_type(u2) # revealed: int | str + def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None: reveal_type(u1) # revealed: int reveal_type(u2) # revealed: int | str @@ -44,6 +47,7 @@ Unions with `object` can be simplified to `object`: ```py from typing_extensions import Never, Any + def _( u1: int | object, u2: object | int, @@ -67,6 +71,7 @@ def _( ```py from typing import Literal + def _( u1: (int | str) | bytes, u2: int | (str | bytes), @@ -84,6 +89,7 @@ The type `S | T` can be simplified to `T` if `S` is a subtype of `T`: ```py from typing_extensions import Literal, LiteralString + def _( u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString ) -> None: @@ -100,6 +106,7 @@ The union `Literal[True] | Literal[False]` is exactly equivalent to `bool`: ```py from typing import Literal + def _( u1: Literal[True, False], u2: bool | Literal[True], @@ -121,11 +128,13 @@ from enum import Enum from typing import Literal, Any from ty_extensions import Intersection + class Color(Enum): RED = "red" GREEN = "green" BLUE = "blue" + def _( u1: Literal[Color.RED, Color.GREEN], u2: Color | Literal[Color.RED], @@ -141,6 +150,7 @@ def _( reveal_type(u5) # revealed: Color reveal_type(u6) # revealed: Color + def _( u1: Intersection[Literal[Color.RED], Any] | Literal[Color.RED], u2: Literal[Color.RED] | Intersection[Literal[Color.RED], Any], @@ -154,6 +164,7 @@ def _( ```py from ty_extensions import Unknown + def _(u1: Unknown | str, u2: str | Unknown) -> None: reveal_type(u1) # revealed: Unknown | str reveal_type(u2) # revealed: str | Unknown @@ -167,6 +178,7 @@ union are still redundant: ```py from ty_extensions import Unknown + def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None: reveal_type(u1) # revealed: Unknown | str reveal_type(u2) # revealed: Unknown | str @@ -180,6 +192,7 @@ Simplifications still apply when `Unknown` is present. ```py from ty_extensions import Unknown + def _(u1: int | Unknown | bool) -> None: reveal_type(u1) # revealed: int | Unknown ``` @@ -191,9 +204,13 @@ We can simplify unions of intersections: ```py from ty_extensions import Intersection, Not + class P: ... + + class Q: ... + def _( i1: Intersection[P, Q] | Intersection[P, Q], i2: Intersection[P, Q] | Intersection[Q, P], @@ -217,6 +234,7 @@ type strings = Literal["foo", ""] type ints = Literal[0, 1] type bytes = Literal[b"foo", b""] + def _( strings_or_truthy: strings | AlwaysTruthy, truthy_or_strings: AlwaysTruthy | strings, @@ -249,6 +267,7 @@ def _( reveal_type(bytes_or_falsy) # revealed: Literal[b"foo"] | AlwaysFalsy reveal_type(falsy_or_bytes) # revealed: AlwaysFalsy | Literal[b"foo"] + type SA = Union[Literal[""], AlwaysTruthy, Literal["foo"]] static_assert(is_equivalent_to(SA, Literal[""] | AlwaysTruthy)) @@ -284,24 +303,29 @@ type SB = Intersection[Literal[""], Any] type SC = SA | SB type SD = SB | SA + def _(c: SC, d: SD): reveal_type(c) # revealed: Literal[""] reveal_type(d) # revealed: Literal[""] + type IA = Literal[0] type IB = Intersection[Literal[0], Any] type IC = IA | IB type ID = IB | IA + def _(c: IC, d: ID): reveal_type(c) # revealed: Literal[0] reveal_type(d) # revealed: Literal[0] + type BA = Literal[b""] type BB = Intersection[Literal[b""], Any] type BC = BA | BB type BD = BB | BA + def _(c: BC, d: BD): reveal_type(c) # revealed: Literal[b""] reveal_type(d) # revealed: Literal[b""] @@ -316,6 +340,7 @@ element, never to the fixed-length element (`tuple[()] | tuple[Any, ...]` -> `tu ```py from typing import Any + def f( a: tuple[()] | tuple[int, ...], b: tuple[int, ...] | tuple[()], @@ -346,18 +371,23 @@ python-version = "3.12" ```py from typing import Any + class Bivariant[T]: ... + class Covariant[T]: def get(self) -> T: raise NotImplementedError + class Contravariant[T]: def receive(self, input: T) -> None: ... + class Invariant[T]: mutable_attribute: T + def _( a: Bivariant[Any] | Bivariant[Any | str], b: Bivariant[Any | str] | Bivariant[Any], diff --git a/crates/ty_python_semantic/resources/mdtest/unpacking.md b/crates/ty_python_semantic/resources/mdtest/unpacking.md index 20a89625ed..5812fbdc4d 100644 --- a/crates/ty_python_semantic/resources/mdtest/unpacking.md +++ b/crates/ty_python_semantic/resources/mdtest/unpacking.md @@ -180,10 +180,12 @@ class Iterator: def __next__(self) -> int: return 42 + class Iterable: def __iter__(self) -> Iterator: return Iterator() + (a, b) = Iterable() reveal_type(a) # revealed: int reveal_type(b) # revealed: int @@ -196,10 +198,12 @@ class Iterator: def __next__(self) -> int: return 42 + class Iterable: def __iter__(self) -> Iterator: return Iterator() + (a, (b, c), d) = (1, Iterable(), 2) reveal_type(a) # revealed: Literal[1] reveal_type(b) # revealed: int @@ -417,10 +421,17 @@ python-version = "3.11" ```py class I0: ... + + class I1: ... + + class I2: ... + + class HeterogeneousTupleSubclass(tuple[I0, I1, I2]): ... + def f(x: HeterogeneousTupleSubclass): a, b, c = x reveal_type(a) # revealed: I0 @@ -462,8 +473,10 @@ def f(x: HeterogeneousTupleSubclass): reveal_type(v) # revealed: Unknown reveal_type(w) # revealed: list[Unknown] + class MixedTupleSubclass(tuple[I0, *tuple[I1, ...], I2]): ... + def f(x: MixedTupleSubclass): (a,) = x # error: [invalid-assignment] "Too many values to unpack: Expected 1" reveal_type(a) # revealed: Unknown @@ -607,6 +620,7 @@ reveal_type(c) # revealed: list[Literal["c", "d"]] ```py from typing_extensions import LiteralString + def _(s: LiteralString): a, b, *c = s reveal_type(a) # revealed: LiteralString @@ -815,6 +829,7 @@ def _(arg: tuple[int, int, int] | tuple[int, str, bytes] | tuple[int, int, str]) ```py from typing import Literal + def _(arg: tuple[int, tuple[str, bytes]] | tuple[tuple[int, bytes], Literal["ab"]]): a, (b, c) = arg reveal_type(a) # revealed: int | tuple[int, bytes] @@ -888,6 +903,7 @@ def _(flag: bool): ```py from typing import Literal + def _(arg: tuple[int, int] | Literal["ab"]): a, b = arg reveal_type(a) # revealed: int | Literal["a"] @@ -901,10 +917,12 @@ class Iterator: def __next__(self) -> tuple[int, int] | tuple[int, str]: return (1, 2) + class Iterable: def __iter__(self) -> Iterator: return Iterator() + ((a, b), c) = Iterable() reveal_type(a) # revealed: int reveal_type(b) # revealed: int | str @@ -918,10 +936,12 @@ class Iterator: def __next__(self) -> bytes: return b"" + class Iterable: def __iter__(self) -> Iterator: return Iterator() + def _(arg: tuple[int, str] | Iterable): a, b = arg reveal_type(a) # revealed: int | bytes @@ -1004,10 +1024,12 @@ class Iterator: def __next__(self) -> tuple[int, int]: return (1, 2) + class Iterable: def __iter__(self) -> Iterator: return Iterator() + for a, b in Iterable(): reveal_type(a) # revealed: int reveal_type(b) # revealed: int @@ -1020,10 +1042,12 @@ class Iterator: def __next__(self) -> bytes: return b"" + class Iterable: def __iter__(self) -> Iterator: return Iterator() + def _(arg: tuple[tuple[int, str], Iterable]): for a, b in arg: reveal_type(a) # revealed: int | bytes @@ -1044,6 +1068,7 @@ class ContextManager: def __exit__(self, exc_type, exc_value, traceback) -> None: pass + with ContextManager() as (a, b): reveal_type(a) # revealed: int reveal_type(b) # revealed: int @@ -1059,6 +1084,7 @@ class ContextManager: def __exit__(self, exc_type, exc_value, traceback) -> None: pass + with ContextManager() as (a, b): reveal_type(a) # revealed: int reveal_type(b) # revealed: str @@ -1074,6 +1100,7 @@ class ContextManager: def __exit__(self, exc_type, exc_value, traceback) -> None: pass + with ContextManager() as (a, (b, c)): reveal_type(a) # revealed: int reveal_type(b) # revealed: str @@ -1090,6 +1117,7 @@ class ContextManager: def __exit__(self, exc_type, exc_value, traceback) -> None: pass + with ContextManager() as (a, *b): reveal_type(a) # revealed: int reveal_type(b) # revealed: list[int] @@ -1114,6 +1142,7 @@ class ContextManager: def __exit__(self, *args) -> None: pass + # error: [invalid-assignment] "Not enough values to unpack: Expected 3" with ContextManager() as (a, b, c): reveal_type(a) # revealed: Unknown @@ -1189,10 +1218,12 @@ class Iterator: def __next__(self) -> tuple[int, int]: return (1, 2) + class Iterable: def __iter__(self) -> Iterator: return Iterator() + # revealed: tuple[int, int] [reveal_type((a, b)) for a, b in Iterable()] ``` @@ -1204,10 +1235,12 @@ class Iterator: def __next__(self) -> bytes: return b"" + class Iterable: def __iter__(self) -> Iterator: return Iterator() + def _(arg: tuple[tuple[int, str], Iterable]): # revealed: tuple[int | bytes, str | bytes] [reveal_type((a, b)) for a, b in arg] diff --git a/crates/ty_python_semantic/resources/mdtest/unreachable.md b/crates/ty_python_semantic/resources/mdtest/unreachable.md index 9ff84162ac..753ce85e63 100644 --- a/crates/ty_python_semantic/resources/mdtest/unreachable.md +++ b/crates/ty_python_semantic/resources/mdtest/unreachable.md @@ -23,12 +23,14 @@ def f1(): # TODO: we should mark this as unreachable print("unreachable") + def f2(): raise Exception() # TODO: we should mark this as unreachable print("unreachable") + def f3(): while True: break @@ -36,6 +38,7 @@ def f3(): # TODO: we should mark this as unreachable print("unreachable") + def f4(): for _ in range(10): continue @@ -66,6 +69,7 @@ def f1(): # TODO: we should mark this as unreachable print("unreachable") + def f2(): if True: return @@ -73,6 +77,7 @@ def f2(): # TODO: we should mark this as unreachable print("unreachable") + def f3(): if False: return @@ -93,9 +98,11 @@ after the call to that function unreachable. ```py from typing_extensions import NoReturn + def always_raises() -> NoReturn: raise Exception() + def f(): always_raises() @@ -251,6 +258,7 @@ def outer(): def inner(): reveal_type(x) # revealed: Literal[1] + while True: pass ``` @@ -263,9 +271,11 @@ from typing import Literal FEATURE_X_ACTIVATED: Literal[False] = False if FEATURE_X_ACTIVATED: + def feature_x(): print("Performing 'X'") + def f(): if FEATURE_X_ACTIVATED: # Type checking this particular section as if it were reachable would @@ -336,6 +346,7 @@ The same works for ternary expressions: ```py class ExceptionGroupPolyfill: ... + MyExceptionGroup1 = ExceptionGroup if sys.version_info >= (3, 11) else ExceptionGroupPolyfill MyExceptionGroup1 = ExceptionGroupPolyfill if sys.version_info < (3, 11) else ExceptionGroup ``` @@ -410,6 +421,7 @@ conceivable that this could be improved, but is not a priority for now. if False: does_not_exist + def f(): return does_not_exist @@ -517,6 +529,7 @@ them: if False: 1 + "a" # error: [unsupported-operator] + def f(): return @@ -548,6 +561,7 @@ This is also supported for function calls, attribute accesses, etc.: from typing import Literal if False: + def f(x: int): ... def g(*, a: int, b: int): ... @@ -560,6 +574,7 @@ if False: number: Literal[1] = 1 else: + def f(x: str): ... def g(*, a: int): ... @@ -567,6 +582,7 @@ else: x: str = "a" class D: ... + number: Literal[0] = 0 if False: @@ -603,6 +619,7 @@ import sys if sys.version_info >= (3, 14): raise RuntimeError("this library doesn't support 3.14 yet!!!") + class AwesomeAPI: ... ``` @@ -611,5 +628,6 @@ class AwesomeAPI: ... ```py import module + def f(x: module.AwesomeAPI): ... # error: [invalid-type-form] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/with/async.md b/crates/ty_python_semantic/resources/mdtest/with/async.md index 9802c85c4e..b47c5d47c7 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/async.md +++ b/crates/ty_python_semantic/resources/mdtest/with/async.md @@ -9,12 +9,14 @@ asserts that it doesn't emit any context manager-related errors. ```py class Target: ... + class Manager: async def __aenter__(self) -> Target: return Target() async def __aexit__(self, exc_type, exc_value, traceback): ... + async def test(): async with Manager() as f: reveal_type(f) # revealed: Target @@ -29,6 +31,7 @@ class Manager: async def __aexit__(self, exc_type, exc_value, traceback): ... + async def test(): async with Manager() as (x, y): reveal_type(x) # revealed: int @@ -40,6 +43,7 @@ async def test(): ```py class Manager: ... + async def main(): # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because it does not implement `__aenter__` and `__aexit__`" async with Manager(): @@ -52,6 +56,7 @@ async def main(): class Manager: async def __aexit__(self, exc_tpe, exc_value, traceback): ... + async def main(): # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because it does not implement `__aenter__`" async with Manager(): @@ -64,6 +69,7 @@ async def main(): class Manager: async def __aenter__(self): ... + async def main(): # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because it does not implement `__aexit__`" async with Manager(): @@ -78,6 +84,7 @@ class Manager: async def __aexit__(self, exc_tpe, exc_value, traceback): ... + async def main(): # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because it does not correctly implement `__aenter__`" async with Manager(): @@ -89,11 +96,14 @@ async def main(): ```py from typing_extensions import Self + class Manager: def __aenter__(self) -> Self: return self + __aexit__: int = 32 + async def main(): # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because it does not correctly implement `__aexit__`" async with Manager(): @@ -111,6 +121,7 @@ async def _(flag: bool): def __aexit__(self, exc_type, exc_value, traceback): ... class NotAContextManager: ... + context_expr = Manager1() if flag else NotAContextManager() # error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `async with` because the methods `__aenter__` and `__aexit__` are possibly missing" @@ -124,6 +135,7 @@ async def _(flag: bool): async def _(flag: bool): class Manager: if flag: + async def __aenter__(self) -> str: return "abcd" @@ -143,6 +155,7 @@ class Manager: async def __aexit__(self, exc_type, exc_value, traceback): ... + async def main(): context_expr = Manager() @@ -163,6 +176,7 @@ class Manager: def __enter__(self): ... def __exit__(self, *args): ... + async def main(): # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because it does not implement `__aenter__` and `__aexit__`" async with Manager(): @@ -179,6 +193,7 @@ class Manager: def __enter__(self): ... def __exit__(self, typ: str, exc, traceback): ... + async def main(): # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because it does not implement `__aenter__` and `__aexit__`" async with Manager(): @@ -194,6 +209,7 @@ class Manager: def __enter__(self, wrong_extra_arg): ... def __exit__(self, typ, exc, traceback, wrong_extra_arg): ... + async def main(): # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because it does not implement `__aenter__` and `__aexit__`" async with Manager(): @@ -206,15 +222,19 @@ async def main(): from contextlib import asynccontextmanager from typing import AsyncGenerator + class Session: ... + @asynccontextmanager async def connect() -> AsyncGenerator[Session]: yield Session() + # revealed: () -> _AsyncGeneratorContextManager[Session, None] reveal_type(connect) + async def main(): async with connect() as session: reveal_type(session) # revealed: Session @@ -225,13 +245,16 @@ This also works with `AsyncIterator` return types: ```py from typing import AsyncIterator + @asynccontextmanager async def connect_iterator() -> AsyncIterator[Session]: yield Session() + # revealed: () -> _AsyncGeneratorContextManager[Session, None] reveal_type(connect_iterator) + async def main_iterator(): async with connect_iterator() as session: reveal_type(session) # revealed: Session @@ -242,13 +265,16 @@ And with `AsyncGeneratorType` return types: ```py from types import AsyncGeneratorType + @asynccontextmanager async def connect_async_generator() -> AsyncGeneratorType[Session]: yield Session() + # revealed: () -> _AsyncGeneratorContextManager[Session, None] reveal_type(connect_async_generator) + async def main_async_generator(): async with connect_async_generator() as session: reveal_type(session) # revealed: Session @@ -264,9 +290,11 @@ python-version = "3.11" ```py import asyncio + async def long_running_task(): await asyncio.sleep(5) + async def main(): async with asyncio.timeout(1): await long_running_task() @@ -282,9 +310,11 @@ python-version = "3.11" ```py import asyncio + async def long_running_task(): await asyncio.sleep(5) + async def main(): async with asyncio.TaskGroup() as tg: reveal_type(tg) # revealed: TaskGroup diff --git a/crates/ty_python_semantic/resources/mdtest/with/sync.md b/crates/ty_python_semantic/resources/mdtest/with/sync.md index 15d6aec51e..c84232cb3f 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/sync.md +++ b/crates/ty_python_semantic/resources/mdtest/with/sync.md @@ -8,12 +8,14 @@ The type of the target variable in a `with` statement is the return type from th ```py class Target: ... + class Manager: def __enter__(self) -> Target: return Target() def __exit__(self, exc_type, exc_value, traceback): ... + with Manager() as f: reveal_type(f) # revealed: Target ``` @@ -45,6 +47,7 @@ def _(flag: bool): ```py class Manager: ... + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" with Manager(): ... @@ -56,6 +59,7 @@ with Manager(): class Manager: def __exit__(self, exc_tpe, exc_value, traceback): ... + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__`" with Manager(): ... @@ -67,6 +71,7 @@ with Manager(): class Manager: def __enter__(self): ... + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__exit__`" with Manager(): ... @@ -80,6 +85,7 @@ class Manager: def __exit__(self, exc_tpe, exc_value, traceback): ... + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`" with Manager(): ... @@ -90,11 +96,14 @@ with Manager(): ```py from typing_extensions import Self + class Manager: def __enter__(self) -> Self: return self + __exit__: int = 32 + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__exit__`" with Manager(): ... @@ -111,6 +120,7 @@ def _(flag: bool): def __exit__(self, exc_type, exc_value, traceback): ... class NotAContextManager: ... + context_expr = Manager1() if flag else NotAContextManager() # error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly missing" @@ -124,6 +134,7 @@ def _(flag: bool): def _(flag: bool): class Manager: if flag: + def __enter__(self) -> str: return "abcd" @@ -143,6 +154,7 @@ class Manager: def __exit__(self, exc_type, exc_value, traceback): ... + context_expr = Manager() # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`" @@ -162,6 +174,7 @@ class Manager: async def __aenter__(self): ... async def __aexit__(self, *args): ... + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" with Manager(): ... @@ -177,6 +190,7 @@ class Manager: async def __aenter__(self): ... async def __aexit__(self, typ: str, exc, traceback): ... + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" with Manager(): ... @@ -191,6 +205,7 @@ class Manager: async def __aenter__(self, wrong_extra_arg): ... async def __aexit__(self, typ, exc, traceback, wrong_extra_arg): ... + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" with Manager(): ...