Coverage for sources/classcore/standard/behaviors.py: 100%
196 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-07-01 05:36 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-07-01 05:36 +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 if dcls_spec and dcls_spec.get( 'kw_only_default', False ):
276 from .decorators import dataclass_with_standard_behaviors
277 decorator_factory = dataclass_with_standard_behaviors
278 if not dcls_spec.get( 'frozen_default', True ):
279 instances_mutables = instances_mutables or '*'
280 else:
281 from .decorators import with_standard_behaviors
282 decorator_factory = with_standard_behaviors
283 decorator: _nomina.Decorator[ __.U ] = decorator_factory(
284 attributes_namer = attributes_namer,
285 error_class_provider = error_class_provider,
286 assigner_core = __.typx.cast(
287 _nomina.AssignerCore, cores[ 'assigner' ] ),
288 deleter_core = __.typx.cast(
289 _nomina.DeleterCore, cores[ 'deleter' ] ),
290 surveyor_core = __.typx.cast(
291 _nomina.SurveyorCore, cores[ 'surveyor' ] ),
292 mutables = instances_mutables,
293 visibles = instances_visibles )
294 decorators.append( decorator )
295 # Dynadoc tracks objects in weakset.
296 # Must decorate after any potential class replacements.
297 dynadoc_cfg = arguments.get( 'dynadoc_configuration', { } )
298 if not dynadoc_cfg: # either metaclass argument or attribute
299 dynadoc_cfg_name = (
300 attributes_namer( 'classes', 'dynadoc_configuration' ) )
301 dynadoc_cfg = getattr( clscls, dynadoc_cfg_name, { } )
302 decorators.append( __.ddoc.with_docstring( **dynadoc_cfg ) )
304 return postprocess
307def produce_class_initialization_completer(
308 attributes_namer: _nomina.AttributesNamer
309) -> _nomina.ClassInitializationCompleter:
310 ''' Produces initialization completer which finalizes class behaviors. '''
311 arguments_name = attributes_namer( 'class', 'construction_arguments' )
313 def complete( cls: type ) -> None:
314 arguments: __.typx.Optional[ dict[ str, __.typx.Any ] ] = (
315 getattr( cls, arguments_name, None ) )
316 if arguments is not None: delattr( cls, arguments_name )
317 arguments = arguments or { }
318 mutables = arguments.get( 'class_mutables', __.mutables_default )
319 visibles = arguments.get( 'class_visibles', __.visibles_default )
320 behaviors: set[ str ] = set( )
321 record_behavior(
322 cls, attributes_namer = attributes_namer,
323 level = 'class', basename = 'mutables',
324 label = _nomina.immutability_label, behaviors = behaviors,
325 verifiers = mutables )
326 record_behavior(
327 cls, attributes_namer = attributes_namer,
328 level = 'class', basename = 'visibles',
329 label = _nomina.concealment_label, behaviors = behaviors,
330 verifiers = visibles )
331 # Set behaviors attribute last since it enables enforcement.
332 behaviors_name = attributes_namer( 'class', 'behaviors' )
333 _utilities.setattr0( cls, behaviors_name, frozenset( behaviors ) )
335 return complete
338def record_behavior( # noqa: PLR0913
339 cls: type, /, *,
340 attributes_namer: _nomina.AttributesNamer,
341 level: str,
342 basename: str,
343 label: str,
344 behaviors: set[ str ],
345 verifiers: _nomina.BehaviorExclusionVerifiersOmni,
346) -> None:
347 ''' Records details of particular class behavior, such as immutability. '''
348 names_name = attributes_namer( level, f"{basename}_names" )
349 if verifiers == '*':
350 setattr( cls, names_name, '*' )
351 return
352 names_omni: _nomina.BehaviorExclusionNamesOmni = (
353 getattr( cls, names_name, frozenset( ) ) )
354 if names_omni == '*': return
355 names, regexes, predicates = (
356 classify_behavior_exclusion_verifiers( verifiers ) )
357 regexes_name = attributes_namer( level, f"{basename}_regexes" )
358 predicates_name = attributes_namer( level, f"{basename}_predicates" )
359 names_: _nomina.BehaviorExclusionNames = (
360 frozenset( { *names, *names_omni } ) )
361 regexes_: _nomina.BehaviorExclusionRegexes = (
362 _deduplicate_merge_sequences(
363 regexes, getattr( cls, regexes_name, ( ) ) ) )
364 predicates_: _nomina.BehaviorExclusionPredicates = (
365 _deduplicate_merge_sequences(
366 predicates, getattr( cls, predicates_name, ( ) ) ) )
367 setattr( cls, names_name, names_ )
368 setattr( cls, regexes_name, regexes_ )
369 setattr( cls, predicates_name, predicates_ )
370 # TODO? Add regexes match cache.
371 # TODO? Add predicates match cache.
372 behaviors.add( label )
375def record_class_construction_arguments(
376 attributes_namer: _nomina.AttributesNamer,
377 namespace: dict[ str, __.typx.Any ],
378 arguments: dict[ str, __.typx.Any ],
379) -> None:
380 ''' Captures metaclass arguments as class attribute for later use. '''
381 arguments_name = attributes_namer( 'class', 'construction_arguments' )
382 arguments_ = namespace.get( arguments_name, { } )
383 # Decorators, which replace classes, will cause construction of the
384 # replacements without arguments. If we had previously recorded them in
385 # the class namespace, then we do not want to clobber them.
386 if arguments_: return
387 arguments_ = { }
388 for name in (
389 'class_mutables', 'class_visibles',
390 'dynadoc_configuration',
391 'instances_assigner_core',
392 'instances_deleter_core',
393 'instances_surveyor_core',
394 'instances_mutables', 'instances_visibles',
395 ):
396 if name not in arguments: continue
397 arguments_[ name ] = arguments.pop( name )
398 namespace[ arguments_name ] = arguments_
401def _deduplicate_merge_sequences(
402 addends: __.cabc.Sequence[ __.typx.Any ],
403 augends: __.cabc.Sequence[ __.typx.Any ],
404) -> __.cabc.Sequence[ __.typx.Any ]:
405 result = list( augends )
406 augends_ = set( augends )
407 for addend in addends:
408 if addend in augends_: continue
409 result.append( addend )
410 return tuple( result )