Coverage for sources/classcore/standard/behaviors.py: 100%
197 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-25 13:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-25 13:22 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
4#============================================================================#
5# #
6# Licensed under the Apache License, Version 2.0 (the "License"); #
7# you may not use this file except in compliance with the License. #
8# You may obtain a copy of the License at #
9# #
10# http://www.apache.org/licenses/LICENSE-2.0 #
11# #
12# Unless required by applicable law or agreed to in writing, software #
13# distributed under the License is distributed on an "AS IS" BASIS, #
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
15# See the License for the specific language governing permissions and #
16# limitations under the License. #
17# #
18#============================================================================#
21''' Implementations for standard behaviors. '''
22# TODO? Support introspection of PEP 593 annotations for behavior exclusions.
23# Maybe enum for mutability and visibility.
26from .. import utilities as _utilities
27from . import __
28from . import nomina as _nomina
31def access_core_function( # noqa: PLR0913
32 cls: type, /, *,
33 attributes_namer: _nomina.AttributesNamer,
34 arguments: __.cabc.Mapping[ str, __.typx.Any ],
35 level: str,
36 name: str,
37 default: __.cabc.Callable[ ..., __.typx.Any ],
38) -> __.cabc.Callable[ ..., __.typx.Any ]:
39 ''' Accesses core behavior function.
41 First checks for override argument, then checks for heritable
42 attribute. Finally, falls back to provided default.
43 '''
44 argument_name = f"{level}_{name}_core"
45 attribute_name = attributes_namer( level, f"{name}_core" )
46 return (
47 arguments.get( argument_name )
48 or getattr( cls, attribute_name, default ) )
51def assign_attribute_if_mutable( # noqa: PLR0913
52 obj: object, /, *,
53 ligation: _nomina.AssignerLigation,
54 attributes_namer: _nomina.AttributesNamer,
55 error_class_provider: _nomina.ErrorClassProvider,
56 level: str,
57 name: str,
58 value: __.typx.Any,
59) -> None:
60 ''' Assigns attribute if it is mutable, else raises error. '''
61 leveli = 'instance' if level == 'instances' else level
62 behaviors_name = attributes_namer( leveli, 'behaviors' )
63 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) )
64 if _nomina.immutability_label not in behaviors:
65 ligation( name, value )
66 return
67 names_name = attributes_namer( level, 'mutables_names' )
68 names: _nomina.BehaviorExclusionNamesOmni = (
69 getattr( obj, names_name, frozenset( ) ) )
70 if names == '*' or name in names:
71 ligation( name, value )
72 return
73 predicates_name = attributes_namer( level, 'mutables_predicates' )
74 predicates: _nomina.BehaviorExclusionPredicates = (
75 getattr( obj, predicates_name, ( ) ) )
76 for predicate in predicates:
77 if predicate( name ):
78 # TODO? Cache predicate hit.
79 ligation( name, value )
80 return
81 regexes_name = attributes_namer( level, 'mutables_regexes' )
82 regexes: _nomina.BehaviorExclusionRegexes = (
83 getattr( obj, regexes_name, ( ) ) )
84 for regex in regexes:
85 if regex.fullmatch( name ):
86 # TODO? Cache regex hit.
87 ligation( name, value )
88 return
89 target = _utilities.describe_object( obj )
90 raise error_class_provider( 'AttributeImmutability' )( name, target )
93def delete_attribute_if_mutable( # noqa: PLR0913
94 obj: object, /, *,
95 ligation: _nomina.DeleterLigation,
96 attributes_namer: _nomina.AttributesNamer,
97 error_class_provider: _nomina.ErrorClassProvider,
98 level: str,
99 name: str,
100) -> None:
101 ''' Deletes attribute if it is mutable, else raises error. '''
102 leveli = 'instance' if level == 'instances' else level
103 behaviors_name = attributes_namer( leveli, 'behaviors' )
104 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) )
105 if _nomina.immutability_label not in behaviors:
106 ligation( name )
107 return
108 names_name = attributes_namer( level, 'mutables_names' )
109 names: _nomina.BehaviorExclusionNamesOmni = (
110 getattr( obj, names_name, frozenset( ) ) )
111 if names == '*' or name in names:
112 ligation( name )
113 return
114 predicates_name = attributes_namer( level, 'mutables_predicates' )
115 predicates: _nomina.BehaviorExclusionPredicates = (
116 getattr( obj, predicates_name, ( ) ) )
117 for predicate in predicates:
118 if predicate( name ):
119 # TODO? Cache predicate hit.
120 ligation( name )
121 return
122 regexes_name = attributes_namer( level, 'mutables_regexes' )
123 regexes: _nomina.BehaviorExclusionRegexes = (
124 getattr( obj, regexes_name, ( ) ) )
125 for regex in regexes:
126 if regex.fullmatch( name ):
127 # TODO? Cache regex hit.
128 ligation( name )
129 return
130 target = _utilities.describe_object( obj )
131 raise error_class_provider( 'AttributeImmutability' )( name, target )
134def survey_visible_attributes(
135 obj: object, /, *,
136 ligation: _nomina.SurveyorLigation,
137 attributes_namer: _nomina.AttributesNamer,
138 level: str,
139) -> __.cabc.Iterable[ str ]:
140 ''' Returns sequence of visible attributes. '''
141 names_base = ligation( )
142 leveli = 'instance' if level == 'instances' else level
143 behaviors_name = attributes_namer( leveli, 'behaviors' )
144 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) )
145 if _nomina.concealment_label not in behaviors: return names_base
146 names_name = attributes_namer( level, 'visibles_names' )
147 names: _nomina.BehaviorExclusionNamesOmni = (
148 getattr( obj, names_name, frozenset( ) ) )
149 if names == '*': return names_base # pragma: no branch
150 regexes_name = attributes_namer( level, 'visibles_regexes' )
151 regexes: _nomina.BehaviorExclusionRegexes = (
152 getattr( obj, regexes_name, ( ) ) )
153 predicates_name = attributes_namer( level, 'visibles_predicates' )
154 predicates: _nomina.BehaviorExclusionPredicates = (
155 getattr( obj, predicates_name, ( ) ) )
156 names_: list[ str ] = [ ]
157 for name in names_base:
158 if name in names:
159 names_.append( name )
160 continue
161 for predicate in predicates:
162 if predicate( name ):
163 # TODO? Cache predicate hit.
164 names_.append( name )
165 continue
166 for regex in regexes:
167 if regex.fullmatch( name ):
168 # TODO? Cache regex hit.
169 names_.append( name )
170 continue
171 return names_
174def augment_class_attributes_allocations(
175 attributes_namer: _nomina.AttributesNamer,
176 namespace: dict[ str, __.typx.Any ],
177) -> None:
178 ''' Adds necessary slots for record-keeping attributes. '''
179 behaviors_name = attributes_namer( 'instance', 'behaviors' )
180 slots: __.typx.Union[
181 __.cabc.Mapping[ str, __.typx.Any ],
182 __.cabc.Sequence[ str ],
183 None
184 ] = namespace.get( '__slots__' )
185 if slots and behaviors_name in slots: return
186 if isinstance( slots, __.cabc.Mapping ):
187 slots_ = dict( slots )
188 slots_[ behaviors_name ] = 'Active behaviors.'
189 slots_ = __.types.MappingProxyType( slots_ )
190 elif isinstance( slots, __.cabc.Sequence ):
191 slots_ = list( slots )
192 slots_.append( behaviors_name )
193 slots_ = tuple( slots_ )
194 else: return # pragma: no cover
195 namespace[ '__slots__' ] = slots_
198def classify_behavior_exclusion_verifiers(
199 verifiers: _nomina.BehaviorExclusionVerifiers
200) -> tuple[
201 _nomina.BehaviorExclusionNames,
202 _nomina.BehaviorExclusionRegexes,
203 _nomina.BehaviorExclusionPredicates,
204]:
205 ''' Threshes sequence of behavior exclusion verifiers into bins. '''
206 names: set[ str ] = set( )
207 regexes: list[ __.re.Pattern[ str ] ] = [ ]
208 predicates: list[ __.cabc.Callable[ ..., bool ] ] = [ ]
209 for verifier in verifiers:
210 if isinstance( verifier, str ):
211 names.add( verifier )
212 elif isinstance( verifier, __.re.Pattern ):
213 regexes.append( verifier )
214 elif callable( verifier ):
215 predicates.append( verifier )
216 else:
217 from ..exceptions import BehaviorExclusionInvalidity
218 raise BehaviorExclusionInvalidity( verifier )
219 return frozenset( names ), tuple( regexes ), tuple( predicates )
222def produce_class_construction_preprocessor(
223 attributes_namer: _nomina.AttributesNamer
224) -> _nomina.ClassConstructionPreprocessor[ __.U ]:
225 ''' Produces construction processor which handles metaclass arguments. '''
227 def preprocess( # noqa: PLR0913
228 clscls: type,
229 name: str,
230 bases: list[ type ],
231 namespace: dict[ str, __.typx.Any ],
232 arguments: dict[ str, __.typx.Any ],
233 decorators: _nomina.DecoratorsMutable[ __.U ],
234 ) -> None:
235 record_class_construction_arguments(
236 attributes_namer, namespace, arguments )
237 if '__slots__' in namespace:
238 augment_class_attributes_allocations( attributes_namer, namespace )
240 return preprocess
243def produce_class_construction_postprocessor(
244 attributes_namer: _nomina.AttributesNamer,
245 error_class_provider: _nomina.ErrorClassProvider,
246) -> _nomina.ClassConstructionPostprocessor[ __.U ]:
247 ''' Produces construction processor which determines class decorators. '''
248 arguments_name = attributes_namer( 'class', 'construction_arguments' )
249 cores_default = dict(
250 assigner = assign_attribute_if_mutable,
251 deleter = delete_attribute_if_mutable,
252 surveyor = survey_visible_attributes )
254 def postprocess(
255 cls: type, decorators: _nomina.DecoratorsMutable[ __.U ]
256 ) -> None:
257 arguments = getattr( cls, arguments_name, { } )
258 clscls = type( cls )
259 dcls_spec = getattr( cls, '__dataclass_transform__', None )
260 if not dcls_spec: # either base class or metaclass may be marked
261 dcls_spec = getattr( clscls, '__dataclass_transform__', None )
262 cores = { }
263 for core_name in ( 'assigner', 'deleter', 'surveyor' ):
264 core_function = access_core_function(
265 cls,
266 attributes_namer = attributes_namer,
267 arguments = arguments,
268 level = 'instances', name = core_name,
269 default = cores_default[ core_name ] )
270 cores[ core_name ] = core_function
271 instances_mutables = arguments.get(
272 'instances_mutables', __.mutables_default )
273 instances_visibles = arguments.get(
274 'instances_visibles', __.visibles_default )
275 instances_ignore_init_arguments = arguments.get(
276 'instances_ignore_init_arguments', False )
277 if dcls_spec and dcls_spec.get( 'kw_only_default', False ):
278 from .decorators import dataclass_with_standard_behaviors
279 decorator_factory = dataclass_with_standard_behaviors
280 if not dcls_spec.get( 'frozen_default', True ):
281 instances_mutables = instances_mutables or '*'
282 else:
283 from .decorators import with_standard_behaviors
284 decorator_factory = with_standard_behaviors
285 decorator: _nomina.Decorator[ __.U ] = decorator_factory(
286 attributes_namer = attributes_namer,
287 error_class_provider = error_class_provider,
288 assigner_core = __.typx.cast(
289 _nomina.AssignerCore, cores[ 'assigner' ] ),
290 deleter_core = __.typx.cast(
291 _nomina.DeleterCore, cores[ 'deleter' ] ),
292 surveyor_core = __.typx.cast(
293 _nomina.SurveyorCore, cores[ 'surveyor' ] ),
294 ignore_init_arguments = instances_ignore_init_arguments,
295 mutables = instances_mutables,
296 visibles = instances_visibles )
297 decorators.append( decorator )
298 # Dynadoc tracks objects in weakset.
299 # Must decorate after any potential class replacements.
300 dynadoc_cfg = arguments.get( 'dynadoc_configuration', { } )
301 if not dynadoc_cfg: # either metaclass argument or attribute
302 dynadoc_cfg_name = (
303 attributes_namer( 'classes', 'dynadoc_configuration' ) )
304 dynadoc_cfg = getattr( clscls, dynadoc_cfg_name, { } )
305 decorators.append( __.ddoc.with_docstring( **dynadoc_cfg ) )
307 return postprocess
310def produce_class_initialization_completer(
311 attributes_namer: _nomina.AttributesNamer
312) -> _nomina.ClassInitializationCompleter:
313 ''' Produces initialization completer which finalizes class behaviors. '''
314 arguments_name = attributes_namer( 'class', 'construction_arguments' )
316 def complete( cls: type ) -> None:
317 arguments: __.typx.Optional[ dict[ str, __.typx.Any ] ] = (
318 getattr( cls, arguments_name, None ) )
319 if arguments is not None: delattr( cls, arguments_name )
320 arguments = arguments or { }
321 mutables = arguments.get( 'class_mutables', __.mutables_default )
322 visibles = arguments.get( 'class_visibles', __.visibles_default )
323 behaviors: set[ str ] = set( )
324 record_behavior(
325 cls, attributes_namer = attributes_namer,
326 level = 'class', basename = 'mutables',
327 label = _nomina.immutability_label, behaviors = behaviors,
328 verifiers = mutables )
329 record_behavior(
330 cls, attributes_namer = attributes_namer,
331 level = 'class', basename = 'visibles',
332 label = _nomina.concealment_label, behaviors = behaviors,
333 verifiers = visibles )
334 # Set behaviors attribute last since it enables enforcement.
335 behaviors_name = attributes_namer( 'class', 'behaviors' )
336 _utilities.setattr0( cls, behaviors_name, frozenset( behaviors ) )
338 return complete
341def record_behavior( # noqa: PLR0913
342 cls: type, /, *,
343 attributes_namer: _nomina.AttributesNamer,
344 level: str,
345 basename: str,
346 label: str,
347 behaviors: set[ str ],
348 verifiers: _nomina.BehaviorExclusionVerifiersOmni,
349) -> None:
350 ''' Records details of particular class behavior, such as immutability. '''
351 names_name = attributes_namer( level, f"{basename}_names" )
352 if verifiers == '*':
353 setattr( cls, names_name, '*' )
354 return
355 names_omni: _nomina.BehaviorExclusionNamesOmni = (
356 getattr( cls, names_name, frozenset( ) ) )
357 if names_omni == '*': return
358 names, regexes, predicates = (
359 classify_behavior_exclusion_verifiers( verifiers ) )
360 regexes_name = attributes_namer( level, f"{basename}_regexes" )
361 predicates_name = attributes_namer( level, f"{basename}_predicates" )
362 names_: _nomina.BehaviorExclusionNames = (
363 frozenset( { *names, *names_omni } ) )
364 regexes_: _nomina.BehaviorExclusionRegexes = (
365 _deduplicate_merge_sequences(
366 regexes, getattr( cls, regexes_name, ( ) ) ) )
367 predicates_: _nomina.BehaviorExclusionPredicates = (
368 _deduplicate_merge_sequences(
369 predicates, getattr( cls, predicates_name, ( ) ) ) )
370 setattr( cls, names_name, names_ )
371 setattr( cls, regexes_name, regexes_ )
372 setattr( cls, predicates_name, predicates_ )
373 # TODO? Add regexes match cache.
374 # TODO? Add predicates match cache.
375 behaviors.add( label )
378def record_class_construction_arguments(
379 attributes_namer: _nomina.AttributesNamer,
380 namespace: dict[ str, __.typx.Any ],
381 arguments: dict[ str, __.typx.Any ],
382) -> None:
383 ''' Captures metaclass arguments as class attribute for later use. '''
384 arguments_name = attributes_namer( 'class', 'construction_arguments' )
385 arguments_ = namespace.get( arguments_name, { } )
386 # Decorators, which replace classes, will cause construction of the
387 # replacements without arguments. If we had previously recorded them in
388 # the class namespace, then we do not want to clobber them.
389 if arguments_: return
390 arguments_ = { }
391 for name in (
392 'class_mutables', 'class_visibles',
393 'dynadoc_configuration',
394 'instances_assigner_core',
395 'instances_deleter_core',
396 'instances_surveyor_core',
397 'instances_ignore_init_arguments',
398 'instances_mutables', 'instances_visibles',
399 ):
400 if name not in arguments: continue
401 arguments_[ name ] = arguments.pop( name )
402 namespace[ arguments_name ] = arguments_
405def _deduplicate_merge_sequences(
406 addends: __.cabc.Sequence[ __.typx.Any ],
407 augends: __.cabc.Sequence[ __.typx.Any ],
408) -> __.cabc.Sequence[ __.typx.Any ]:
409 result = list( augends )
410 augends_ = set( augends )
411 for addend in addends:
412 if addend in augends_: continue
413 result.append( addend )
414 return tuple( result )