[ty] Add support for PEP 800 (#20084)

This commit is contained in:
Alex Waygood 2025-08-25 19:39:05 +01:00 committed by GitHub
parent 33c5f6f4f8
commit ecf3c4ca11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 275 additions and 271 deletions

View file

@ -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<SolidBase<'db>> {
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<DisjointBase<'db>> {
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<SolidBase<'db>> {
/// 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<DisjointBase<'db>> {
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<SolidBase<'db>> {
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<DisjointBase<'db>> {
// TODO: Typeshed cannot add `@disjoint_base` to its `tuple` definition without breaking pyright.
// See <https://github.com/microsoft/pyright/issues/10836>.
// 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 {

View file

@ -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<SolidBase<'db>, IncompatibleBaseInfo<'db>>);
pub(super) struct IncompatibleBases<'db>(FxIndexMap<DisjointBase<'db>, 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>,
}

View file

@ -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

View file

@ -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,
);
}
}