mirror of https://github.com/astral-sh/ruff
Merge f1b356ceb3 into 682d29c256
This commit is contained in:
commit
3856f4889a
|
|
@ -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]
|
||||
```
|
||||
|
|
@ -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::*;
|
||||
|
|
|
|||
Loading…
Reference in New Issue