From a857a8072eda344a3e390d2eef40c9d50c91625d Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Wed, 28 Jan 2026 15:27:53 -0800 Subject: [PATCH] Apply ruff formatting to mdtests ``` $ ruff format --no-cache --preview --isolated --line-length 130 crates/*/resources/mdtest/*.md ``` --- .../resources/mdtest/async.md | 11 + .../resources/mdtest/attributes.md | 224 +++++++++++ .../resources/mdtest/bidirectional.md | 21 + .../resources/mdtest/classes.md | 11 + .../resources/mdtest/cycle.md | 10 + .../resources/mdtest/decorators.md | 48 +++ .../resources/mdtest/del.md | 18 + .../resources/mdtest/deprecated.md | 41 ++ .../resources/mdtest/descriptor_protocol.md | 67 ++++ .../resources/mdtest/enums.md | 99 +++++ .../mdtest/exhaustiveness_checking.md | 11 + .../resources/mdtest/final.md | 125 +++++- .../resources/mdtest/implicit_type_aliases.md | 98 +++++ .../mdtest/instance_layout_conflict.md | 53 +++ .../resources/mdtest/intersection_types.md | 130 ++++++ .../resources/mdtest/liskov.md | 5 +- .../resources/mdtest/literal_promotion.md | 36 ++ .../mdtest/mdtest_custom_typeshed.md | 2 + .../resources/mdtest/metaclass.md | 86 ++++ .../resources/mdtest/mro.md | 189 +++++++++ .../resources/mdtest/named_tuple.md | 94 +++++ .../resources/mdtest/overloads.md | 54 ++- .../resources/mdtest/override.md | 87 ++-- .../resources/mdtest/pep613_type_aliases.md | 38 ++ .../resources/mdtest/pep695_type_aliases.md | 49 +++ .../resources/mdtest/properties.md | 15 + .../resources/mdtest/protocols.md | 374 ++++++++++++++++++ .../resources/mdtest/public_types.md | 59 +++ .../mdtest/statically_known_branches.md | 47 +++ .../resources/mdtest/terminal_statements.md | 48 +++ .../resources/mdtest/ty_extensions.md | 29 ++ .../resources/mdtest/typed_dict.md | 298 ++++++++++++++ .../resources/mdtest/union_types.md | 18 + .../resources/mdtest/unpacking.md | 24 ++ .../resources/mdtest/unreachable.md | 17 + 35 files changed, 2466 insertions(+), 70 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/async.md b/crates/ty_python_semantic/resources/mdtest/async.md index 03255545514997..189b9e955b2b46 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 aafcd750064451..9dafd909c173e9 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 @@ -423,6 +445,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 +464,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 +486,7 @@ class C: class D: [[... for self.a in [1]] for _ in [1]] + reveal_type(C().a) # revealed: Unknown | int ``` @@ -473,16 +499,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 +530,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 +542,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 +555,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 +572,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 +592,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 +607,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 +637,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 +663,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 +698,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 +714,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 +730,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 +757,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 +766,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 +786,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 +800,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 +825,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 +858,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 +889,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 +933,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 +980,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 +1042,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 +1061,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 +1098,7 @@ def _(flag: bool): attr1: str = "metaclass value" class C4(metaclass=Meta4): ... + # error: [possibly-missing-attribute] reveal_type(C4.attr1) # revealed: str ``` @@ -1104,6 +1174,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 +1230,7 @@ in the sub-diagnostic. ```py from typing import ClassVar + class Foo: x: ClassVar[int] = 42 @@ -1174,6 +1246,7 @@ the first parameter is keyword-only: ```py from typing import ClassVar + class Foo: x: ClassVar[int] = 42 @@ -1195,11 +1268,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 +1304,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 +1335,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 +1416,7 @@ def _(flag: bool, flag1: bool, flag2: bool): ```py from typing import Any + def _(flag: bool): class Base: x: Any @@ -1455,7 +1535,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 +1557,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 +1574,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 +1614,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 +1640,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 +1677,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 +1690,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 +1713,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 +1736,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 +1755,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 +1765,7 @@ def _(flag: bool): a_and_b.x = R() class A4: ... + class B4: ... def inner4(a_and_b: Intersection[A4, B4]): @@ -1653,6 +1774,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 +1789,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 +1820,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 +1834,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 +1859,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 +1927,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 +1950,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 +1966,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 +1998,7 @@ class C: def __getattr__(self, name: str) -> str: return "a" + c = C() reveal_type(c.x) # revealed: int @@ -1865,11 +2010,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 +2050,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 +2074,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 +2105,7 @@ of type `Never`): ```py from typing_extensions import Never, Any + def _(n: Never): reveal_type(n.__setattr__) # revealed: Never @@ -1978,14 +2130,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 +2154,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 +2172,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 +2204,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 +2250,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 +2267,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 +2355,7 @@ global_symbol: str = "a" import mod1 import mod2 + def _(flag: bool): if flag: mod = mod1 @@ -2210,6 +2378,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 +2450,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 +2475,7 @@ class Outer: def __init__(self): self.x: str = "a" + reveal_type(Outer().x) # revealed: int # error: [unresolved-attribute] @@ -2318,12 +2490,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 +2508,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 +2527,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 +2543,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 +2563,7 @@ class C: def copy(self, other: "C"): self.x = other.x + reveal_type(C().x) # revealed: Unknown | Literal[1] ``` @@ -2393,6 +2574,7 @@ class D: def copy(self, other: "D"): self.x = other.x + reveal_type(D().x) # revealed: Unknown ``` @@ -2407,8 +2589,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 +2600,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 +2622,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 +2650,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 +2661,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 +2714,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 +2757,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 +2769,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 +2785,7 @@ class Counter: def increment(self: "Counter"): self.count = self.count + 1 + reveal_type(Counter().count) # revealed: Unknown | int ``` @@ -2599,8 +2799,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 +2810,7 @@ class NestedMixed: def g(self: "NestedMixed"): self.x = {self.x} + reveal_type(NestedMixed().x) # revealed: Unknown | list[Divergent] | set[Divergent] ``` @@ -2618,13 +2821,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 +2855,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 @@ -2696,15 +2903,18 @@ declarations. # error: [unresolved-import] from unknown_library import unknown_decorator + class C: @unknown_decorator def f(self): self.x: int = 1 + # error: [unresolved-attribute] reveal_type(C.x) # revealed: Unknown reveal_type(C().x) # revealed: int + class D: def __init__(self): self.x: int = 1 @@ -2713,6 +2923,7 @@ class D: def f(self): self.x = 2 + reveal_type(D().x) # revealed: int ``` @@ -2726,6 +2937,7 @@ include `Unknown`. ```py from functools import cache + class C: def __init__(self) -> None: self.x: int = 0 @@ -2734,6 +2946,7 @@ class C: def method(self) -> None: self.x = 1 + def f(c: C) -> None: reveal_type(c.x) # revealed: int ``` @@ -2750,6 +2963,7 @@ class C: self.x: int = 1 return self.x + def f(c: C) -> None: reveal_type(c.x) # revealed: int ``` @@ -2761,10 +2975,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] @@ -2782,6 +2998,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 ``` @@ -2793,13 +3010,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]] ``` @@ -2809,10 +3029,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] ``` @@ -2822,10 +3044,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, ...] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index 602ba79d6b1fbe..6d09535b34b6ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -55,9 +55,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 +71,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] @@ -160,6 +164,7 @@ Function parameter annotations: ```py def b(x: list[Literal[1]]): ... + b([1]) ``` @@ -170,6 +175,7 @@ class C: def __init__(self, x: list[Literal[1]]): ... def foo(self, x: list[Literal[1]]): ... + C([1]).foo([1]) ``` @@ -187,6 +193,7 @@ class E: a: list[Literal[1]] b: list[Literal[1]] + def _(e: E): e.a = [1] E.b = [1] @@ -242,15 +249,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) ``` @@ -340,24 +351,31 @@ Similarly, the value type for augmented assignment dunder calls is inferred with ```py from typing import TypedDict + def lst[T](x: T) -> list[T]: return [x] + class Bar(TypedDict, closed=False): bar: list[int] + def _(bar: Bar): bar |= reveal_type({"bar": lst(1)}) # revealed: Bar + class Bar2(TypedDict): bar: list[int | None] + class X: def __ior__(self, other: Bar2): ... + def _(x: X): x |= reveal_type({"bar": lst(1)}) # revealed: Bar2 + def _(x: X | Bar): x |= {"bar": lst(1)} ``` @@ -435,12 +453,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/classes.md b/crates/ty_python_semantic/resources/mdtest/classes.md index f825034324bbda..2b882989d93691 100644 --- a/crates/ty_python_semantic/resources/mdtest/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/classes.md @@ -33,14 +33,22 @@ Fixed-length tuples are unpacked when used as starred base classes: ```py from ty_extensions import reveal_mro + class A: ... + + class B: ... + + class C: ... + bases = (A, B, C) + class Foo(*bases): ... + # revealed: (, , , , ) reveal_mro(Foo) ``` @@ -50,12 +58,15 @@ Variable-length tuples cannot be unpacked, so they fall back to `Unknown`: ```py from ty_extensions import reveal_mro + def get_bases() -> tuple[type, ...]: return (int, str) + # error: [unsupported-base] "Unsupported class base" class Bar(*get_bases()): ... + # revealed: (, Unknown, ) reveal_mro(Bar) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index 775b4e8ac6d589..6ac65f3a214fe8 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 @@ -65,6 +68,7 @@ from typing import Generic, TypeVar B = TypeVar("B", bound="Base") + class Base(Generic[B]): pass ``` @@ -86,24 +90,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 +124,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 +162,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/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md index 4a46f265a9fc80..cb1a21ca6a33ab 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): ... @@ -244,16 +276,20 @@ emit an error: ```py class NoInit: ... + # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" @NoInit def foo(): ... + reveal_type(foo) # revealed: NoInit + # error: [invalid-argument-type] @int def bar(): ... + reveal_type(bar) # revealed: int ``` @@ -264,25 +300,31 @@ When a class's constructor accepts the decorated function/class, no error is emi ```py from typing import Callable + class Wrapper: def __init__(self, func: Callable[..., object]) -> None: self.func = func + @Wrapper def my_func() -> int: return 42 + reveal_type(my_func) # revealed: Wrapper + class AcceptsType: def __init__(self, cls: type) -> None: self.cls = cls + # Decorator call is validated, but the type transformation isn't applied yet. # TODO: Class decorator return types should transform the class binding type. @AcceptsType class MyClass: ... + reveal_type(MyClass) # revealed: ``` @@ -295,10 +337,12 @@ from typing import Generic, TypeVar, Callable T = TypeVar("T") + class Box(Generic[T]): def __init__(self, value: T) -> None: self.value = value + # error: [invalid-argument-type] @Box[int] def returns_str() -> str: @@ -312,6 +356,7 @@ Using `type[SomeClass]` as a decorator validates against the class's constructor ```py class Base: ... + def apply_decorator(cls: type[Base]) -> None: # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" @cls @@ -326,6 +371,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: ... @@ -345,10 +391,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/del.md b/crates/ty_python_semantic/resources/mdtest/del.md index 945502ee826b28..38f464e4a51789 100644 --- a/crates/ty_python_semantic/resources/mdtest/del.md +++ b/crates/ty_python_semantic/resources/mdtest/del.md @@ -86,6 +86,7 @@ local error: ```py x = 1 + def foo(): print(x) # error: [unresolved-reference] "Name `x` used when not defined" if False: @@ -104,11 +105,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 +123,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 +148,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 +168,7 @@ reveal_type(c.x) # revealed: int class C: x: int = 1 + c = C() reveal_type(c.x) # revealed: int @@ -202,17 +211,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 +242,7 @@ class OnlyGetItem: def __getitem__(self, key: int) -> str: return "value" + g = OnlyGetItem() reveal_type(g[0]) # revealed: str @@ -247,18 +261,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 9497c77fe452da..23a3bd04d8d098 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!" ``` @@ -68,9 +74,11 @@ invalid_deco() # error: [missing-argument] ```py from typing_extensions import deprecated + @deprecated() # error: [missing-argument] "message" def invalid_deco(): ... + invalid_deco() ``` @@ -82,9 +90,11 @@ from typing_extensions import deprecated x = "message" + @deprecated(x) def invalid_deco(): ... + invalid_deco() # error: [deprecated] "message" ``` @@ -93,12 +103,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 +121,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 +138,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 +151,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" ``` @@ -176,9 +196,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 +216,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 +239,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 +256,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 +276,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 +293,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 +313,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 +338,7 @@ diagnostic. ```py from typing_extensions import deprecated + class MyInt: def __init__(self, val): self.val = val @@ -311,6 +347,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 +361,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 +370,7 @@ def f(x: int): ... def f(x): print(x) + f(1) f("hello") # TODO: error: [deprecated] "strings are no longer supported" ``` @@ -342,6 +381,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 +390,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 ce59da02c5b6ac..1a363aa2473a38 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/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index c3b1e55c53e852..b7ce81c34eae32 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)) ``` @@ -174,6 +190,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 +198,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 +212,7 @@ from enum import Enum from ty_extensions import enum_members from types import DynamicClassAttribute + class Answer(Enum): YES = 1 NO = 2 @@ -202,6 +221,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 +252,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 +271,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 +279,7 @@ class Color(Enum): red = 1 green = 2 + # revealed: tuple[Literal["RED"], Literal["GREEN"]] reveal_type(enum_members(Color)) @@ -269,6 +293,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 +302,7 @@ class ManyAliases(Enum): other_member = "other_real_member" + # revealed: tuple[Literal["real_member"], Literal["other_member"]] reveal_type(enum_members(ManyAliases)) @@ -334,6 +360,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 +372,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 +394,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 +409,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 +427,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 +465,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)) @@ -463,6 +508,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 +517,7 @@ class Answer(Enum): def maybe(self) -> None: return + # revealed: tuple[Literal["yes"], Literal["no"], Literal["maybe"]] reveal_type(enum_members(Answer)) ``` @@ -484,6 +531,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 +539,7 @@ class Answer(Enum): __private_member = 3 __maybe__ = 4 + # revealed: tuple[Literal["YES"], Literal["NO"], Literal["__maybe__"]] reveal_type(enum_members(Answer)) ``` @@ -504,6 +553,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 +564,7 @@ class Answer(Enum): _other_ignored = "test" also_ignored = "test2" + # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` @@ -530,6 +581,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 +596,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 +614,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 +635,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 +668,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 +686,7 @@ from enum import Enum from typing import Callable import sys + class Printer(Enum): STDOUT = 1 STDERR = 2 @@ -632,6 +697,7 @@ class Printer(Enum): elif self == Printer.STDERR: print(msg, file=sys.stderr) + Printer.STDOUT("Hello, world!") Printer.STDERR("An error occurred!") @@ -649,16 +715,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 @@ -706,15 +776,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 +799,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 +819,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 +843,7 @@ from enum import Enum from typing import Literal from ty_extensions import enum_members + class Answer(Enum): YES = 1 NO = 2 @@ -771,6 +852,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 +868,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 ``` @@ -873,11 +958,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 +975,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 +985,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 +994,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" @@ -978,10 +1069,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 +1084,7 @@ reveal_type(Color.RED != Color.RED) # revealed: Literal[False] ```py from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 @@ -998,6 +1092,7 @@ class Color(Enum): def __eq__(self, other: object) -> bool: return False + reveal_type(Color.RED == Color.RED) # revealed: bool ``` @@ -1006,6 +1101,7 @@ reveal_type(Color.RED == Color.RED) # revealed: bool ```py from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 @@ -1013,6 +1109,7 @@ class Color(Enum): def __ne__(self, other: object) -> bool: return False + reveal_type(Color.RED != Color.RED) # revealed: bool ``` @@ -1049,6 +1146,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 +1163,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 diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 1d7cbfd401341c..b0d828ad7beae4 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -98,11 +98,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 +117,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 +126,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 +138,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 +152,7 @@ def match_exhaustive(x: Color): assert_never(x) + def match_exhaustive_2(x: Color): match x: case Color.RED: @@ -158,6 +164,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 +174,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: @@ -384,6 +392,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 +404,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 +421,7 @@ def as_pattern_non_exhaustive(subject: int | str): ```py from enum import Enum + class Answer(Enum): YES = "yes" NO = "no" diff --git a/crates/ty_python_semantic/resources/mdtest/final.md b/crates/ty_python_semantic/resources/mdtest/final.md index c457709f1005f4..7e69fc73c904fa 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, @@ -42,39 +53,30 @@ def lossy_decorator(fn: Callable) -> Callable: ... class Parent: @final def foo(self): ... - @final @property def my_property1(self) -> int: ... - @property @final def my_property2(self) -> int: ... - @property @final def my_property3(self) -> int: ... - @final @classmethod def class_method1(cls) -> int: ... - @classmethod @final def class_method2(cls) -> int: ... - @final @staticmethod def static_method1() -> int: ... - @staticmethod @final def static_method2() -> int: ... - @lossy_decorator @final def decorated_1(self): ... - @final @lossy_decorator def decorated_2(self): ... @@ -87,31 +89,23 @@ class Child(Parent): def foo(self): ... @property def my_property1(self) -> int: ... # error: [override-of-final-method] - @property def my_property2(self) -> int: ... # error: [override-of-final-method] @my_property2.setter def my_property2(self, x: int) -> None: ... - @property def my_property3(self) -> int: ... # error: [override-of-final-method] @my_property3.deleter def my_proeprty3(self) -> None: ... - @classmethod def class_method1(cls) -> int: ... # error: [override-of-final-method] - @staticmethod def static_method1() -> int: ... # error: [override-of-final-method] - @classmethod def class_method2(cls) -> int: ... # error: [override-of-final-method] - @staticmethod def static_method2() -> int: ... # error: [override-of-final-method] - def decorated_1(self): ... # TODO: should emit [override-of-final-method] - @lossy_decorator def decorated_2(self): ... # TODO: should emit [override-of-final-method] @@ -175,6 +169,7 @@ class Baz(Foo): ```py from typing import final + class Foo: @final def f(self): ... @@ -185,6 +180,7 @@ class Foo: ```py import module1 + class Foo(module1.Foo): def f(self): ... # error: [override-of-final-method] ``` @@ -207,7 +203,6 @@ class Good: def bar(self, x: str) -> str: ... @overload def bar(self, x: int) -> int: ... - @final @overload def baz(self, x: str) -> str: ... @@ -219,7 +214,6 @@ class ChildOfGood(Good): def bar(self, x: str) -> str: ... @overload def bar(self, x: int) -> int: ... # error: [override-of-final-method] - @overload def baz(self, x: str) -> str: ... @overload @@ -232,7 +226,6 @@ class Bad: @final # error: [invalid-overload] def bar(self, x: int) -> int: ... - @overload def baz(self, x: str) -> str: ... @final @@ -245,7 +238,6 @@ class ChildOfBad(Bad): def bar(self, x: str) -> str: ... @overload def bar(self, x: int) -> int: ... # error: [override-of-final-method] - @overload def baz(self, x: str) -> str: ... @overload @@ -257,6 +249,7 @@ class ChildOfBad(Bad): ```py from typing import overload, final + class Good: @overload def f(self, x: str) -> str: ... @@ -266,6 +259,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 +270,7 @@ class ChildOfGood(Good): def f(self, x: int | str) -> int | str: return x + class Bad: @overload @final @@ -309,6 +304,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] @@ -335,13 +331,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) ``` @@ -358,6 +357,7 @@ diagnostic but do not provide an autofix (since the function may be defined in a ```py from typing import final + class Base: @final def method(self) -> None: ... @@ -375,6 +375,7 @@ def replacement_method() -> None: ... from base import Base from other import replacement_method + class Derived(Base): method = replacement_method # error: [override-of-final-method] ``` @@ -384,10 +385,12 @@ class Derived(Base): ```py from typing import final + class A: @final def __init__(self) -> None: ... + class B(A): def __init__(self) -> None: ... # error: [override-of-final-method] ``` @@ -401,14 +404,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 @@ -420,6 +426,7 @@ class C(B): ```py from typing import final, Final + @final @final @final @@ -434,6 +441,7 @@ class A: @final def method(self): ... + @final @final @final @@ -442,9 +450,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): ... @@ -455,10 +465,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 @@ -471,37 +483,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] @@ -515,19 +544,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: @@ -651,11 +687,13 @@ clear from the use of `@abstractmethod`. from abc import ABC, abstractmethod from typing import final + class Base(ABC): @abstractmethod def foo(self) -> int: raise NotImplementedError + @final class Derived(Base): # error: [abstract-method-in-final-class] "Final class `Derived` does not implement abstract method `foo`" pass @@ -669,6 +707,7 @@ class Derived(Base): # error: [abstract-method-in-final-class] "Final class `De from abc import ABC, abstractmethod from typing import final + class Base(ABC): @abstractmethod def foo(self) -> int: ... @@ -677,10 +716,12 @@ class Base(ABC): @abstractmethod def baz(self) -> None: ... + @final class MissingAll(Base): # error: [abstract-method-in-final-class] pass + @final class PartiallyImplemented(Base): # error: [abstract-method-in-final-class] def foo(self) -> int: @@ -696,10 +737,12 @@ class PartiallyImplemented(Base): # error: [abstract-method-in-final-class] from abc import abstractmethod from typing import Protocol, final + class MyProtocol(Protocol): @abstractmethod def method(self) -> int: ... + @final class Implementer(MyProtocol): # error: [abstract-method-in-final-class] pass @@ -711,12 +754,14 @@ class Implementer(MyProtocol): # error: [abstract-method-in-final-class] from abc import ABC, abstractmethod from typing import final + class Base(ABC): @abstractmethod def foo(self) -> int: ... @abstractmethod def bar(self) -> str: ... + @final class FullyImplemented(Base): def foo(self) -> int: @@ -734,13 +779,16 @@ class FullyImplemented(Base): from abc import ABC, abstractmethod from typing import final + class GrandParent(ABC): @abstractmethod def method(self) -> int: ... + class Parent(GrandParent): pass + @final class Child(Parent): # error: [abstract-method-in-final-class] pass @@ -755,17 +803,21 @@ a subclass. A `@final` class inheriting from that subclass must implement it. from abc import ABC, abstractmethod from typing import final + class GreatGrandparent(ABC): @abstractmethod def f(self): ... + class Grandparent(GreatGrandparent): def f(self): ... + class Parent(Grandparent): @abstractmethod def f(self): ... + @final class Child(Parent): # error: [abstract-method-in-final-class] pass @@ -779,12 +831,15 @@ A dynamic class created with `type()` can provide concrete implementations of ab from abc import ABC, abstractmethod from typing import final + class Base(ABC): @abstractmethod def foo(self) -> int: ... + DynamicMiddle = type("DynamicMiddle", (Base,), {"foo": lambda self: 42}) + @final class Final(DynamicMiddle): # No error; `foo` is implemented by `DynamicMiddle` pass @@ -798,10 +853,12 @@ subclasses. ```py from abc import ABC, abstractmethod + class Base(ABC): @abstractmethod def foo(self) -> int: ... + class Derived(Base): # No error - not final, can be subclassed pass ``` @@ -814,10 +871,12 @@ Enum classes cannot be subclassed if they have any members, so they are treated from abc import abstractmethod from enum import Enum + class Stringable: @abstractmethod def stringify(self) -> str: ... + class MyEnum(Stringable, Enum): # error: [abstract-method-in-final-class] A = 1 B = 2 @@ -832,6 +891,7 @@ never be implemented by a subclass. from abc import abstractmethod from typing import final + @final class Broken: # error: [abstract-method-in-final-class] @abstractmethod @@ -846,17 +906,20 @@ A `@final` class must also implement abstract properties. from abc import ABC, abstractmethod from typing import final + class Base(ABC): @property @abstractmethod def value(self) -> int: ... + @final class Good(Base): @property def value(self) -> int: return 42 + @final class Bad(Base): # error: [abstract-method-in-final-class] pass @@ -868,6 +931,7 @@ A property with an abstract setter (but concrete getter) is also abstract: from abc import ABC, abstractmethod from typing import final + class Base(ABC): @property def value(self) -> int: @@ -877,12 +941,14 @@ class Base(ABC): @abstractmethod def value(self, v: int) -> None: ... + @final class Good(Base): @Base.value.setter def value(self, v: int) -> None: pass + @final class Bad(Base): # error: [abstract-method-in-final-class] pass @@ -895,6 +961,7 @@ property deleters, so this is a TODO test: from abc import ABC, abstractmethod from typing import final + class Base(ABC): @property def value(self) -> int: @@ -908,6 +975,7 @@ class Base(ABC): @abstractmethod def value(self) -> None: ... + @final # TODO: should emit [abstract-method-in-final-class] class Bad(Base): @@ -924,15 +992,18 @@ value (`f: int = 42`) also overrides the abstract property. from abc import ABC, abstractmethod from typing import final + class Base(ABC): @property @abstractmethod def f(self) -> int: ... + @final class Child1(Base): f = 42 # OK: binding overrides the abstract property + @final class Child2(Base): f: int = 42 # OK: annotated assignment with value also overrides @@ -947,10 +1018,12 @@ method. Attempting to instantiate the class will still fail at runtime. from abc import ABC, abstractmethod from typing import final + class Base(ABC): @abstractmethod def method(self) -> int: ... + @final class Bad(Base): # error: [abstract-method-in-final-class] method: int @@ -962,11 +1035,13 @@ The same applies to abstract properties: from abc import ABC, abstractmethod from typing import final + class Base(ABC): @property @abstractmethod def f(self) -> int: ... + @final class BadChild(Base): # error: [abstract-method-in-final-class] f: int @@ -980,17 +1055,20 @@ A `@final` class must also implement abstract classmethods. from abc import ABC, abstractmethod from typing import final + class Base(ABC): @classmethod @abstractmethod def make(cls) -> "Base": ... + @final class Good(Base): @classmethod def make(cls) -> "Good": return cls() + @final class Bad(Base): # error: [abstract-method-in-final-class] pass @@ -1004,17 +1082,20 @@ A `@final` class must also implement abstract staticmethods. from abc import ABC, abstractmethod from typing import final + class Base(ABC): @staticmethod @abstractmethod def create() -> int: ... + @final class Good(Base): @staticmethod def create() -> int: return 42 + @final class Bad(Base): # error: [abstract-method-in-final-class] pass @@ -1034,6 +1115,7 @@ from abc import ( ) from typing import final + class Base(ABC): @abstractproperty # error: [deprecated] def value(self) -> int: @@ -1047,6 +1129,7 @@ class Base(ABC): def create() -> int: return 0 + @final # TODO: should emit [abstract-method-in-final-class] for `value`, `make`, and `create` class Bad(Base): 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 1fe375910f705c..cb5f48724d1c8b 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]", ): @@ -754,6 +790,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 +808,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 +826,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 +845,7 @@ T = TypeVar("T") MyAlias = list[T] + def outer(): MyAlias = set[T] @@ -829,6 +870,7 @@ if False: else: MyAlias2 = set[T] + def _( x1: MyAlias1[int], x2: MyAlias2[int], @@ -846,14 +888,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 +924,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 +964,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 +983,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 +997,11 @@ T = TypeVar("T") Deprecated = Annotated[T, "deprecated attribute"] + class C: old: Deprecated[int] + reveal_type(C().old) # revealed: int ``` @@ -959,6 +1012,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 +1030,7 @@ MyOptionalInt = Optional[int] reveal_type(MyOptionalInt) # revealed: + def _(optional_int: MyOptionalInt): reveal_type(optional_int) # revealed: int | None ``` @@ -987,6 +1042,7 @@ JustNone = Optional[None] reveal_type(JustNone) # revealed: None + def _(just_none: JustNone): reveal_type(just_none) # revealed: None ``` @@ -1011,6 +1067,7 @@ reveal_type(MyLiteralString) # revealed: reveal_type(MyNoReturn) # revealed: reveal_type(MyNever) # revealed: + def _( ls: MyLiteralString, nr: MyNoReturn, @@ -1033,6 +1090,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 +1106,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 +1124,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 +1140,7 @@ JustInt = Union[int] reveal_type(JustInt) # revealed: + def _(just_int: JustInt): reveal_type(just_int) # revealed: int ``` @@ -1093,6 +1154,7 @@ EmptyUnion = Union[()] reveal_type(EmptyUnion) # revealed: + def _(empty: EmptyUnion): reveal_type(empty) # revealed: Never ``` @@ -1103,6 +1165,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 +1183,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 +1215,7 @@ reveal_type(SubclassOfG) # revealed: reveal_type(SubclassOfGInt) # revealed: reveal_type(SubclassOfP) # revealed: + def _( subclass_of_a: SubclassOfA, subclass_of_any: SubclassOfAny, @@ -1184,9 +1255,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 +1271,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 +1294,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 +1313,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 +1345,7 @@ reveal_type(SubclassOfG) # revealed: reveal_type(SubclassOfGInt) # revealed: reveal_type(SubclassOfP) # revealed: + def _( subclass_of_a: SubclassOfA, subclass_of_any: SubclassOfAny, @@ -1329,6 +1414,7 @@ reveal_type(MyDefaultDict) # revealed: reveal_type(MyDeque) # revealed: reveal_type(MyOrderedDict) # revealed: + def _( my_list: MyList, my_set: MySet, @@ -1394,6 +1480,7 @@ reveal_type(DefaultDictOrNone) # revealed: reveal_type(OrderedDictOrNone) # revealed: + def _( none_or_list: NoneOrList, none_or_set: NoneOrSet, @@ -1458,6 +1545,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 +1577,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 +1594,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 +1612,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 +1632,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 +1661,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 +1692,7 @@ from typing import Union Recursive = list[Union["Recursive", None]] + def _(r: Recursive): reveal_type(r) # revealed: list[Divergent] ``` @@ -1643,6 +1740,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/instance_layout_conflict.md b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md index bb8e013083adf0..333215048617c1 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 022e09c43be945..a30083f00f67eb 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], @@ -831,6 +949,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 +966,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 +979,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 +999,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 +1014,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 +1036,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 +1044,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 +1061,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 +1079,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/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index dc0e09989a0d14..773ea175f2d73b 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -72,7 +72,7 @@ class Sub8(Super): def method(self, x: int, *args, **kwargs): ... # fine class Sub9(Super): - def method(self, x: int, extra_positional_arg=42, /): ... # fine + def method(self, x: int, extra_positional_arg=42, /): ... # fine class Sub10(Super): def method(self, x: int, extra_pos_or_kw_arg=42): ... # fine @@ -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_promotion.md b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md index 250b5e87700224..0fbba09a8623ed 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md @@ -45,6 +45,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 +75,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 +106,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 +136,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 +188,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 +202,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 +220,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 +229,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 +249,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 +285,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 +389,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 +413,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/mdtest_custom_typeshed.md b/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md index 6de0978fc99734..b7619f9bf9b595 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 3c4762fe8fefec..19440052979532 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: ``` @@ -80,13 +92,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 +118,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 +139,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 +159,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 +180,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 +212,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 +237,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 +257,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]` @@ -251,14 +329,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 42d84ba5752847..e50f123d8ab77c 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 bf96bdb88e76a8..3ff4b7e284024f 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, Sequence 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") @@ -185,10 +187,12 @@ a "dangling call". The types are still properly inferred: ```py from typing import NamedTuple + class Point(NamedTuple("Point", [("x", int), ("y", int)])): def magnitude(self) -> float: return (self.x**2 + self.y**2) ** 0.5 + p = Point(3, 4) reveal_type(p.x) # revealed: int reveal_type(p.y) # revealed: int @@ -201,17 +205,25 @@ same scope. This allows recursive types: ```py from typing import NamedTuple + class Node(NamedTuple("Node", [("value", int), ("next", "Node | None")])): pass + n = Node(1, None) reveal_type(n.value) # revealed: int reveal_type(n.next) # revealed: Node | None + class A(NamedTuple("A", [("x", "B | None")])): ... + + class B(NamedTuple("B", [("x", "C")])): ... + + class C(NamedTuple("C", [("x", "A")])): ... + reveal_type(A(x=B(x=C(x=A(x=None))))) # revealed: A # error: [invalid-argument-type] "Argument is incorrect: Expected `B | None`, found `C`" @@ -228,12 +240,14 @@ internal NamedTuple name (if different from the class name) won't work: ```py from typing import NamedTuple + # The string "X" in "next"'s type refers to the internal name, not "BadNode", so it won't resolve: # # error: [unresolved-reference] "Name `X` used when not defined" class BadNode(NamedTuple("X", [("value", int), ("next", "X | None")])): pass + n = BadNode(1, None) reveal_type(n.value) # revealed: int # X is not in scope, so it resolves to Unknown; None is correctly resolved @@ -245,10 +259,12 @@ Dangling calls cannot contain other dangling calls; that's an invalid type form: ```py from ty_extensions import reveal_mro + # error: [invalid-type-form] class A(NamedTuple("B", [("x", NamedTuple("C", [("x", "A" | None)]))])): pass + # revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(A) ``` @@ -371,9 +387,11 @@ Similarly for `collections.namedtuple`: import collections from ty_extensions import reveal_mro + def get_field_names() -> tuple[str, *tuple[str, ...]]: return ("x", "y") + field_names = get_field_names() NT = collections.namedtuple("NT", field_names) @@ -393,9 +411,11 @@ properly inherited: from typing import NamedTuple from ty_extensions import reveal_mro + class Url(NamedTuple("Url", [("host", str), ("path", str)])): pass + reveal_type(Url) # revealed: # revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(Url) @@ -421,12 +441,14 @@ Subclasses can add methods that use inherited fields: from typing import NamedTuple from typing_extensions import Self + class Url(NamedTuple("Url", [("host", str), ("port", int)])): def with_port(self, port: int) -> Self: reveal_type(self.host) # revealed: str reveal_type(self.port) # revealed: int return self._replace(port=port) + url = Url("localhost", 8080) reveal_type(url.with_port(9000)) # revealed: Url ``` @@ -437,6 +459,7 @@ outer class is just a regular class inheriting from it. This is equivalent to: ```py class _Foo(NamedTuple): ... + class Foo(_Foo): # Regular class, not a namedtuple ... ``` @@ -447,6 +470,7 @@ Because the outer class is not itself a namedtuple, it can use `super()` and ove from collections import namedtuple from typing import NamedTuple + class ExtType(namedtuple("ExtType", "code data")): """Override __new__ to add validation.""" @@ -455,6 +479,7 @@ class ExtType(namedtuple("ExtType", "code data")): raise TypeError("code must be int") return super().__new__(cls, code, data) + class Url(NamedTuple("Url", [("host", str), ("path", str)])): """Override __new__ to normalize the path.""" @@ -463,6 +488,7 @@ class Url(NamedTuple("Url", [("host", str), ("path", str)])): path = "/" + path return super().__new__(cls, host, path) + # Both work correctly. ext = ExtType(42, b"hello") reveal_type(ext) # revealed: ExtType @@ -482,6 +508,7 @@ from typing_extensions import Self fields = [("host", str), ("port", int)] + # error: [invalid-named-tuple] "Invalid argument to parameter `fields` of `NamedTuple()`: `fields` must be a literal list or tuple" class Url(NamedTuple("Url", fields)): def with_port(self, port: int) -> Self: @@ -876,6 +903,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" @@ -883,6 +911,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 @@ -891,6 +920,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] @@ -907,10 +937,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( @@ -920,6 +952,7 @@ class D( # fmt: on + # error: [invalid-named-tuple] class E(NamedTuple, Protocol): ... ``` @@ -932,12 +965,15 @@ from abc import ABC from collections import namedtuple from typing import NamedTuple + class Point(namedtuple("Point", ["x", "y"]), ABC): """No error - functional namedtuple inheritance allows multiple inheritance.""" + class Url(NamedTuple("Url", [("host", str), ("port", int)]), ABC): """No error - typing.NamedTuple functional syntax also allows multiple inheritance.""" + p = Point(1, 2) reveal_type(p.x) # revealed: Any reveal_type(p.y) # revealed: Any @@ -956,11 +992,13 @@ methods (`__lt__`, `__le__`, `__gt__`, `__ge__`). from collections import namedtuple from typing import NamedTuple + # typing.NamedTuple inherits tuple methods class Point(NamedTuple): x: int y: int + p = Point(1, 2) reveal_type(p.count(1)) # revealed: int reveal_type(p.index(2)) # revealed: int @@ -980,22 +1018,26 @@ from collections import namedtuple from functools import total_ordering from typing import NamedTuple + # No error - __lt__ is inherited from the tuple base class @total_ordering class Point(namedtuple("Point", "x y")): ... + p1 = Point(1, 2) p2 = Point(3, 4) # TODO: should be `bool`, not `Any | Literal[False]` reveal_type(p1 < p2) # revealed: Any | Literal[False] reveal_type(p1 <= p2) # revealed: Any | Literal[True] + # Same for typing.NamedTuple - no error @total_ordering class Person(NamedTuple): name: str age: int + alice = Person("Alice", 30) bob = Person("Bob", 25) reveal_type(alice < bob) # revealed: bool @@ -1010,13 +1052,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 @@ -1032,12 +1077,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 @@ -1059,6 +1106,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 @@ -1139,10 +1187,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.__slots__) # revealed: tuple[()] @@ -1170,12 +1220,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 ``` @@ -1224,6 +1277,7 @@ from typing_extensions import Self field_names = ["host", "port"] + class Url(namedtuple("Url", field_names)): def with_port(self, port: int) -> Self: # Fields are unknown, so attribute access returns `Any`. @@ -1267,8 +1321,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) ``` @@ -1300,9 +1356,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]): ... ``` @@ -1315,10 +1373,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 @@ -1342,6 +1402,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 @@ -1355,12 +1416,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) ``` @@ -1373,6 +1436,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 @@ -1411,9 +1475,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() @@ -1432,9 +1498,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 ``` @@ -1445,13 +1513,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 ``` @@ -1486,6 +1557,7 @@ assign to these attributes (without type annotations) will raise an `AttributeEr ```py from typing import NamedTuple + class F(NamedTuple): x: int @@ -1519,6 +1591,7 @@ However, other attributes (including those starting with underscores) can be ass ```py from typing import NamedTuple + class G(NamedTuple): x: int @@ -1535,6 +1608,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, @@ -1548,6 +1622,7 @@ The check also applies to assignments within conditional blocks: ```py from typing import NamedTuple + class I(NamedTuple): x: int @@ -1561,6 +1636,7 @@ Method definitions with prohibited names are also flagged: ```py from typing import NamedTuple + class J(NamedTuple): x: int @@ -1580,9 +1656,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 @@ -1597,6 +1675,7 @@ instantiating the class at runtime: from dataclasses import dataclass from typing import NamedTuple + @dataclass # error: [invalid-dataclass] "`NamedTuple` class `Foo` cannot be decorated with `@dataclass`" class Foo(NamedTuple): @@ -1610,6 +1689,7 @@ The same error occurs with `dataclasses.dataclass` used with parentheses: from dataclasses import dataclass from typing import NamedTuple + @dataclass() # error: [invalid-dataclass] class Bar(NamedTuple): @@ -1622,6 +1702,7 @@ It also applies when using `frozen=True` or other dataclass parameters: from dataclasses import dataclass from typing import NamedTuple + @dataclass(frozen=True) # error: [invalid-dataclass] class Baz(NamedTuple): @@ -1634,9 +1715,11 @@ Classes that inherit from a `NamedTuple` class also cannot be decorated with `@d from dataclasses import dataclass from typing import NamedTuple + class Base(NamedTuple): x: int + @dataclass # error: [invalid-dataclass] class Child(Base): @@ -1650,11 +1733,13 @@ from dataclasses import dataclass from collections import namedtuple from typing import NamedTuple + @dataclass # error: [invalid-dataclass] class Foo(namedtuple("Foo", ["x", "y"])): pass + @dataclass # error: [invalid-dataclass] class Bar(NamedTuple("Bar", [("x", int), ("y", str)])): @@ -1678,9 +1763,11 @@ X = dataclass(NamedTuple("X", [("x", int)])) ```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" @@ -1700,34 +1787,41 @@ This is a regression test for . from typing import NamedTuple, Generic, TypeVar from typing_extensions import Self + class Base(NamedTuple): x: int y: int + class Child(Base): def __new__(cls, x: int, y: int) -> Self: instance = super().__new__(cls, x, y) reveal_type(instance) # revealed: Self@__new__ return instance + reveal_type(Child(1, 2)) # revealed: Child T = TypeVar("T") + class GenericBase(NamedTuple, Generic[T]): x: T + class ConcreteChild(GenericBase[str]): def __new__(cls, x: str) -> "ConcreteChild": instance = super().__new__(cls, x) reveal_type(instance) # revealed: Self@__new__ return instance + class GenericChild(GenericBase[T]): def __new__(cls, x: T) -> Self: instance = super().__new__(cls, x) reveal_type(instance) # revealed: @Todo(super in generic class) return instance + reveal_type(GenericChild(x=3.14)) # revealed: GenericChild[int | float] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index 6fb3e13ff8c983..66d844b675e96e 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 @@ -328,9 +347,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 +377,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 +413,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 +429,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 +445,10 @@ from it. ```py from abc import ABCMeta + class CustomAbstractMetaclass(ABCMeta): ... + class Fine(metaclass=CustomAbstractMetaclass): @overload @abstractmethod @@ -430,6 +457,7 @@ class Fine(metaclass=CustomAbstractMetaclass): @abstractmethod def f(self, x: str) -> str: ... + class Foo: @overload @abstractmethod @@ -451,6 +479,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 +499,7 @@ an `if TYPE_CHECKING` block: from typing import overload, TYPE_CHECKING if TYPE_CHECKING: + @overload def a() -> str: ... @overload @@ -481,25 +511,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 +557,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 +588,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 +612,7 @@ from __future__ import annotations from typing import overload + class CheckStaticMethod: @overload def method1(x: int) -> int: ... @@ -619,6 +665,7 @@ from __future__ import annotations from typing import overload + class CheckClassMethod: def __init__(self, x: int) -> None: self.x = x @@ -683,6 +730,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: ... @@ -723,14 +771,12 @@ class Foo: def method1(self, x: int) -> int: ... @overload def method1(self, x: str) -> str: ... - @overload def method2(self, x: int) -> int: ... @final @overload # error: [invalid-overload] def method2(self, x: str) -> str: ... - @overload def method3(self, x: int) -> int: ... @final @@ -752,6 +798,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: ... @@ -760,6 +807,7 @@ class Base: def method(self, x: int | str) -> int | str: return x + class Sub1(Base): @overload def method(self, x: int) -> int: ... @@ -769,6 +817,7 @@ class Sub1(Base): def method(self, x: int | str) -> int | str: return x + class Sub2(Base): @overload def method(self, x: int) -> int: ... @@ -779,6 +828,7 @@ class Sub2(Base): def method(self, x: int | str) -> int | str: return x + class Sub3(Base): @overload @override diff --git a/crates/ty_python_semantic/resources/mdtest/override.md b/crates/ty_python_semantic/resources/mdtest/override.md index 31aafc2a2555a0..a6bc8b5edc2a05 100644 --- a/crates/ty_python_semantic/resources/mdtest/override.md +++ b/crates/ty_python_semantic/resources/mdtest/override.md @@ -19,10 +19,8 @@ class A: class Parent: def foo(self): ... - @property def my_property1(self) -> int: ... - @property def my_property2(self) -> int: ... @@ -30,63 +28,47 @@ class Parent: @classmethod def class_method1(cls) -> int: ... - @staticmethod def static_method1() -> int: ... - @classmethod def class_method2(cls) -> int: ... - @staticmethod def static_method2() -> int: ... - @lossy_decorator def decorated_1(self): ... - @lossy_decorator def decorated_2(self): ... - @lossy_decorator def decorated_3(self): ... class Child(Parent): @override def foo(self): ... # fine: overrides `Parent.foo` - @property @override def my_property1(self) -> int: ... # fine: overrides `Parent.my_property1` - @override @property def my_property2(self) -> int: ... # fine: overrides `Parent.my_property2` - @override def baz(self): ... # fine: overrides `Parent.baz` - @classmethod @override def class_method1(cls) -> int: ... # fine: overrides `Parent.class_method1` - @staticmethod @override def static_method1() -> int: ... # fine: overrides `Parent.static_method1` - @override @classmethod def class_method2(cls) -> int: ... # fine: overrides `Parent.class_method2` - @override @staticmethod def static_method2() -> int: ... # fine: overrides `Parent.static_method2` - @override def decorated_1(self): ... # fine: overrides `Parent.decorated_1` - @override @lossy_decorator def decorated_2(self): ... # fine: overrides `Parent.decorated_2` - @lossy_decorator @override def decorated_3(self): ... # fine: overrides `Parent.decorated_3` @@ -96,37 +78,28 @@ class OtherChild(Parent): ... class Grandchild(OtherChild): @override def foo(self): ... # fine: overrides `Parent.foo` - @override @property def my_property1(self) -> int: ... # fine: overrides `Parent.my_property1` - @override def baz(self): ... # fine: overrides `Parent.baz` - @classmethod @override def class_method1(cls) -> int: ... # fine: overrides `Parent.class_method1` - @staticmethod @override def static_method1() -> int: ... # fine: overrides `Parent.static_method1` - @override @classmethod def class_method2(cls) -> int: ... # fine: overrides `Parent.class_method2` - @override @staticmethod def static_method2() -> int: ... # fine: overrides `Parent.static_method2` - @override def decorated_1(self): ... # fine: overrides `Parent.decorated_1` - @override @lossy_decorator def decorated_2(self): ... # fine: overrides `Parent.decorated_2` - @lossy_decorator @override def decorated_3(self): ... # fine: overrides `Parent.decorated_3` @@ -134,41 +107,32 @@ class Grandchild(OtherChild): class Invalid: @override def ___reprrr__(self): ... # error: [invalid-explicit-override] - @override @classmethod def foo(self): ... # error: [invalid-explicit-override] - @classmethod @override def bar(self): ... # error: [invalid-explicit-override] - @staticmethod @override def baz(): ... # error: [invalid-explicit-override] - @override @staticmethod def eggs(): ... # error: [invalid-explicit-override] - @property @override def bad_property1(self) -> int: ... # error: [invalid-explicit-override] - @override @property def bad_property2(self) -> int: ... # error: [invalid-explicit-override] - @property @override def bad_settable_property(self) -> int: ... # error: [invalid-explicit-override] @bad_settable_property.setter def bad_settable_property(self, x: int) -> None: ... - @lossy_decorator @override def lossy(self): ... # TODO: should emit `invalid-explicit-override` here - @override @lossy_decorator def lossy2(self): ... # TODO: should emit `invalid-explicit-override` here @@ -179,7 +143,6 @@ class LiskovViolatingButNotOverrideViolating(Parent): @override @property def foo(self) -> int: ... - @override def my_property1(self) -> int: ... @@ -189,7 +152,6 @@ class LiskovViolatingButNotOverrideViolating(Parent): @staticmethod @override def class_method1() -> int: ... # error: [invalid-method-override] - @classmethod @override def static_method1(cls) -> int: ... @@ -228,24 +190,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 +222,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 +277,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 +296,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): ... ``` @@ -326,6 +314,7 @@ def coinflip() -> bool: class Foo: if coinflip(): def method(self, x): ... + elif coinflip(): @overload @override @@ -335,19 +324,21 @@ class Foo: if coinflip(): def method2(self, x): ... + elif coinflip(): @overload @override def method2(self, x: str) -> str: ... @overload def method2(self, x: int) -> int: ... + else: - # TODO: not sure why this is being emitted on this line rather than on - # the first overload in the `elif` block? Ideally it would be emitted - # on the first reachable definition, but perhaps this is due to the way - # name lookups are deferred in stub files...? -- AW - @override - def method2(self, x): ... # error: [invalid-explicit-override] + # TODO: not sure why this is being emitted on this line rather than on + # the first overload in the `elif` block? Ideally it would be emitted + # on the first reachable definition, but perhaps this is due to the way + # name lookups are deferred in stub files...? -- AW + @override + def method2(self, x): ... # error: [invalid-explicit-override] ``` ## Definitions in statically known branches @@ -404,6 +395,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: ... @@ -452,7 +444,6 @@ class Spam: @override # error: [invalid-overload] "`@override` decorator should be applied only to the first overload" def foo(self, x: int) -> int: ... - @overload @override def bar(self, x: str) -> str: ... # error: [invalid-explicit-override] @@ -460,7 +451,6 @@ class Spam: @override # error: [invalid-overload] "`@override` decorator should be applied only to the first overload" def bar(self, x: int) -> int: ... - @overload @override def baz(self, x: str) -> str: ... # error: [invalid-explicit-override] @@ -507,13 +497,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 7251d6f62cdfa0..e02aa7b8ade674 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)) @@ -360,13 +392,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 +415,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 0b39f45bb44b3d..3039f819c68682 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -48,6 +48,7 @@ type IntOrStrOrBytes = IntOrStr | bytes x: IntOrStrOrBytes = 1 + def f() -> None: reveal_type(x) # revealed: int | str | bytes ``` @@ -69,6 +70,7 @@ y: MyIntOrStr = None ```py type T = tuple[int, str] + def f(x: T): a, b = x reveal_type(a) # revealed: int @@ -83,9 +85,11 @@ eager) nested scope. ```py type Alias = Foo | str + def f(x: Alias): reveal_type(x) # revealed: Foo | str + class Foo: pass ``` @@ -97,6 +101,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 +113,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 +150,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 +160,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 +187,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] @@ -222,6 +243,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 +256,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 +272,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 +286,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 +295,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 +307,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 +317,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 +337,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 +352,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 +372,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 +380,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 +399,7 @@ from typing import Callable type C = Callable[[], C | None] + def _(x: C): reveal_type(x) # revealed: () -> C | None ``` @@ -385,12 +425,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 +447,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 +459,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 +473,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 +485,7 @@ def f(x: A): ```py type X = tuple[X, int] + def _(x: X): reveal_type(x is x) # revealed: bool ``` @@ -449,6 +496,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 +510,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 f0f88ae050f34b..a74a63bfb51730 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 19cb4a8edf1bdf..386eda14568aba 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -38,6 +38,7 @@ class's bases: ```py class Foo(Protocol, Protocol): ... # error: [duplicate-base] + reveal_mro(Foo) # revealed: (, Unknown, ) ``` @@ -50,19 +51,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 +81,7 @@ simultaneously: class DuplicateBases(Protocol, Protocol[T]): # error: [duplicate-base] x: T + # revealed: (, Unknown, ) reveal_mro(DuplicateBases) ``` @@ -91,8 +98,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 +112,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 +135,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 +148,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 +172,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 +203,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 +211,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 +233,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 +325,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 +346,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)) @@ -369,10 +405,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 +435,7 @@ via `typing_extensions`. ```py from typing_extensions import Protocol, get_protocol_members + class Foo(Protocol): x: int @@ -412,6 +452,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 +494,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 +545,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 +555,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)) ``` @@ -575,6 +628,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) ``` @@ -745,27 +799,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 +841,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 +886,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 +902,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 +928,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 +945,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 +968,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 +991,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 +1051,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 +1064,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 +1079,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 +1110,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 +1118,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 +1172,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 +1193,7 @@ checkers: def needs_something_hashable(x: Hashable): hash(x) + needs_something_hashable([]) ``` @@ -1118,9 +1210,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 +1243,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 +1258,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 ``` @@ -1196,12 +1293,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 +1313,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 +1334,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 +1383,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 +1405,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 +1418,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 +1445,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 +1477,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 +1514,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 +1541,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 +1583,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 +1633,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 +1689,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 +1721,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 +1756,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 +1775,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 +1803,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 +1831,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 +1851,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 +1871,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 +1903,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 +1920,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 +1951,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 +1965,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 +1984,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 +1992,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 +2017,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 +2039,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 +2100,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 +2131,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 +2148,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 +2169,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 +2197,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)) @@ -2065,41 +2288,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 +2367,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 +2403,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 +2500,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 +2517,7 @@ class P3(Protocol): @z.setter def z(self, value: int) -> None: ... + class P4(Protocol): @property def y(self) -> str: ... @@ -2275,6 +2526,7 @@ class P4(Protocol): @z.setter def z(self, value: int) -> None: ... + static_assert(is_equivalent_to(P1, P2)) # TODO: should pass @@ -2286,8 +2538,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 +2559,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 +2597,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 +2640,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 +2651,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 +2672,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 +2700,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: [isinstance-against-protocol] reveal_type(arg) # revealed: HasX @@ -2445,10 +2723,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,16 +2747,19 @@ satisfy two conditions: class OnlyMethodMembers(Protocol): def method(self) -> None: ... + @runtime_checkable class OnlyClassmethodMembers(Protocol): @classmethod def method(cls) -> None: ... + @runtime_checkable class MultipleNonMethodMembers(Protocol): b: int a: int + def f(arg1: type): # error: [isinstance-against-protocol] "`RuntimeCheckableHasX` cannot be used as the second argument to `issubclass` as it is a protocol with non-method members" if issubclass(arg1, RuntimeCheckableHasX): @@ -2510,13 +2793,16 @@ will raise `TypeError` at runtime. We emit an error for these cases: ```py from typing_extensions import Protocol, runtime_checkable + class HasX(Protocol): x: int + @runtime_checkable class RuntimeCheckableHasX(Protocol): x: int + def match_non_runtime_checkable(arg: object): match arg: case HasX(): # error: [isinstance-against-protocol] @@ -2524,6 +2810,7 @@ def match_non_runtime_checkable(arg: object): case _: reveal_type(arg) # revealed: ~HasX + def match_runtime_checkable(arg: object): match arg: case RuntimeCheckableHasX(): # no error! @@ -2538,6 +2825,7 @@ The same applies to nested class patterns: class Wrapper: inner: object + def match_nested_non_runtime_checkable(arg: Wrapper): match arg: case Wrapper(inner=HasX()): # error: [isinstance-against-protocol] @@ -2551,9 +2839,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 ``` @@ -2564,15 +2854,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] @@ -2584,9 +2878,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] ``` @@ -2598,9 +2894,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] @@ -2620,6 +2918,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] @@ -2632,9 +2931,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)) @@ -2643,21 +2944,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)) ``` @@ -2671,9 +2977,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 @@ -2692,16 +3000,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)) @@ -2730,11 +3042,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 @@ -2752,10 +3067,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] ``` @@ -2767,11 +3084,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 @@ -2788,14 +3107,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)) @@ -2803,24 +3125,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 @@ -2830,25 +3158,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 @@ -2921,12 +3254,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)) ``` @@ -2936,12 +3272,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)) ``` @@ -2952,12 +3291,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)): @@ -2996,12 +3338,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) ``` @@ -3013,12 +3358,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) ``` @@ -3107,15 +3455,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] @@ -3165,19 +3517,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)) @@ -3209,11 +3566,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)] @@ -3230,18 +3590,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])) @@ -3286,24 +3651,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 f8e522e767202d..43e143368978f5 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 + 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" ``` @@ -438,6 +493,7 @@ if flag(): def g(x: int) -> int: ... @overload def g(x: str) -> str: ... + else: g: str @@ -450,10 +506,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/statically_known_branches.md b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md index 4b7e73498c6666..9a8249051f4617 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/terminal_statements.md b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md index 25d458ae67b312..afcde95edfd436 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 09315697dac76b..ce7b2ec4f3f9f1 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` @@ -96,6 +99,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 +111,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 +137,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 +193,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 +241,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 +336,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 +420,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 +438,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 +459,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 +481,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 +498,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 +513,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/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index ed1ec1cbf97c28..c00bca9ca66e49 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -60,12 +60,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 +79,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 +136,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 +158,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 +195,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 +223,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 +341,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 +367,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 +399,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 +419,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 +438,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,14 +465,17 @@ positional argument passing or keyword unpacking: ```py from typing import TypedDict + class Data(TypedDict): id: int name: str value: float + def process_data_positional(data: Data) -> Data: return Data(data) + def process_data_unpacking(data: Data) -> Data: return Data(**data) ``` @@ -449,17 +485,21 @@ Constructing from a compatible TypedDict (with same fields) works: ```py from typing import TypedDict + class PersonBase(TypedDict): name: str age: int + class PersonAlias(TypedDict): name: str age: int + def copy_person(p: PersonBase) -> PersonAlias: return PersonAlias(**p) + def copy_person_positional(p: PersonBase) -> PersonAlias: return PersonAlias(p) ``` @@ -470,13 +510,16 @@ behavior when passing all keys as explicit keyword arguments: ```py from typing import TypedDict + class Person(TypedDict): name: str age: int + class Employee(Person): employee_id: int + def get_person_from_employee(emp: Employee) -> Person: # error: [invalid-key] "Unknown key "employee_id" for TypedDict `Person`" return Person(**emp) @@ -498,18 +541,22 @@ Type mismatches in unpacked TypedDict fields should be detected: ```py from typing import TypedDict + class Source(TypedDict): name: int # Note: int, not str age: int + class Target(TypedDict): name: str age: int + def convert(src: Source) -> Target: # error: [invalid-argument-type] return Target(**src) + def convert_positional(src: Source) -> Target: # error: [invalid-argument-type] return Target(src) @@ -521,13 +568,16 @@ have any keys: ```py from typing import Any, TypedDict, Never + class Info(TypedDict): name: str value: int + def unpack_never(data: Never) -> Info: return Info(**data) + def unpack_any(data: Any) -> Info: return Info(**data) ``` @@ -561,10 +611,12 @@ Intersection types containing a TypedDict (e.g., from truthiness narrowing) are ```py from typing import TypedDict + class OptionalInfo(TypedDict, total=False): id: int name: str + def process_truthy(data: OptionalInfo) -> OptionalInfo: if data: reveal_type(data) # revealed: OptionalInfo & ~AlwaysFalsy @@ -572,6 +624,7 @@ def process_truthy(data: OptionalInfo) -> OptionalInfo: return OptionalInfo(data) return {} + def process_truthy_unpacking(data: OptionalInfo) -> OptionalInfo: if data: return OptionalInfo(**data) @@ -586,21 +639,26 @@ has all their keys. For keys that appear in multiple TypedDicts, the types are i from typing import TypedDict from ty_extensions import Intersection + class TdA(TypedDict): name: str a_only: int + class TdB(TypedDict): name: str b_only: int + class NameOnly(TypedDict): name: str + # Positional form allows extra keys (like assignment) def construct_from_intersection(data: Intersection[TdA, TdB]) -> NameOnly: return NameOnly(data) + # Unpacking form flags extra keys as errors def construct_from_intersection_unpacking(data: Intersection[TdA, TdB]) -> NameOnly: # error: [invalid-key] "Unknown key "a_only" for TypedDict `NameOnly`" @@ -616,10 +674,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") @@ -654,18 +714,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 @@ -675,9 +738,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 @@ -716,16 +781,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)) @@ -742,12 +811,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)) @@ -756,12 +828,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)) @@ -781,42 +856,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)) @@ -925,10 +1013,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 @@ -951,12 +1041,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`" @@ -994,24 +1087,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)) @@ -1025,17 +1124,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] ``` @@ -1046,24 +1149,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)) @@ -1076,21 +1184,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)) @@ -1130,20 +1242,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)) @@ -1175,35 +1291,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)) ``` @@ -1214,25 +1339,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)) ``` @@ -1243,25 +1374,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)) ``` @@ -1276,12 +1413,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] @@ -1294,16 +1434,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, @@ -1345,18 +1489,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 @@ -1364,13 +1512,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" @@ -1378,6 +1529,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" @@ -1388,6 +1540,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 @@ -1396,12 +1549,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 @@ -1409,6 +1564,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" @@ -1421,11 +1577,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 @@ -1441,6 +1599,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" @@ -1455,11 +1614,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] @@ -1508,10 +1669,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: @@ -1525,10 +1688,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 @@ -1543,10 +1708,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] @@ -1579,6 +1746,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) ``` @@ -1589,17 +1757,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 @@ -1617,15 +1789,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 @@ -1638,14 +1813,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 @@ -1660,14 +1838,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 @@ -1692,22 +1873,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)} @@ -1773,10 +1959,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} @@ -1794,10 +1982,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))) ``` @@ -1840,6 +2030,7 @@ x: TypedDict = {"name": "Alice"} ```py from typing_extensions import Required, NotRequired, ReadOnly + def bad( # error: [invalid-type-form] "`Required` is not allowed in function parameter annotations" a: Required[int], @@ -1857,13 +2048,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}) ``` @@ -1873,10 +2067,12 @@ x: Person = MyDict({"name": "Alice", "age": 30}) ```py from typing import TypedDict + class Person(TypedDict): name: str age: int | None + def _(obj: object, obj2: type): # error: [isinstance-against-typed-dict] "`TypedDict` class `Person` cannot be used as the second argument to `isinstance`" isinstance(obj, Person) @@ -1906,30 +2102,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"} @@ -1943,10 +2148,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] ``` @@ -1967,10 +2174,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) @@ -1989,16 +2198,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 @@ -2020,16 +2233,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. @@ -2039,11 +2256,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)) ``` @@ -2054,30 +2273,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)) @@ -2097,12 +2326,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. @@ -2115,12 +2347,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)) @@ -2136,9 +2371,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)) @@ -2151,24 +2388,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)) @@ -2188,9 +2431,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)) @@ -2211,12 +2456,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)) @@ -2233,9 +2481,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)) @@ -2280,11 +2530,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])) @@ -2310,18 +2563,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 @@ -2339,6 +2597,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 @@ -2362,9 +2621,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... @@ -2373,12 +2634,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 ``` @@ -2389,6 +2652,7 @@ Intersections containing a TypedDict with literal fields can be narrowed with eq from ty_extensions import Intersection from typing import Any + def _(x: Intersection[Foo, Any]): if x["tag"] == "foo": reveal_type(x) # revealed: Foo & Any @@ -2402,6 +2666,7 @@ But intersections with non-literal fields cannot be narrowed: from ty_extensions import Intersection from typing import Any + def _(x: Intersection[NonLiteralTD, Any]): if x["tag"] == 42: reveal_type(x) # revealed: NonLiteralTD & Any @@ -2417,9 +2682,11 @@ though `str` and `int` are disjoint, we can't narrow here because a `str` subcla from ty_extensions import Intersection from typing import Any + class StrTagTD(TypedDict): tag: str + def _(x: Intersection[StrTagTD, Any]): if x["tag"] == 42: reveal_type(x) # revealed: StrTagTD & Any @@ -2436,11 +2703,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 @@ -2454,12 +2723,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... @@ -2473,11 +2745,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)) ``` @@ -2490,6 +2764,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 @@ -2570,9 +2845,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": @@ -2638,8 +2915,10 @@ PEP 695 type aliases also work with `in`/`not in` narrowing: class Baz(TypedDict): baz: int + type ThingWithBaz = Foo | Baz + def test_in(x: ThingWithBaz): if "baz" not in x: reveal_type(x) # revealed: Foo @@ -2653,12 +2932,14 @@ Nested PEP 695 type aliases (an alias referring to another alias) also work: type Inner = Foo | Bar type Outer = Inner + def test_nested_if(x: Outer): if x["tag"] == "foo": reveal_type(x) # revealed: Foo else: reveal_type(x) # revealed: Bar + def test_nested_match(x: Outer): match x["tag"]: case "foo": @@ -2666,9 +2947,11 @@ def test_nested_match(x: Outer): case "bar": reveal_type(x) # revealed: Bar + type InnerWithBaz = Foo | Baz type OuterWithBaz = InnerWithBaz + def test_nested_in(x: OuterWithBaz): if "baz" not in x: reveal_type(x) # revealed: Foo @@ -2687,6 +2970,7 @@ not allowed to have a value. ```py from typing import TypedDict + class Foo(TypedDict): """docstring""" @@ -2698,12 +2982,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): ... ``` @@ -2728,6 +3014,7 @@ when instantiating the class at runtime: from dataclasses import dataclass from typing import TypedDict + @dataclass # error: [invalid-dataclass] "`TypedDict` class `Foo` cannot be decorated with `@dataclass`" class Foo(TypedDict): @@ -2741,6 +3028,7 @@ The same error occurs with `dataclasses.dataclass` used with parentheses: from dataclasses import dataclass from typing import TypedDict + @dataclass() # error: [invalid-dataclass] class Bar(TypedDict): @@ -2753,6 +3041,7 @@ It also applies when using `frozen=True` or other dataclass parameters: from dataclasses import dataclass from typing import TypedDict + @dataclass(frozen=True) # error: [invalid-dataclass] class Baz(TypedDict): @@ -2766,9 +3055,11 @@ TypedDict classes and cannot be decorated with `@dataclass`: from dataclasses import dataclass from typing import TypedDict + class Base(TypedDict): x: int + @dataclass # error: [invalid-dataclass] class Child(Base): @@ -2782,6 +3073,7 @@ for it. Once functional `TypedDict` support is added, this should also emit an e from dataclasses import dataclass from typing import TypedDict + # TODO: This should error once functional TypedDict is supported @dataclass class Foo(TypedDict("Foo", {"x": int, "y": str})): @@ -2797,8 +3089,10 @@ A `TypedDict` may not inherit from a non-`TypedDict`: ```py from typing import TypedDict + class Foo(TypedDict, int): ... # error: [invalid-typed-dict-header] + # This even fails at runtime! class Foo2(TypedDict, object): ... # error: [invalid-typed-dict-header] ``` @@ -2807,6 +3101,8 @@ It is invalid to pass non-`bool`s to the `total` and `closed` keyword arguments: ```py class Bar(TypedDict, total=42): ... # error: [invalid-argument-type] + + class Baz(TypedDict, closed=None): ... # error: [invalid-argument-type] ``` @@ -2830,8 +3126,10 @@ Specifying a custom metaclass is not permitted: ```py from abc import ABCMeta + class Spam(TypedDict, metaclass=ABCMeta): ... # error: [invalid-typed-dict-header] + # This one works at runtime, but the metaclass is still `typing._TypedDictMeta`, # so there doesn't seem to be any reason why you'd want to do this class Ham(TypedDict, metaclass=type): ... # error: [invalid-typed-dict-header] diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md index 63f63d355325c7..39680bbd88ec16 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], u3: type[int] | type[str]) -> None: reveal_type(u1) # revealed: int | str reveal_type(u2) # revealed: Literal[0, 1] @@ -29,10 +30,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 @@ -45,6 +48,7 @@ Unions with `object` can be simplified to `object`: ```py from typing_extensions import Never, Any + def _( u1: int | object, u2: object | int, @@ -68,6 +72,7 @@ def _( ```py from typing import Literal + def _( u1: (int | str) | bytes, u2: int | (str | bytes), @@ -85,6 +90,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: @@ -101,6 +107,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], @@ -122,11 +129,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], @@ -142,6 +151,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], @@ -155,6 +165,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 @@ -168,6 +179,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 @@ -181,6 +193,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 ``` @@ -192,9 +205,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], @@ -317,6 +334,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[()], diff --git a/crates/ty_python_semantic/resources/mdtest/unpacking.md b/crates/ty_python_semantic/resources/mdtest/unpacking.md index b8889f93ccca60..3f6b038d0589ca 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 @@ -607,6 +611,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 +820,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 +894,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 +908,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 +927,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 +1015,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 +1033,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 +1059,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 +1075,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 +1091,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 +1108,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 +1133,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 +1209,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 +1226,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 9ff84162ac750d..c2a60cbba58b4c 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: @@ -611,5 +627,6 @@ class AwesomeAPI: ... ```py import module + def f(x: module.AwesomeAPI): ... # error: [invalid-type-form] ```