From 68a2f6c57d70052d0805b46b0e3a2538598b856f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 7 Jan 2026 19:56:09 -0500 Subject: [PATCH] [ty] Fix `super()` with TypeVar-annotated `self` and `cls` parameter (#22208) ## Summary This PR fixes `super()` handling when the first parameter (`self` or `cls`) is annotated with a TypeVar, like `Self`. Previously, `super()` would incorrectly resolve TypeVars to their bounds before creating the `BoundSuperType`. So if you had `self: Self` where `Self` is bounded by `Parent`, we'd process `Parent` as a `NominalInstance` and end up with `SuperOwnerKind::Instance(Parent)`. As a result: ```python class Parent: @classmethod def create(cls) -> Self: return cls() class Child(Parent): @classmethod def create(cls) -> Self: return super().create() # Error: Argument type `Self@create` does not satisfy upper bound `Parent` ``` We now track two additional variants on `SuperOwnerKind` for TypeVar owners: - `InstanceTypeVar`: for instance methods where self is a TypeVar (e.g., `self: Self`). - `ClassTypeVar`: for classmethods where `cls` is a `TypeVar` wrapped in `type[...]` (e.g., `cls: type[Self]`). Closes https://github.com/astral-sh/ty/issues/2122. --------- Co-authored-by: Carl Meyer --- .../resources/mdtest/annotations/self.md | 45 +++ .../resources/mdtest/class/super.md | 86 +++++- ...licit_Super_Objec…_(f9e5e48e3a4a4c12).snap | 76 +++-- .../src/types/bound_super.rs | 270 ++++++++++++++---- .../ty_python_semantic/src/types/display.rs | 4 +- .../src/types/type_ordering.rs | 11 + 6 files changed, 393 insertions(+), 99 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 767e2e1f17..72fa7acbc8 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -359,6 +359,51 @@ reveal_type(GenericCircle[int].bar()) # revealed: GenericCircle[int] reveal_type(GenericCircle.baz(1)) # revealed: GenericShape[Literal[1]] ``` +### Calling `super()` in overridden methods with `Self` return type + +This is a regression test for . + +When a child class overrides a parent method with a `Self` return type and calls `super().method()`, +the return type should be the child's `Self` type variable, not the concrete child class type. + +```py +from typing import Self + +class Parent: + def copy(self) -> Self: + return self + +class Child(Parent): + def copy(self) -> Self: + result = super().copy() + reveal_type(result) # revealed: Self@copy + return result + +# When called on concrete types, Self is substituted correctly. +reveal_type(Child().copy()) # revealed: Child +``` + +The same applies to classmethods with `Self` return types: + +```py +from typing import Self + +class Parent: + @classmethod + def create(cls) -> Self: + return cls() + +class Child(Parent): + @classmethod + def create(cls) -> Self: + result = super().create() + reveal_type(result) # revealed: Self@create + return result + +# When called on concrete types, Self is substituted correctly. +reveal_type(Child.create()) # revealed: Child +``` + ## Attributes TODO: The use of `Self` to annotate the `next_node` attribute should be diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index de5fe0b3dd..24676024b9 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -168,13 +168,13 @@ class A: class B(A): def __init__(self, a: int): - reveal_type(super()) # revealed: , B> + reveal_type(super()) # revealed: , Self@__init__> reveal_type(super(object, super())) # revealed: , super> super().__init__(a) @classmethod def f(cls): - reveal_type(super()) # revealed: , > + reveal_type(super()) # revealed: , type[Self@f]> super().f() super(B, B(42)).__init__(42) @@ -229,16 +229,16 @@ class Foo[T]: reveal_type(super()) def method4(self: Self): - # revealed: , Foo[T@Foo]> + # revealed: , Self@method4> reveal_type(super()) def method5[S: Foo[int]](self: S, other: S) -> S: - # revealed: , Foo[int]> + # revealed: , S@method5> reveal_type(super()) return self def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S: - # revealed: , Foo[int]> | , Foo[str]> + # revealed: , S@method6> | , S@method6> reveal_type(super()) return self @@ -265,6 +265,19 @@ class Foo[T]: # revealed: Unknown reveal_type(super()) return self + # TypeVar bounded by `type[Foo]` rather than `Foo` + # TODO: Should error on signature - `self` is annotated as a class type, not an instance type + def method11[S: type[Foo[int]]](self: S, other: S) -> S: + # Delegates to the bound to resolve the super type + reveal_type(super()) # revealed: , > + return self + # TypeVar bounded by `type[Foo]`, used in `type[T]` position + # TODO: Should error on signature - `cls` would be `type[type[Foo[int]]]`, a metaclass + # Delegates to `type[Unknown]` since `type[type[Foo[int]]]` can't be constructed + @classmethod + def method12[S: type[Foo[int]]](cls: type[S]) -> S: + reveal_type(super()) # revealed: , Unknown> + raise NotImplementedError type Alias = Bar @@ -359,15 +372,15 @@ from __future__ import annotations class A: def test(self): - reveal_type(super()) # revealed: , A> + reveal_type(super()) # revealed: , Self@test> class B: def test(self): - reveal_type(super()) # revealed: , B> + reveal_type(super()) # revealed: , Self@test> class C(A.B): def test(self): - reveal_type(super()) # revealed: , C> + reveal_type(super()) # revealed: , Self@test> def inner(t: C): reveal_type(super()) # revealed: , C> @@ -645,7 +658,7 @@ class A: class B(A): def __init__(self, a: int): super().__init__(a) - # error: [unresolved-attribute] "Object of type `, B>` has no attribute `a`" + # error: [unresolved-attribute] "Object of type `, Self@__init__>` has no attribute `a`" super().a # error: [unresolved-attribute] "Object of type `, B>` has no attribute `a`" @@ -670,3 +683,58 @@ reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(k # error: [not-subscriptable] "Cannot subscript object of type `, B>` with no `__getitem__` method" super(B, B())[0] ``` + +## Subclass Using Concrete Type Instead of `Self` + +When a parent class uses `Self` in a parameter type and a subclass overrides it with a concrete +type, passing that parameter to `super().__init__()` is a type error. This is because `Self` in the +parent could represent a further subclass. The fix is to use `Self` consistently in the subclass. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from __future__ import annotations +from collections.abc import Mapping +from typing import Self + +class Parent: + def __init__(self, children: Mapping[str, Self] | None = None) -> None: + self.children = children + +class Child(Parent): + def __init__(self, children: Mapping[str, Child] | None = None) -> None: + # error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `Mapping[str, Self@__init__] | None`, found `Mapping[str, Child] | None`" + super().__init__(children) + +# The fix is to use `Self` consistently in the subclass: + +class Parent2: + def __init__(self, children: Mapping[str, Self] | None = None) -> None: + self.children = children + +class Child2(Parent2): + def __init__(self, children: Mapping[str, Self] | None = None) -> None: + super().__init__(children) # OK +``` + +## Super in Protocol Classes + +Using `super()` in a class that inherits from `typing.Protocol` (similar to beartype's caching +Protocol): + +```py +from typing import Protocol, Generic, TypeVar + +_T_co = TypeVar("_T_co", covariant=True) + +class MyProtocol(Protocol, Generic[_T_co]): + def __class_getitem__(cls, item): + # Accessing parent's __class_getitem__ through super() + reveal_type(super()) # revealed: , type[Self@__class_getitem__]> + parent_method = super().__class_getitem__ + reveal_type(parent_method) # revealed: @Todo(super in generic class) + return parent_method(item) +``` 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 f3bf848253..e4060727bb 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 @@ -22,13 +22,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md 7 | 8 | class B(A): 9 | def __init__(self, a: int): - 10 | reveal_type(super()) # revealed: , B> + 10 | reveal_type(super()) # revealed: , Self@__init__> 11 | reveal_type(super(object, super())) # revealed: , super> 12 | super().__init__(a) 13 | 14 | @classmethod 15 | def f(cls): - 16 | reveal_type(super()) # revealed: , > + 16 | reveal_type(super()) # revealed: , type[Self@f]> 17 | super().f() 18 | 19 | super(B, B(42)).__init__(42) @@ -78,16 +78,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md 63 | reveal_type(super()) 64 | 65 | def method4(self: Self): - 66 | # revealed: , Foo[T@Foo]> + 66 | # revealed: , Self@method4> 67 | reveal_type(super()) 68 | 69 | def method5[S: Foo[int]](self: S, other: S) -> S: - 70 | # revealed: , Foo[int]> + 70 | # revealed: , S@method5> 71 | reveal_type(super()) 72 | return self 73 | 74 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S: - 75 | # revealed: , Foo[int]> | , Foo[str]> + 75 | # revealed: , S@method6> | , S@method6> 76 | reveal_type(super()) 77 | return self 78 | @@ -114,35 +114,48 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md 99 | # revealed: Unknown 100 | reveal_type(super()) 101 | return self -102 | -103 | type Alias = Bar -104 | -105 | class Bar: -106 | def method(self: Alias): -107 | # revealed: , Bar> -108 | reveal_type(super()) -109 | -110 | def pls_dont_call_me(self: Never): -111 | # revealed: , Unknown> -112 | reveal_type(super()) -113 | -114 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): -115 | # revealed: , Bar> -116 | reveal_type(super()) +102 | # TypeVar bounded by `type[Foo]` rather than `Foo` +103 | # TODO: Should error on signature - `self` is annotated as a class type, not an instance type +104 | def method11[S: type[Foo[int]]](self: S, other: S) -> S: +105 | # Delegates to the bound to resolve the super type +106 | reveal_type(super()) # revealed: , > +107 | return self +108 | # TypeVar bounded by `type[Foo]`, used in `type[T]` position +109 | # TODO: Should error on signature - `cls` would be `type[type[Foo[int]]]`, a metaclass +110 | # Delegates to `type[Unknown]` since `type[type[Foo[int]]]` can't be constructed +111 | @classmethod +112 | def method12[S: type[Foo[int]]](cls: type[S]) -> S: +113 | reveal_type(super()) # revealed: , Unknown> +114 | raise NotImplementedError +115 | +116 | type Alias = Bar 117 | -118 | class P(Protocol): -119 | def method(self: P): -120 | # revealed: , P> +118 | class Bar: +119 | def method(self: Alias): +120 | # revealed: , Bar> 121 | reveal_type(super()) 122 | -123 | class E(enum.Enum): -124 | X = 1 -125 | -126 | def method(self: E): -127 | match self: -128 | case E.X: -129 | # revealed: , E> -130 | reveal_type(super()) +123 | def pls_dont_call_me(self: Never): +124 | # revealed: , Unknown> +125 | reveal_type(super()) +126 | +127 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): +128 | # revealed: , Bar> +129 | reveal_type(super()) +130 | +131 | class P(Protocol): +132 | def method(self: P): +133 | # revealed: , P> +134 | reveal_type(super()) +135 | +136 | class E(enum.Enum): +137 | X = 1 +138 | +139 | def method(self: E): +140 | match self: +141 | case E.X: +142 | # revealed: , E> +143 | reveal_type(super()) ``` # Diagnostics @@ -205,6 +218,7 @@ error[invalid-super-argument]: `S@method10` is a type variable with an abstract/ 100 | reveal_type(super()) | ^^^^^^^ 101 | return self +102 | # TypeVar bounded by `type[Foo]` rather than `Foo` | info: Type variable `S` has upper bound `(...) -> str` info: rule `invalid-super-argument` is enabled by default diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 13fafc47b4..56f47a1b20 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -8,9 +8,10 @@ use crate::{ Db, DisplaySettings, place::{Place, PlaceAndQualifiers}, types::{ - ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass, MemberLookupPolicy, - NominalInstanceType, NormalizedVisitor, SpecialFormType, SubclassOfInner, Type, - TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, + BoundTypeVarInstance, ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass, + MemberLookupPolicy, NominalInstanceType, NormalizedVisitor, SpecialFormType, + SubclassOfInner, SubclassOfType, Type, TypeVarBoundOrConstraints, TypeVarConstraints, + TypeVarInstance, UnionBuilder, context::InferContext, diagnostic::{INVALID_SUPER_ARGUMENT, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS}, todo_type, visitor, @@ -185,6 +186,12 @@ pub enum SuperOwnerKind<'db> { Dynamic(DynamicType<'db>), Class(ClassType<'db>), Instance(NominalInstanceType<'db>), + /// An instance-like type variable owner (e.g., `self: Self` in an instance method). + /// The second element is the class extracted from the `TypeVar` bound for MRO lookup. + InstanceTypeVar(BoundTypeVarInstance<'db>, ClassType<'db>), + /// A class-like type variable owner (e.g., `cls: type[Self]` in a classmethod). + /// The second element is the class extracted from the `TypeVar` bound for MRO lookup. + ClassTypeVar(BoundTypeVarInstance<'db>, ClassType<'db>), } impl<'db> SuperOwnerKind<'db> { @@ -199,6 +206,16 @@ impl<'db> SuperOwnerKind<'db> { .as_nominal_instance() .map(Self::Instance) .unwrap_or(Self::Dynamic(DynamicType::Any)), + SuperOwnerKind::InstanceTypeVar(bound_typevar, class) => { + SuperOwnerKind::InstanceTypeVar( + bound_typevar.normalized_impl(db, visitor), + class.normalized_impl(db, visitor), + ) + } + SuperOwnerKind::ClassTypeVar(bound_typevar, class) => SuperOwnerKind::ClassTypeVar( + bound_typevar.normalized_impl(db, visitor), + class.normalized_impl(db, visitor), + ), } } @@ -218,6 +235,10 @@ impl<'db> SuperOwnerKind<'db> { SuperOwnerKind::Instance(instance) => Some(SuperOwnerKind::Instance( instance.recursive_type_normalized_impl(db, div, nested)?, )), + SuperOwnerKind::InstanceTypeVar(_, _) | SuperOwnerKind::ClassTypeVar(_, _) => { + // TODO: we might need to normalize the nested class here? + Some(self) + } } } @@ -228,6 +249,9 @@ impl<'db> SuperOwnerKind<'db> { } SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)), SuperOwnerKind::Instance(instance) => Either::Right(instance.class(db).iter_mro(db)), + SuperOwnerKind::InstanceTypeVar(_, class) | SuperOwnerKind::ClassTypeVar(_, class) => { + Either::Right(class.iter_mro(db)) + } } } @@ -236,16 +260,31 @@ impl<'db> SuperOwnerKind<'db> { SuperOwnerKind::Dynamic(_) => None, SuperOwnerKind::Class(class) => Some(class), SuperOwnerKind::Instance(instance) => Some(instance.class(db)), + SuperOwnerKind::InstanceTypeVar(_, class) | SuperOwnerKind::ClassTypeVar(_, class) => { + Some(class) + } } } -} -impl<'db> From> for Type<'db> { - fn from(owner: SuperOwnerKind<'db>) -> Self { - match owner { + /// Returns the `TypeVar` instance if this owner is a `TypeVar` variant. + fn typevar(self, db: &'db dyn Db) -> Option> { + match self { + SuperOwnerKind::InstanceTypeVar(bound_typevar, _) + | SuperOwnerKind::ClassTypeVar(bound_typevar, _) => Some(bound_typevar.typevar(db)), + _ => None, + } + } + + /// Returns the type representation of this owner. + pub(super) fn owner_type(self, db: &'db dyn Db) -> Type<'db> { + match self { SuperOwnerKind::Dynamic(dynamic) => Type::Dynamic(dynamic), SuperOwnerKind::Class(class) => class.into(), SuperOwnerKind::Instance(instance) => instance.into(), + SuperOwnerKind::InstanceTypeVar(bound_typevar, _) => Type::TypeVar(bound_typevar), + SuperOwnerKind::ClassTypeVar(bound_typevar, _) => { + SubclassOfType::from(db, SubclassOfInner::TypeVar(bound_typevar)) + } } } } @@ -266,7 +305,7 @@ pub(super) fn walk_bound_super_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( visitor: &V, ) { visitor.visit_type(db, Type::from(bound_super.pivot_class(db))); - visitor.visit_type(db, Type::from(bound_super.owner(db))); + visitor.visit_type(db, bound_super.owner(db).owner_type(db)); } impl<'db> BoundSuperType<'db> { @@ -285,6 +324,7 @@ impl<'db> BoundSuperType<'db> { let delegate_to = |type_to_delegate_to| BoundSuperType::build(db, pivot_class_type, type_to_delegate_to); + // Delegate but rewrite errors to preserve TypeVar context. let delegate_with_error_mapped = |type_to_delegate_to, error_context: Option>| { delegate_to(type_to_delegate_to).map_err(|err| match err { @@ -315,19 +355,128 @@ impl<'db> BoundSuperType<'db> { }) }; + // We don't use `ClassBase::try_from_type` here because: + // - There are objects that may validly be present in a class's bases list + // but are not valid as pivot classes, e.g. `typing.ChainMap` + // - There are objects that are not valid in a class's bases list + // but are valid as pivot classes, e.g. unsubscripted `typing.Generic` + 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::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, + Type::SpecialForm(SpecialFormType::TypedDict) => ClassBase::TypedDict, + Type::Dynamic(dynamic) => ClassBase::Dynamic(dynamic), + _ => { + return Err(BoundSuperError::InvalidPivotClassType { + pivot_class: pivot_class_type, + }); + } + }; + + // Helper to build a union of bound-super instances for constrained TypeVars. + // Each constraint must be a subclass of the pivot class. + let build_constrained_union = + |constraints: TypeVarConstraints<'db>, + bound_typevar: BoundTypeVarInstance<'db>, + typevar: TypeVarInstance<'db>, + make_owner: fn(BoundTypeVarInstance<'db>, ClassType<'db>) -> SuperOwnerKind<'db>| + -> Result, BoundSuperError<'db>> { + let pivot_class_literal = pivot_class.into_class().map(|c| c.class_literal(db).0); + let mut builder = UnionBuilder::new(db); + for constraint in constraints.elements(db) { + let class = match constraint { + Type::NominalInstance(instance) => Some(instance.class(db)), + _ => constraint.to_class_type(db), + }; + match class { + Some(class) => { + // Validate constraint is a subclass of pivot class. + if let Some(pivot) = pivot_class_literal { + if !class.iter_mro(db).any(|superclass| match superclass { + ClassBase::Dynamic(_) => true, + ClassBase::Generic + | ClassBase::Protocol + | ClassBase::TypedDict => false, + ClassBase::Class(superclass) => { + superclass.class_literal(db).0 == pivot + } + }) { + return Err(BoundSuperError::FailingConditionCheck { + pivot_class: pivot_class_type, + owner: owner_type, + typevar_context: Some(typevar), + }); + } + } + let owner = make_owner(bound_typevar, class); + builder = builder.add(Type::BoundSuper(BoundSuperType::new( + db, + pivot_class, + owner, + ))); + } + None => { + // Delegate to the constraint to get better error messages + // if the constraint is incompatible with the pivot class. + builder = builder.add(delegate_to(*constraint)?); + } + } + } + Ok(builder.build()) + }; + let owner = match owner_type { 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().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::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { + SubclassOfInner::Class(class) => SuperOwnerKind::Class(class), + SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic), + SubclassOfInner::TypeVar(bound_typevar) => { + let typevar = bound_typevar.typevar(db); + match typevar.bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + let class = match bound { + Type::NominalInstance(instance) => Some(instance.class(db)), + Type::ProtocolInstance(protocol) => protocol + .to_nominal_instance() + .map(|instance| instance.class(db)), + _ => None, + }; + if let Some(class) = class { + SuperOwnerKind::ClassTypeVar(bound_typevar, class) + } else { + let subclass_of = SubclassOfType::try_from_instance(db, bound) + .unwrap_or_else(SubclassOfType::subclass_of_unknown); + return delegate_with_error_mapped(subclass_of, Some(typevar)); + } + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + return build_constrained_union( + constraints, + bound_typevar, + typevar, + SuperOwnerKind::ClassTypeVar, + ); + } + None => { + // No bound means the implicit upper bound is `object`. + SuperOwnerKind::ClassTypeVar(bound_typevar, ClassType::object(db)) + } } } - } + }, Type::NominalInstance(instance) => SuperOwnerKind::Instance(instance), Type::ProtocolInstance(protocol) => { @@ -375,19 +524,38 @@ impl<'db> BoundSuperType<'db> { return Ok(builder.build()); } Type::TypeAlias(alias) => { - return delegate_with_error_mapped(alias.value_type(db), None); + return delegate_to(alias.value_type(db)); } - Type::TypeVar(type_var) => { - let type_var = type_var.typevar(db); - return match type_var.bound_or_constraints(db) { + Type::TypeVar(bound_typevar) => { + let typevar = bound_typevar.typevar(db); + match typevar.bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - delegate_with_error_mapped(bound, Some(type_var)) + let class = match bound { + Type::NominalInstance(instance) => Some(instance.class(db)), + Type::ProtocolInstance(protocol) => protocol + .to_nominal_instance() + .map(|instance| instance.class(db)), + _ => None, + }; + if let Some(class) = class { + SuperOwnerKind::InstanceTypeVar(bound_typevar, class) + } else { + return delegate_with_error_mapped(bound, Some(typevar)); + } } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - delegate_with_error_mapped(constraints.as_type(db), Some(type_var)) + return build_constrained_union( + constraints, + bound_typevar, + typevar, + SuperOwnerKind::InstanceTypeVar, + ); } - None => delegate_with_error_mapped(Type::object(), Some(type_var)), - }; + None => { + // No bound means the implicit upper bound is `object`. + SuperOwnerKind::InstanceTypeVar(bound_typevar, ClassType::object(db)) + } + } } Type::BooleanLiteral(_) | Type::TypeIs(_) | Type::TypeGuard(_) => { return delegate_to(KnownClass::Bool.to_instance(db)); @@ -456,35 +624,6 @@ impl<'db> BoundSuperType<'db> { } }; - // We don't use `Classbase::try_from_type` here because: - // - There are objects that may validly be present in a class's bases list - // but are not valid as pivot classes, e.g. `typing.ChainMap` - // - There are objects that are not valid in a class's bases list - // but are valid as pivot classes, e.g. unsubscripted `typing.Generic` - 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::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, - Type::SpecialForm(SpecialFormType::TypedDict) => ClassBase::TypedDict, - Type::Dynamic(dynamic) => ClassBase::Dynamic(dynamic), - _ => { - return Err(BoundSuperError::InvalidPivotClassType { - pivot_class: pivot_class_type, - }); - } - }; - if let Some(pivot_class) = pivot_class.into_class() && let Some(owner_class) = owner.into_class(db) { @@ -497,7 +636,7 @@ impl<'db> BoundSuperType<'db> { return Err(BoundSuperError::FailingConditionCheck { pivot_class: pivot_class_type, owner: owner_type, - typevar_context: None, + typevar_context: owner.typevar(db), }); } } @@ -556,18 +695,30 @@ impl<'db> BoundSuperType<'db> { db, attribute, Type::none(db), - Type::from(owner), + owner.owner_type(db), ) .0, ), - SuperOwnerKind::Instance(_) => { - let owner = Type::from(owner); + SuperOwnerKind::Instance(_) | SuperOwnerKind::InstanceTypeVar(..) => { + let owner_type = owner.owner_type(db); Some( Type::try_call_dunder_get_on_attribute( db, attribute, - owner, - owner.to_meta_type(db), + owner_type, + owner_type.to_meta_type(db), + ) + .0, + ) + } + SuperOwnerKind::ClassTypeVar(..) => { + let owner_type = owner.owner_type(db); + Some( + Type::try_call_dunder_get_on_attribute( + db, + attribute, + Type::none(db), + owner_type, ) .0, ) @@ -592,6 +743,9 @@ impl<'db> BoundSuperType<'db> { } SuperOwnerKind::Class(class) => class, SuperOwnerKind::Instance(instance) => instance.class(db), + SuperOwnerKind::InstanceTypeVar(_, class) | SuperOwnerKind::ClassTypeVar(_, class) => { + class + } }; let (class_literal, _) = class.class_literal(db); diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 693f27dcc7..859a12d0c3 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -988,7 +988,9 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .display_with(self.db, self.settings.singleline()) .fmt_detailed(f)?; f.write_str(", ")?; - Type::from(bound_super.owner(self.db)) + bound_super + .owner(self.db) + .owner_type(self.db) .display_with(self.db, self.settings.singleline()) .fmt_detailed(f)?; f.write_str(">") diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index fdf834b8b9..e49bdd1cba 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -189,6 +189,17 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( } (SuperOwnerKind::Instance(_), _) => Ordering::Less, (_, SuperOwnerKind::Instance(_)) => Ordering::Greater, + ( + SuperOwnerKind::InstanceTypeVar(left, _), + SuperOwnerKind::InstanceTypeVar(right, _), + ) => left.cmp(&right), + (SuperOwnerKind::InstanceTypeVar(..), _) => Ordering::Less, + (_, SuperOwnerKind::InstanceTypeVar(..)) => Ordering::Greater, + (SuperOwnerKind::ClassTypeVar(left, _), SuperOwnerKind::ClassTypeVar(right, _)) => { + left.cmp(&right) + } + (SuperOwnerKind::ClassTypeVar(..), _) => Ordering::Less, + (_, SuperOwnerKind::ClassTypeVar(..)) => Ordering::Greater, (SuperOwnerKind::Dynamic(left), SuperOwnerKind::Dynamic(right)) => { dynamic_elements_ordering(left, right) }