3939)
4040from weakref import WeakValueDictionary
4141
42+ import inflection
4243from jinja2 import Environment , PackageLoader
4344
4445from .about import __version__
6970# Public helpers
7071
7172
72- def public_class (instance_or_type : Union [Type [T ], T ], * property_path : str ) -> Type [T ]:
73+ def public_class (
74+ instance_or_type : Union [Type [T ], T ], * property_path : str , preserve_subtraction : bool = False
75+ ) -> Type [T ]:
7376 """
7477 Given a data class or instance of, give us the public facing
7578 class.
79+
80+ preserve_subtraction indicates that we want the public facing subtracted class as opposed
81+ to its root parent.
7682 """
7783 if not isinstance (instance_or_type , type ):
7884 cls : Type [T ] = type (instance_or_type )
@@ -93,6 +99,8 @@ def public_class(instance_or_type: Union[Type[T], T], *property_path: str) -> Ty
9399 next_cls , = cls ._nested_atomic_collection_keys [key ]
94100 return public_class (next_cls , * property_path [1 :])
95101 cls = getattr (cls , "_parent" , cls )
102+ if preserve_subtraction and cls ._skipped_fields :
103+ return cls
96104 if cls ._skipped_fields :
97105 bases = tuple (x for x in cls .__bases__ if ismetasubclass (x , Atomic ))
98106 while len (bases ) == 1 and bases [0 ]._skipped_fields :
@@ -123,6 +131,7 @@ def keys(
123131 if instance is not None and not all :
124132 return AtomicKeysView (instance )
125133 if all :
134+ # Known as a KeysView as well
126135 return cls ._all_accessible_fields
127136 return KeysView (tuple (cls ._slots ))
128137 if len (property_path ) == 1 :
@@ -208,6 +217,28 @@ def aslist(instance: T) -> List[Any]:
208217# End of public helpers
209218
210219
220+ def _dump_skipped_fields (cls ) -> Optional [FrozenMapping [str , Any ]]:
221+ assert isinstance (cls , type ), f"{ cls } is not a class"
222+ skipped = {key : None for key in cls ._skipped_fields }
223+ for key in cls ._slots :
224+ typedef = cls ._slots [key ]
225+ if key in cls ._nested_atomic_collection_keys :
226+ skipped_on_typedef_merged = {}
227+ for typedef in cls ._nested_atomic_collection_keys [key ]:
228+ skipped_on_typedef = _dump_skipped_fields (typedef )
229+ if skipped_on_typedef :
230+ skipped_on_typedef_merged .update (skipped_on_typedef )
231+ if skipped_on_typedef_merged :
232+ skipped [key ] = skipped_on_typedef_merged
233+ elif ismetasubclass (typedef , Atomic ):
234+ skipped_on_typedef = _dump_skipped_fields (typedef )
235+ if skipped_on_typedef :
236+ skipped [key ] = skipped_on_typedef
237+ if not skipped :
238+ return None
239+ return FrozenMapping (skipped )
240+
241+
211242class ItemsView (_ItemsView ):
212243 __slots__ = ()
213244 if TYPE_CHECKING :
@@ -796,8 +827,16 @@ def create_union_coerce_function(
796827
797828 def cast_values (value ):
798829 if isinstance (value , cast_type_cls ):
830+ # The current value is already the parent type, so apply a down coerce
831+ # The function is probably not ready for encountering the parent type
832+ # as normal use would be a no-op.
799833 return complex_type_cast (value )
800- return custom_cast_function (value )
834+ value = custom_cast_function (value )
835+ # Did the original coerce function make a parent type?
836+ if isinstance (value , cast_type_cls ):
837+ # Yes, so let's down down-coerce it to the end type:
838+ return complex_type_cast (value )
839+ return value
801840
802841 cast_values .__union_subtypes__ = (custom_cast_types , custom_cast_function )
803842 if isinstance (custom_cast_types , tuple ):
@@ -861,22 +900,20 @@ def apply_skip_keys(
861900 current_definition = e .value
862901
863902 if replace_class_refs :
903+ # Coerce functions may produce the parent type.
904+ # So what we'll do is a two pass function
905+ # - one pass that just downcoerces functions that are the
906+ # final parent type
907+ # - second pass after the original coerce that *will* produce the
908+ # parent type.
864909 parent_type_path , parent_type_coerce_function = transform_typing_to_coerce (
865910 original_definition , dict (replace_class_refs )
866911 )
867- if current_coerce is not None :
868- current_coerce_types , existing_coerce_function = current_coerce
869- new_coerce_function = replace_class_references (
870- existing_coerce_function , * replace_class_refs
871- )
872- else :
873- new_coerce_function = None
874- current_coerce_types = None
875912 new_coerce_definition = create_union_coerce_function (
876913 parent_type_path ,
877914 parent_type_coerce_function ,
878- current_coerce_types ,
879- new_coerce_function ,
915+ current_coerce [ 0 ] if current_coerce else None ,
916+ current_coerce [ 1 ] if current_coerce else None ,
880917 )
881918 return ModifiedSkipTypes (
882919 current_definition ,
@@ -1040,7 +1077,7 @@ def __sub__(self: Atomic, skip_fields: Union[Mapping[str, Any], Iterable[Any]])
10401077 parent .__name__ : (parent , child ) for parent , child in mutant_classes
10411078 }
10421079 if debug_mode :
1043- print (f"Mutants: { mutant_class_parent_names } " )
1080+ logger . debug (f"Mutants: { mutant_class_parent_names } " )
10441081
10451082 attrs = {"__slots__" : ()}
10461083
@@ -1053,7 +1090,7 @@ def __sub__(self: Atomic, skip_fields: Union[Mapping[str, Any], Iterable[Any]])
10531090 * [mutant_class_parent_names [parent_name ] for parent_name in parents_to_replace ],
10541091 )
10551092 if debug_mode :
1056- print (f"{ function_name } has { parents_to_replace } " )
1093+ logger . debug (f"{ function_name } has { parents_to_replace } " )
10571094 attrs [function_name ] = mutated_function_value
10581095
10591096 skip_entire_keys = FrozenMapping (skip_entire_keys )
@@ -1065,9 +1102,11 @@ def __sub__(self: Atomic, skip_fields: Union[Mapping[str, Any], Iterable[Any]])
10651102
10661103 changes = ""
10671104 if skip_entire_keys :
1068- changes = "Minus {}" .format ("And" .join (skip_entire_keys ))
1105+ changes = "Without {}" .format ("And" .join (key . capitalize () for key in skip_entire_keys ))
10691106 if redefinitions :
1070- changes = "{}Modified{}" .format (changes , "And" .join (sorted (redefinitions )))
1107+ changes = "{}ButModified{}" .format (
1108+ changes , "And" .join (sorted (key .capitalize () for key in redefinitions ))
1109+ )
10711110 if not changes :
10721111 return self
10731112 value = type (f"{ cls .__name__ } { changes } " , (cls ,), attrs , skip_fields = skip_entire_keys )
@@ -1486,8 +1525,8 @@ def __new__(
14861525 ns_globals ,
14871526 ns_globals ,
14881527 )
1489- support_cls_attrs ["__new__" ] = ns_globals [ "__new__" ]
1490- class_cell_fixups .append (("__new__" , cast (FunctionType , ns_globals [ "__new__" ] )))
1528+ support_cls_attrs ["__new__" ] = new_new = ns_globals . pop ( "__new__" )
1529+ class_cell_fixups .append (("__new__" , cast (FunctionType , new_new )))
14911530 for key in (
14921531 "__iter__" ,
14931532 "__getstate__" ,
@@ -1539,8 +1578,9 @@ def __new__(
15391578 extra_slots = tuple (_dedupe (pending_extra_slots ))
15401579 support_cls_attrs ["__extra_slots__" ] = ReadOnly (extra_slots )
15411580 support_cls_attrs ["_properties" ] = properties = KeysView (properties )
1581+ # create a constant ordered keys view representing the columns and the properties
15421582 support_cls_attrs ["_all_accessible_fields" ] = ReadOnly (
1543- combined_columns . keys () | KeysView ( properties )
1583+ KeysView ( tuple ( field for field in chain ( combined_columns , properties )) )
15441584 )
15451585 support_cls_attrs ["_configuration" ] = ReadOnly (conf )
15461586
@@ -1805,7 +1845,9 @@ def wrapper(func):
18051845 return wrapper
18061846
18071847
1808- def load_cls (cls , args , kwargs ):
1848+ def load_cls (cls , args , kwargs , skip_fields : Optional [FrozenMapping ] = None ):
1849+ if skip_fields :
1850+ cls = cls - skip_fields
18091851 return cls (* args , ** kwargs )
18101852
18111853
@@ -1896,13 +1938,17 @@ def __len__(self):
18961938 return len (keys (self .__class__ ))
18971939
18981940 def __contains__ (self , item ):
1899- return item in self ._all_accessible_fields
1941+ cls = type (self )
1942+ if item in cls ._skipped_fields :
1943+ return False
1944+ return item in cls ._all_accessible_fields
19001945
19011946 def __reduce__ (self ):
19021947 # Create an empty class then let __setstate__ in the autogen
19031948 # code to handle passing raw values.
1904- data_class , support_cls , * _ = self .__class__ .__mro__
1905- return load_cls , (support_cls , (), {}), self .__getstate__ ()
1949+ cls = public_class (type (self ))
1950+ skipped_fields = _dump_skipped_fields (type (self ))
1951+ return load_cls , (cls , (), {}, skipped_fields ), self .__getstate__ ()
19061952
19071953 @classmethod
19081954 def _create_invalid_type (cls , field_name , val , val_type , types_required ):
@@ -1930,7 +1976,7 @@ def _handle_init_errors(self, errors, errored_keys, unrecognized_keys):
19301976 fields = ", " .join (unrecognized_keys )
19311977 errors .append (self ._create_invalid_value (f"Unrecognized fields { fields } " ))
19321978 if errors :
1933- typename = type (self ).__name__ [1 :]
1979+ typename = inflection . titleize ( type (self ).__name__ [1 :])
19341980 if len (errors ) == 1 :
19351981 raise ClassCreationFailed (
19361982 f"Unable to construct { typename } , encountered { len (errors )} "
0 commit comments