Coverage for sources/accretive/classes.py: 100%
108 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-01 20:40 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-01 20:40 +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 attribute is set, it cannot be reassigned or deleted.
27 The implementation includes:
29 * ``Class``: Standard metaclass for accretive classes; derived from
30 :py:class:`type`.
31 * ``ABCFactory``: Metaclass for abstract base classes; derived from
32 :py:class:`abc.ABCMeta`.
33 * ``ProtocolClass``: Metaclass for protocol classes; derived from
34 :py:class:`typing.Protocol`.
36 These metaclasses are particularly useful for:
38 * Creating classes with constant class attributes
39 * Defining stable abstract base classes
40 * Building protocol classes with fixed interfaces
42 >>> from accretive import Class
43 >>> class Example( metaclass = Class ):
44 ... x = 1
45 >>> Example.y = 2 # Add new class attribute
46 >>> Example.x = 3 # Attempt reassignment
47 Traceback (most recent call last):
48 ...
49 accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete attribute 'x'.
51 For cases where some attributes need to remain mutable, use the ``mutables`` parameter:
53 >>> class Config( metaclass = Class, mutables = ( 'version', ) ):
54 ... name = 'MyApp'
55 ... version = '1.0.0'
56 >>> Config.version = '1.0.1' # Can modify designated mutable attributes
57 >>> Config.version
58 '1.0.1'
59 >>> Config.name = 'NewApp' # Other attributes remain immutable
60 Traceback (most recent call last):
61 ...
62 accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete attribute 'name'.
63'''
64# pylint: enable=line-too-long
66# TODO? Allow predicate functions and regex patterns as mutability checkers.
69from __future__ import annotations
71from . import __
74ClassDecorators: __.typx.TypeAlias = (
75 __.cabc.Iterable[ __.cabc.Callable[ [ type ], type ] ] )
78_behavior = 'accretion'
81class Class( type ):
82 ''' Accretive class factory. '''
84 def __new__( # pylint: disable=too-many-arguments
85 clscls: type[ Class ],
86 name: str,
87 bases: tuple[ type, ... ],
88 namespace: dict[ str, __.typx.Any ], *,
89 decorators: ClassDecorators = ( ),
90 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
91 mutables: __.cabc.Collection[ str ] = ( ),
92 **args: __.typx.Any
93 ) -> Class:
94 class_ = type.__new__(
95 clscls, name, bases, namespace, **args )
96 return _class__new__(
97 class_,
98 decorators = decorators,
99 docstring = docstring,
100 mutables = mutables )
102 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
103 super( ).__init__( *posargs, **nomargs )
104 _class__init__( selfclass )
106 def __delattr__( selfclass, name: str ) -> None:
107 if not _class__delattr__( selfclass, name ):
108 super( ).__delattr__( name )
110 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
111 if not _class__setattr__( selfclass, name ):
112 super( ).__setattr__( name, value )
114Class.__doc__ = __.generate_docstring(
115 Class,
116 'description of class factory class',
117 'class attributes accretion' )
120@__.typx.dataclass_transform( kw_only_default = True )
121class Dataclass( Class ):
122 ''' Accretive dataclass factory. '''
124 def __new__( # pylint: disable=too-many-arguments
125 clscls: type[ Dataclass ],
126 name: str,
127 bases: tuple[ type, ... ],
128 namespace: dict[ str, __.typx.Any ], *,
129 decorators: ClassDecorators = ( ),
130 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
131 mutables: __.cabc.Collection[ str ] = ( ),
132 **args: __.typx.Any
133 ) -> Dataclass:
134 decorators_ = (
135 __.dcls.dataclass( kw_only = True, slots = True ),
136 *decorators )
137 return Class.__new__( # pyright: ignore
138 clscls, name, bases, namespace,
139 decorators = decorators_,
140 docstring = docstring,
141 mutables = mutables,
142 **args )
144Dataclass.__doc__ = __.generate_docstring(
145 Dataclass,
146 'description of class factory class',
147 'class attributes accretion' )
150@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True )
151class CompleteDataclass( Class ):
152 ''' Accretive dataclass factory.
154 Dataclasses from this factory produce immutable instances. '''
155 def __new__( # pylint: disable=too-many-arguments
156 clscls: type[ CompleteDataclass ],
157 name: str,
158 bases: tuple[ type, ... ],
159 namespace: dict[ str, __.typx.Any ], *,
160 decorators: ClassDecorators = ( ),
161 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
162 mutables: __.cabc.Collection[ str ] = ( ),
163 **args: __.typx.Any
164 ) -> CompleteDataclass:
165 decorators_ = (
166 __.dcls.dataclass( frozen = True, kw_only = True, slots = True ),
167 *decorators )
168 return Class.__new__( # pyright: ignore
169 clscls, name, bases, namespace,
170 decorators = decorators_,
171 docstring = docstring,
172 mutables = mutables,
173 **args )
175CompleteDataclass.__doc__ = __.generate_docstring(
176 CompleteDataclass,
177 'description of class factory class',
178 'class attributes accretion' )
181class ABCFactory( __.abc.ABCMeta ):
182 ''' Accretive abstract base class factory. '''
184 def __new__( # pylint: disable=too-many-arguments
185 clscls: type[ ABCFactory ],
186 name: str,
187 bases: tuple[ type, ... ],
188 namespace: dict[ str, __.typx.Any ], *,
189 decorators: ClassDecorators = ( ),
190 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
191 mutables: __.cabc.Collection[ str ] = ( ),
192 **args: __.typx.Any
193 ) -> ABCFactory:
194 class_ = __.abc.ABCMeta.__new__(
195 clscls, name, bases, namespace, **args )
196 return _class__new__(
197 class_,
198 decorators = decorators,
199 docstring = docstring,
200 mutables = mutables )
202 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
203 super( ).__init__( *posargs, **nomargs )
204 _class__init__( selfclass )
206 def __delattr__( selfclass, name: str ) -> None:
207 if not _class__delattr__( selfclass, name ):
208 super( ).__delattr__( name )
210 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
211 if not _class__setattr__( selfclass, name ):
212 super( ).__setattr__( name, value )
214ABCFactory.__doc__ = __.generate_docstring(
215 ABCFactory,
216 'description of class factory class',
217 'class attributes accretion' )
220# pylint: disable=bad-classmethod-argument,no-self-argument
221class ProtocolClass( type( __.typx.Protocol ) ):
222 ''' Accretive protocol class factory. '''
224 def __new__( # pylint: disable=too-many-arguments
225 clscls: type[ ProtocolClass ],
226 name: str,
227 bases: tuple[ type, ... ],
228 namespace: dict[ str, __.typx.Any ], *,
229 decorators: ClassDecorators = ( ),
230 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
231 mutables: __.cabc.Collection[ str ] = ( ),
232 **args: __.typx.Any
233 ) -> ProtocolClass:
234 class_ = super( ProtocolClass, clscls ).__new__( # pylint: disable=too-many-function-args
235 clscls, name, bases, namespace, **args )
236 return _class__new__(
237 class_,
238 decorators = decorators,
239 docstring = docstring,
240 mutables = mutables )
242 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
243 super( ).__init__( *posargs, **nomargs )
244 _class__init__( selfclass )
246 def __delattr__( selfclass, name: str ) -> None:
247 if not _class__delattr__( selfclass, name ):
248 super( ).__delattr__( name )
250 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
251 if not _class__setattr__( selfclass, name ):
252 super( ).__setattr__( name, value )
253# pylint: enable=bad-classmethod-argument,no-self-argument
255ProtocolClass.__doc__ = __.generate_docstring(
256 ProtocolClass,
257 'description of class factory class',
258 'class attributes accretion' )
261# pylint: disable=bad-classmethod-argument,no-self-argument
262@__.typx.dataclass_transform( kw_only_default = True )
263class ProtocolDataclass( ProtocolClass ):
264 ''' Accretive protocol dataclass factory. '''
265 def __new__( # pylint: disable=too-many-arguments
266 clscls: type[ ProtocolDataclass ],
267 name: str,
268 bases: tuple[ type, ... ],
269 namespace: dict[ str, __.typx.Any ], *,
270 decorators: ClassDecorators = ( ),
271 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
272 mutables: __.cabc.Collection[ str ] = ( ),
273 **args: __.typx.Any
274 ) -> ProtocolDataclass:
275 decorators_ = (
276 __.dcls.dataclass( kw_only = True, slots = True ),
277 *decorators )
278 return ProtocolClass.__new__( # pyright: ignore
279 clscls, name, bases, namespace,
280 decorators = decorators_,
281 docstring = docstring,
282 mutables = mutables,
283 **args )
284# pylint: enable=bad-classmethod-argument,no-self-argument
286ProtocolDataclass.__doc__ = __.generate_docstring(
287 ProtocolDataclass,
288 'description of class factory class',
289 'class attributes accretion' )
292# pylint: disable=bad-classmethod-argument,no-self-argument
293@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True )
294class CompleteProtocolDataclass( ProtocolClass ):
295 ''' Accretive protocol dataclass factory.
297 Dataclasses from this factory produce immutable instances. '''
298 def __new__( # pylint: disable=too-many-arguments
299 clscls: type[ CompleteProtocolDataclass ],
300 name: str,
301 bases: tuple[ type, ... ],
302 namespace: dict[ str, __.typx.Any ], *,
303 decorators: ClassDecorators = ( ),
304 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
305 mutables: __.cabc.Collection[ str ] = ( ),
306 **args: __.typx.Any
307 ) -> CompleteProtocolDataclass:
308 decorators_ = (
309 __.dcls.dataclass( frozen = True, kw_only = True, slots = True ),
310 *decorators )
311 return ProtocolClass.__new__( # pyright: ignore
312 clscls, name, bases, namespace,
313 decorators = decorators_,
314 docstring = docstring,
315 mutables = mutables,
316 **args )
317# pylint: enable=bad-classmethod-argument,no-self-argument
319CompleteProtocolDataclass.__doc__ = __.generate_docstring(
320 CompleteProtocolDataclass,
321 'description of class factory class',
322 'class attributes accretion' )
325def _accumulate_mutables(
326 class_: type, mutables: __.cabc.Collection[ str ]
327) -> frozenset[ str ]:
328 return frozenset( mutables ).union( *(
329 frozenset( base.__dict__.get( '_class_mutables_', ( ) ) )
330 for base in class_.__mro__ ) )
332# pylint: disable=protected-access
333def _class__new__(
334 original: type,
335 decorators: ClassDecorators = ( ),
336 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
337 mutables: __.cabc.Collection[ str ] = ( ),
338) -> type:
339 # Some decorators create new classes, which invokes this method again.
340 # Short-circuit to prevent recursive decoration and other tangles.
341 class_decorators_ = original.__dict__.get( '_class_decorators_', [ ] )
342 if class_decorators_: return original
343 if not __.is_absent( docstring ): original.__doc__ = docstring
344 original._class_mutables_ = _accumulate_mutables( original, mutables )
345 original._class_decorators_ = class_decorators_
346 reproduction = original
347 for decorator in decorators:
348 class_decorators_.append( decorator )
349 reproduction = decorator( original )
350 if original is not reproduction:
351 __.repair_class_reproduction( original, reproduction )
352 original = reproduction
353 class_decorators_.clear( ) # Flag '__init__' to enable accretion
354 return reproduction
355# pylint: enable=protected-access
358# pylint: disable=protected-access
359def _class__init__( class_: type ) -> None:
360 # Some metaclasses add class attributes in '__init__' method.
361 # So, we wait until last possible moment to set immutability.
362 # Consult class attributes dictionary to ignore immutable base classes.
363 cdict = class_.__dict__
364 if cdict.get( '_class_decorators_' ): return
365 del class_._class_decorators_
366 if ( class_behaviors := cdict.get( '_class_behaviors_' ) ):
367 class_behaviors.add( _behavior )
368 else: class_._class_behaviors_ = { _behavior }
369# pylint: enable=protected-access
372def _class__delattr__( class_: type, name: str ) -> bool:
373 # Consult class attributes dictionary to ignore accretive base classes.
374 cdict = class_.__dict__
375 if name in cdict.get( '_class_mutables_', ( ) ): return False
376 if _behavior not in cdict.get( '_class_behaviors_', ( ) ): return False
377 from .exceptions import AttributeImmutabilityError
378 raise AttributeImmutabilityError( name )
381def _class__setattr__( class_: type, name: str ) -> bool:
382 # Consult class attributes dictionary to ignore accretive base classes.
383 cdict = class_.__dict__
384 if name in cdict.get( '_class_mutables_', ( ) ): return False
385 if _behavior not in cdict.get( '_class_behaviors_', ( ) ): return False
386 if hasattr( class_, name ):
387 from .exceptions import AttributeImmutabilityError
388 raise AttributeImmutabilityError( name )
389 return False # Allow setting new attributes