Coverage for sources/classcore/standard/behaviors.py: 100%
175 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-08 04:17 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-08 04:17 +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 assign_attribute_if_mutable( # noqa: PLR0913
32 obj: object, /, *,
33 ligation: _nomina.AssignerLigation,
34 attributes_namer: _nomina.AttributesNamer,
35 error_class_provider: _nomina.ErrorClassProvider,
36 level: str,
37 name: str,
38 value: __.typx.Any,
39) -> None:
40 ''' Assigns attribute if it is mutable, else raises error. '''
41 leveli = 'instance' if level == 'instances' else level
42 behaviors_name = attributes_namer( leveli, 'behaviors' )
43 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) )
44 if _nomina.immutability_label not in behaviors:
45 ligation( name, value )
46 return
47 names_name = attributes_namer( level, 'mutables_names' )
48 names: _nomina.BehaviorExclusionNamesOmni = (
49 getattr( obj, names_name, frozenset( ) ) )
50 if names == '*' or name in names:
51 ligation( name, value )
52 return
53 predicates_name = attributes_namer( level, 'mutables_predicates' )
54 predicates: _nomina.BehaviorExclusionPredicates = (
55 getattr( obj, predicates_name, ( ) ) )
56 for predicate in predicates:
57 if predicate( name ):
58 # TODO? Cache predicate hit.
59 ligation( name, value )
60 return
61 regexes_name = attributes_namer( level, 'mutables_regexes' )
62 regexes: _nomina.BehaviorExclusionRegexes = (
63 getattr( obj, regexes_name, ( ) ) )
64 for regex in regexes:
65 if regex.fullmatch( name ):
66 # TODO? Cache regex hit.
67 ligation( name, value )
68 return
69 target = _utilities.describe_object( obj )
70 raise error_class_provider( 'AttributeImmutability' )( name, target )
73def delete_attribute_if_mutable( # noqa: PLR0913
74 obj: object, /, *,
75 ligation: _nomina.DeleterLigation,
76 attributes_namer: _nomina.AttributesNamer,
77 error_class_provider: _nomina.ErrorClassProvider,
78 level: str,
79 name: str,
80) -> None:
81 ''' Deletes attribute if it is mutable, else raises error. '''
82 leveli = 'instance' if level == 'instances' else level
83 behaviors_name = attributes_namer( leveli, 'behaviors' )
84 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) )
85 if _nomina.immutability_label not in behaviors:
86 ligation( name )
87 return
88 names_name = attributes_namer( level, 'mutables_names' )
89 names: _nomina.BehaviorExclusionNamesOmni = (
90 getattr( obj, names_name, frozenset( ) ) )
91 if names == '*' or name in names:
92 ligation( name )
93 return
94 predicates_name = attributes_namer( level, 'mutables_predicates' )
95 predicates: _nomina.BehaviorExclusionPredicates = (
96 getattr( obj, predicates_name, ( ) ) )
97 for predicate in predicates:
98 if predicate( name ):
99 # TODO? Cache predicate hit.
100 ligation( name )
101 return
102 regexes_name = attributes_namer( level, 'mutables_regexes' )
103 regexes: _nomina.BehaviorExclusionRegexes = (
104 getattr( obj, regexes_name, ( ) ) )
105 for regex in regexes:
106 if regex.fullmatch( name ):
107 # TODO? Cache regex hit.
108 ligation( name )
109 return
110 target = _utilities.describe_object( obj )
111 raise error_class_provider( 'AttributeImmutability' )( name, target )
114def survey_visible_attributes(
115 obj: object, /, *,
116 ligation: _nomina.SurveyorLigation,
117 attributes_namer: _nomina.AttributesNamer,
118 level: str,
119) -> __.cabc.Iterable[ str ]:
120 ''' Returns sequence of visible attributes. '''
121 names_base = ligation( )
122 leveli = 'instance' if level == 'instances' else level
123 behaviors_name = attributes_namer( leveli, 'behaviors' )
124 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) )
125 if _nomina.concealment_label not in behaviors: return names_base
126 names_name = attributes_namer( level, 'visibles_names' )
127 names: _nomina.BehaviorExclusionNamesOmni = (
128 getattr( obj, names_name, frozenset( ) ) )
129 if names == '*': return names_base # pragma: no branch
130 regexes_name = attributes_namer( level, 'visibles_regexes' )
131 regexes: _nomina.BehaviorExclusionRegexes = (
132 getattr( obj, regexes_name, ( ) ) )
133 predicates_name = attributes_namer( level, 'visibles_predicates' )
134 predicates: _nomina.BehaviorExclusionPredicates = (
135 getattr( obj, predicates_name, ( ) ) )
136 names_: list[ str ] = [ ]
137 for name in names_base:
138 if name in names:
139 names_.append( name )
140 continue
141 for predicate in predicates:
142 if predicate( name ):
143 # TODO? Cache predicate hit.
144 names_.append( name )
145 continue
146 for regex in regexes:
147 if regex.fullmatch( name ):
148 # TODO? Cache regex hit.
149 names_.append( name )
150 continue
151 return names_
154def classify_behavior_exclusion_verifiers(
155 verifiers: _nomina.BehaviorExclusionVerifiers
156) -> tuple[
157 _nomina.BehaviorExclusionNames,
158 _nomina.BehaviorExclusionRegexes,
159 _nomina.BehaviorExclusionPredicates,
160]:
161 ''' Threshes sequence of behavior exclusion verifiers into bins. '''
162 names: set[ str ] = set( )
163 regexes: list[ __.re.Pattern[ str ] ] = [ ]
164 predicates: list[ __.cabc.Callable[ ..., bool ] ] = [ ]
165 for verifier in verifiers:
166 if isinstance( verifier, str ):
167 names.add( verifier )
168 elif isinstance( verifier, __.re.Pattern ):
169 regexes.append( verifier )
170 elif callable( verifier ):
171 predicates.append( verifier )
172 else:
173 from ..exceptions import BehaviorExclusionInvalidity
174 raise BehaviorExclusionInvalidity( verifier )
175 return frozenset( names ), tuple( regexes ), tuple( predicates )
178def produce_class_construction_preprocessor(
179 attributes_namer: _nomina.AttributesNamer
180) -> _nomina.ClassConstructionPreprocessor[ __.U ]:
181 ''' Produces construction processor which handles metaclass arguments. '''
183 def preprocess( # noqa: PLR0913
184 clscls: type,
185 name: str,
186 bases: list[ type ],
187 namespace: dict[ str, __.typx.Any ],
188 arguments: dict[ str, __.typx.Any ],
189 decorators: _nomina.DecoratorsMutable[ __.U ],
190 ) -> None:
191 record_class_construction_arguments(
192 attributes_namer, namespace, arguments )
194 return preprocess
197def produce_class_construction_postprocessor(
198 attributes_namer: _nomina.AttributesNamer,
199 error_class_provider: _nomina.ErrorClassProvider,
200) -> _nomina.ClassConstructionPostprocessor[ __.U ]:
201 ''' Produces construction processor which determines class decorators. '''
202 arguments_name = attributes_namer( 'class', 'construction_arguments' )
204 def postprocess(
205 cls: type, decorators: _nomina.DecoratorsMutable[ __.U ]
206 ) -> None:
207 arguments = getattr( cls, arguments_name, { } )
208 clscls = type( cls )
209 dynadoc_cfg = arguments.get( 'dynadoc_configuration', { } )
210 if not dynadoc_cfg: # either metaclass argument or attribute
211 dynadoc_cfg_name = (
212 attributes_namer( 'classes', 'dynadoc_configuration' ) )
213 dynadoc_cfg = getattr( clscls, dynadoc_cfg_name, { } )
214 decorators.append( __.dynadoc.with_docstring( **dynadoc_cfg ) )
215 dcls_spec = getattr( cls, '__dataclass_transform__', None )
216 if not dcls_spec: # either base class or metaclass may be marked
217 dcls_spec = getattr( clscls, '__dataclass_transform__', None )
218 instances_assigner = arguments.get(
219 'instances_assigner_core', assign_attribute_if_mutable )
220 instances_deleter = arguments.get(
221 'instances_deleter_core', delete_attribute_if_mutable )
222 instances_surveyor = arguments.get(
223 'instances_surveyor_core', survey_visible_attributes )
224 instances_mutables = arguments.get(
225 'instances_mutables', __.mutables_default )
226 instances_visibles = arguments.get(
227 'instances_visibles', __.visibles_default )
228 if dcls_spec and dcls_spec.get( 'kw_only_default', False ):
229 from .decorators import dataclass_with_standard_behaviors
230 decorator_factory = dataclass_with_standard_behaviors
231 if not dcls_spec.get( 'frozen_default', True ):
232 instances_mutables = instances_mutables or '*'
233 else:
234 from .decorators import with_standard_behaviors
235 decorator_factory = with_standard_behaviors
236 decorator: _nomina.Decorator[ __.U ] = decorator_factory(
237 attributes_namer = attributes_namer,
238 error_class_provider = error_class_provider,
239 assigner_core = instances_assigner,
240 deleter_core = instances_deleter,
241 surveyor_core = instances_surveyor,
242 mutables = instances_mutables,
243 visibles = instances_visibles )
244 decorators.append( decorator )
246 return postprocess
249def produce_class_initialization_completer(
250 attributes_namer: _nomina.AttributesNamer
251) -> _nomina.ClassInitializationCompleter:
252 ''' Produces initialization completer which finalizes class behaviors. '''
253 arguments_name = attributes_namer( 'class', 'construction_arguments' )
255 def complete( cls: type ) -> None:
256 arguments: __.typx.Optional[ dict[ str, __.typx.Any ] ] = (
257 getattr( cls, arguments_name, None ) )
258 if arguments is not None: delattr( cls, arguments_name )
259 arguments = arguments or { }
260 mutables = arguments.get( 'class_mutables', __.mutables_default )
261 visibles = arguments.get( 'class_visibles', __.visibles_default )
262 behaviors: set[ str ] = set( )
263 record_behavior(
264 cls, attributes_namer = attributes_namer,
265 level = 'class', basename = 'mutables',
266 label = _nomina.immutability_label, behaviors = behaviors,
267 verifiers = mutables )
268 record_behavior(
269 cls, attributes_namer = attributes_namer,
270 level = 'class', basename = 'visibles',
271 label = _nomina.concealment_label, behaviors = behaviors,
272 verifiers = visibles )
273 # Set behaviors attribute last since it enables enforcement.
274 behaviors_name = attributes_namer( 'class', 'behaviors' )
275 _utilities.setattr0( cls, behaviors_name, frozenset( behaviors ) )
277 return complete
280def record_behavior( # noqa: PLR0913
281 cls: type, /, *,
282 attributes_namer: _nomina.AttributesNamer,
283 level: str,
284 basename: str,
285 label: str,
286 behaviors: set[ str ],
287 verifiers: _nomina.BehaviorExclusionVerifiersOmni,
288) -> None:
289 ''' Records details of particular class behavior, such as immutability. '''
290 names_name = attributes_namer( level, f"{basename}_names" )
291 if verifiers == '*':
292 setattr( cls, names_name, '*' )
293 return
294 names_omni: _nomina.BehaviorExclusionNamesOmni = (
295 getattr( cls, names_name, frozenset( ) ) )
296 if names_omni == '*': return
297 names, regexes, predicates = (
298 classify_behavior_exclusion_verifiers( verifiers ) )
299 regexes_name = attributes_namer( level, f"{basename}_regexes" )
300 predicates_name = attributes_namer( level, f"{basename}_predicates" )
301 names_: _nomina.BehaviorExclusionNames = (
302 frozenset( { *names, *names_omni } ) )
303 regexes_: _nomina.BehaviorExclusionRegexes = (
304 _deduplicate_merge_sequences(
305 regexes, getattr( cls, regexes_name, ( ) ) ) )
306 predicates_: _nomina.BehaviorExclusionPredicates = (
307 _deduplicate_merge_sequences(
308 predicates, getattr( cls, predicates_name, ( ) ) ) )
309 setattr( cls, names_name, names_ )
310 setattr( cls, regexes_name, regexes_ )
311 setattr( cls, predicates_name, predicates_ )
312 # TODO? Add regexes match cache.
313 # TODO? Add predicates match cache.
314 behaviors.add( label )
317def record_class_construction_arguments(
318 attributes_namer: _nomina.AttributesNamer,
319 namespace: dict[ str, __.typx.Any ],
320 arguments: dict[ str, __.typx.Any ],
321) -> None:
322 ''' Captures metaclass arguments as class attribute for later use. '''
323 arguments_name = attributes_namer( 'class', 'construction_arguments' )
324 arguments_ = namespace.get( arguments_name, { } )
325 # Decorators, which replace classes, will cause construction of the
326 # replacements without arguments. If we had previously recorded them in
327 # the class namespace, then we do not want to clobber them.
328 if arguments_: return
329 arguments_ = { }
330 for name in (
331 'class_mutables', 'class_visibles',
332 'instances_assigner_core',
333 'instances_deleter_core',
334 'instances_surveyor_core',
335 'instances_mutables', 'instances_visibles',
336 'dynadoc_configuration',
337 ):
338 if name not in arguments: continue
339 arguments_[ name ] = arguments.pop( name )
340 namespace[ arguments_name ] = arguments_
343def _deduplicate_merge_sequences(
344 addends: __.cabc.Sequence[ __.typx.Any ],
345 augends: __.cabc.Sequence[ __.typx.Any ],
346) -> __.cabc.Sequence[ __.typx.Any ]:
347 result = list( augends )
348 augends_ = set( augends )
349 for addend in addends:
350 if addend in augends_: continue
351 result.append( addend )
352 return tuple( result )