From 3ed537e9f1a166d51f4ec4f94191645e695a5a25 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 28 Nov 2025 03:20:24 -0500 Subject: [PATCH] [ty] Support `type[T]` with type variables (#21650) ## Summary Adds support for `type[T]`, where `T` is a type variable. - Resolves https://github.com/astral-sh/ty/issues/501 - Resolves https://github.com/astral-sh/ty/issues/783 - Resolves https://github.com/astral-sh/ty/issues/662 --- crates/ty_ide/src/completion.rs | 2 +- .../resources/mdtest/annotations/self.md | 6 +- .../resources/mdtest/attributes.md | 4 +- .../resources/mdtest/class/super.md | 12 +- .../resources/mdtest/enums.md | 4 +- .../mdtest/generics/legacy/functions.md | 2 +- .../mdtest/generics/legacy/variables.md | 11 +- .../mdtest/generics/pep695/functions.md | 2 +- .../mdtest/generics/pep695/variables.md | 11 +- .../mdtest/ide_support/all_members.md | 13 +- .../resources/mdtest/protocols.md | 6 +- ...licit_Super_Objec…_(f9e5e48e3a4a4c12).snap | 210 ++++++------ .../resources/mdtest/type_of/generics.md | 247 ++++++++++++++ crates/ty_python_semantic/src/types.rs | 322 +++++++++++++----- .../src/types/bound_super.rs | 22 +- .../src/types/diagnostic.rs | 2 +- .../ty_python_semantic/src/types/display.rs | 5 + .../ty_python_semantic/src/types/generics.rs | 13 +- .../src/types/ide_support.rs | 22 +- .../src/types/infer/builder.rs | 4 +- .../types/infer/builder/type_expression.rs | 23 +- crates/ty_python_semantic/src/types/narrow.rs | 4 + .../src/types/subclass_of.rs | 211 ++++++++++-- .../src/types/type_ordering.rs | 5 + 24 files changed, 867 insertions(+), 296 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/type_of/generics.md diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 9e1233cce1..4387c09346 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -2898,7 +2898,7 @@ Answer. __itemsize__ :: int __iter__ :: bound method .__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT@__iter__] __len__ :: bound method .__len__() -> int - __members__ :: MappingProxyType[str, Unknown] + __members__ :: MappingProxyType[str, Answer] __module__ :: str __mro__ :: tuple[type, ...] __name__ :: str diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 8fc0802bac..4be59a5a63 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -260,15 +260,13 @@ class Shape: @classmethod def bar(cls: type[Self]) -> Self: - # TODO: type[Shape] - reveal_type(cls) # revealed: @Todo(unsupported type[X] special form) + reveal_type(cls) # revealed: type[Self@bar] return cls() class Circle(Shape): ... reveal_type(Shape().foo()) # revealed: Shape -# TODO: Shape -reveal_type(Shape.bar()) # revealed: Unknown +reveal_type(Shape.bar()) # revealed: Shape ``` ## Attributes diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index bef0451ea2..f2eb886223 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2650,7 +2650,7 @@ reveal_type(C().x) # revealed: int ```py import enum -reveal_type(enum.Enum.__members__) # revealed: MappingProxyType[str, Unknown] +reveal_type(enum.Enum.__members__) # revealed: MappingProxyType[str, Enum] class Answer(enum.Enum): NO = 0 @@ -2658,7 +2658,7 @@ class Answer(enum.Enum): reveal_type(Answer.NO) # revealed: Literal[Answer.NO] reveal_type(Answer.NO.value) # revealed: Literal[0] -reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown] +reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Answer] ``` ## Divergent inferred implicit instance attribute types diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index b3996159ba..e4dd9b77bc 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -210,9 +210,7 @@ class BuilderMeta2(type): ) -> BuilderMeta2: # revealed: , > s = reveal_type(super()) - # TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501) - # revealed: Unknown - return reveal_type(s.__new__(cls, name, bases, dct)) + return reveal_type(s.__new__(cls, name, bases, dct)) # revealed: BuilderMeta2 class Foo[T]: x: T @@ -395,6 +393,14 @@ class E(Enum): reveal_type(super(E, E.X)) # revealed: , E> ``` +## `type[Self]` + +```py +class Foo: + def method(self): + super(self.__class__, self) +``` + ## Descriptor Behavior with Super Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 8ae0dce62f..43cd712a1d 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -15,10 +15,8 @@ reveal_type(Color.RED) # revealed: Literal[Color.RED] reveal_type(Color.RED.name) # revealed: Literal["RED"] reveal_type(Color.RED.value) # revealed: Literal[1] -# TODO: Should be `Color` or `Literal[Color.RED]` -reveal_type(Color["RED"]) # revealed: Unknown - # TODO: Could be `Literal[Color.RED]` to be more precise +reveal_type(Color["RED"]) # revealed: Color reveal_type(Color(1)) # revealed: Color reveal_type(Color.RED in Color) # revealed: bool diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 2bbe85b5ec..2fce911026 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -106,7 +106,7 @@ def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None: def takes_in_type(x: type[T]) -> type[T]: return x -reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form) +reveal_type(takes_in_type(int)) # revealed: type[int] ``` This also works when passing in arguments that are subclasses of the parameter type. diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index 5485fe4477..5917e340ab 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -383,8 +383,7 @@ def constrained(f: T): ## Meta-type -The meta-type of a typevar is the same as the meta-type of the upper bound, or the union of the -meta-types of the constraints: +The meta-type of a typevar is `type[T]`. ```py from typing import TypeVar @@ -392,22 +391,22 @@ from typing import TypeVar T_normal = TypeVar("T_normal") def normal(x: T_normal): - reveal_type(type(x)) # revealed: type + reveal_type(type(x)) # revealed: type[T_normal@normal] T_bound_object = TypeVar("T_bound_object", bound=object) def bound_object(x: T_bound_object): - reveal_type(type(x)) # revealed: type + reveal_type(type(x)) # revealed: type[T_bound_object@bound_object] T_bound_int = TypeVar("T_bound_int", bound=int) def bound_int(x: T_bound_int): - reveal_type(type(x)) # revealed: type[int] + reveal_type(type(x)) # revealed: type[T_bound_int@bound_int] T_constrained = TypeVar("T_constrained", int, str) def constrained(x: T_constrained): - reveal_type(type(x)) # revealed: type[int] | type[str] + reveal_type(type(x)) # revealed: type[T_constrained@constrained] ``` ## Cycles diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 5db84cfd5a..8af1b948ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -101,7 +101,7 @@ def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None: def takes_in_type[T](x: type[T]) -> type[T]: return x -reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form) +reveal_type(takes_in_type(int)) # revealed: type[int] ``` This also works when passing in arguments that are subclasses of the parameter type. diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md index 6c13dc559b..eaa4b8923e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -754,21 +754,20 @@ def constrained[T: (Callable[[], int], Callable[[], str])](f: T): ## Meta-type -The meta-type of a typevar is the same as the meta-type of the upper bound, or the union of the -meta-types of the constraints: +The meta-type of a typevar is `type[T]`. ```py def normal[T](x: T): - reveal_type(type(x)) # revealed: type + reveal_type(type(x)) # revealed: type[T@normal] def bound_object[T: object](x: T): - reveal_type(type(x)) # revealed: type + reveal_type(type(x)) # revealed: type[T@bound_object] def bound_int[T: int](x: T): - reveal_type(type(x)) # revealed: type[int] + reveal_type(type(x)) # revealed: type[T@bound_int] def constrained[T: (int, str)](x: T): - reveal_type(type(x)) # revealed: type[int] | type[str] + reveal_type(type(x)) # revealed: type[T@constrained] ``` ## Cycles diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md index 1a20940c0e..f0f7db664c 100644 --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -110,6 +110,11 @@ static_assert(not has_member(C(), "non_existent")) ### Class objects +```toml +[environment] +python-version = "3.12" +``` + Class-level attributes can also be accessed through the class itself: ```py @@ -154,7 +159,13 @@ static_assert(has_member(D, "meta_attr")) static_assert(has_member(D, "base_attr")) static_assert(has_member(D, "class_attr")) -def f(x: type[D]): +def _(x: type[D]): + static_assert(has_member(x, "meta_base_attr")) + static_assert(has_member(x, "meta_attr")) + static_assert(has_member(x, "base_attr")) + static_assert(has_member(x, "class_attr")) + +def _[T: D](x: type[T]): static_assert(has_member(x, "meta_base_attr")) static_assert(has_member(x, "meta_attr")) static_assert(has_member(x, "base_attr")) diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 1e1ac07aab..cfa4c68914 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -255,12 +255,10 @@ And it is also an error to use `Protocol` in type expressions: def f( x: Protocol, # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions" - y: type[Protocol], # TODO: should emit `[invalid-type-form]` here too + y: type[Protocol], # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions" ): reveal_type(x) # revealed: Unknown - - # TODO: should be `type[Unknown]` - reveal_type(y) # revealed: @Todo(unsupported type[X] special form) + reveal_type(y) # revealed: type[Unknown] # fmt: on ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap index c9fcea3d5a..75eefd748b 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap @@ -58,106 +58,104 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md 44 | ) -> BuilderMeta2: 45 | # revealed: , > 46 | s = reveal_type(super()) - 47 | # TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501) - 48 | # revealed: Unknown - 49 | return reveal_type(s.__new__(cls, name, bases, dct)) - 50 | - 51 | class Foo[T]: - 52 | x: T - 53 | - 54 | def method(self: Any): - 55 | reveal_type(super()) # revealed: , Any> - 56 | - 57 | if isinstance(self, Foo): - 58 | reveal_type(super()) # revealed: , Any> - 59 | - 60 | def method2(self: Foo[T]): - 61 | # revealed: , Foo[T@Foo]> - 62 | reveal_type(super()) - 63 | - 64 | def method3(self: Foo): - 65 | # revealed: , Foo[Unknown]> - 66 | reveal_type(super()) - 67 | - 68 | def method4(self: Self): - 69 | # revealed: , Foo[T@Foo]> - 70 | reveal_type(super()) - 71 | - 72 | def method5[S: Foo[int]](self: S, other: S) -> S: - 73 | # revealed: , Foo[int]> - 74 | reveal_type(super()) - 75 | return self - 76 | - 77 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S: - 78 | # revealed: , Foo[int]> | , Foo[str]> - 79 | reveal_type(super()) - 80 | return self - 81 | - 82 | def method7[S](self: S, other: S) -> S: - 83 | # error: [invalid-super-argument] - 84 | # revealed: Unknown - 85 | reveal_type(super()) - 86 | return self - 87 | - 88 | def method8[S: int](self: S, other: S) -> S: - 89 | # error: [invalid-super-argument] - 90 | # revealed: Unknown - 91 | reveal_type(super()) - 92 | return self - 93 | - 94 | def method9[S: (int, str)](self: S, other: S) -> S: - 95 | # error: [invalid-super-argument] - 96 | # revealed: Unknown - 97 | reveal_type(super()) - 98 | return self - 99 | -100 | def method10[S: Callable[..., str]](self: S, other: S) -> S: -101 | # error: [invalid-super-argument] -102 | # revealed: Unknown -103 | reveal_type(super()) -104 | return self + 47 | return reveal_type(s.__new__(cls, name, bases, dct)) # revealed: BuilderMeta2 + 48 | + 49 | class Foo[T]: + 50 | x: T + 51 | + 52 | def method(self: Any): + 53 | reveal_type(super()) # revealed: , Any> + 54 | + 55 | if isinstance(self, Foo): + 56 | reveal_type(super()) # revealed: , Any> + 57 | + 58 | def method2(self: Foo[T]): + 59 | # revealed: , Foo[T@Foo]> + 60 | reveal_type(super()) + 61 | + 62 | def method3(self: Foo): + 63 | # revealed: , Foo[Unknown]> + 64 | reveal_type(super()) + 65 | + 66 | def method4(self: Self): + 67 | # revealed: , Foo[T@Foo]> + 68 | reveal_type(super()) + 69 | + 70 | def method5[S: Foo[int]](self: S, other: S) -> S: + 71 | # revealed: , Foo[int]> + 72 | reveal_type(super()) + 73 | return self + 74 | + 75 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S: + 76 | # revealed: , Foo[int]> | , Foo[str]> + 77 | reveal_type(super()) + 78 | return self + 79 | + 80 | def method7[S](self: S, other: S) -> S: + 81 | # error: [invalid-super-argument] + 82 | # revealed: Unknown + 83 | reveal_type(super()) + 84 | return self + 85 | + 86 | def method8[S: int](self: S, other: S) -> S: + 87 | # error: [invalid-super-argument] + 88 | # revealed: Unknown + 89 | reveal_type(super()) + 90 | return self + 91 | + 92 | def method9[S: (int, str)](self: S, other: S) -> S: + 93 | # error: [invalid-super-argument] + 94 | # revealed: Unknown + 95 | reveal_type(super()) + 96 | return self + 97 | + 98 | def method10[S: Callable[..., str]](self: S, other: S) -> S: + 99 | # error: [invalid-super-argument] +100 | # revealed: Unknown +101 | reveal_type(super()) +102 | return self +103 | +104 | type Alias = Bar 105 | -106 | type Alias = Bar -107 | -108 | class Bar: -109 | def method(self: Alias): -110 | # revealed: , Bar> -111 | reveal_type(super()) -112 | -113 | def pls_dont_call_me(self: Never): -114 | # revealed: , Unknown> -115 | reveal_type(super()) -116 | -117 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): -118 | # revealed: , Bar> -119 | reveal_type(super()) -120 | -121 | class P(Protocol): -122 | def method(self: P): -123 | # revealed: , P> -124 | reveal_type(super()) -125 | -126 | class E(enum.Enum): -127 | X = 1 -128 | -129 | def method(self: E): -130 | match self: -131 | case E.X: -132 | # revealed: , E> -133 | reveal_type(super()) +106 | class Bar: +107 | def method(self: Alias): +108 | # revealed: , Bar> +109 | reveal_type(super()) +110 | +111 | def pls_dont_call_me(self: Never): +112 | # revealed: , Unknown> +113 | reveal_type(super()) +114 | +115 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): +116 | # revealed: , Bar> +117 | reveal_type(super()) +118 | +119 | class P(Protocol): +120 | def method(self: P): +121 | # revealed: , P> +122 | reveal_type(super()) +123 | +124 | class E(enum.Enum): +125 | X = 1 +126 | +127 | def method(self: E): +128 | match self: +129 | case E.X: +130 | # revealed: , E> +131 | reveal_type(super()) ``` # Diagnostics ``` error[invalid-super-argument]: `S@method7` is not an instance or subclass of `` in `super(, S@method7)` call - --> src/mdtest_snippet.py:85:21 + --> src/mdtest_snippet.py:83:21 | -83 | # error: [invalid-super-argument] -84 | # revealed: Unknown -85 | reveal_type(super()) +81 | # error: [invalid-super-argument] +82 | # revealed: Unknown +83 | reveal_type(super()) | ^^^^^^^ -86 | return self +84 | return self | info: Type variable `S` has `object` as its implicit upper bound info: `object` is not an instance or subclass of `` @@ -168,13 +166,13 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `S@method8` is not an instance or subclass of `` in `super(, S@method8)` call - --> src/mdtest_snippet.py:91:21 + --> src/mdtest_snippet.py:89:21 | -89 | # error: [invalid-super-argument] -90 | # revealed: Unknown -91 | reveal_type(super()) +87 | # error: [invalid-super-argument] +88 | # revealed: Unknown +89 | reveal_type(super()) | ^^^^^^^ -92 | return self +90 | return self | info: Type variable `S` has upper bound `int` info: `int` is not an instance or subclass of `` @@ -184,13 +182,13 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `S@method9` is not an instance or subclass of `` in `super(, S@method9)` call - --> src/mdtest_snippet.py:97:21 + --> src/mdtest_snippet.py:95:21 | -95 | # error: [invalid-super-argument] -96 | # revealed: Unknown -97 | reveal_type(super()) +93 | # error: [invalid-super-argument] +94 | # revealed: Unknown +95 | reveal_type(super()) | ^^^^^^^ -98 | return self +96 | return self | info: Type variable `S` has constraints `int, str` info: `int | str` is not an instance or subclass of `` @@ -200,13 +198,13 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `S@method10` is a type variable with an abstract/structural type as its bounds or constraints, in `super(, S@method10)` call - --> src/mdtest_snippet.py:103:21 + --> src/mdtest_snippet.py:101:21 | -101 | # error: [invalid-super-argument] -102 | # revealed: Unknown -103 | reveal_type(super()) + 99 | # error: [invalid-super-argument] +100 | # revealed: Unknown +101 | reveal_type(super()) | ^^^^^^^ -104 | return self +102 | return self | info: Type variable `S` has upper bound `(...) -> str` info: rule `invalid-super-argument` is enabled by default diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md new file mode 100644 index 0000000000..c134e2f533 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -0,0 +1,247 @@ +# `type[T]` + +`type[T]` with a type variable represents the class objects of `T`. + +```toml +[environment] +python-version = "3.13" +``` + +## Basic + +The meta-type of a typevar is `type[T]`. + +```py +def _[T](x: T): + reveal_type(type(x)) # revealed: type[T@_] +``` + +`type[T]` with an unbounded type variable represents any subclass of `object`. + +```py +def unbounded[T](x: type[T]) -> T: + reveal_type(x) # revealed: type[T@unbounded] + reveal_type(x.__repr__) # revealed: def __repr__(self) -> str + reveal_type(x.__init__) # revealed: def __init__(self) -> None + reveal_type(x.__qualname__) # revealed: str + reveal_type(x()) # revealed: T@unbounded + + return x() +``` + +`type[T]` with an upper bound of `T: A` represents any subclass of `A`. + +```py +class A: + x: str + + def __init__(self, value: str): ... + +class B(A): ... +class C: ... + +def upper_bound[T: A](x: type[T]) -> T: + reveal_type(x) # revealed: type[T@upper_bound] + reveal_type(x.__qualname__) # revealed: str + reveal_type(x("hello")) # revealed: T@upper_bound + + return x("hello") + +reveal_type(upper_bound(A)) # revealed: A +reveal_type(upper_bound(B)) # revealed: B + +# error: [invalid-argument-type] "Argument to function `upper_bound` is incorrect: Argument type `C` does not satisfy upper bound `A` of type variable `T`" +upper_bound(C) +``` + +`type[T]` with a constraints `T: (A, B)` represents exactly the class object `A`, or exactly `B`: + +```py +def constrained[T: (int, str)](x: type[T]) -> T: + reveal_type(x) # revealed: type[T@constrained] + reveal_type(x.__qualname__) # revealed: str + reveal_type(x("hello")) # revealed: T@constrained + + return x("hello") + +reveal_type(constrained(int)) # revealed: int +reveal_type(constrained(str)) # revealed: str + +# error: [invalid-argument-type] "Argument to function `constrained` is incorrect: Argument type `A` does not satisfy constraints (`int`, `str`) of type variable `T`" +constrained(A) +``` + +## Union + +```py +from ty_extensions import Intersection, Unknown + +def _[T: int](x: type | type[T]): + reveal_type(x()) # revealed: Any + +def _[T: int](x: type[int] | type[T]): + reveal_type(x()) # revealed: int + +def _[T](x: type[int] | type[T]): + reveal_type(x()) # revealed: int | T@_ +``` + +## Narrowing + +```py +from typing import TypeVar + +class A: ... + +def narrow_a[B: A](a: A, b: B): + type_of_a = type(a) + + reveal_type(a) # revealed: A + reveal_type(type_of_a) # revealed: type[A] + + if isinstance(a, type(b)): + reveal_type(a) # revealed: B@narrow_a + + if issubclass(type_of_a, type(b)): + reveal_type(type_of_a) # revealed: type[B@narrow_a] +``` + +## `__class__` + +```py +from typing import Self + +class A: + def copy(self: Self) -> Self: + reveal_type(self.__class__) # revealed: type[Self@copy] + reveal_type(self.__class__()) # revealed: Self@copy + return self.__class__() +``` + +## Subtyping + +A class `A` is a subtype of `type[T]` if any instance of `A` is a subtype of `T`. + +```py +from typing import Callable, Protocol +from ty_extensions import is_assignable_to, is_subtype_of, is_disjoint_from, static_assert + +class IntCallback(Protocol): + def __call__(self, *args, **kwargs) -> int: ... + +def _[T](_: T): + static_assert(not is_subtype_of(type[T], T)) + static_assert(not is_subtype_of(T, type[T])) + static_assert(not is_disjoint_from(type[T], T)) + static_assert(not is_disjoint_from(type[type], type)) + + static_assert(is_subtype_of(type[T], type[T])) + static_assert(not is_disjoint_from(type[T], type[T])) + + static_assert(is_assignable_to(type[T], Callable[..., T])) + static_assert(not is_disjoint_from(type[T], Callable[..., T])) + + static_assert(not is_assignable_to(type[T], IntCallback)) + static_assert(not is_disjoint_from(type[T], IntCallback)) + +def _[T: int](_: T): + static_assert(not is_subtype_of(type[T], T)) + static_assert(not is_subtype_of(T, type[T])) + static_assert(is_disjoint_from(type[T], T)) + + static_assert(not is_subtype_of(type[T], int)) + static_assert(not is_subtype_of(int, type[T])) + static_assert(is_disjoint_from(type[T], int)) + + static_assert(not is_subtype_of(type[int], type[T])) + static_assert(is_subtype_of(type[T], type[int])) + static_assert(not is_disjoint_from(type[T], type[int])) + + static_assert(is_subtype_of(type[T], type[T])) + static_assert(not is_disjoint_from(type[T], type[T])) + + static_assert(is_assignable_to(type[T], Callable[..., T])) + static_assert(not is_disjoint_from(type[T], Callable[..., T])) + + static_assert(is_assignable_to(type[T], IntCallback)) + static_assert(not is_disjoint_from(type[T], IntCallback)) + + static_assert(is_subtype_of(type[T], type[T] | None)) + static_assert(not is_disjoint_from(type[T], type[T] | None)) + + static_assert(is_subtype_of(type[T], type[T] | type[float])) + static_assert(not is_disjoint_from(type[T], type[T] | type[float])) + +def _[T: (int, str)](_: T): + static_assert(not is_subtype_of(type[T], T)) + static_assert(not is_subtype_of(T, type[T])) + static_assert(is_disjoint_from(type[T], T)) + + static_assert(is_subtype_of(type[T], type[T])) + static_assert(not is_disjoint_from(type[T], type[T])) + + static_assert(is_assignable_to(type[T], Callable[..., T])) + static_assert(not is_disjoint_from(type[T], Callable[..., T])) + + static_assert(not is_assignable_to(type[T], IntCallback)) + static_assert(not is_disjoint_from(type[T], IntCallback)) + + static_assert(is_subtype_of(type[T], type[T] | None)) + static_assert(not is_disjoint_from(type[T], type[T] | None)) + + static_assert(is_subtype_of(type[T], type[T] | type[float])) + static_assert(not is_disjoint_from(type[T], type[T] | type[float])) + + static_assert(not is_subtype_of(type[T], type[int])) + static_assert(not is_subtype_of(type[int], type[T])) + static_assert(not is_subtype_of(type[T], type[str])) + static_assert(not is_subtype_of(type[str], type[T])) + static_assert(not is_disjoint_from(type[T], type[int])) + static_assert(not is_disjoint_from(type[T], type[str])) + + static_assert(is_subtype_of(type[T], type[int] | type[str])) + static_assert(is_subtype_of(type[T], type[int | str])) + static_assert(not is_disjoint_from(type[T], type[int | str])) + static_assert(not is_disjoint_from(type[T], type[int] | type[str])) + +def _[T: (int | str, int)](_: T): + static_assert(is_subtype_of(type[int], type[T])) + static_assert(not is_disjoint_from(type[int], type[T])) +``` + +## Generic Type Inference + +```py +def f1[T](x: type[T]) -> type[T]: + return x + +reveal_type(f1(int)) # revealed: type[int] +reveal_type(f1(object)) # revealed: type + +def f2[T](x: T) -> type[T]: + return type(x) + +reveal_type(f2(int(1))) # revealed: type[int] +reveal_type(f2(object())) # revealed: type + +# TODO: This should reveal `type[Literal[1]]`. +reveal_type(f2(1)) # revealed: type[Unknown] + +def f3[T](x: type[T]) -> T: + return x() + +reveal_type(f3(int)) # revealed: int +reveal_type(f3(object)) # revealed: object +``` + +## Default Parameter + +```py +from typing import Any + +class Foo[T]: ... + +# TODO: This should not error. +# error: [invalid-parameter-default] "Default value of type `` is not assignable to annotated parameter type `type[T@f]`" +def f[T: Foo[Any]](x: type[T] = Foo): ... +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 56b794e96b..01385a776d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1787,10 +1787,11 @@ impl<'db> Type<'db> { // TODO: This is unsound so in future we can consider an opt-in option to disable it. Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { SubclassOfInner::Class(class) => Some(class.into_callable(db)), - SubclassOfInner::Dynamic(dynamic) => { + + SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => { Some(CallableTypes::one(CallableType::single( db, - Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))), + Signature::new(Parameters::unknown(), Some(Type::from(subclass_of_ty))), ))) } }, @@ -2075,13 +2076,14 @@ impl<'db> Type<'db> { // // However, there is one exception to this general rule: for any given typevar `T`, // `T` will always be a subtype of any union containing `T`. - // A similar rule applies in reverse to intersection types. (Type::TypeVar(bound_typevar), Type::Union(union)) if !bound_typevar.is_inferable(db, inferable) && union.elements(db).contains(&self) => { ConstraintSet::from(true) } + + // A similar rule applies in reverse to intersection types. (Type::Intersection(intersection), Type::TypeVar(bound_typevar)) if !bound_typevar.is_inferable(db, inferable) && intersection.positive(db).contains(&target) => @@ -2107,6 +2109,45 @@ impl<'db> Type<'db> { ConstraintSet::from(true) } + // `type[T]` is a subtype of the class object `A` if every instance of `T` is a subtype of an instance + // of `A`, and vice versa. + (Type::SubclassOf(subclass_of), _) + if subclass_of.is_type_var() + && !matches!(target, Type::Callable(_) | Type::ProtocolInstance(_)) => + { + let this_instance = Type::TypeVar(subclass_of.into_type_var().unwrap()); + let other_instance = match target { + Type::Union(union) => Some( + union.map(db, |element| element.to_instance(db).unwrap_or(Type::Never)), + ), + _ => target.to_instance(db), + }; + + other_instance.when_some_and(|other_instance| { + this_instance.has_relation_to_impl( + db, + other_instance, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } + (_, Type::SubclassOf(subclass_of)) if subclass_of.is_type_var() => { + let other_instance = Type::TypeVar(subclass_of.into_type_var().unwrap()); + self.to_instance(db).when_some_and(|this_instance| { + this_instance.has_relation_to_impl( + db, + other_instance, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } + // A fully static typevar is a subtype of its upper bound, and to something similar to // the union of its constraints. An unbound, unconstrained, fully static typevar has an // implicit upper bound of `object` (which is handled above). @@ -2613,7 +2654,7 @@ impl<'db> Type<'db> { // since `type[B]` describes all possible runtime subclasses of the class object `B`. (Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() - .into_class() + .into_class(db) .map(|subclass_of_class| { class.default_specialization(db).has_relation_to_impl( db, @@ -2627,7 +2668,7 @@ impl<'db> Type<'db> { .unwrap_or_else(|| ConstraintSet::from(relation.is_assignability())), (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() - .into_class() + .into_class(db) .map(|subclass_of_class| { ClassType::Generic(alias).has_relation_to_impl( db, @@ -2725,7 +2766,7 @@ impl<'db> Type<'db> { // however, as they are not fully static types. (Type::SubclassOf(subclass_of_ty), _) => subclass_of_ty .subclass_of() - .into_class() + .into_class(db) .map(|class| class.metaclass_instance_type(db)) .unwrap_or_else(|| KnownClass::Type.to_instance(db)) .has_relation_to_impl( @@ -3043,6 +3084,57 @@ impl<'db> Type<'db> { ConstraintSet::from(false) } + // `type[T]` is disjoint from a callable or protocol instance if its upper bound or + // constraints are. + (Type::SubclassOf(subclass_of), Type::Callable(_) | Type::ProtocolInstance(_)) + | (Type::Callable(_) | Type::ProtocolInstance(_), Type::SubclassOf(subclass_of)) + if subclass_of.is_type_var() => + { + let type_var = subclass_of + .subclass_of() + .with_transposed_type_var(db) + .into_type_var() + .unwrap(); + + Type::TypeVar(type_var).is_disjoint_from_impl( + db, + other, + inferable, + disjointness_visitor, + relation_visitor, + ) + } + + // `type[T]` is disjoint from a class object `A` if every instance of `T` is disjoint from an instance of `A`. + (Type::SubclassOf(subclass_of), other) | (other, Type::SubclassOf(subclass_of)) + if subclass_of.is_type_var() => + { + let this_instance = Type::TypeVar(subclass_of.into_type_var().unwrap()); + let other_instance = match other { + Type::Union(union) => Some( + union.map(db, |element| element.to_instance(db).unwrap_or(Type::Never)), + ), + // An unbounded typevar `U` may have instances of type `object` if specialized to + // an instance of `type`. + Type::TypeVar(typevar) + if typevar.typevar(db).bound_or_constraints(db).is_none() => + { + Some(Type::object()) + } + _ => other.to_instance(db), + }; + + other_instance.when_none_or(|other_instance| { + this_instance.is_disjoint_from_impl( + db, + other_instance, + inferable, + disjointness_visitor, + relation_visitor, + ) + }) + } + // A typevar is never disjoint from itself, since all occurrences of the typevar must // be specialized to the same type. (This is an important difference between typevars // and `Any`!) Different typevars might be disjoint, depending on their bounds and @@ -3382,6 +3474,7 @@ impl<'db> Type<'db> { SubclassOfInner::Class(class_a) => { class_b.when_subclass_of(db, None, class_a).negate(db) } + SubclassOfInner::TypeVar(_) => unreachable!(), } } @@ -3392,6 +3485,7 @@ impl<'db> Type<'db> { SubclassOfInner::Class(class_a) => ClassType::from(alias_b) .when_subclass_of(db, class_a, inferable) .negate(db), + SubclassOfInner::TypeVar(_) => unreachable!(), } } @@ -3402,26 +3496,31 @@ impl<'db> Type<'db> { // for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`, // so although the type is dynamic we can still determine disjointedness in some situations (Type::SubclassOf(subclass_of_ty), other) - | (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() { - SubclassOfInner::Dynamic(_) => { - KnownClass::Type.to_instance(db).is_disjoint_from_impl( - db, - other, - inferable, - disjointness_visitor, - relation_visitor, - ) + | (other, Type::SubclassOf(subclass_of_ty)) + if !subclass_of_ty.is_type_var() => + { + match subclass_of_ty.subclass_of() { + SubclassOfInner::Dynamic(_) => { + KnownClass::Type.to_instance(db).is_disjoint_from_impl( + db, + other, + inferable, + disjointness_visitor, + relation_visitor, + ) + } + SubclassOfInner::Class(class) => { + class.metaclass_instance_type(db).is_disjoint_from_impl( + db, + other, + inferable, + disjointness_visitor, + relation_visitor, + ) + } + SubclassOfInner::TypeVar(_) => unreachable!(), } - SubclassOfInner::Class(class) => { - class.metaclass_instance_type(db).is_disjoint_from_impl( - db, - other, - inferable, - disjointness_visitor, - relation_visitor, - ) - } - }, + } (Type::SpecialForm(special_form), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => { @@ -3683,6 +3782,11 @@ impl<'db> Type<'db> { relation_visitor, ) } + + (Type::SubclassOf(_), _) | (_, Type::SubclassOf(_)) => { + // All cases should have been handled above. + unreachable!() + } } } @@ -4959,7 +5063,7 @@ impl<'db> Type<'db> { Type::ClassLiteral(literal) => Some(literal), Type::SubclassOf(subclass_of) => subclass_of .subclass_of() - .into_class() + .into_class(db) .map(|class| class.class_literal(db).0), _ => None, } { @@ -4975,7 +5079,7 @@ impl<'db> Type<'db> { } } - let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str,policy).expect( + let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str, policy).expect( "Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`", ); @@ -5245,12 +5349,16 @@ impl<'db> Type<'db> { .metaclass_instance_type(db) .try_bool_impl(db, allow_short_circuit, visitor)?, - Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, - SubclassOfInner::Class(class) => { - Type::from(class).try_bool_impl(db, allow_short_circuit, visitor)? + Type::SubclassOf(subclass_of_ty) => { + match subclass_of_ty.subclass_of().with_transposed_type_var(db) { + SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, + SubclassOfInner::Class(class) => { + Type::from(class).try_bool_impl(db, allow_short_circuit, visitor)? + } + SubclassOfInner::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar) + .try_bool_impl(db, allow_short_circuit, visitor)?, } - }, + } Type::TypeVar(bound_typevar) => { match bound_typevar.typevar(db).bound_or_constraints(db) { @@ -5980,6 +6088,14 @@ impl<'db> Type<'db> { // evaluating callable subtyping). TODO improve this definition (intersection of // `__new__` and `__init__` signatures? and respect metaclass `__call__`). SubclassOfInner::Class(class) => Type::from(class).bindings(db), + + // TODO annotated return type on `__new__` or metaclass `__call__` + // TODO check call vs signatures of `__new__` and/or `__init__` + SubclassOfInner::TypeVar(_) => Binding::single( + self, + Signature::new(Parameters::gradual_form(), self.to_instance(db)), + ) + .into(), }, Type::NominalInstance(_) | Type::ProtocolInstance(_) | Type::NewTypeInstance(_) => { @@ -6853,6 +6969,17 @@ impl<'db> Type<'db> { Type::TypeVar(bound_typevar) => Some(Type::TypeVar(bound_typevar.to_instance(db)?)), Type::TypeAlias(alias) => alias.value_type(db).to_instance(db), Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance")), + // An instance of class `C` may itself have instances if `C` is a subclass of `type`. + Type::NominalInstance(instance) + if KnownClass::Type + .to_class_literal(db) + .to_class_type(db) + .is_some_and(|type_class| { + instance.class(db).is_subclass_of(db, type_class) + }) => + { + Some(Type::object()) + } Type::BooleanLiteral(_) | Type::BytesLiteral(_) | Type::EnumLiteral(_) @@ -7254,35 +7381,22 @@ impl<'db> Type<'db> { Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db), Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { - None => KnownClass::Type.to_instance(db), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - // TODO: If we add a proper `OneOf` connector, we should use that here instead - // of union. (Using a union here doesn't break anything, but it is imprecise.) - constraints.map(db, |constraint| constraint.to_meta_type(db)) - } - } + SubclassOfType::from(db, SubclassOfInner::TypeVar(bound_typevar)) } - Type::ClassLiteral(class) => class.metaclass(db), Type::GenericAlias(alias) => ClassType::from(alias).metaclass(db), - Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - SubclassOfInner::Dynamic(_) => self, - SubclassOfInner::Class(class) => SubclassOfType::from( - db, - SubclassOfInner::try_from_type(db, class.metaclass(db)) - .unwrap_or(SubclassOfInner::unknown()), - ), + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of().into_class(db) { + None => self, + Some(class) => SubclassOfType::try_from_type(db, class.metaclass(db)) + .unwrap_or(SubclassOfType::subclass_of_unknown()), }, Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class_literal(db), Type::Dynamic(dynamic) => SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)), // TODO intersections - Type::Intersection(_) => SubclassOfType::from( - db, - SubclassOfInner::try_from_type(db, todo_type!("Intersection meta-type")) - .expect("Type::Todo should be a valid `SubclassOfInner`"), - ), + Type::Intersection(_) => { + SubclassOfType::try_from_type(db, todo_type!("Intersection meta-type")) + .expect("Type::Todo should be a valid `SubclassOfInner`") + } Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), @@ -7378,41 +7492,7 @@ impl<'db> Type<'db> { } match self { - Type::TypeVar(bound_typevar) => match type_mapping { - TypeMapping::Specialization(specialization) => { - specialization.get(db, bound_typevar).unwrap_or(self) - } - TypeMapping::PartialSpecialization(partial) => { - partial.get(db, bound_typevar).unwrap_or(self) - } - TypeMapping::BindSelf(self_type) => { - if bound_typevar.typevar(db).is_self(db) { - *self_type - } else { - self - } - } - TypeMapping::ReplaceSelf { new_upper_bound } => { - if bound_typevar.typevar(db).is_self(db) { - Type::TypeVar( - BoundTypeVarInstance::synthetic_self( - db, - *new_upper_bound, - bound_typevar.binding_context(db) - ) - ) - } else { - self - } - } - TypeMapping::PromoteLiterals(_) - | TypeMapping::ReplaceParameterDefaults - | TypeMapping::BindLegacyTypevars(_) - | TypeMapping::EagerExpansion => self, - TypeMapping::Materialize(materialization_kind) => { - Type::TypeVar(bound_typevar.materialize_impl(db, *materialization_kind, visitor)) - } - } + Type::TypeVar(bound_typevar) => bound_typevar.apply_type_mapping_impl(db, type_mapping, visitor), Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => match type_mapping { TypeMapping::BindLegacyTypevars(binding_context) => { @@ -7869,6 +7949,7 @@ impl<'db> Type<'db> { Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { SubclassOfInner::Class(class) => Some(TypeDefinition::Class(class.definition(db))), SubclassOfInner::Dynamic(_) => None, + SubclassOfInner::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)), }, Self::TypeAlias(alias) => alias.value_type(db).definition(db), @@ -9556,6 +9637,25 @@ impl<'db> BoundTypeVarInstance<'db> { Self::new(db, typevar, binding_context) } + /// Returns an identical type variable with its `TypeVarBoundOrConstraints` mapped by the + /// provided closure. + pub(crate) fn map_bound_or_constraints( + self, + db: &'db dyn Db, + f: impl FnOnce(Option>) -> Option>, + ) -> Self { + let bound_or_constraints = f(self.typevar(db).bound_or_constraints(db)); + let typevar = TypeVarInstance::new( + db, + self.typevar(db).identity(db), + bound_or_constraints.map(TypeVarBoundOrConstraintsEvaluation::Eager), + self.typevar(db).explicit_variance(db), + self.typevar(db)._default(db), + ); + + Self::new(db, typevar, self.binding_context(db)) + } + pub(crate) fn variance_with_polarity( self, db: &'db dyn Db, @@ -9576,6 +9676,47 @@ impl<'db> BoundTypeVarInstance<'db> { pub fn variance(self, db: &'db dyn Db) -> TypeVarVariance { self.variance_with_polarity(db, TypeVarVariance::Covariant) } + + fn apply_type_mapping_impl<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Type<'db> { + match type_mapping { + TypeMapping::Specialization(specialization) => { + specialization.get(db, self).unwrap_or(Type::TypeVar(self)) + } + TypeMapping::PartialSpecialization(partial) => { + partial.get(db, self).unwrap_or(Type::TypeVar(self)) + } + TypeMapping::BindSelf(self_type) => { + if self.typevar(db).is_self(db) { + *self_type + } else { + Type::TypeVar(self) + } + } + TypeMapping::ReplaceSelf { new_upper_bound } => { + if self.typevar(db).is_self(db) { + Type::TypeVar(BoundTypeVarInstance::synthetic_self( + db, + *new_upper_bound, + self.binding_context(db), + )) + } else { + Type::TypeVar(self) + } + } + TypeMapping::PromoteLiterals(_) + | TypeMapping::ReplaceParameterDefaults + | TypeMapping::BindLegacyTypevars(_) + | TypeMapping::EagerExpansion => Type::TypeVar(self), + TypeMapping::Materialize(materialization_kind) => { + Type::TypeVar(self.materialize_impl(db, *materialization_kind, visitor)) + } + } + } } fn walk_bound_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -9741,8 +9882,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> { .elements(db) .iter() .map(|ty| ty.materialize(db, materialization_kind, visitor)) - .collect::>() - .into_boxed_slice(), + .collect::>(), )) } } diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 95ac7b6148..e963e6a366 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -310,10 +310,15 @@ impl<'db> BoundSuperType<'db> { Type::Never => SuperOwnerKind::Dynamic(DynamicType::Unknown), Type::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic), Type::ClassLiteral(class) => SuperOwnerKind::Class(ClassType::NonGeneric(class)), - Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - SubclassOfInner::Class(class) => SuperOwnerKind::Class(class), - SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic), - }, + Type::SubclassOf(subclass_of_type) => { + match subclass_of_type.subclass_of().with_transposed_type_var(db) { + SubclassOfInner::Class(class) => SuperOwnerKind::Class(class), + SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic), + SubclassOfInner::TypeVar(bound_typevar) => { + return delegate_to(Type::TypeVar(bound_typevar)); + } + } + } Type::NominalInstance(instance) => SuperOwnerKind::Instance(instance), Type::ProtocolInstance(protocol) => { @@ -450,8 +455,15 @@ impl<'db> BoundSuperType<'db> { let pivot_class = match pivot_class_type { Type::ClassLiteral(class) => ClassBase::Class(ClassType::NonGeneric(class)), Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { - SubclassOfInner::Class(class) => ClassBase::Class(class), SubclassOfInner::Dynamic(dynamic) => ClassBase::Dynamic(dynamic), + _ => match subclass_of.subclass_of().into_class(db) { + Some(class) => ClassBase::Class(class), + None => { + return Err(BoundSuperError::InvalidPivotClassType { + pivot_class: pivot_class_type, + }); + } + }, }, Type::SpecialForm(SpecialFormType::Protocol) => ClassBase::Protocol, Type::SpecialForm(SpecialFormType::Generic) => ClassBase::Generic, diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 368c04601f..ae33d378a2 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -3122,7 +3122,7 @@ pub(crate) fn report_undeclared_protocol_member( Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { SubclassOfInner::Class(class) => class, SubclassOfInner::Dynamic(DynamicType::Any) => return true, - SubclassOfInner::Dynamic(_) => return false, + SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => return false, }, Type::NominalInstance(instance) => instance.class(db), Type::Union(union) => { diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 10912385bf..428a65ca9c 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -688,6 +688,11 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { write!(f.with_type(Type::Dynamic(dynamic)), "{dynamic}")?; f.write_char(']') } + SubclassOfInner::TypeVar(bound_typevar) => write!( + f, + "type[{}]", + bound_typevar.identity(self.db).display(self.db) + ), }, Type::SpecialForm(special_form) => { write!(f.with_type(self.ty), "{special_form}") diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 47b9b51cd7..cf14bc861a 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1557,9 +1557,16 @@ impl<'db> SpecializationBuilder<'db> { argument: ty, }); } - _ => { - self.add_type_mapping(bound_typevar, ty, polarity, f); - } + _ => self.add_type_mapping(bound_typevar, ty, polarity, f), + } + } + + (Type::SubclassOf(subclass_of), ty) | (ty, Type::SubclassOf(subclass_of)) + if subclass_of.is_type_var() => + { + let formal_instance = Type::TypeVar(subclass_of.into_type_var().unwrap()); + if let Some(actual_instance) = ty.to_instance(self.db) { + return self.infer_map_impl(formal_instance, actual_instance, polarity, f); } } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index a6e33011f6..74eab52e72 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -176,17 +176,19 @@ impl<'db> AllMembers<'db> { } Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - SubclassOfInner::Class(class_type) => { - let (class_literal, specialization) = class_type.class_literal(db); - self.extend_with_class_members(db, ty, class_literal); - self.extend_with_synthetic_members(db, ty, class_literal, specialization); - if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) { - self.extend_with_class_members(db, ty, metaclass); - } - } SubclassOfInner::Dynamic(_) => { self.extend_with_type(db, KnownClass::Type.to_instance(db)); } + _ => { + if let Some(class_type) = subclass_of_type.subclass_of().into_class(db) { + let (class_literal, specialization) = class_type.class_literal(db); + self.extend_with_class_members(db, ty, class_literal); + self.extend_with_synthetic_members(db, ty, class_literal, specialization); + if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) { + self.extend_with_class_members(db, ty, metaclass); + } + } + } }, Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy | Type::AlwaysFalsy => { @@ -241,7 +243,7 @@ impl<'db> AllMembers<'db> { self.extend_with_class_members(db, ty, class_literal); } Type::SubclassOf(subclass_of) => { - if let Some(class) = subclass_of.subclass_of().into_class() { + if let Some(class) = subclass_of.subclass_of().into_class(db) { self.extend_with_class_members(db, ty, class.class_literal(db).0); } } @@ -777,7 +779,7 @@ pub fn definitions_for_attribute<'db>( }; let class_literal = match meta_type { Type::ClassLiteral(class_literal) => class_literal, - Type::SubclassOf(subclass) => match subclass.subclass_of().into_class() { + Type::SubclassOf(subclass) => match subclass.subclass_of().into_class(db) { Some(cls) => cls.class_literal(db).0, None => continue, }, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b8005da421..68b35bec62 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8133,7 +8133,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let class = match callable_type { Type::ClassLiteral(class) => Some(ClassType::NonGeneric(class)), Type::GenericAlias(generic) => Some(ClassType::Generic(generic)), - Type::SubclassOf(subclass) => subclass.subclass_of().into_class(), + Type::SubclassOf(subclass) => subclass.subclass_of().into_class(self.db()), _ => None, }; @@ -9112,7 +9112,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }); let attr_name = &attr.id; - let resolved_type = fallback_place.unwrap_with_diagnostic(|lookup_err| match lookup_err { LookupError::Undefined(_) => { let fallback = || { @@ -9140,6 +9139,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "Attribute lookup on a dynamic `SubclassOf` type \ should always return a bound symbol" ), + SubclassOfInner::TypeVar(_) => false, } } _ => false, diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 7c2addf9f1..14a4079eb4 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -641,27 +641,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { fn infer_subclass_of_type_expression(&mut self, slice: &ast::Expr) -> Type<'db> { match slice { ast::Expr::Name(_) | ast::Expr::Attribute(_) => { - let name_ty = self.infer_expression(slice, TypeContext::default()); - match name_ty { - Type::ClassLiteral(class_literal) => { - if class_literal.is_protocol(self.db()) { - SubclassOfType::from( - self.db(), - todo_type!("type[T] for protocols").expect_dynamic(), - ) - } else { - SubclassOfType::from( - self.db(), - class_literal.default_specialization(self.db()), - ) - } - } - Type::SpecialForm(SpecialFormType::Any) => SubclassOfType::subclass_of_any(), - Type::SpecialForm(SpecialFormType::Unknown) => { - SubclassOfType::subclass_of_unknown() - } - _ => todo_type!("unsupported type[X] special form"), - } + SubclassOfType::try_from_instance(self.db(), self.infer_type_expression(slice)) + .unwrap_or(todo_type!("unsupported type[X] special form")) } ast::Expr::BinOp(binary) if binary.op == ast::Operator::BitOr => { let union_ty = UnionType::from_elements_leave_aliases( diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index ae5ec4a4d6..32ac648dc4 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -171,6 +171,10 @@ impl ClassInfoConstraintFunction { // e.g. `isinstance(x, list[int])` fails at runtime. SubclassOfInner::Class(ClassType::Generic(_)) => None, SubclassOfInner::Dynamic(dynamic) => Some(Type::Dynamic(dynamic)), + SubclassOfInner::TypeVar(bound_typevar) => match self { + ClassInfoConstraintFunction::IsSubclass => Some(classinfo), + ClassInfoConstraintFunction::IsInstance => Some(Type::TypeVar(bound_typevar)), + }, }, Type::Dynamic(_) => Some(classinfo), Type::Intersection(intersection) => { diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index b5b9e00c06..ed69554c8c 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -8,7 +8,7 @@ use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassType, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, KnownClass, MaterializationKind, MemberLookupPolicy, NormalizedVisitor, SpecialFormType, Type, TypeContext, - TypeMapping, TypeRelation, + TypeMapping, TypeRelation, TypeVarBoundOrConstraints, UnionType, todo_type, }; use crate::{Db, FxOrderSet}; @@ -26,7 +26,7 @@ pub(super) fn walk_subclass_of_type<'db, V: super::visitor::TypeVisitor<'db> + ? subclass_of: SubclassOfType<'db>, visitor: &V, ) { - visitor.visit_type(db, Type::from(subclass_of.subclass_of)); + visitor.visit_type(db, Type::from(subclass_of)); } impl<'db> SubclassOfType<'db> { @@ -44,19 +44,44 @@ impl<'db> SubclassOfType<'db> { pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into>) -> Type<'db> { let subclass_of = subclass_of.into(); match subclass_of { - SubclassOfInner::Dynamic(_) => Type::SubclassOf(Self { subclass_of }), SubclassOfInner::Class(class) => { if class.is_final(db) { Type::from(class) } else if class.is_object(db) { - KnownClass::Type.to_instance(db) + Self::subclass_of_object(db) } else { Type::SubclassOf(Self { subclass_of }) } } + SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => { + Type::SubclassOf(Self { subclass_of }) + } } } + /// Given the class object `T`, returns a [`Type`] instance representing `type[T]`. + pub(crate) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option> { + let subclass_of = match ty { + Type::Dynamic(dynamic) => SubclassOfInner::Dynamic(dynamic), + Type::ClassLiteral(literal) => { + SubclassOfInner::Class(literal.default_specialization(db)) + } + Type::GenericAlias(generic) => SubclassOfInner::Class(ClassType::Generic(generic)), + Type::SpecialForm(SpecialFormType::Any) => SubclassOfInner::Dynamic(DynamicType::Any), + Type::SpecialForm(SpecialFormType::Unknown) => { + SubclassOfInner::Dynamic(DynamicType::Unknown) + } + _ => return None, + }; + + Some(Self::from(db, subclass_of)) + } + + /// Given an instance of the class or type variable `T`, returns a [`Type`] instance representing `type[T]`. + pub(crate) fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option> { + SubclassOfInner::try_from_instance(db, ty).map(|subclass_of| Self::from(db, subclass_of)) + } + /// Return a [`Type`] instance representing the type `type[Unknown]`. pub(crate) const fn subclass_of_unknown() -> Type<'db> { Type::SubclassOf(SubclassOfType { @@ -65,12 +90,19 @@ impl<'db> SubclassOfType<'db> { } /// Return a [`Type`] instance representing the type `type[Any]`. + #[cfg(test)] pub(crate) const fn subclass_of_any() -> Type<'db> { Type::SubclassOf(SubclassOfType { subclass_of: SubclassOfInner::Dynamic(DynamicType::Any), }) } + /// Return a [`Type`] instance representing the type `type[object]`. + pub(crate) fn subclass_of_object(db: &'db dyn Db) -> Type<'db> { + // See the documentation of `SubclassOfType::from` for details. + KnownClass::Type.to_instance(db) + } + /// Return the inner [`SubclassOfInner`] value wrapped by this `SubclassOfType`. pub(crate) const fn subclass_of(self) -> SubclassOfInner<'db> { self.subclass_of @@ -82,6 +114,15 @@ impl<'db> SubclassOfType<'db> { subclass_of.is_dynamic() } + pub(crate) const fn is_type_var(self) -> bool { + let Self { subclass_of } = self; + subclass_of.is_type_var() + } + + pub(crate) const fn into_type_var(self) -> Option> { + self.subclass_of.into_type_var() + } + pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, @@ -105,6 +146,11 @@ impl<'db> SubclassOfType<'db> { }, _ => Type::SubclassOf(self), }, + SubclassOfInner::TypeVar(typevar) => SubclassOfType::try_from_instance( + db, + typevar.apply_type_mapping_impl(db, type_mapping, visitor), + ) + .unwrap_or(SubclassOfType::subclass_of_unknown()), } } @@ -116,10 +162,18 @@ impl<'db> SubclassOfType<'db> { visitor: &FindLegacyTypeVarsVisitor<'db>, ) { match self.subclass_of { + SubclassOfInner::Dynamic(_) => {} SubclassOfInner::Class(class) => { class.find_legacy_typevars_impl(db, binding_context, typevars, visitor); } - SubclassOfInner::Dynamic(_) => {} + SubclassOfInner::TypeVar(typevar) => { + Type::TypeVar(typevar).find_legacy_typevars_impl( + db, + binding_context, + typevars, + visitor, + ); + } } } @@ -129,7 +183,19 @@ impl<'db> SubclassOfType<'db> { name: &str, policy: MemberLookupPolicy, ) -> Option> { - Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy) + let class_like = match self.subclass_of.with_transposed_type_var(db) { + SubclassOfInner::Class(class) => Type::from(class), + SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic), + SubclassOfInner::TypeVar(bound_typevar) => { + match bound_typevar.typevar(db).bound_or_constraints(db) { + None => unreachable!(), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound, + Some(TypeVarBoundOrConstraints::Constraints(union)) => Type::Union(union), + } + } + }; + + class_like.find_name_in_mro_with_policy(db, name, policy) } /// Return `true` if `self` has a certain relation to `other`. @@ -165,6 +231,10 @@ impl<'db> SubclassOfType<'db> { relation_visitor, disjointness_visitor, ), + + (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => { + unreachable!() + } } } @@ -185,6 +255,9 @@ impl<'db> SubclassOfType<'db> { (SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => { ConstraintSet::from(!self_class.could_coexist_in_mro_with(db, other_class)) } + (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => { + unreachable!() + } } } @@ -212,12 +285,13 @@ impl<'db> SubclassOfType<'db> { match self.subclass_of { SubclassOfInner::Class(class) => Type::instance(db, class), SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), + SubclassOfInner::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar), } } pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool { self.subclass_of - .into_class() + .into_class(db) .is_some_and(|class| class.class_literal(db).0.is_typed_dict(db)) } } @@ -225,8 +299,8 @@ impl<'db> SubclassOfType<'db> { impl<'db> VarianceInferable<'db> for SubclassOfType<'db> { fn variance_of(self, db: &dyn Db, typevar: BoundTypeVarInstance<'_>) -> TypeVarVariance { match self.subclass_of { - SubclassOfInner::Dynamic(_) => TypeVarVariance::Bivariant, SubclassOfInner::Class(class) => class.variance_of(db, typevar), + SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => TypeVarVariance::Bivariant, } } } @@ -235,6 +309,7 @@ impl<'db> VarianceInferable<'db> for SubclassOfType<'db> { /// /// 1. A "subclass of a class": `type[C]` for any class object `C` /// 2. A "subclass of a dynamic type": `type[Any]`, `type[Unknown]` and `type[@Todo]` +/// 3. A "subclass of a type variable": `type[T]` for any type variable `T` /// /// In the long term, we may want to implement . /// Doing this would allow us to get rid of this enum, @@ -249,6 +324,7 @@ impl<'db> VarianceInferable<'db> for SubclassOfType<'db> { pub(crate) enum SubclassOfInner<'db> { Class(ClassType<'db>), Dynamic(DynamicType), + TypeVar(BoundTypeVarInstance<'db>), } impl<'db> SubclassOfInner<'db> { @@ -260,24 +336,111 @@ impl<'db> SubclassOfInner<'db> { matches!(self, Self::Dynamic(_)) } - pub(crate) const fn into_class(self) -> Option> { + pub(crate) const fn is_type_var(self) -> bool { + matches!(self, Self::TypeVar(_)) + } + + pub(crate) fn into_class(self, db: &'db dyn Db) -> Option> { match self { - Self::Class(class) => Some(class), Self::Dynamic(_) => None, + Self::Class(class) => Some(class), + Self::TypeVar(bound_typevar) => { + match bound_typevar.typevar(db).bound_or_constraints(db) { + None => Some(ClassType::object(db)), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + Self::try_from_instance(db, bound) + .and_then(|subclass_of| subclass_of.into_class(db)) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + match constraints.elements(db) { + [bound] => Self::try_from_instance(db, *bound) + .and_then(|subclass_of| subclass_of.into_class(db)), + _ => Some(ClassType::object(db)), + } + } + } + } } } pub(crate) const fn into_dynamic(self) -> Option { match self { - Self::Class(_) => None, + Self::Class(_) | Self::TypeVar(_) => None, Self::Dynamic(dynamic) => Some(dynamic), } } + pub(crate) const fn into_type_var(self) -> Option> { + match self { + Self::Class(_) | Self::Dynamic(_) => None, + Self::TypeVar(bound_typevar) => Some(bound_typevar), + } + } + + pub(crate) fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option { + Some(match ty { + Type::NominalInstance(instance) => SubclassOfInner::Class(instance.class(db)), + Type::TypedDict(typed_dict) => SubclassOfInner::Class(typed_dict.defining_class()), + Type::TypeVar(bound_typevar) => SubclassOfInner::TypeVar(bound_typevar), + Type::Dynamic(DynamicType::Any) => SubclassOfInner::Dynamic(DynamicType::Any), + Type::Dynamic(DynamicType::Unknown) => SubclassOfInner::Dynamic(DynamicType::Unknown), + Type::ProtocolInstance(_) => { + SubclassOfInner::Dynamic(todo_type!("type[T] for protocols").expect_dynamic()) + } + _ => return None, + }) + } + + /// Transposes `type[T]` with a type variable `T` into `T: type[...]`. + /// + /// In particular: + /// - If `T` has an upper bound of `T: Bound`, this returns `T: type[Bound]`. + /// - If `T` has constraints `T: (A, B)`, this returns `T: (type[A], type[B])`. + /// - Otherwise, for an unbounded type variable, this returns `type[object]`. + /// + /// If this is type of a concrete type `C`, returns the type unchanged. + pub(crate) fn with_transposed_type_var(self, db: &'db dyn Db) -> Self { + let Some(bound_typevar) = self.into_type_var() else { + return self; + }; + + let bound_typevar = bound_typevar.map_bound_or_constraints(db, |bound_or_constraints| { + Some(match bound_or_constraints { + None => TypeVarBoundOrConstraints::UpperBound( + SubclassOfType::try_from_instance(db, Type::object()) + .unwrap_or(SubclassOfType::subclass_of_unknown()), + ), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + TypeVarBoundOrConstraints::UpperBound( + SubclassOfType::try_from_instance(db, bound) + .unwrap_or(SubclassOfType::subclass_of_unknown()), + ) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + let constraints = constraints + .elements(db) + .iter() + .map(|constraint| { + SubclassOfType::try_from_instance(db, *constraint) + .unwrap_or(SubclassOfType::subclass_of_unknown()) + }) + .collect::>(); + + TypeVarBoundOrConstraints::Constraints(UnionType::new(db, constraints)) + } + }) + }); + + Self::TypeVar(bound_typevar) + } + pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { match self { Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)), Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()), + Self::TypeVar(bound_typevar) => { + Self::TypeVar(bound_typevar.normalized_impl(db, visitor)) + } } } @@ -293,16 +456,7 @@ impl<'db> SubclassOfInner<'db> { class.recursive_type_normalized_impl(db, div, nested, visitor)?, )), Self::Dynamic(dynamic) => Some(Self::Dynamic(dynamic.recursive_type_normalized())), - } - } - - pub(crate) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { - match ty { - Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), - Type::ClassLiteral(literal) => Some(Self::Class(literal.default_specialization(db))), - Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), - Type::SpecialForm(SpecialFormType::Any) => Some(Self::Dynamic(DynamicType::Any)), - _ => None, + Self::TypeVar(_) => Some(self), } } } @@ -325,11 +479,18 @@ impl<'db> From> for SubclassOfInner<'db> { } } -impl<'db> From> for Type<'db> { - fn from(value: SubclassOfInner<'db>) -> Self { - match value { - SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic), +impl<'db> From> for SubclassOfInner<'db> { + fn from(value: BoundTypeVarInstance<'db>) -> Self { + SubclassOfInner::TypeVar(value) + } +} + +impl<'db> From> for Type<'db> { + fn from(value: SubclassOfType<'db>) -> Self { + match value.subclass_of { SubclassOfInner::Class(class) => class.into(), + SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic), + SubclassOfInner::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar), } } } diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index cb2f75b108..d2c9a71208 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -117,6 +117,11 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (SubclassOfInner::Dynamic(left), SubclassOfInner::Dynamic(right)) => { dynamic_elements_ordering(left, right) } + (SubclassOfInner::TypeVar(left), SubclassOfInner::TypeVar(right)) => { + left.as_id().cmp(&right.as_id()) + } + (SubclassOfInner::TypeVar(_), _) => Ordering::Less, + (_, SubclassOfInner::TypeVar(_)) => Ordering::Greater, } }