From 41fa082414c7277082d7e19b307baea0a4771d4c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 12 May 2025 19:07:11 -0400 Subject: [PATCH] [ty] Allow a class to inherit from an intersection if the intersection contains a dynamic type and the intersection is not disjoint from `type` (#18055) --- .../resources/mdtest/mro.md | 23 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 7 ++++++ .../src/types/class_base.rs | 13 ++++++++++- 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md index 0b54d3e2bb..70369b5b05 100644 --- a/crates/ty_python_semantic/resources/mdtest/mro.md +++ b/crates/ty_python_semantic/resources/mdtest/mro.md @@ -154,6 +154,29 @@ reveal_type(E.__mro__) # revealed: tuple[, , , reveal_type(F.__mro__) ``` +## Inheritance with intersections that include `Unknown` + +An intersection that includes `Unknown` or `Any` is permitted as long as the intersection is not +disjoint from `type`. + +```py +from does_not_exist import DoesNotExist # error: [unresolved-import] + +reveal_type(DoesNotExist) # revealed: Unknown + +if hasattr(DoesNotExist, "__mro__"): + reveal_type(DoesNotExist) # revealed: Unknown & + + class Foo(DoesNotExist): ... # no error! + reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + +if not isinstance(DoesNotExist, type): + reveal_type(DoesNotExist) # revealed: Unknown & ~type + + class Foo(DoesNotExist): ... # error: [invalid-base] + reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +``` + ## `__bases__` lists that cause errors at runtime If the class's `__bases__` cause an exception to be raised at runtime and therefore the class diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cd48fa9877..9212781eab 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -543,6 +543,13 @@ impl<'db> Type<'db> { Self::Dynamic(DynamicType::Unknown) } + pub(crate) fn into_dynamic(self) -> Option { + match self { + Type::Dynamic(dynamic_type) => Some(dynamic_type), + _ => None, + } + } + pub fn object(db: &'db dyn Db) -> Self { KnownClass::Object.to_instance(db) } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index db5d239321..2069ec257e 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -107,8 +107,19 @@ impl<'db> ClassBase<'db> { { Self::try_from_type(db, todo_type!("GenericAlias instance")) } + Type::Intersection(inter) => { + let dynamic_element = inter + .positive(db) + .iter() + .find_map(|elem| elem.into_dynamic())?; + + if ty.is_disjoint_from(db, KnownClass::Type.to_instance(db)) { + None + } else { + Some(ClassBase::Dynamic(dynamic_element)) + } + } Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs? - Type::Intersection(_) => None, // TODO -- probably incorrect? Type::NominalInstance(_) => None, // TODO -- handle `__mro_entries__`? Type::PropertyInstance(_) => None, Type::Never