Coverage for sources/classcore/standard/behaviors.py: 81%
169 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-29 23:23 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-29 23:23 +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 __future__ import annotations
28from .. import utilities as _utilities
29from . import __
30from . import nomina as _nomina
33concealment_label = 'concealment'
34immutability_label = 'immutability'
37def assign_attribute_if_mutable( # noqa: PLR0913
38 obj: object, /, *,
39 ligation: _nomina.AssignerLigation,
40 attributes_namer: _nomina.AttributesNamer,
41 error_class_provider: _nomina.ErrorClassProvider,
42 level: str,
43 name: str,
44 value: __.typx.Any,
45) -> None:
46 leveli = 'instance' if level == 'instances' else level
47 behaviors_name = attributes_namer( leveli, 'behaviors' )
48 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) )
49 if immutability_label not in behaviors:
50 ligation( name, value )
51 return
52 names_name = attributes_namer( level, 'mutables_names' )
53 names: _nomina.BehaviorExclusionNamesOmni = (
54 getattr( obj, names_name, frozenset( ) ) )
55 if names == '*' or name in names: 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true
56 ligation( name, value )
57 return
58 predicates_name = attributes_namer( level, 'mutables_predicates' )
59 predicates: _nomina.BehaviorExclusionPredicates = (
60 getattr( obj, predicates_name, ( ) ) )
61 for predicate in predicates: 61 ↛ 62line 61 didn't jump to line 62 because the loop on line 61 never started
62 if predicate( name ):
63 # TODO? Cache predicate hit.
64 ligation( name, value )
65 return
66 regexes_name = attributes_namer( level, 'mutables_regexes' )
67 regexes: _nomina.BehaviorExclusionRegexes = (
68 getattr( obj, regexes_name, ( ) ) )
69 for regex in regexes: 69 ↛ 70line 69 didn't jump to line 70 because the loop on line 69 never started
70 if regex.fullmatch( name ):
71 # TODO? Cache regex hit.
72 ligation( name, value )
73 return
74 target = _utilities.describe_object( obj )
75 raise error_class_provider( 'AttributeImmutability' )( name, target )
78def delete_attribute_if_mutable( # noqa: PLR0913
79 obj: object, /, *,
80 ligation: _nomina.DeleterLigation,
81 attributes_namer: _nomina.AttributesNamer,
82 error_class_provider: _nomina.ErrorClassProvider,
83 level: str,
84 name: str,
85) -> None:
86 leveli = 'instance' if level == 'instances' else level
87 behaviors_name = attributes_namer( leveli, 'behaviors' )
88 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) )
89 if immutability_label not in behaviors:
90 ligation( name )
91 return
92 names_name = attributes_namer( level, 'mutables_names' )
93 names: _nomina.BehaviorExclusionNamesOmni = (
94 getattr( obj, names_name, frozenset( ) ) )
95 if names == '*' or name in names: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 ligation( name )
97 return
98 predicates_name = attributes_namer( level, 'mutables_predicates' )
99 predicates: _nomina.BehaviorExclusionPredicates = (
100 getattr( obj, predicates_name, ( ) ) )
101 for predicate in predicates: 101 ↛ 102line 101 didn't jump to line 102 because the loop on line 101 never started
102 if predicate( name ):
103 # TODO? Cache predicate hit.
104 ligation( name )
105 return
106 regexes_name = attributes_namer( level, 'mutables_regexes' )
107 regexes: _nomina.BehaviorExclusionRegexes = (
108 getattr( obj, regexes_name, ( ) ) )
109 for regex in regexes: 109 ↛ 110line 109 didn't jump to line 110 because the loop on line 109 never started
110 if regex.fullmatch( name ):
111 # TODO? Cache regex hit.
112 ligation( name )
113 return
114 target = _utilities.describe_object( obj )
115 raise error_class_provider( 'AttributeImmutability' )( name, target )
118def survey_visible_attributes(
119 obj: object, /, *,
120 ligation: _nomina.SurveyorLigation,
121 attributes_namer: _nomina.AttributesNamer,
122 level: str,
123) -> __.cabc.Iterable[ str ]:
124 names_base = ligation( )
125 leveli = 'instance' if level == 'instances' else level
126 behaviors_name = attributes_namer( leveli, 'behaviors' )
127 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) )
128 if concealment_label not in behaviors: return names_base 128 ↛ exitline 128 didn't return from function 'survey_visible_attributes' because the return on line 128 wasn't executed
129 names_name = attributes_namer( level, 'visibles_names' )
130 names: _nomina.BehaviorExclusionNamesOmni = (
131 getattr( obj, names_name, frozenset( ) ) )
132 if names == '*': return names_base # pragma: no branch
133 regexes_name = attributes_namer( level, 'visibles_regexes' )
134 regexes: _nomina.BehaviorExclusionRegexes = (
135 getattr( obj, regexes_name, ( ) ) )
136 predicates_name = attributes_namer( level, 'visibles_predicates' )
137 predicates: _nomina.BehaviorExclusionPredicates = (
138 getattr( obj, predicates_name, ( ) ) )
139 names_: list[ str ] = [ ]
140 for name in names_base:
141 if name in names:
142 names_.append( name )
143 continue
144 for predicate in predicates:
145 if predicate( name ):
146 # TODO? Cache predicate hit.
147 names_.append( name )
148 continue
149 for regex in regexes: 149 ↛ 150line 149 didn't jump to line 150 because the loop on line 149 never started
150 if regex.fullmatch( name ):
151 # TODO? Cache regex hit.
152 names_.append( name )
153 continue
154 return names_
157def classify_behavior_exclusion_verifiers(
158 verifiers: _nomina.BehaviorExclusionVerifiers
159) -> tuple[
160 _nomina.BehaviorExclusionNames,
161 _nomina.BehaviorExclusionRegexes,
162 _nomina.BehaviorExclusionPredicates,
163]:
164 names: set[ str ] = set( )
165 regexes: list[ __.re.Pattern[ str ] ] = [ ]
166 predicates: list[ __.cabc.Callable[ ..., bool ] ] = [ ]
167 for verifier in verifiers:
168 if isinstance( verifier, str ):
169 names.add( verifier )
170 elif isinstance( verifier, __.re.Pattern ): 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true
171 regexes.append( verifier )
172 elif callable( verifier ): 172 ↛ 175line 172 didn't jump to line 175 because the condition on line 172 was always true
173 predicates.append( verifier )
174 else:
175 from ..exceptions import BehaviorExclusionInvalidity
176 raise BehaviorExclusionInvalidity( verifier )
177 return frozenset( names ), tuple( regexes ), tuple( predicates )
180def produce_class_construction_preprocessor(
181 attributes_namer: _nomina.AttributesNamer
182) -> _nomina.ClassConstructionPreprocessor:
184 def preprocess( # noqa: PLR0913
185 clscls: type,
186 name: str,
187 bases: list[ type ],
188 namespace: dict[ str, __.typx.Any ],
189 arguments: dict[ str, __.typx.Any ],
190 decorators: _nomina.DecoratorsMutable,
191 ) -> None:
192 record_class_construction_arguments(
193 attributes_namer, namespace, arguments )
195 return preprocess
198def produce_class_construction_postprocessor(
199 attributes_namer: _nomina.AttributesNamer
200) -> _nomina.ClassConstructionPostprocessor:
201 arguments_name = attributes_namer( 'class', 'construction_arguments' )
203 def postprocess(
204 cls: type, decorators: _nomina.DecoratorsMutable
205 ) -> None:
206 arguments = getattr( cls, arguments_name, { } )
207 dcls_spec = getattr( cls, '__dataclass_transform__', None )
208 if not dcls_spec: # either base class or metaclass may be marked
209 clscls = type( cls )
210 dcls_spec = getattr( clscls, '__dataclass_transform__', None )
211 instances_mutables = arguments.get(
212 'instances_mutables', __.mutables_default )
213 instances_visibles = arguments.get(
214 'instances_visibles', __.visibles_default )
215 if dcls_spec and dcls_spec.get( 'kw_only_default', False ):
216 from .decorators import dataclass_with_standard_behaviors
217 decorator_factory = dataclass_with_standard_behaviors
218 if not dcls_spec.get( 'frozen_default', True ):
219 instances_mutables = instances_mutables or '*'
220 else:
221 from .decorators import with_standard_behaviors
222 decorator_factory = with_standard_behaviors
223 decorator = decorator_factory(
224 mutables = instances_mutables, visibles = instances_visibles )
225 decorators.append( decorator )
227 return postprocess
230def produce_class_initialization_completer(
231 attributes_namer: _nomina.AttributesNamer
232) -> _nomina.ClassInitializationCompleter:
233 arguments_name = attributes_namer( 'class', 'construction_arguments' )
235 def complete( cls: type ) -> None:
236 arguments: __.typx.Optional[ dict[ str, __.typx.Any ] ] = (
237 getattr( cls, arguments_name, None ) )
238 if arguments is not None: delattr( cls, arguments_name )
239 arguments = arguments or { }
240 mutables = arguments.get( 'class_mutables', __.mutables_default )
241 visibles = arguments.get( 'class_visibles', __.visibles_default )
242 behaviors: set[ str ] = set( )
243 record_behavior(
244 cls, attributes_namer = attributes_namer,
245 level = 'class', basename = 'mutables',
246 label = immutability_label, behaviors = behaviors,
247 verifiers = mutables )
248 record_behavior(
249 cls, attributes_namer = attributes_namer,
250 level = 'class', basename = 'visibles',
251 label = concealment_label, behaviors = behaviors,
252 verifiers = visibles )
253 # Set behaviors attribute last since it enables enforcement.
254 setattr( cls, attributes_namer( 'class', 'behaviors' ), behaviors )
256 return complete
259def record_behavior( # noqa: PLR0913
260 cls: type, /, *,
261 attributes_namer: _nomina.AttributesNamer,
262 level: str,
263 basename: str,
264 label: str,
265 behaviors: set[ str ],
266 verifiers: _nomina.BehaviorExclusionVerifiersOmni,
267) -> None:
268 names_name = attributes_namer( level, f"{basename}_names" )
269 if verifiers == '*':
270 setattr( cls, names_name, '*' )
271 return
272 names_omni: _nomina.BehaviorExclusionNamesOmni = (
273 getattr( cls, names_name, frozenset( ) ) )
274 if names_omni == '*': return 274 ↛ exitline 274 didn't return from function 'record_behavior' because the return on line 274 wasn't executed
275 names, regexes, predicates = (
276 classify_behavior_exclusion_verifiers( verifiers ) )
277 regexes_name = attributes_namer( level, f"{basename}_regexes" )
278 predicates_name = attributes_namer( level, f"{basename}_predicates" )
279 names_: _nomina.BehaviorExclusionNames = (
280 frozenset( { *names, *names_omni } ) )
281 regexes_: _nomina.BehaviorExclusionRegexes = (
282 _deduplicate_merge_sequences(
283 regexes, getattr( cls, regexes_name, ( ) ) ) )
284 predicates_: _nomina.BehaviorExclusionPredicates = (
285 _deduplicate_merge_sequences(
286 predicates, getattr( cls, predicates_name, ( ) ) ) )
287 setattr( cls, names_name, names_ )
288 setattr( cls, regexes_name, regexes_ )
289 setattr( cls, predicates_name, predicates_ )
290 # TODO? Add regexes match cache.
291 # TODO? Add predicates match cache.
292 behaviors.add( label )
295def record_class_construction_arguments(
296 attributes_namer: _nomina.AttributesNamer,
297 namespace: dict[ str, __.typx.Any ],
298 arguments: dict[ str, __.typx.Any ],
299) -> None:
300 arguments_name = attributes_namer( 'class', 'construction_arguments' )
301 arguments_ = namespace.get( arguments_name, { } )
302 # Decorators, which replace classes, will cause construction of the
303 # replacements without arguments. If we had previously recorded them in
304 # the class namespace, then we do not want to clobber them.
305 if arguments_: return 305 ↛ exitline 305 didn't return from function 'record_class_construction_arguments' because the return on line 305 wasn't executed
306 arguments_ = { }
307 for name in (
308 'class_mutables', 'class_visibles',
309 'instances_mutables', 'instances_visibles',
310 ):
311 if name not in arguments: continue
312 arguments_[ name ] = arguments.pop( name )
313 namespace[ arguments_name ] = arguments_
316def _deduplicate_merge_sequences(
317 addends: __.cabc.Sequence[ __.typx.Any ],
318 augends: __.cabc.Sequence[ __.typx.Any ],
319) -> __.cabc.Sequence[ __.typx.Any ]:
320 result = list( augends )
321 augends_ = set( augends )
322 for addend in addends:
323 if addend in augends_: continue
324 result.append( addend )
325 return tuple( result )