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
|
// The attribute is not *declared* in the class body. It could still be declared/bound
|
||||||
// in a method.
|
// 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 {
|
} else {
|
||||||
// This attribute is neither declared nor bound in the class body.
|
// This attribute is neither declared nor bound in the class body.
|
||||||
// It could still be implicitly defined in a method.
|
// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue