@@ -24,6 +24,7 @@ use crate::{
2424 Parameter , Parameters , PropertyInstanceType , Signature , SpecialFormType , StaticMroError ,
2525 SubclassOfType , Truthiness , Type , TypeContext , TypeMapping , TypeVarVariance ,
2626 TypedDictModule , UnionBuilder , UnionType ,
27+ bound_super:: BoundSuperType ,
2728 call:: { CallError , CallErrorKind } ,
2829 callable:: { CallableFunctionProvenance , CallableTypeKind } ,
2930 class:: {
@@ -119,6 +120,36 @@ pub struct StaticClassLiteral<'db> {
119120// The Salsa heap is tracked separately.
120121impl get_size2:: GetSize for StaticClassLiteral < ' _ > { }
121122
123+ /// The outcome of dispatching a frozen-dataclass method on a subclass instance.
124+ #[ derive( Clone , Copy ) ]
125+ pub ( crate ) enum FrozenDataclassDispatch < ' db > {
126+ /// A reachable frozen dataclass rejects modification of one of its fields.
127+ FrozenField ,
128+ /// Every reachable frozen method delegates past this base.
129+ Delegate ( StaticClassLiteral < ' db > ) ,
130+ }
131+
132+ impl < ' db > FrozenDataclassDispatch < ' db > {
133+ /// Returns `object` for a frozen field or `super(frozen_base, object)` for a non-field.
134+ pub ( crate ) fn receiver ( self , db : & ' db dyn Db , object_ty : Type < ' db > ) -> Type < ' db > {
135+ match self {
136+ Self :: FrozenField => object_ty,
137+ Self :: Delegate ( frozen_base) => BoundSuperType :: build (
138+ db,
139+ Type :: ClassLiteral ( ClassLiteral :: Static ( frozen_base) ) ,
140+ object_ty,
141+ )
142+ . unwrap_or ( object_ty) ,
143+ }
144+ }
145+ }
146+
147+ /// Fields protected by reachable frozen-dataclass methods.
148+ struct InheritedFrozenDataclassFields < ' db > {
149+ names : Box < [ Name ] > ,
150+ last_frozen_base : StaticClassLiteral < ' db > ,
151+ }
152+
122153#[ salsa:: tracked]
123154impl < ' db > StaticClassLiteral < ' db > {
124155 /// Return `true` if this class represents `known_class`
@@ -1850,7 +1881,7 @@ impl<'db> StaticClassLiteral<'db> {
18501881 }
18511882
18521883 let frozen_base_fields =
1853- self . inherited_non_slotted_frozen_dataclass_fields ( db, specialization) ?;
1884+ self . inherited_non_slotted_frozen_dataclass_fields ( db, specialization, "__setattr__" ) ?;
18541885
18551886 let instance_ty =
18561887 Type :: instance ( db, self . apply_optional_specialization ( db, specialization) ) ;
@@ -1868,7 +1899,8 @@ impl<'db> StaticClassLiteral<'db> {
18681899 } ;
18691900
18701901 let overloads = frozen_base_fields
1871- . keys ( )
1902+ . names
1903+ . iter ( )
18721904 . map ( |field| setattr_signature ( Type :: string_literal ( db, field) , Type :: Never ) )
18731905 . chain ( [ setattr_signature (
18741906 KnownClass :: Str . to_instance ( db) ,
@@ -1883,15 +1915,60 @@ impl<'db> StaticClassLiteral<'db> {
18831915 ) ) )
18841916 }
18851917
1886- /// Return the inherited frozen dataclass fields whose generated `__setattr__` still controls
1887- /// assignments on this class.
1918+ /// Returns the outcome of an inherited frozen-dataclass method for `name`.
1919+ ///
1920+ /// For a non-field, CPython delegates past each generated frozen method. Preserving the final
1921+ /// frozen base lets assignment validation perform the same `super()` lookup without hiding a
1922+ /// later method or an attribute's descriptor.
1923+ pub ( crate ) fn inherited_frozen_dataclass_dispatch (
1924+ self ,
1925+ db : & ' db dyn Db ,
1926+ specialization : Option < Specialization < ' db > > ,
1927+ method : & str ,
1928+ name : & str ,
1929+ ) -> Option < FrozenDataclassDispatch < ' db > > {
1930+ if CodeGeneratorKind :: from_static_class ( db, self ) . is_some ( )
1931+ || class_member ( db, self . body_scope ( db) , method)
1932+ . ignore_possibly_undefined ( )
1933+ . is_some ( )
1934+ {
1935+ return None ;
1936+ }
1937+
1938+ let frozen_base_fields =
1939+ self . inherited_non_slotted_frozen_dataclass_fields ( db, specialization, method) ?;
1940+
1941+ if frozen_base_fields
1942+ . names
1943+ . iter ( )
1944+ . any ( |field| field. as_str ( ) == name)
1945+ {
1946+ Some ( FrozenDataclassDispatch :: FrozenField )
1947+ } else {
1948+ Some ( FrozenDataclassDispatch :: Delegate (
1949+ frozen_base_fields. last_frozen_base ,
1950+ ) )
1951+ }
1952+ }
1953+
1954+ /// Returns the inherited fields protected by a generated frozen-dataclass method.
18881955 fn inherited_non_slotted_frozen_dataclass_fields (
18891956 self ,
18901957 db : & ' db dyn Db ,
18911958 specialization : Option < Specialization < ' db > > ,
1892- ) -> Option < & ' db FxIndexMap < Name , Field < ' db > > > {
1959+ method : & str ,
1960+ ) -> Option < InheritedFrozenDataclassFields < ' db > > {
1961+ let mut names = FxIndexSet :: default ( ) ;
1962+ let mut last_frozen_base = None ;
1963+
18931964 for base in self . iter_mro ( db, specialization) . skip ( 1 ) {
1894- let ( base_class, base_specialization) = base. into_class ( ) ?. static_class_literal ( db) ?;
1965+ let Some ( base_class_type) = base. into_class ( ) else {
1966+ break ;
1967+ } ;
1968+ let Some ( ( base_class, base_specialization) ) = base_class_type. static_class_literal ( db)
1969+ else {
1970+ break ;
1971+ } ;
18951972
18961973 // Stop if another class in the MRO replaces the generated frozen setter:
18971974 //
@@ -1905,29 +1982,47 @@ impl<'db> StaticClassLiteral<'db> {
19051982 //
19061983 // Writes to `Child().x` dispatch to `Mutable.__setattr__`, not to the synthesized
19071984 // `Frozen.__setattr__`.
1908- if class_member ( db, base_class. body_scope ( db) , "__setattr__" )
1985+ if class_member ( db, base_class. body_scope ( db) , method )
19091986 . ignore_possibly_undefined ( )
19101987 . is_some ( )
19111988 {
1912- return None ;
1989+ break ;
19131990 }
19141991
19151992 if base_class. is_frozen_dataclass ( db) == Some ( true ) {
19161993 let field_policy @ CodeGeneratorKind :: DataclassLike ( _) =
19171994 CodeGeneratorKind :: from_static_class ( db, base_class) ?
19181995 else {
1919- return None ;
1996+ break ;
19201997 } ;
19211998
19221999 if base_class. has_dataclass_param ( db, field_policy, DataclassFlags :: SLOTS ) {
1923- return None ;
2000+ break ;
19242001 }
19252002
1926- return Some ( base_class. fields ( db, base_specialization, field_policy) ) ;
2003+ names. extend (
2004+ base_class
2005+ . fields ( db, base_specialization, field_policy)
2006+ . iter ( )
2007+ . filter ( |( _, field) | {
2008+ !matches ! (
2009+ field. kind,
2010+ FieldKind :: Dataclass {
2011+ init_only: true ,
2012+ ..
2013+ }
2014+ )
2015+ } )
2016+ . map ( |( name, _) | name. clone ( ) ) ,
2017+ ) ;
2018+ last_frozen_base = Some ( base_class) ;
19272019 }
19282020 }
19292021
1930- None
2022+ Some ( InheritedFrozenDataclassFields {
2023+ names : names. into_iter ( ) . collect ( ) ,
2024+ last_frozen_base : last_frozen_base?,
2025+ } )
19312026 }
19322027
19332028 /// Member lookup for classes that inherit from `typing.TypedDict`.
0 commit comments