diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 3043c84eaa..25290865c8 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -422,7 +422,7 @@ class D(A, B, C): ... **Known problems** Classes that have "dynamic" definitions of `__slots__` (definitions do not consist -of string literals, or tuples of string literals) are not currently considered solid +of string literals, or tuples of string literals) are not currently considered disjoint bases by ty. Additionally, this check is not exhaustive: many C extensions (including several in diff --git a/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md index 45f5c5d53c..f64412f4f3 100644 --- a/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md +++ b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md @@ -103,7 +103,7 @@ class E( # error: [instance-layout-conflict] ): ... ``` -## A single "solid base" +## A single "disjoint base" ```py class A: @@ -152,14 +152,15 @@ class Baz(Foo, Bar): ... # fine Certain classes implemented in C extensions also have an extended instance memory layout, in the -same way as classes that define non-empty `__slots__`. (CPython internally calls all such classes -with a unique instance memory layout "solid bases", and we also borrow this term.) There is -currently no generalized way for ty to detect such a C-extension class, as there is currently no way -of expressing the fact that a class is a solid base in a stub file. However, ty special-cases -certain builtin classes in order to detect that attempting to combine them in a single MRO would -fail: +same way as classes that define non-empty `__slots__`. CPython internally calls all such classes +with a unique instance memory layout "solid bases", but [PEP 800](https://peps.python.org/pep-0800/) +calls these classes "disjoint bases", and this is the term we generally use. The `@disjoint_base` +decorator introduced by this PEP provides a generalised way for type checkers to identify such +classes. ```py +from typing_extensions import disjoint_base + # fmt: off class A( # error: [instance-layout-conflict] @@ -183,6 +184,17 @@ class E( # error: [instance-layout-conflict] class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] +@disjoint_base +class G: ... + +@disjoint_base +class H: ... + +class I( # error: [instance-layout-conflict] + G, + H +): ... + # fmt: on ``` @@ -193,9 +205,9 @@ We avoid emitting an `instance-layout-conflict` diagnostic for this class defini class Foo(range, str): ... # error: [subclass-of-final-class] ``` -## Multiple "solid bases" where one is a subclass of the other +## Multiple "disjoint bases" where one is a subclass of the other -A class is permitted to multiple-inherit from multiple solid bases if one is a subclass of the +A class is permitted to multiple-inherit from multiple disjoint bases if one is a subclass of the other: ```py diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_Built-ins_with_impli…_(f5857d64ce69ca1d).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_Built-ins_with_impli…_(f5857d64ce69ca1d).snap index 8c9a693f17..ddeee94396 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_Built-ins_with_impli…_(f5857d64ce69ca1d).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_Built-ins_with_impli…_(f5857d64ce69ca1d).snap @@ -12,59 +12,72 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict ## mdtest_snippet.py ``` - 1 | # fmt: off + 1 | from typing_extensions import disjoint_base 2 | - 3 | class A( # error: [instance-layout-conflict] - 4 | int, - 5 | str - 6 | ): ... - 7 | - 8 | class B: - 9 | __slots__ = ("b",) -10 | -11 | class C( # error: [instance-layout-conflict] -12 | int, -13 | B, -14 | ): ... -15 | class D(int): ... -16 | -17 | class E( # error: [instance-layout-conflict] -18 | D, -19 | str -20 | ): ... -21 | -22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] + 3 | # fmt: off + 4 | + 5 | class A( # error: [instance-layout-conflict] + 6 | int, + 7 | str + 8 | ): ... + 9 | +10 | class B: +11 | __slots__ = ("b",) +12 | +13 | class C( # error: [instance-layout-conflict] +14 | int, +15 | B, +16 | ): ... +17 | class D(int): ... +18 | +19 | class E( # error: [instance-layout-conflict] +20 | D, +21 | str +22 | ): ... 23 | -24 | # fmt: on -25 | class Foo(range, str): ... # error: [subclass-of-final-class] +24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] +25 | +26 | @disjoint_base +27 | class G: ... +28 | +29 | @disjoint_base +30 | class H: ... +31 | +32 | class I( # error: [instance-layout-conflict] +33 | G, +34 | H +35 | ): ... +36 | +37 | # fmt: on +38 | class Foo(range, str): ... # error: [subclass-of-final-class] ``` # Diagnostics ``` error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases - --> src/mdtest_snippet.py:3:7 - | -1 | # fmt: off -2 | -3 | class A( # error: [instance-layout-conflict] - | _______^ -4 | | int, -5 | | str -6 | | ): ... - | |_^ Bases `int` and `str` cannot be combined in multiple inheritance -7 | -8 | class B: - | + --> src/mdtest_snippet.py:5:7 + | + 3 | # fmt: off + 4 | + 5 | class A( # error: [instance-layout-conflict] + | _______^ + 6 | | int, + 7 | | str + 8 | | ): ... + | |_^ Bases `int` and `str` cannot be combined in multiple inheritance + 9 | +10 | class B: + | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts - --> src/mdtest_snippet.py:4:5 + --> src/mdtest_snippet.py:6:5 | -3 | class A( # error: [instance-layout-conflict] -4 | int, +5 | class A( # error: [instance-layout-conflict] +6 | int, | --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension -5 | str +7 | str | --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension -6 | ): ... +8 | ): ... | info: rule `instance-layout-conflict` is enabled by default @@ -72,28 +85,28 @@ info: rule `instance-layout-conflict` is enabled by default ``` error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases - --> src/mdtest_snippet.py:11:7 + --> src/mdtest_snippet.py:13:7 | - 9 | __slots__ = ("b",) -10 | -11 | class C( # error: [instance-layout-conflict] +11 | __slots__ = ("b",) +12 | +13 | class C( # error: [instance-layout-conflict] | _______^ -12 | | int, -13 | | B, -14 | | ): ... +14 | | int, +15 | | B, +16 | | ): ... | |_^ Bases `int` and `B` cannot be combined in multiple inheritance -15 | class D(int): ... +17 | class D(int): ... | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts - --> src/mdtest_snippet.py:12:5 + --> src/mdtest_snippet.py:14:5 | -11 | class C( # error: [instance-layout-conflict] -12 | int, +13 | class C( # error: [instance-layout-conflict] +14 | int, | --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension -13 | B, +15 | B, | - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__` -14 | ): ... -15 | class D(int): ... +16 | ): ... +17 | class D(int): ... | info: rule `instance-layout-conflict` is enabled by default @@ -101,31 +114,31 @@ info: rule `instance-layout-conflict` is enabled by default ``` error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases - --> src/mdtest_snippet.py:17:7 + --> src/mdtest_snippet.py:19:7 | -15 | class D(int): ... -16 | -17 | class E( # error: [instance-layout-conflict] +17 | class D(int): ... +18 | +19 | class E( # error: [instance-layout-conflict] | _______^ -18 | | D, -19 | | str -20 | | ): ... +20 | | D, +21 | | str +22 | | ): ... | |_^ Bases `D` and `str` cannot be combined in multiple inheritance -21 | -22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] +23 | +24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts - --> src/mdtest_snippet.py:18:5 + --> src/mdtest_snippet.py:20:5 | -17 | class E( # error: [instance-layout-conflict] -18 | D, +19 | class E( # error: [instance-layout-conflict] +20 | D, | - | | | `D` instances have a distinct memory layout because `D` inherits from `int` | `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension -19 | str +21 | str | --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension -20 | ): ... +22 | ): ... | info: rule `instance-layout-conflict` is enabled by default @@ -133,28 +146,57 @@ info: rule `instance-layout-conflict` is enabled by default ``` error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases - --> src/mdtest_snippet.py:22:7 + --> src/mdtest_snippet.py:24:7 | -20 | ): ... -21 | -22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `str`, `bytes` and `bytearray` cannot be combined in multiple inheritance +22 | ): ... 23 | -24 | # fmt: on +24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `str`, `bytes` and `bytearray` cannot be combined in multiple inheritance +25 | +26 | @disjoint_base | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts - --> src/mdtest_snippet.py:22:9 + --> src/mdtest_snippet.py:24:9 | -20 | ): ... -21 | -22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] +22 | ): ... +23 | +24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] | --- --- ----- --------- `bytearray` instances have a distinct memory layout because of the way `bytearray` is implemented in a C extension | | | | | | | `bytes` instances have a distinct memory layout because of the way `bytes` is implemented in a C extension | | `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension | `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension -23 | -24 | # fmt: on +25 | +26 | @disjoint_base + | +info: rule `instance-layout-conflict` is enabled by default + +``` + +``` +error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases + --> src/mdtest_snippet.py:32:7 + | +30 | class H: ... +31 | +32 | class I( # error: [instance-layout-conflict] + | _______^ +33 | | G, +34 | | H +35 | | ): ... + | |_^ Bases `G` and `H` cannot be combined in multiple inheritance +36 | +37 | # fmt: on + | +info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts + --> src/mdtest_snippet.py:33:5 + | +32 | class I( # error: [instance-layout-conflict] +33 | G, + | - `G` instances have a distinct memory layout because of the way `G` is implemented in a C extension +34 | H + | - `H` instances have a distinct memory layout because of the way `H` is implemented in a C extension +35 | ): ... | info: rule `instance-layout-conflict` is enabled by default @@ -162,10 +204,10 @@ info: rule `instance-layout-conflict` is enabled by default ``` error[subclass-of-final-class]: Class `Foo` cannot inherit from final class `range` - --> src/mdtest_snippet.py:25:11 + --> src/mdtest_snippet.py:38:11 | -24 | # fmt: on -25 | class Foo(range, str): ... # error: [subclass-of-final-class] +37 | # fmt: on +38 | class Foo(range, str): ... # error: [subclass-of-final-class] | ^^^^^ | info: rule `subclass-of-final-class` is enabled by default diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index 6ec192fb8c..aa5c3eaee2 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -87,7 +87,7 @@ static_assert(is_disjoint_from(memoryview, Foo)) static_assert(is_disjoint_from(type[memoryview], type[Foo])) ``` -## "Solid base" builtin types +## "Disjoint base" builtin types Most other builtins can be subclassed and can even be used in multiple inheritance. However, builtin classes *cannot* generally be used in multiple inheritance with other builtin types. This is because @@ -95,11 +95,14 @@ the CPython interpreter considers these classes "solid bases": due to the way th in C, they have atypical instance memory layouts. No class can ever have more than one "solid base" in its MRO. -It's not currently possible for ty to detect in a generalized way whether a class is a "solid base" -or not, but we special-case some commonly used builtin types: +[PEP 800](https://peps.python.org/pep-0800/) provides a generalised way for type checkers to know +whether a class has an atypical instance memory layout via the `@disjoint_base` decorator; we +generally use the term "disjoint base" for these classes. ```py +import asyncio from typing import Any +from typing_extensions import disjoint_base from ty_extensions import static_assert, is_disjoint_from class Foo: ... @@ -114,12 +117,23 @@ static_assert(is_disjoint_from(list, dict[Any, Any])) static_assert(is_disjoint_from(list[Foo], dict[Any, Any])) static_assert(is_disjoint_from(list[Any], dict[Any, Any])) static_assert(is_disjoint_from(type[list], type[dict])) + +static_assert(is_disjoint_from(asyncio.Task, dict)) + +@disjoint_base +class A: ... + +@disjoint_base +class B: ... + +static_assert(is_disjoint_from(A, B)) ``` -## Other solid bases +## Other disjoint bases As well as certain classes that are implemented in C extensions, any class that declares non-empty -`__slots__` is also considered a "solid base"; these types are also considered to be disjoint by ty: +`__slots__` is also considered a "disjoint base"; these types are also considered to be disjoint by +ty: ```py from ty_extensions import static_assert, is_disjoint_from @@ -141,7 +155,7 @@ static_assert(not is_disjoint_from(B, C)) static_assert(not is_disjoint_from(type[B], type[C])) ``` -Two solid bases are not disjoint if one inherits from the other, however: +Two disjoint bases are not disjoint if one inherits from the other, however: ```py class D(A): diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 2baeffd80e..ef706b18f9 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -465,9 +465,9 @@ impl<'db> ClassType<'db> { class_literal.definition(db) } - /// Return `Some` if this class is known to be a [`SolidBase`], or `None` if it is not. - pub(super) fn as_solid_base(self, db: &'db dyn Db) -> Option> { - self.class_literal(db).0.as_solid_base(db) + /// Return `Some` if this class is known to be a [`DisjointBase`], or `None` if it is not. + pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + self.class_literal(db).0.as_disjoint_base(db) } /// Return `true` if this class represents `known_class` @@ -633,13 +633,13 @@ impl<'db> ClassType<'db> { .apply_optional_specialization(db, specialization) } - /// Return the [`SolidBase`] that appears first in the MRO of this class. + /// Return the [`DisjointBase`] that appears first in the MRO of this class. /// - /// Returns `None` if this class does not have any solid bases in its MRO. - pub(super) fn nearest_solid_base(self, db: &'db dyn Db) -> Option> { + /// Returns `None` if this class does not have any disjoint bases in its MRO. + pub(super) fn nearest_disjoint_base(self, db: &'db dyn Db) -> Option> { self.iter_mro(db) .filter_map(ClassBase::into_class) - .find_map(|base| base.as_solid_base(db)) + .find_map(|base| base.as_disjoint_base(db)) } /// Return `true` if this class could coexist in an MRO with `other`. @@ -660,12 +660,17 @@ impl<'db> ClassType<'db> { return other.is_subclass_of(db, self); } - // Two solid bases can only coexist in an MRO if one is a subclass of the other. - if self.nearest_solid_base(db).is_some_and(|solid_base_1| { - other.nearest_solid_base(db).is_some_and(|solid_base_2| { - !solid_base_1.could_coexist_in_mro_with(db, &solid_base_2) + // Two disjoint bases can only coexist in an MRO if one is a subclass of the other. + if self + .nearest_disjoint_base(db) + .is_some_and(|disjoint_base_1| { + other + .nearest_disjoint_base(db) + .is_some_and(|disjoint_base_2| { + !disjoint_base_1.could_coexist_in_mro_with(db, &disjoint_base_2) + }) }) - }) { + { return false; } @@ -1519,14 +1524,19 @@ impl<'db> ClassLiteral<'db> { } } - /// Return `Some()` if this class is known to be a [`SolidBase`], or `None` if it is not. - pub(super) fn as_solid_base(self, db: &'db dyn Db) -> Option> { - if let Some(known_class) = self.known(db) { - known_class - .is_solid_base() - .then_some(SolidBase::hard_coded(self)) + /// Return `Some()` if this class is known to be a [`DisjointBase`], or `None` if it is not. + pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + // TODO: Typeshed cannot add `@disjoint_base` to its `tuple` definition without breaking pyright. + // See . + // This should be fixed soon; we can remove this workaround then. + if self.is_known(db, KnownClass::Tuple) + || self + .known_function_decorators(db) + .contains(&KnownFunction::DisjointBase) + { + Some(DisjointBase::due_to_decorator(self)) } else if SlotsKind::from(db, self) == SlotsKind::NotEmpty { - Some(SolidBase::due_to_dunder_slots(self)) + Some(DisjointBase::due_to_dunder_slots(self)) } else { None } @@ -3375,39 +3385,47 @@ impl InheritanceCycle { /// CPython internally considers a class a "solid base" if it has an atypical instance memory layout, /// with additional memory "slots" for each instance, besides the default object metadata and an -/// attribute dictionary. A "solid base" can be a class defined in a C extension which defines C-level -/// instance slots, or a Python class that defines non-empty `__slots__`. +/// attribute dictionary. Per [PEP 800], however, we use the term "disjoint base" for this concept. /// -/// Two solid bases can only coexist in a class's MRO if one is a subclass of the other. Knowing if -/// a class is "solid base" or not is therefore valuable for inferring whether two instance types or +/// A "disjoint base" can be a class defined in a C extension which defines C-level instance slots, +/// or a Python class that defines non-empty `__slots__`. C-level instance slots are not generally +/// visible to Python code, but PEP 800 specifies that any class decorated with +/// `@typing_extensions.disjoint_base` should be treated by type checkers as a disjoint base; it is +/// assumed that classes with C-level instance slots will be decorated as such when they appear in +/// stub files. +/// +/// Two disjoint bases can only coexist in a class's MRO if one is a subclass of the other. Knowing if +/// a class is "disjoint base" or not is therefore valuable for inferring whether two instance types or /// two subclass-of types are disjoint from each other. It also allows us to detect possible /// `TypeError`s resulting from class definitions. +/// +/// [PEP 800]: https://peps.python.org/pep-0800/ #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub(super) struct SolidBase<'db> { +pub(super) struct DisjointBase<'db> { pub(super) class: ClassLiteral<'db>, - pub(super) kind: SolidBaseKind, + pub(super) kind: DisjointBaseKind, } -impl<'db> SolidBase<'db> { - /// Creates a [`SolidBase`] instance where we know the class is a solid base - /// because it is special-cased by ty. - fn hard_coded(class: ClassLiteral<'db>) -> Self { +impl<'db> DisjointBase<'db> { + /// Creates a [`DisjointBase`] instance where we know the class is a disjoint base + /// because it has the `@disjoint_base` decorator on its definition + fn due_to_decorator(class: ClassLiteral<'db>) -> Self { Self { class, - kind: SolidBaseKind::HardCoded, + kind: DisjointBaseKind::DisjointBaseDecorator, } } - /// Creates a [`SolidBase`] instance where we know the class is a solid base + /// Creates a [`DisjointBase`] instance where we know the class is a disjoint base /// because of its `__slots__` definition. fn due_to_dunder_slots(class: ClassLiteral<'db>) -> Self { Self { class, - kind: SolidBaseKind::DefinesSlots, + kind: DisjointBaseKind::DefinesSlots, } } - /// Two solid bases can only coexist in a class's MRO if one is a subclass of the other + /// Two disjoint bases can only coexist in a class's MRO if one is a subclass of the other fn could_coexist_in_mro_with(&self, db: &'db dyn Db, other: &Self) -> bool { self == other || self @@ -3420,10 +3438,11 @@ impl<'db> SolidBase<'db> { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(super) enum SolidBaseKind { - /// We know the class is a solid base because of some hardcoded knowledge in ty. - HardCoded, - /// We know the class is a solid base because it has a non-empty `__slots__` definition. +pub(super) enum DisjointBaseKind { + /// We know the class is a disjoint base because it's either hardcoded in ty + /// or has the `@disjoint_base` decorator. + DisjointBaseDecorator, + /// We know the class is a disjoint base because it has a non-empty `__slots__` definition. DefinesSlots, } @@ -3624,94 +3643,6 @@ impl KnownClass { } } - /// Return `true` if this class is a [`SolidBase`] - const fn is_solid_base(self) -> bool { - match self { - Self::Object => false, - - // Most non-`@final` builtins (other than `object`) are solid bases. - Self::Set - | Self::FrozenSet - | Self::BaseException - | Self::Bytearray - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Tuple - | Self::Dict - | Self::Slice - | Self::Property - | Self::Staticmethod - | Self::Classmethod - | Self::Deprecated - | Self::Type - | Self::ModuleType - | Self::Super - | Self::GenericAlias - | Self::Deque - | Self::Bytes => true, - - // It doesn't really make sense to ask the question for `@final` types, - // since these are "more than solid bases". But we'll anyway infer a `@final` - // class as being disjoint from a class that doesn't appear in its MRO, - // and we'll anyway complain if we see a class definition that includes a - // `@final` class in its bases. We therefore return `false` here to avoid - // unnecessary duplicate diagnostics elsewhere. - Self::TypeVarTuple - | Self::TypeAliasType - | Self::UnionType - | Self::NoDefaultType - | Self::MethodType - | Self::MethodWrapperType - | Self::FunctionType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::StdlibAlias - | Self::SpecialForm - | Self::TypeVar - | Self::ParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::WrapperDescriptorType - | Self::EllipsisType - | Self::NotImplementedType - | Self::KwOnly - | Self::InitVar - | Self::VersionInfo - | Self::Bool - | Self::NoneType - | Self::CoroutineType => false, - - // Anything with a *runtime* MRO (N.B. sometimes different from the MRO that typeshed gives!) - // with length >2, or anything that is implemented in pure Python, is not a solid base. - Self::ABCMeta - | Self::Awaitable - | Self::Generator - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::ChainMap - | Self::Exception - | Self::ExceptionGroup - | Self::Field - | Self::SupportsIndex - | Self::NamedTupleFallback - | Self::NamedTupleLike - | Self::TypedDictFallback - | Self::Counter - | Self::DefaultDict - | Self::OrderedDict - | Self::NewType - | Self::Iterable - | Self::Iterator - | Self::BaseExceptionGroup => false, - } - } - /// Return `true` if this class is a subclass of `enum.Enum` *and* has enum members, i.e. /// if it is an "actual" enum, not `enum.Enum` itself or a similar custom enum class. pub(crate) const fn is_enum_subclass_with_members(self) -> bool { diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index bc2fcfb2e3..5377614e5a 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -10,7 +10,7 @@ use crate::semantic_index::SemanticIndex; use crate::semantic_index::definition::Definition; use crate::semantic_index::place::{PlaceTable, ScopedPlaceId}; use crate::suppression::FileSuppressionId; -use crate::types::class::{Field, SolidBase, SolidBaseKind}; +use crate::types::class::{DisjointBase, DisjointBaseKind, Field}; use crate::types::function::KnownFunction; use crate::types::string_annotation::{ BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION, @@ -405,7 +405,7 @@ declare_lint! { /// /// ## Known problems /// Classes that have "dynamic" definitions of `__slots__` (definitions do not consist - /// of string literals, or tuples of string literals) are not currently considered solid + /// of string literals, or tuples of string literals) are not currently considered disjoint /// bases by ty. /// /// Additionally, this check is not exhaustive: many C extensions (including several in @@ -2170,9 +2170,9 @@ pub(crate) fn report_instance_layout_conflict( context: &InferContext, class: ClassLiteral, node: &ast::StmtClassDef, - solid_bases: &IncompatibleBases, + disjoint_bases: &IncompatibleBases, ) { - debug_assert!(solid_bases.len() > 1); + debug_assert!(disjoint_bases.len() > 1); let db = context.db(); @@ -2186,7 +2186,7 @@ pub(crate) fn report_instance_layout_conflict( diagnostic.set_primary_message(format_args!( "Bases {} cannot be combined in multiple inheritance", - solid_bases.describe_problematic_class_bases(db) + disjoint_bases.describe_problematic_class_bases(db) )); let mut subdiagnostic = SubDiagnostic::new( @@ -2195,23 +2195,23 @@ pub(crate) fn report_instance_layout_conflict( have incompatible memory layouts", ); - for (solid_base, solid_base_info) in solid_bases { + for (disjoint_base, disjoint_base_info) in disjoint_bases { let IncompatibleBaseInfo { node_index, originating_base, - } = solid_base_info; + } = disjoint_base_info; let span = context.span(&node.bases()[*node_index]); let mut annotation = Annotation::secondary(span.clone()); - if solid_base.class == *originating_base { - match solid_base.kind { - SolidBaseKind::DefinesSlots => { + if disjoint_base.class == *originating_base { + match disjoint_base.kind { + DisjointBaseKind::DefinesSlots => { annotation = annotation.message(format_args!( "`{base}` instances have a distinct memory layout because `{base}` defines non-empty `__slots__`", base = originating_base.name(db) )); } - SolidBaseKind::HardCoded => { + DisjointBaseKind::DisjointBaseDecorator => { annotation = annotation.message(format_args!( "`{base}` instances have a distinct memory layout because of the way `{base}` \ is implemented in a C extension", @@ -2223,26 +2223,28 @@ pub(crate) fn report_instance_layout_conflict( } else { annotation = annotation.message(format_args!( "`{base}` instances have a distinct memory layout \ - because `{base}` inherits from `{solid_base}`", + because `{base}` inherits from `{disjoint_base}`", base = originating_base.name(db), - solid_base = solid_base.class.name(db) + disjoint_base = disjoint_base.class.name(db) )); subdiagnostic.annotate(annotation); let mut additional_annotation = Annotation::secondary(span); - additional_annotation = match solid_base.kind { - SolidBaseKind::DefinesSlots => additional_annotation.message(format_args!( - "`{solid_base}` instances have a distinct memory layout because `{solid_base}` \ + additional_annotation = match disjoint_base.kind { + DisjointBaseKind::DefinesSlots => additional_annotation.message(format_args!( + "`{disjoint_base}` instances have a distinct memory layout because `{disjoint_base}` \ defines non-empty `__slots__`", - solid_base = solid_base.class.name(db), + disjoint_base = disjoint_base.class.name(db), )), - SolidBaseKind::HardCoded => additional_annotation.message(format_args!( - "`{solid_base}` instances have a distinct memory layout \ - because of the way `{solid_base}` is implemented in a C extension", - solid_base = solid_base.class.name(db), - )), + DisjointBaseKind::DisjointBaseDecorator => { + additional_annotation.message(format_args!( + "`{disjoint_base}` instances have a distinct memory layout \ + because of the way `{disjoint_base}` is implemented in a C extension", + disjoint_base = disjoint_base.class.name(db), + )) + } }; subdiagnostic.annotate(additional_annotation); @@ -2252,20 +2254,20 @@ pub(crate) fn report_instance_layout_conflict( diagnostic.sub(subdiagnostic); } -/// Information regarding the conflicting solid bases a class is inferred to have in its MRO. +/// Information regarding the conflicting disjoint bases a class is inferred to have in its MRO. /// -/// For each solid base, we record information about which element in the class's bases list -/// caused the solid base to be included in the class's MRO. +/// For each disjoint base, we record information about which element in the class's bases list +/// caused the disjoint base to be included in the class's MRO. /// -/// The inner data is an `IndexMap` to ensure that diagnostics regarding conflicting solid bases +/// The inner data is an `IndexMap` to ensure that diagnostics regarding conflicting disjoint bases /// are reported in a stable order. #[derive(Debug, Default)] -pub(super) struct IncompatibleBases<'db>(FxIndexMap, IncompatibleBaseInfo<'db>>); +pub(super) struct IncompatibleBases<'db>(FxIndexMap, IncompatibleBaseInfo<'db>>); impl<'db> IncompatibleBases<'db> { pub(super) fn insert( &mut self, - base: SolidBase<'db>, + base: DisjointBase<'db>, node_index: usize, class: ClassLiteral<'db>, ) { @@ -2287,19 +2289,19 @@ impl<'db> IncompatibleBases<'db> { self.0.len() } - /// Two solid bases are allowed to coexist in an MRO if one is a subclass of the other. + /// Two disjoint bases are allowed to coexist in an MRO if one is a subclass of the other. /// This method therefore removes any entry in `self` that is a subclass of one or more /// other entries also contained in `self`. pub(super) fn remove_redundant_entries(&mut self, db: &'db dyn Db) { self.0 = self .0 .iter() - .filter(|(solid_base, _)| { + .filter(|(disjoint_base, _)| { self.0 .keys() - .filter(|other_base| other_base != solid_base) + .filter(|other_base| other_base != disjoint_base) .all(|other_base| { - !solid_base.class.is_subclass_of( + !disjoint_base.class.is_subclass_of( db, None, other_base.class.default_specialization(db), @@ -2312,25 +2314,25 @@ impl<'db> IncompatibleBases<'db> { } impl<'a, 'db> IntoIterator for &'a IncompatibleBases<'db> { - type Item = (&'a SolidBase<'db>, &'a IncompatibleBaseInfo<'db>); - type IntoIter = indexmap::map::Iter<'a, SolidBase<'db>, IncompatibleBaseInfo<'db>>; + type Item = (&'a DisjointBase<'db>, &'a IncompatibleBaseInfo<'db>); + type IntoIter = indexmap::map::Iter<'a, DisjointBase<'db>, IncompatibleBaseInfo<'db>>; fn into_iter(self) -> Self::IntoIter { self.0.iter() } } -/// Information about which class base the "solid base" stems from +/// Information about which class base the "disjoint base" stems from #[derive(Debug, Copy, Clone)] pub(super) struct IncompatibleBaseInfo<'db> { /// The index of the problematic base in the [`ast::StmtClassDef`]'s bases list. node_index: usize, /// The base class in the [`ast::StmtClassDef`]'s bases list that caused - /// the solid base to be included in the class's MRO. + /// the disjoint base to be included in the class's MRO. /// - /// This won't necessarily be the same class as the `SolidBase`'s class, - /// as the `SolidBase` may have found its way into the class's MRO by dint of it being a + /// This won't necessarily be the same class as the `DisjointBase`'s class, + /// as the `DisjointBase` may have found its way into the class's MRO by dint of it being a /// superclass of one of the classes in the class definition's bases list. originating_base: ClassLiteral<'db>, } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 691068f896..5fc02fc0c3 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1109,7 +1109,8 @@ pub enum KnownFunction { /// `typing(_extensions).final` Final, - + /// `typing(_extensions).disjoint_base` + DisjointBase, /// [`typing(_extensions).no_type_check`](https://typing.python.org/en/latest/spec/directives.html#no-type-check) NoTypeCheck, @@ -1212,6 +1213,7 @@ impl KnownFunction { | Self::GetProtocolMembers | Self::RuntimeCheckable | Self::DataclassTransform + | Self::DisjointBase | Self::NoTypeCheck => { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } @@ -1574,6 +1576,7 @@ pub(crate) mod tests { | KnownFunction::GetProtocolMembers | KnownFunction::RuntimeCheckable | KnownFunction::DataclassTransform + | KnownFunction::DisjointBase | KnownFunction::NoTypeCheck => KnownModule::TypingExtensions, KnownFunction::IsSingleton diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 27c23dcf97..97392af811 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -1147,7 +1147,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let is_protocol = class.is_protocol(self.db()); - let mut solid_bases = IncompatibleBases::default(); + let mut disjoint_bases = IncompatibleBases::default(); // (3) Iterate through the class's explicit bases to check for various possible errors: // - Check for inheritance from plain `Generic`, @@ -1209,8 +1209,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ => continue, }; - if let Some(solid_base) = base_class.nearest_solid_base(self.db()) { - solid_bases.insert(solid_base, i, base_class.class_literal(self.db()).0); + if let Some(disjoint_base) = base_class.nearest_disjoint_base(self.db()) { + disjoint_bases.insert(disjoint_base, i, base_class.class_literal(self.db()).0); } if is_protocol @@ -1301,14 +1301,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }, Ok(_) => { - solid_bases.remove_redundant_entries(self.db()); + disjoint_bases.remove_redundant_entries(self.db()); - if solid_bases.len() > 1 { + if disjoint_bases.len() > 1 { report_instance_layout_conflict( &self.context, class, class_node, - &solid_bases, + &disjoint_bases, ); } } diff --git a/ty.schema.json b/ty.schema.json index 6b55406977..a9261dfefb 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -433,7 +433,7 @@ }, "instance-layout-conflict": { "title": "detects class definitions that raise `TypeError` due to instance layout conflict", - "description": "## What it does\nChecks for classes definitions which will fail at runtime due to\n\"instance memory layout conflicts\".\n\nThis error is usually caused by attempting to combine multiple classes\nthat define non-empty `__slots__` in a class's [Method Resolution Order]\n(MRO), or by attempting to combine multiple builtin classes in a class's\nMRO.\n\n## Why is this bad?\nInheriting from bases with conflicting instance memory layouts\nwill lead to a `TypeError` at runtime.\n\nAn instance memory layout conflict occurs when CPython cannot determine\nthe memory layout instances of a class should have, because the instance\nmemory layout of one of its bases conflicts with the instance memory layout\nof one or more of its other bases.\n\nFor example, if a Python class defines non-empty `__slots__`, this will\nimpact the memory layout of instances of that class. Multiple inheritance\nfrom more than one different class defining non-empty `__slots__` is not\nallowed:\n\n```python\nclass A:\n __slots__ = (\"a\", \"b\")\n\nclass B:\n __slots__ = (\"a\", \"b\") # Even if the values are the same\n\n# TypeError: multiple bases have instance lay-out conflict\nclass C(A, B): ...\n```\n\nAn instance layout conflict can also be caused by attempting to use\nmultiple inheritance with two builtin classes, due to the way that these\nclasses are implemented in a CPython C extension:\n\n```python\nclass A(int, float): ... # TypeError: multiple bases have instance lay-out conflict\n```\n\nNote that pure-Python classes with no `__slots__`, or pure-Python classes\nwith empty `__slots__`, are always compatible:\n\n```python\nclass A: ...\nclass B:\n __slots__ = ()\nclass C:\n __slots__ = (\"a\", \"b\")\n\n# fine\nclass D(A, B, C): ...\n```\n\n## Known problems\nClasses that have \"dynamic\" definitions of `__slots__` (definitions do not consist\nof string literals, or tuples of string literals) are not currently considered solid\nbases by ty.\n\nAdditionally, this check is not exhaustive: many C extensions (including several in\nthe standard library) define classes that use extended memory layouts and thus cannot\ncoexist in a single MRO. Since it is currently not possible to represent this fact in\nstub files, having a full knowledge of these classes is also impossible. When it comes\nto classes that do not define `__slots__` at the Python level, therefore, ty, currently\nonly hard-codes a number of cases where it knows that a class will produce instances with\nan atypical memory layout.\n\n## Further reading\n- [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots)\n- [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order)\n\n[Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order", + "description": "## What it does\nChecks for classes definitions which will fail at runtime due to\n\"instance memory layout conflicts\".\n\nThis error is usually caused by attempting to combine multiple classes\nthat define non-empty `__slots__` in a class's [Method Resolution Order]\n(MRO), or by attempting to combine multiple builtin classes in a class's\nMRO.\n\n## Why is this bad?\nInheriting from bases with conflicting instance memory layouts\nwill lead to a `TypeError` at runtime.\n\nAn instance memory layout conflict occurs when CPython cannot determine\nthe memory layout instances of a class should have, because the instance\nmemory layout of one of its bases conflicts with the instance memory layout\nof one or more of its other bases.\n\nFor example, if a Python class defines non-empty `__slots__`, this will\nimpact the memory layout of instances of that class. Multiple inheritance\nfrom more than one different class defining non-empty `__slots__` is not\nallowed:\n\n```python\nclass A:\n __slots__ = (\"a\", \"b\")\n\nclass B:\n __slots__ = (\"a\", \"b\") # Even if the values are the same\n\n# TypeError: multiple bases have instance lay-out conflict\nclass C(A, B): ...\n```\n\nAn instance layout conflict can also be caused by attempting to use\nmultiple inheritance with two builtin classes, due to the way that these\nclasses are implemented in a CPython C extension:\n\n```python\nclass A(int, float): ... # TypeError: multiple bases have instance lay-out conflict\n```\n\nNote that pure-Python classes with no `__slots__`, or pure-Python classes\nwith empty `__slots__`, are always compatible:\n\n```python\nclass A: ...\nclass B:\n __slots__ = ()\nclass C:\n __slots__ = (\"a\", \"b\")\n\n# fine\nclass D(A, B, C): ...\n```\n\n## Known problems\nClasses that have \"dynamic\" definitions of `__slots__` (definitions do not consist\nof string literals, or tuples of string literals) are not currently considered disjoint\nbases by ty.\n\nAdditionally, this check is not exhaustive: many C extensions (including several in\nthe standard library) define classes that use extended memory layouts and thus cannot\ncoexist in a single MRO. Since it is currently not possible to represent this fact in\nstub files, having a full knowledge of these classes is also impossible. When it comes\nto classes that do not define `__slots__` at the Python level, therefore, ty, currently\nonly hard-codes a number of cases where it knows that a class will produce instances with\nan atypical memory layout.\n\n## Further reading\n- [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots)\n- [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order)\n\n[Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order", "default": "error", "oneOf": [ {