Coverage for sources/accretive/classes.py: 100%
108 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-05 04:28 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-05 04:28 +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# pylint: disable=line-too-long
22''' Accretive classes.
24 Provides metaclasses for creating classes with accretive attributes. Once a
25 class is initialized, its attributes cannot be reassigned or deleted.
26 However, it may still accrete new attribute assignments.
28 The implementation includes:
30 * ``Class``: Standard metaclass for accretive classes; derived from
31 :py:class:`type`.
32 * ``ABCFactory``: Metaclass for abstract base classes; derived from
33 :py:class:`abc.ABCMeta`.
34 * ``ProtocolClass``: Metaclass for protocol classes; derived from
35 :py:class:`typing.Protocol`.
37 Additionally, metaclasses for dataclasses are provided as a convenience.
39 >>> from accretive import Class
40 >>> class Example( metaclass = Class ):
41 ... x = 1
42 >>> Example.y = 2 # Add new class attribute
43 >>> Example.x = 3 # Attempt reassignment
44 Traceback (most recent call last):
45 ...
46 accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete attribute 'x'.
48 For cases where some attributes need to remain mutable, use the ``mutables`` parameter:
50 >>> class Config( metaclass = Class, mutables = ( 'version', ) ):
51 ... name = 'MyApp'
52 ... version = '1.0.0'
53 >>> Config.version = '1.0.1' # Can modify designated mutable attributes
54 >>> Config.version
55 '1.0.1'
56 >>> Config.name = 'NewApp' # Other attributes remain immutable
57 Traceback (most recent call last):
58 ...
59 accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete attribute 'name'.
60'''
61# pylint: enable=line-too-long
63# TODO? Allow predicate functions and regex patterns as mutability checkers.
66from __future__ import annotations
68from . import __
71ClassDecorators: __.typx.TypeAlias = (
72 __.cabc.Iterable[ __.cabc.Callable[ [ type ], type ] ] )
75_behavior = 'accretion'
78class Class( type ):
79 ''' Accretive class factory. '''
81 def __new__( # pylint: disable=too-many-arguments
82 clscls: type[ Class ],
83 name: str,
84 bases: tuple[ type, ... ],
85 namespace: dict[ str, __.typx.Any ], *,
86 decorators: ClassDecorators = ( ),
87 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
88 mutables: __.cabc.Collection[ str ] = ( ),
89 **args: __.typx.Any
90 ) -> Class:
91 class_ = type.__new__(
92 clscls, name, bases, namespace, **args )
93 return _class__new__(
94 class_,
95 decorators = decorators,
96 docstring = docstring,
97 mutables = mutables )
99 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
100 super( ).__init__( *posargs, **nomargs )
101 _class__init__( selfclass )
103 def __delattr__( selfclass, name: str ) -> None:
104 if not _class__delattr__( selfclass, name ):
105 super( ).__delattr__( name )
107 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
108 if not _class__setattr__( selfclass, name ):
109 super( ).__setattr__( name, value )
111Class.__doc__ = __.generate_docstring(
112 Class,
113 'description of class factory class',
114 'class attributes accretion' )
117@__.typx.dataclass_transform( kw_only_default = True )
118class Dataclass( Class ):
119 ''' Accretive dataclass factory. '''
121 def __new__( # pylint: disable=too-many-arguments
122 clscls: type[ Dataclass ],
123 name: str,
124 bases: tuple[ type, ... ],
125 namespace: dict[ str, __.typx.Any ], *,
126 decorators: ClassDecorators = ( ),
127 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
128 mutables: __.cabc.Collection[ str ] = ( ),
129 **args: __.typx.Any
130 ) -> Dataclass:
131 decorators_ = (
132 __.dcls.dataclass( kw_only = True, slots = True ),
133 *decorators )
134 return Class.__new__( # pyright: ignore
135 clscls, name, bases, namespace,
136 decorators = decorators_,
137 docstring = docstring,
138 mutables = mutables,
139 **args )
141Dataclass.__doc__ = __.generate_docstring(
142 Dataclass,
143 'description of class factory class',
144 'class attributes accretion' )
147@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True )
148class CompleteDataclass( Class ):
149 ''' Accretive dataclass factory.
151 Dataclasses from this factory produce immutable instances. '''
152 def __new__( # pylint: disable=too-many-arguments
153 clscls: type[ CompleteDataclass ],
154 name: str,
155 bases: tuple[ type, ... ],
156 namespace: dict[ str, __.typx.Any ], *,
157 decorators: ClassDecorators = ( ),
158 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
159 mutables: __.cabc.Collection[ str ] = ( ),
160 **args: __.typx.Any
161 ) -> CompleteDataclass:
162 decorators_ = (
163 __.dcls.dataclass( frozen = True, kw_only = True, slots = True ),
164 *decorators )
165 return Class.__new__( # pyright: ignore
166 clscls, name, bases, namespace,
167 decorators = decorators_,
168 docstring = docstring,
169 mutables = mutables,
170 **args )
172CompleteDataclass.__doc__ = __.generate_docstring(
173 CompleteDataclass,
174 'description of class factory class',
175 'class attributes accretion' )
178class ABCFactory( __.abc.ABCMeta ):
179 ''' Accretive abstract base class factory. '''
181 def __new__( # pylint: disable=too-many-arguments
182 clscls: type[ ABCFactory ],
183 name: str,
184 bases: tuple[ type, ... ],
185 namespace: dict[ str, __.typx.Any ], *,
186 decorators: ClassDecorators = ( ),
187 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
188 mutables: __.cabc.Collection[ str ] = ( ),
189 **args: __.typx.Any
190 ) -> ABCFactory:
191 class_ = __.abc.ABCMeta.__new__(
192 clscls, name, bases, namespace, **args )
193 return _class__new__(
194 class_,
195 decorators = decorators,
196 docstring = docstring,
197 mutables = mutables )
199 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
200 super( ).__init__( *posargs, **nomargs )
201 _class__init__( selfclass )
203 def __delattr__( selfclass, name: str ) -> None:
204 if not _class__delattr__( selfclass, name ):
205 super( ).__delattr__( name )
207 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
208 if not _class__setattr__( selfclass, name ):
209 super( ).__setattr__( name, value )
211ABCFactory.__doc__ = __.generate_docstring(
212 ABCFactory,
213 'description of class factory class',
214 'class attributes accretion' )
217# pylint: disable=bad-classmethod-argument,no-self-argument
218class ProtocolClass( type( __.typx.Protocol ) ):
219 ''' Accretive protocol class factory. '''
221 def __new__( # pylint: disable=too-many-arguments
222 clscls: type[ ProtocolClass ],
223 name: str,
224 bases: tuple[ type, ... ],
225 namespace: dict[ str, __.typx.Any ], *,
226 decorators: ClassDecorators = ( ),
227 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
228 mutables: __.cabc.Collection[ str ] = ( ),
229 **args: __.typx.Any
230 ) -> ProtocolClass:
231 class_ = super( ProtocolClass, clscls ).__new__( # pylint: disable=too-many-function-args
232 clscls, name, bases, namespace, **args )
233 return _class__new__(
234 class_,
235 decorators = decorators,
236 docstring = docstring,
237 mutables = mutables )
239 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
240 super( ).__init__( *posargs, **nomargs )
241 _class__init__( selfclass )
243 def __delattr__( selfclass, name: str ) -> None:
244 if not _class__delattr__( selfclass, name ):
245 super( ).__delattr__( name )
247 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
248 if not _class__setattr__( selfclass, name ):
249 super( ).__setattr__( name, value )
250# pylint: enable=bad-classmethod-argument,no-self-argument
252ProtocolClass.__doc__ = __.generate_docstring(
253 ProtocolClass,
254 'description of class factory class',
255 'class attributes accretion' )
258# pylint: disable=bad-classmethod-argument,no-self-argument
259@__.typx.dataclass_transform( kw_only_default = True )
260class ProtocolDataclass( ProtocolClass ):
261 ''' Accretive protocol dataclass factory. '''
262 def __new__( # pylint: disable=too-many-arguments
263 clscls: type[ ProtocolDataclass ],
264 name: str,
265 bases: tuple[ type, ... ],
266 namespace: dict[ str, __.typx.Any ], *,
267 decorators: ClassDecorators = ( ),
268 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
269 mutables: __.cabc.Collection[ str ] = ( ),
270 **args: __.typx.Any
271 ) -> ProtocolDataclass:
272 decorators_ = (
273 __.dcls.dataclass( kw_only = True, slots = True ),
274 *decorators )
275 return ProtocolClass.__new__( # pyright: ignore
276 clscls, name, bases, namespace,
277 decorators = decorators_,
278 docstring = docstring,
279 mutables = mutables,
280 **args )
281# pylint: enable=bad-classmethod-argument,no-self-argument
283ProtocolDataclass.__doc__ = __.generate_docstring(
284 ProtocolDataclass,
285 'description of class factory class',
286 'class attributes accretion' )
289# pylint: disable=bad-classmethod-argument,no-self-argument
290@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True )
291class CompleteProtocolDataclass( ProtocolClass ):
292 ''' Accretive protocol dataclass factory.
294 Dataclasses from this factory produce immutable instances. '''
295 def __new__( # pylint: disable=too-many-arguments
296 clscls: type[ CompleteProtocolDataclass ],
297 name: str,
298 bases: tuple[ type, ... ],
299 namespace: dict[ str, __.typx.Any ], *,
300 decorators: ClassDecorators = ( ),
301 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
302 mutables: __.cabc.Collection[ str ] = ( ),
303 **args: __.typx.Any
304 ) -> CompleteProtocolDataclass:
305 decorators_ = (
306 __.dcls.dataclass( frozen = True, kw_only = True, slots = True ),
307 *decorators )
308 return ProtocolClass.__new__( # pyright: ignore
309 clscls, name, bases, namespace,
310 decorators = decorators_,
311 docstring = docstring,
312 mutables = mutables,
313 **args )
314# pylint: enable=bad-classmethod-argument,no-self-argument
316CompleteProtocolDataclass.__doc__ = __.generate_docstring(
317 CompleteProtocolDataclass,
318 'description of class factory class',
319 'class attributes accretion' )
322def _accumulate_mutables(
323 class_: type, mutables: __.cabc.Collection[ str ]
324) -> frozenset[ str ]:
325 return frozenset( mutables ).union( *(
326 frozenset( base.__dict__.get( '_class_mutables_', ( ) ) )
327 for base in class_.__mro__ ) )
329# pylint: disable=protected-access
330def _class__new__(
331 original: type,
332 decorators: ClassDecorators = ( ),
333 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
334 mutables: __.cabc.Collection[ str ] = ( ),
335) -> type:
336 # Some decorators create new classes, which invokes this method again.
337 # Short-circuit to prevent recursive decoration and other tangles.
338 class_decorators_ = original.__dict__.get( '_class_decorators_', [ ] )
339 if class_decorators_: return original
340 if not __.is_absent( docstring ): original.__doc__ = docstring
341 original._class_mutables_ = _accumulate_mutables( original, mutables )
342 original._class_decorators_ = class_decorators_
343 reproduction = original
344 for decorator in decorators:
345 class_decorators_.append( decorator )
346 reproduction = decorator( original )
347 if original is not reproduction:
348 __.repair_class_reproduction( original, reproduction )
349 original = reproduction
350 class_decorators_.clear( ) # Flag '__init__' to enable accretion
351 return reproduction
352# pylint: enable=protected-access
355# pylint: disable=protected-access
356def _class__init__( class_: type ) -> None:
357 # Some metaclasses add class attributes in '__init__' method.
358 # So, we wait until last possible moment to set immutability.
359 # Consult class attributes dictionary to ignore immutable base classes.
360 cdict = class_.__dict__
361 if cdict.get( '_class_decorators_' ): return
362 del class_._class_decorators_
363 if ( class_behaviors := cdict.get( '_class_behaviors_' ) ):
364 class_behaviors.add( _behavior )
365 else: class_._class_behaviors_ = { _behavior }
366# pylint: enable=protected-access
369def _class__delattr__( class_: type, name: str ) -> bool:
370 # Consult class attributes dictionary to ignore accretive base classes.
371 cdict = class_.__dict__
372 if name in cdict.get( '_class_mutables_', ( ) ): return False
373 if _behavior not in cdict.get( '_class_behaviors_', ( ) ): return False
374 from .exceptions import AttributeImmutabilityError
375 raise AttributeImmutabilityError( name )
378def _class__setattr__( class_: type, name: str ) -> bool:
379 # Consult class attributes dictionary to ignore accretive base classes.
380 cdict = class_.__dict__
381 if name in cdict.get( '_class_mutables_', ( ) ): return False
382 if _behavior not in cdict.get( '_class_behaviors_', ( ) ): return False
383 if hasattr( class_, name ):
384 from .exceptions import AttributeImmutabilityError
385 raise AttributeImmutabilityError( name )
386 return False # Allow setting new attributes.