-
Notifications
You must be signed in to change notification settings - Fork 316
Add __slots__ validation for attribute assignments #2029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1a15119
40cfc1f
a7fde3d
93c46f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ use dupe::Dupe; | |
| use itertools::Either; | ||
| use itertools::Itertools; | ||
| use pyrefly_graph::index::Idx; | ||
| use pyrefly_python::dunder; | ||
| use pyrefly_python::module_name::ModuleName; | ||
| use pyrefly_python::short_identifier::ShortIdentifier; | ||
| use pyrefly_types::annotation::Annotation; | ||
|
|
@@ -44,6 +45,7 @@ use crate::alt::types::class_metadata::InitDefaults; | |
| use crate::alt::types::class_metadata::Metaclass; | ||
| use crate::alt::types::class_metadata::NamedTupleMetadata; | ||
| use crate::alt::types::class_metadata::ProtocolMetadata; | ||
| use crate::alt::types::class_metadata::SlotsMetadata; | ||
| use crate::alt::types::class_metadata::TotalOrderingMetadata; | ||
| use crate::alt::types::class_metadata::TypedDictMetadata; | ||
| use crate::alt::types::decorated_function::Decorator; | ||
|
|
@@ -69,6 +71,7 @@ use crate::types::keywords::DataclassKeywords; | |
| use crate::types::keywords::DataclassTransformMetadata; | ||
| use crate::types::keywords::TypeMap; | ||
| use crate::types::literal::Lit; | ||
| use crate::types::tuple::Tuple; | ||
| use crate::types::types::CalleeKind; | ||
| use crate::types::types::Type; | ||
|
|
||
|
|
@@ -339,6 +342,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { | |
| .as_ref() | ||
| .map(|m| m.pydantic_model_kind.clone()); | ||
|
|
||
| // Compute slots metadata from __slots__ or dataclass(slots=True) | ||
| let slots_metadata = self.compute_slots_metadata(cls, dataclass_metadata.as_ref()); | ||
|
|
||
| ClassMetadata::new( | ||
| bases, | ||
| calculated_metaclass, | ||
|
|
@@ -359,6 +365,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { | |
| dataclass_transform_metadata, | ||
| pydantic_model_kind, | ||
| django_model_metadata, | ||
| slots_metadata, | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -1254,4 +1261,83 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { | |
| } | ||
| false | ||
| } | ||
|
|
||
| /// Compute slots metadata for a class. | ||
| /// | ||
| /// Returns `Some(SlotsMetadata)` if: | ||
| /// - The class has a manual `__slots__` definition, OR | ||
| /// - The class is a `@dataclass(slots=True)` | ||
| /// | ||
| /// Returns `None` if the class has no slots (has implicit `__dict__`). | ||
| /// | ||
| /// Note: This function should be called on-demand during slots checks | ||
| /// to ensure the `__slots__` field is fully solved. | ||
| pub fn compute_slots_metadata( | ||
| &self, | ||
| cls: &Class, | ||
| dataclass_metadata: Option<&DataclassMetadata>, | ||
| ) -> Option<SlotsMetadata> { | ||
| // For dataclass(slots=True), use the dataclass fields as slots. | ||
| // Note: The synthesized __slots__ field is added by dataclass.rs, but we compute | ||
| // the SlotsMetadata here based on the dataclass fields. | ||
| if let Some(dc) = dataclass_metadata | ||
| && dc.kws.slots | ||
| { | ||
| return Some(SlotsMetadata { | ||
| slots: dc.fields.clone(), | ||
| has_dict_slot: false, | ||
| }); | ||
| } | ||
|
|
||
| // Check for manual __slots__ definition on this class. | ||
| // We only look at the current class, not ancestors (handled during validation). | ||
| if !cls.contains(&dunder::SLOTS) { | ||
| return None; | ||
| } | ||
|
|
||
| let slots_field = self.get_field_from_current_class_only(cls, &dunder::SLOTS)?; | ||
| let slots_ty = slots_field.ty(); | ||
|
|
||
| // Extract string literals from the slots type. | ||
| let mut slots = SmallSet::new(); | ||
| let mut has_dict_slot = false; | ||
|
|
||
| match &slots_ty { | ||
| Type::Tuple(Tuple::Concrete(elts)) => { | ||
| for elt in elts.iter() { | ||
| if let Type::Literal(Lit::Str(s)) = elt { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am seeing various new errors coming from mypy primer. Curious if there is any sense on whether those are legitimate? Also, do we know that at this point, pyrefly is inferring the literal types we expect for slots?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looked into these, most are false positives. The slots check was running for all attribute types including properties/descriptors, but Python's Fixed by only checking slots for For the literal type inference, the code extracts literal strings from the tuple (the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, those are the latest results. I'm not completely sure myself which ones are still false positives (after adding the descriptor/property fix), but I believe they appear because slots checking didn't exist before this PR, so they're "new" in the sense that we're now checking something we weren't before. Open to more feedback for the primer results. |
||
| let name = Name::new(s.as_str()); | ||
| if name.as_str() == "__dict__" { | ||
| has_dict_slot = true; | ||
| } else { | ||
| slots.insert(name); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Empty tuple: __slots__ = () | ||
| Type::Tuple(Tuple::Unbounded(_)) => { | ||
| // No slots defined, but __slots__ exists (restricts to no dynamic attrs) | ||
| } | ||
| // Single string literal: __slots__ = "x" (rare but valid) | ||
| Type::Literal(Lit::Str(s)) => { | ||
| let name = Name::new(s.as_str()); | ||
| if name.as_str() == "__dict__" { | ||
| has_dict_slot = true; | ||
| } else { | ||
| slots.insert(name); | ||
| } | ||
| } | ||
| _ => { | ||
| // Cannot determine slots statically (e.g., __slots__ = get_slots()). | ||
| // Treat as having __dict__ to avoid false positives. | ||
| has_dict_slot = true; | ||
| } | ||
| } | ||
|
|
||
| Some(SlotsMetadata { | ||
| slots, | ||
| has_dict_slot, | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we stored the slots metadata in the class, why are we computing it here as well? More generally, why is recomputing needed/can we find a way to compute this only once for each class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, I actually tried switching to use the stored
metadata.slots_metadata()but it broke the tests. I believe the issue is timing:slots_metadatagets computed duringclass_metadata_of()which happens early in the solving process, before the__slots__field type is fully resolved. At that point the type might still beAnyor not have the literal strings extracted yet. If we want to avoid recomputation in the future, we'd probably need some kind of lazy/deferred computation pattern (compute on first access rather than during class metadata construction). Could potentially be a followup discussion?