From 6021222fc21e6499416f0f996e946993c139208b Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sun, 15 Feb 2026 04:40:16 +0900 Subject: [PATCH 1/4] fix --- pyrefly/lib/alt/attr.rs | 104 +++++++++++++++++++++++++++++++- pyrefly/lib/test/dataclasses.rs | 5 +- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index c0e103d5d7..d6e1b7795c 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -13,6 +13,7 @@ use pyrefly_python::module_name::ModuleName; use pyrefly_types::heap::TypeHeap; use pyrefly_types::literal::LitEnum; use pyrefly_types::special_form::SpecialForm; +use pyrefly_types::tuple::Tuple; use pyrefly_types::typed_dict::TypedDictInner; use pyrefly_types::types::Forall; use pyrefly_types::types::Forallable; @@ -899,9 +900,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { .add_to(errors, range, attr_name, todo_ctx); return None; }; - let (lookup_found, lookup_not_found, lookup_error) = self + let (mut lookup_found, mut lookup_not_found, lookup_error) = self .lookup_attr_from_base(attr_base.clone(), attr_name) .decompose(); + let slot_violations = self.apply_slots_restriction_for_write(attr_name, &mut lookup_found); + if !slot_violations.is_empty() { + should_narrow = false; + lookup_not_found.extend(slot_violations); + } for e in lookup_error { e.add_to(errors, range, attr_name, todo_ctx); should_narrow = false; @@ -1028,9 +1034,11 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { .add_to(errors, range, attr_name, todo_ctx); return; }; - let (lookup_found, lookup_not_found, lookup_error) = self + let (mut lookup_found, mut lookup_not_found, lookup_error) = self .lookup_attr_from_base(attr_base.clone(), attr_name) .decompose(); + let slot_violations = self.apply_slots_restriction_for_write(attr_name, &mut lookup_found); + lookup_not_found.extend(slot_violations); for not_found in lookup_not_found { self.check_delattr( attr_base.clone(), @@ -1069,6 +1077,98 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } + fn apply_slots_restriction_for_write( + &self, + attr_name: &Name, + lookup_found: &mut Vec<(Attribute, AttributeBase1)>, + ) -> Vec { + if lookup_found.is_empty() { + return Vec::new(); + } + let mut kept = Vec::with_capacity(lookup_found.len()); + let mut violations = Vec::new(); + for (attr, base) in lookup_found.drain(..) { + let Some(class) = self.class_for_slots_restriction(&base) else { + kept.push((attr, base)); + continue; + }; + let Some(slots) = self.slots_for_class(&class) else { + kept.push((attr, base)); + continue; + }; + if slots.contains(attr_name) { + kept.push((attr, base)); + } else { + violations.push(NotFoundOn::ClassInstance( + class.class_object().dupe(), + base.clone(), + )); + } + } + *lookup_found = kept; + violations + } + + fn class_for_slots_restriction(&self, base: &AttributeBase1) -> Option { + match base { + AttributeBase1::ClassInstance(cls) + | AttributeBase1::SelfType(cls) + | AttributeBase1::Quantified(_, cls) + | AttributeBase1::SuperInstance(cls, _) => Some(cls.clone()), + AttributeBase1::EnumLiteral(lit) => Some(lit.class.clone()), + _ => None, + } + } + + fn slots_for_class(&self, cls: &ClassType) -> Option> { + let mro = self.get_mro_for_class(cls.class_object()); + let mut slots = SmallSet::new(); + let dict_name = Name::new_static("__dict__"); + let classes = std::iter::once(cls.class_object().dupe()).chain( + mro.ancestors_no_object() + .iter() + .map(|c| c.class_object().dupe()), + ); + for class in classes { + let Some(field) = self.get_field_from_current_class_only(&class, &dunder::SLOTS) else { + return None; + }; + let Some(names) = self.extract_slot_names_from_type(&field.ty()) else { + return None; + }; + if names.contains(&dict_name) { + return None; + } + slots.extend(names); + } + Some(slots) + } + + fn extract_slot_names_from_type(&self, ty: &Type) -> Option> { + let mut slots = SmallSet::new(); + match ty { + Type::Tuple(Tuple::Concrete(elts)) => { + for elt in elts { + let Type::Literal(lit) = elt else { + return None; + }; + let Lit::Str(name) = &lit.value else { + return None; + }; + slots.insert(Name::new(name.as_str())); + } + } + Type::Literal(lit) => { + let Lit::Str(name) = &lit.value else { + return None; + }; + slots.insert(Name::new(name.as_str())); + } + _ => return None, + } + Some(slots) + } + /// Predicate for whether a specific attribute name matches a protocol during structural /// subtyping checks. /// diff --git a/pyrefly/lib/test/dataclasses.rs b/pyrefly/lib/test/dataclasses.rs index 0042d5eec6..feb351561a 100644 --- a/pyrefly/lib/test/dataclasses.rs +++ b/pyrefly/lib/test/dataclasses.rs @@ -1729,7 +1729,6 @@ assert_type(dc2.y, str) # E: assert_type(Desc2[str], str) failed ); testcase!( - bug = "conformance: Dataclass with slots=True should error when setting undeclared attributes", test_dataclass_slots_undeclared_attr_conformance, r#" from dataclasses import dataclass @@ -1741,7 +1740,7 @@ class DC2: def __init__(self): self.x = 3 # should error: y is not in slots - self.y = 3 + self.y = 3 # E: Object of class `DC2` has no attribute `y` @dataclass(slots=False) class DC3: @@ -1751,6 +1750,6 @@ class DC3: def __init__(self): self.x = 3 # should error: y is not in slots - self.y = 3 + self.y = 3 # E: Object of class `DC3` has no attribute `y` "#, ); From 938ef0f6efc9aa2a21fd68f77b5d0b64b97042c4 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Tue, 17 Feb 2026 14:38:01 +0900 Subject: [PATCH 2/4] . --- pyrefly/lib/alt/attr.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index 425a982862..babb57a22a 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -16,6 +16,9 @@ use pyrefly_types::lit_int::LitInt; use pyrefly_types::literal::LitEnum; use pyrefly_types::special_form::SpecialForm; use pyrefly_types::tuple::Tuple; +use pyrefly_types::tensor::TensorShape; +use pyrefly_types::tensor::TensorType; +use pyrefly_types::tuple::Tuple; use pyrefly_types::typed_dict::TypedDictInner; use pyrefly_types::types::Forall; use pyrefly_types::types::Forallable; From 07fcb17d8f13cec189c692ed4c7a92bbc569761b Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Tue, 17 Feb 2026 14:41:39 +0900 Subject: [PATCH 3/4] mypy_primer --- pyrefly/lib/alt/attr.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index babb57a22a..b031b1f121 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -1126,6 +1126,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { let mut kept = Vec::with_capacity(lookup_found.len()); let mut violations = Vec::new(); for (attr, base) in lookup_found.drain(..) { + match &attr { + Attribute::ClassAttribute(ClassAttribute::Property(..)) + | Attribute::ClassAttribute(ClassAttribute::Descriptor(..)) => { + kept.push((attr, base)); + continue; + } + _ => {} + } let Some(class) = self.class_for_slots_restriction(&base) else { kept.push((attr, base)); continue; From 631bbce01d3b2f9f6bcfe1d480d90f1711810480 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Tue, 17 Feb 2026 14:43:03 +0900 Subject: [PATCH 4/4] . --- pyrefly/lib/alt/attr.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index b031b1f121..a0b3a7d48d 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -15,7 +15,6 @@ use pyrefly_types::heap::TypeHeap; use pyrefly_types::lit_int::LitInt; use pyrefly_types::literal::LitEnum; use pyrefly_types::special_form::SpecialForm; -use pyrefly_types::tuple::Tuple; use pyrefly_types::tensor::TensorShape; use pyrefly_types::tensor::TensorType; use pyrefly_types::tuple::Tuple; @@ -61,7 +60,6 @@ use crate::types::module::ModuleType; use crate::types::quantified::Quantified; use crate::types::quantified::QuantifiedKind; use crate::types::read_only::ReadOnlyReason; -use crate::types::tuple::Tuple; use crate::types::type_var::Restriction; use crate::types::typed_dict::TypedDict; use crate::types::types::AnyStyle;