Coverage for sources/classcore/standard/behaviors.py: 100%
172 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-05 22:50 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-05 22:50 +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) -> _nomina.ClassConstructionPostprocessor[ __.U ]:
200 ''' Produces construction processor which determines class decorators. '''
201 arguments_name = attributes_namer( 'class', 'construction_arguments' )
203 def postprocess(
204 cls: type, decorators: _nomina.DecoratorsMutable[ __.U ]
205 ) -> None:
206 arguments = getattr( cls, arguments_name, { } )
207 clscls = type( cls )
208 dynadoc_cfg = arguments.get( 'dynadoc_configuration', { } )
209 if not dynadoc_cfg: # either metaclass argument or attribute
210 dynadoc_cfg_name = (
211 attributes_namer( 'classes', 'dynadoc_configuration' ) )
212 dynadoc_cfg = getattr( clscls, dynadoc_cfg_name, { } )
213 decorators.append( __.dynadoc.with_docstring( **dynadoc_cfg ) )
214 dcls_spec = getattr( cls, '__dataclass_transform__', None )
215 if not dcls_spec: # either base class or metaclass may be marked
216 dcls_spec = getattr( clscls, '__dataclass_transform__', None )
217 instances_mutables = arguments.get(
218 'instances_mutables', __.mutables_default )
219 instances_visibles = arguments.get(
220 'instances_visibles', __.visibles_default )
221 if dcls_spec and dcls_spec.get( 'kw_only_default', False ):
222 from .decorators import dataclass_with_standard_behaviors
223 decorator_factory = dataclass_with_standard_behaviors
224 if not dcls_spec.get( 'frozen_default', True ):
225 instances_mutables = instances_mutables or '*'
226 else:
227 from .decorators import with_standard_behaviors
228 decorator_factory = with_standard_behaviors
229 decorator: _nomina.Decorator[ __.U ] = decorator_factory(
230 mutables = instances_mutables, visibles = instances_visibles )
231 decorators.append( decorator )
233 return postprocess
236def produce_class_initialization_completer(
237 attributes_namer: _nomina.AttributesNamer
238) -> _nomina.ClassInitializationCompleter:
239 ''' Produces initialization completer which finalizes class behaviors. '''
240 arguments_name = attributes_namer( 'class', 'construction_arguments' )
242 def complete( cls: type ) -> None:
243 arguments: __.typx.Optional[ dict[ str, __.typx.Any ] ] = (
244 getattr( cls, arguments_name, None ) )
245 if arguments is not None: delattr( cls, arguments_name )
246 arguments = arguments or { }
247 mutables = arguments.get( 'class_mutables', __.mutables_default )
248 visibles = arguments.get( 'class_visibles', __.visibles_default )
249 behaviors: set[ str ] = set( )
250 record_behavior(
251 cls, attributes_namer = attributes_namer,
252 level = 'class', basename = 'mutables',
253 label = _nomina.immutability_label, behaviors = behaviors,
254 verifiers = mutables )
255 record_behavior(
256 cls, attributes_namer = attributes_namer,
257 level = 'class', basename = 'visibles',
258 label = _nomina.concealment_label, behaviors = behaviors,
259 verifiers = visibles )
260 # Set behaviors attribute last since it enables enforcement.
261 behaviors_name = attributes_namer( 'class', 'behaviors' )
262 _utilities.setattr0( cls, behaviors_name, frozenset( behaviors ) )
264 return complete
267def record_behavior( # noqa: PLR0913
268 cls: type, /, *,
269 attributes_namer: _nomina.AttributesNamer,
270 level: str,
271 basename: str,
272 label: str,
273 behaviors: set[ str ],
274 verifiers: _nomina.BehaviorExclusionVerifiersOmni,
275) -> None:
276 ''' Records details of particular class behavior, such as immutability. '''
277 names_name = attributes_namer( level, f"{basename}_names" )
278 if verifiers == '*':
279 setattr( cls, names_name, '*' )
280 return
281 names_omni: _nomina.BehaviorExclusionNamesOmni = (
282 getattr( cls, names_name, frozenset( ) ) )
283 if names_omni == '*': return
284 names, regexes, predicates = (
285 classify_behavior_exclusion_verifiers( verifiers ) )
286 regexes_name = attributes_namer( level, f"{basename}_regexes" )
287 predicates_name = attributes_namer( level, f"{basename}_predicates" )
288 names_: _nomina.BehaviorExclusionNames = (
289 frozenset( { *names, *names_omni } ) )
290 regexes_: _nomina.BehaviorExclusionRegexes = (
291 _deduplicate_merge_sequences(
292 regexes, getattr( cls, regexes_name, ( ) ) ) )
293 predicates_: _nomina.BehaviorExclusionPredicates = (
294 _deduplicate_merge_sequences(
295 predicates, getattr( cls, predicates_name, ( ) ) ) )
296 setattr( cls, names_name, names_ )
297 setattr( cls, regexes_name, regexes_ )
298 setattr( cls, predicates_name, predicates_ )
299 # TODO? Add regexes match cache.
300 # TODO? Add predicates match cache.
301 behaviors.add( label )
304def record_class_construction_arguments(
305 attributes_namer: _nomina.AttributesNamer,
306 namespace: dict[ str, __.typx.Any ],
307 arguments: dict[ str, __.typx.Any ],
308) -> None:
309 ''' Captures metaclass arguments as class attribute for later use. '''
310 arguments_name = attributes_namer( 'class', 'construction_arguments' )
311 arguments_ = namespace.get( arguments_name, { } )
312 # Decorators, which replace classes, will cause construction of the
313 # replacements without arguments. If we had previously recorded them in
314 # the class namespace, then we do not want to clobber them.
315 if arguments_: return
316 arguments_ = { }
317 for name in (
318 'class_mutables', 'class_visibles',
319 'instances_mutables', 'instances_visibles',
320 'dynadoc_configuration',
321 ):
322 if name not in arguments: continue
323 arguments_[ name ] = arguments.pop( name )
324 namespace[ arguments_name ] = arguments_
327def _deduplicate_merge_sequences(
328 addends: __.cabc.Sequence[ __.typx.Any ],
329 augends: __.cabc.Sequence[ __.typx.Any ],
330) -> __.cabc.Sequence[ __.typx.Any ]:
331 result = list( augends )
332 augends_ = set( augends )
333 for addend in addends:
334 if addend in augends_: continue
335 result.append( addend )
336 return tuple( result )