This commit is contained in:
Blair Allan 2025-12-15 23:04:44 -05:00 committed by GitHub
commit 3856f4889a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 153 additions and 2 deletions

View File

@ -0,0 +1,62 @@
# `__slots__`
## Basic slot access
```py
class A:
__slots__ = ("foo", "bar")
def __init__(self, foo: int, bar: str):
self.foo = foo
self.bar = bar
a = A(1, "zip")
a.foo = 2
a.bar = "woo"
a.baz = 3 # error: [unresolved-attribute]
```
## Accessing undefined attributes
```py
class A:
__slots__ = ("x",)
a = A()
a.y = 1 # error: [unresolved-attribute]
```
## Empty slots
```py
class A:
__slots__ = ()
a = A()
a.x = 1 # error: [unresolved-attribute]
```
## Single character string
```py
class A:
__slots__ = "x"
a = A()
a.x = 1 # error: [possibly-missing-attribute]
a.y = 2 # error: [unresolved-attribute]
```
## Multi-character string
```py
class A:
__slots__ = "xyz"
a = A()
a.x = 1 # error: [possibly-missing-attribute]
a.y = 2 # error: [possibly-missing-attribute]
a.z = 3 # error: [possibly-missing-attribute]
a.xyz = 4 # error: [unresolved-attribute]
a.q = 5 # error: [unresolved-attribute]
```

View File

@ -3773,14 +3773,17 @@ impl<'db> ClassLiteral<'db> {
// The attribute is not *declared* in the class body. It could still be declared/bound
// in a method.
Self::implicit_attribute(db, body_scope, name, MethodDecorator::None)
let result =
Self::implicit_attribute(db, body_scope, name, MethodDecorator::None);
self.apply_slots_constraints(db, name, result.inner)
}
}
} else {
// This attribute is neither declared nor bound in the class body.
// It could still be implicitly defined in a method.
Self::implicit_attribute(db, body_scope, name, MethodDecorator::None)
let result = Self::implicit_attribute(db, body_scope, name, MethodDecorator::None);
self.apply_slots_constraints(db, name, result.inner)
}
}
@ -5957,6 +5960,92 @@ impl SlotsKind {
}
}
/// Helper functions for __slots__ support
impl<'db> ClassLiteral<'db> {
/// Extract the names of attributes defined in __slots__ as a set of strings.
/// Returns None if __slots__ is not defined, empty, or dynamic.
pub(super) fn slots_members(self, db: &'db dyn Db) -> Option<FxHashSet<String>> {
let Place::Defined(slots_ty, _, definedness) = self
.own_class_member(db, self.generic_context(db), None, "__slots__")
.inner
.place
else {
return None;
};
if matches!(definedness, Definedness::PossiblyUndefined) {
return None;
}
match slots_ty {
// __slots__ = ("a", "b")
Type::NominalInstance(nominal) => {
if let Some(tuple_spec) = nominal.tuple_spec(db) {
let mut slots = FxHashSet::default();
for element in tuple_spec.all_elements() {
if let Type::StringLiteral(string_literal) = element {
slots.insert(string_literal.value(db).to_string());
} else {
// Non-string element, consider it dynamic
return None;
}
}
Some(slots)
} else {
None
}
}
// __slots__ = "abc" # Expands to slots "a", "b", "c"
Type::StringLiteral(string_literal) => {
let mut slots = FxHashSet::default();
let slot_value = string_literal.value(db);
// Python treats a bare string as a sequence of slot names (one per character)
for ch in slot_value.chars() {
slots.insert(ch.to_string());
}
Some(slots)
}
_ => None,
}
}
/// Apply __slots__ constraints to attribute access.
fn apply_slots_constraints(
self,
db: &'db dyn Db,
name: &str,
result: PlaceAndQualifiers<'db>,
) -> Member<'db> {
// TODO: This function will be extended to support:
// - Inheritance: Check slots across the MRO chain
// - `__dict__` special case: Allow dynamic attributes when `__dict__` is in slots
if let Some(slots) = self.slots_members(db) {
if slots.contains(name) {
// Attribute is in __slots__, so it's allowed even if not found elsewhere
if result.place.is_undefined() {
// Return as possibly unbound since it's declared but not necessarily initialized
return Member {
inner: Place::Defined(
Type::unknown(),
TypeOrigin::Inferred,
Definedness::PossiblyUndefined,
)
.into(),
};
}
return Member { inner: result };
}
// Attribute is not in __slots__
return Member::unbound();
}
Member { inner: result }
}
}
#[cfg(test)]
mod tests {
use super::*;