diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index cd4f6ff26e..c5f7cf79ef 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -1179,6 +1179,34 @@ def _( reveal_type(subclass_of_p) # revealed: type[P] ``` +Using `type[]` with a union type alias distributes the `type[]` over the union elements: + +```py +from typing import Union + +class C: ... +class D: ... + +UnionAlias1 = C | D +UnionAlias2 = Union[C, D] + +SubclassOfUnionAlias1 = type[UnionAlias1] +SubclassOfUnionAlias2 = type[UnionAlias2] + +reveal_type(SubclassOfUnionAlias1) # revealed: +reveal_type(SubclassOfUnionAlias2) # revealed: + +def _( + subclass_of_union_alias1: SubclassOfUnionAlias1, + subclass_of_union_alias2: SubclassOfUnionAlias2, +): + reveal_type(subclass_of_union_alias1) # revealed: type[C] | type[D] + reveal_type(subclass_of_union_alias1()) # revealed: C | D + + reveal_type(subclass_of_union_alias2) # revealed: type[C] | type[D] + reveal_type(subclass_of_union_alias2()) # revealed: C | D +``` + Invalid uses result in diagnostics: ```py diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md index 198390adb9..12d8d0a0e7 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -71,6 +71,29 @@ reveal_type(constrained(str)) # revealed: str constrained(A) ``` +`type[T]` with a union upper bound `T: A | B` represents the metatype of a type variable `T` where +`T` can be solved to any subtype of `A` or any subtype of `B`. It behaves similarly to a type +variable that can be solved to any subclass of `A` or any subclass of `B`. Since all classes are +instances of `type`, attributes on instances of `type` like `__name__` and `__qualname__` should +still be accessible: + +```py +class Replace: ... +class Multiply: ... + +def union_bound[T: Replace | Multiply](x: type[T]) -> T: + reveal_type(x) # revealed: type[T@union_bound] + # All classes have __name__ and __qualname__ from type's metaclass + reveal_type(x.__name__) # revealed: str + reveal_type(x.__qualname__) # revealed: str + reveal_type(x()) # revealed: T@union_bound + + return x() + +reveal_type(union_bound(Replace)) # revealed: Replace +reveal_type(union_bound(Multiply)) # revealed: Multiply +``` + ## Union ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 2ec65ea8bf..da46f40709 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7743,11 +7743,7 @@ impl<'db> Type<'db> { } 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().into_class(db) { - None => self, - Some(class) => SubclassOfType::try_from_type(db, class.metaclass(db)) - .unwrap_or(SubclassOfType::subclass_of_unknown()), - }, + Type::SubclassOf(subclass_of_ty) => subclass_of_ty.to_meta_type(db), Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class_literal(db), Type::Dynamic(dynamic) => SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)), // TODO intersections diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index eb016199a5..b27132d795 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, TypeVarBoundOrConstraints, TypedDictType, todo_type, + TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypedDictType, UnionType, todo_type, }; use crate::{Db, FxOrderSet}; @@ -79,6 +79,18 @@ impl<'db> SubclassOfType<'db> { /// 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> { + // Handle unions by distributing `type[]` over each element: + // `type[A | B]` -> `type[A] | type[B]` + if let Type::Union(union) = ty { + return UnionType::try_from_elements( + db, + union + .elements(db) + .iter() + .map(|element| Self::try_from_instance(db, *element)), + ); + } + SubclassOfInner::try_from_instance(db, ty).map(|subclass_of| Self::from(db, subclass_of)) } @@ -290,6 +302,35 @@ impl<'db> SubclassOfType<'db> { } } + /// Compute the metatype of this `type[T]`. + /// + /// For `type[C]` where `C` is a concrete class, this returns `type[metaclass(C)]`. + /// For `type[T]` where `T` is a `TypeVar`, this computes the metatype based on the + /// `TypeVar`'s bounds or constraints. + pub(crate) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + match self.subclass_of.with_transposed_type_var(db) { + SubclassOfInner::Dynamic(dynamic) => { + SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)) + } + SubclassOfInner::Class(class) => SubclassOfType::try_from_type(db, class.metaclass(db)) + .unwrap_or(SubclassOfType::subclass_of_unknown()), + // For `type[T]` where `T` is a TypeVar, `with_transposed_type_var` transforms + // the bounds from instance types to `type[]` types. For example, `type[T]` where + // `T: A | B` becomes a TypeVar with bound `type[A] | type[B]`. The metatype is + // then the metatype of that bound. + SubclassOfInner::TypeVar(bound_typevar) => { + match bound_typevar.typevar(db).bound_or_constraints(db) { + // `with_transposed_type_var` always adds a bound for unbounded TypeVars + None => unreachable!(), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db), + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + constraints.as_type(db).to_meta_type(db) + } + } + } + } + } + pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool { self.subclass_of .into_class(db)