Coverage for sources/classcore/standard/behaviors.py: 100%
195 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 18:47 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 18:47 +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 isinstance( slots, __.cabc.Mapping ):
186 slots_ = dict( slots )
187 slots_[ behaviors_name ] = 'Active behaviors.'
188 slots_ = __.types.MappingProxyType( slots_ )
189 elif isinstance( slots, __.cabc.Sequence ):
190 slots_ = list( slots )
191 slots_.append( behaviors_name )
192 slots_ = tuple( slots_ )
193 else: return # pragma: no cover
194 namespace[ '__slots__' ] = slots_
197def classify_behavior_exclusion_verifiers(
198 verifiers: _nomina.BehaviorExclusionVerifiers
199) -> tuple[
200 _nomina.BehaviorExclusionNames,
201 _nomina.BehaviorExclusionRegexes,
202 _nomina.BehaviorExclusionPredicates,
203]:
204 ''' Threshes sequence of behavior exclusion verifiers into bins. '''
205 names: set[ str ] = set( )
206 regexes: list[ __.re.Pattern[ str ] ] = [ ]
207 predicates: list[ __.cabc.Callable[ ..., bool ] ] = [ ]
208 for verifier in verifiers:
209 if isinstance( verifier, str ):
210 names.add( verifier )
211 elif isinstance( verifier, __.re.Pattern ):
212 regexes.append( verifier )
213 elif callable( verifier ):
214 predicates.append( verifier )
215 else:
216 from ..exceptions import BehaviorExclusionInvalidity
217 raise BehaviorExclusionInvalidity( verifier )
218 return frozenset( names ), tuple( regexes ), tuple( predicates )
221def produce_class_construction_preprocessor(
222 attributes_namer: _nomina.AttributesNamer
223) -> _nomina.ClassConstructionPreprocessor[ __.U ]:
224 ''' Produces construction processor which handles metaclass arguments. '''
226 def preprocess( # noqa: PLR0913
227 clscls: type,
228 name: str,
229 bases: list[ type ],
230 namespace: dict[ str, __.typx.Any ],
231 arguments: dict[ str, __.typx.Any ],
232 decorators: _nomina.DecoratorsMutable[ __.U ],
233 ) -> None:
234 record_class_construction_arguments(
235 attributes_namer, namespace, arguments )
236 if '__slots__' in namespace:
237 augment_class_attributes_allocations( attributes_namer, namespace )
239 return preprocess
242def produce_class_construction_postprocessor(
243 attributes_namer: _nomina.AttributesNamer,
244 error_class_provider: _nomina.ErrorClassProvider,
245) -> _nomina.ClassConstructionPostprocessor[ __.U ]:
246 ''' Produces construction processor which determines class decorators. '''
247 arguments_name = attributes_namer( 'class', 'construction_arguments' )
248 cores_default = dict(
249 assigner = assign_attribute_if_mutable,
250 deleter = delete_attribute_if_mutable,
251 surveyor = survey_visible_attributes )
253 def postprocess(
254 cls: type, decorators: _nomina.DecoratorsMutable[ __.U ]
255 ) -> None:
256 arguments = getattr( cls, arguments_name, { } )
257 clscls = type( cls )
258 dynadoc_cfg = arguments.get( 'dynadoc_configuration', { } )
259 if not dynadoc_cfg: # either metaclass argument or attribute
260 dynadoc_cfg_name = (
261 attributes_namer( 'classes', 'dynadoc_configuration' ) )
262 dynadoc_cfg = getattr( clscls, dynadoc_cfg_name, { } )
263 decorators.append( __.ddoc.with_docstring( **dynadoc_cfg ) )
264 dcls_spec = getattr( cls, '__dataclass_transform__', None )
265 if not dcls_spec: # either base class or metaclass may be marked
266 dcls_spec = getattr( clscls, '__dataclass_transform__', None )
267 cores = { }
268 for core_name in ( 'assigner', 'deleter', 'surveyor' ):
269 core_function = access_core_function(
270 cls,
271 attributes_namer = attributes_namer,
272 arguments = arguments,
273 level = 'instances', name = core_name,
274 default = cores_default[ core_name ] )
275 cores[ core_name ] = core_function
276 instances_mutables = arguments.get(
277 'instances_mutables', __.mutables_default )
278 instances_visibles = arguments.get(
279 'instances_visibles', __.visibles_default )
280 if dcls_spec and dcls_spec.get( 'kw_only_default', False ):
281 from .decorators import dataclass_with_standard_behaviors
282 decorator_factory = dataclass_with_standard_behaviors
283 if not dcls_spec.get( 'frozen_default', True ):
284 instances_mutables = instances_mutables or '*'
285 else:
286 from .decorators import with_standard_behaviors
287 decorator_factory = with_standard_behaviors
288 decorator: _nomina.Decorator[ __.U ] = decorator_factory(
289 attributes_namer = attributes_namer,
290 error_class_provider = error_class_provider,
291 assigner_core = __.typx.cast(
292 _nomina.AssignerCore, cores[ 'assigner' ] ),
293 deleter_core = __.typx.cast(
294 _nomina.DeleterCore, cores[ 'deleter' ] ),
295 surveyor_core = __.typx.cast(
296 _nomina.SurveyorCore, cores[ 'surveyor' ] ),
297 mutables = instances_mutables,
298 visibles = instances_visibles )
299 decorators.append( decorator )
301 return postprocess
304def produce_class_initialization_completer(
305 attributes_namer: _nomina.AttributesNamer
306) -> _nomina.ClassInitializationCompleter:
307 ''' Produces initialization completer which finalizes class behaviors. '''
308 arguments_name = attributes_namer( 'class', 'construction_arguments' )
310 def complete( cls: type ) -> None:
311 arguments: __.typx.Optional[ dict[ str, __.typx.Any ] ] = (
312 getattr( cls, arguments_name, None ) )
313 if arguments is not None: delattr( cls, arguments_name )
314 arguments = arguments or { }
315 mutables = arguments.get( 'class_mutables', __.mutables_default )
316 visibles = arguments.get( 'class_visibles', __.visibles_default )
317 behaviors: set[ str ] = set( )
318 record_behavior(
319 cls, attributes_namer = attributes_namer,
320 level = 'class', basename = 'mutables',
321 label = _nomina.immutability_label, behaviors = behaviors,
322 verifiers = mutables )
323 record_behavior(
324 cls, attributes_namer = attributes_namer,
325 level = 'class', basename = 'visibles',
326 label = _nomina.concealment_label, behaviors = behaviors,
327 verifiers = visibles )
328 # Set behaviors attribute last since it enables enforcement.
329 behaviors_name = attributes_namer( 'class', 'behaviors' )
330 _utilities.setattr0( cls, behaviors_name, frozenset( behaviors ) )
332 return complete
335def record_behavior( # noqa: PLR0913
336 cls: type, /, *,
337 attributes_namer: _nomina.AttributesNamer,
338 level: str,
339 basename: str,
340 label: str,
341 behaviors: set[ str ],
342 verifiers: _nomina.BehaviorExclusionVerifiersOmni,
343) -> None:
344 ''' Records details of particular class behavior, such as immutability. '''
345 names_name = attributes_namer( level, f"{basename}_names" )
346 if verifiers == '*':
347 setattr( cls, names_name, '*' )
348 return
349 names_omni: _nomina.BehaviorExclusionNamesOmni = (
350 getattr( cls, names_name, frozenset( ) ) )
351 if names_omni == '*': return
352 names, regexes, predicates = (
353 classify_behavior_exclusion_verifiers( verifiers ) )
354 regexes_name = attributes_namer( level, f"{basename}_regexes" )
355 predicates_name = attributes_namer( level, f"{basename}_predicates" )
356 names_: _nomina.BehaviorExclusionNames = (
357 frozenset( { *names, *names_omni } ) )
358 regexes_: _nomina.BehaviorExclusionRegexes = (
359 _deduplicate_merge_sequences(
360 regexes, getattr( cls, regexes_name, ( ) ) ) )
361 predicates_: _nomina.BehaviorExclusionPredicates = (
362 _deduplicate_merge_sequences(
363 predicates, getattr( cls, predicates_name, ( ) ) ) )
364 setattr( cls, names_name, names_ )
365 setattr( cls, regexes_name, regexes_ )
366 setattr( cls, predicates_name, predicates_ )
367 # TODO? Add regexes match cache.
368 # TODO? Add predicates match cache.
369 behaviors.add( label )
372def record_class_construction_arguments(
373 attributes_namer: _nomina.AttributesNamer,
374 namespace: dict[ str, __.typx.Any ],
375 arguments: dict[ str, __.typx.Any ],
376) -> None:
377 ''' Captures metaclass arguments as class attribute for later use. '''
378 arguments_name = attributes_namer( 'class', 'construction_arguments' )
379 arguments_ = namespace.get( arguments_name, { } )
380 # Decorators, which replace classes, will cause construction of the
381 # replacements without arguments. If we had previously recorded them in
382 # the class namespace, then we do not want to clobber them.
383 if arguments_: return
384 arguments_ = { }
385 for name in (
386 'class_mutables', 'class_visibles',
387 'dynadoc_configuration',
388 'instances_assigner_core',
389 'instances_deleter_core',
390 'instances_surveyor_core',
391 'instances_mutables', 'instances_visibles',
392 ):
393 if name not in arguments: continue
394 arguments_[ name ] = arguments.pop( name )
395 namespace[ arguments_name ] = arguments_
398def _deduplicate_merge_sequences(
399 addends: __.cabc.Sequence[ __.typx.Any ],
400 augends: __.cabc.Sequence[ __.typx.Any ],
401) -> __.cabc.Sequence[ __.typx.Any ]:
402 result = list( augends )
403 augends_ = set( augends )
404 for addend in addends:
405 if addend in augends_: continue
406 result.append( addend )
407 return tuple( result )