Coverage for sources/frigid/classes.py: 100%
106 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-05 02:47 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-05 02:47 +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''' Immutable classes.
24 Provides metaclasses for creating classes with immutable attributes. Once a
25 class is initialized, new attributes may not be assigned to it and its
26 existing attributes cannot be reassigned or deleted.
28 The implementation includes:
30 * ``Class``: Standard metaclass for immutable 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 frigid import Class
40 >>> class Example( metaclass = Class ):
41 ... x = 1
42 >>> Example.y = 2 # Attempt assignment
43 Traceback (most recent call last):
44 ...
45 frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'y'.
46 >>> Example.x = 3 # Attempt reassignment
47 Traceback (most recent call last):
48 ...
49 frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'.
50'''
51# pylint: enable=line-too-long
53# TODO? Allow predicate functions and regex patterns as mutability checkers.
56from __future__ import annotations
58from . import __
61ClassDecorators: __.typx.TypeAlias = (
62 __.cabc.Iterable[ __.cabc.Callable[ [ type ], type ] ] )
65_behavior = 'immutability'
68class Class( type ):
69 ''' Immutable class factory. '''
71 def __new__( # pylint: disable=too-many-arguments
72 clscls: type[ Class ],
73 name: str,
74 bases: tuple[ type, ... ],
75 namespace: dict[ str, __.typx.Any ], *,
76 decorators: ClassDecorators = ( ),
77 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
78 mutables: __.cabc.Collection[ str ] = ( ),
79 **args: __.typx.Any
80 ) -> Class:
81 class_ = type.__new__( clscls, name, bases, namespace, **args )
82 return _class__new__(
83 class_,
84 decorators = decorators,
85 docstring = docstring,
86 mutables = mutables )
88 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
89 super( ).__init__( *posargs, **nomargs )
90 _class__init__( selfclass )
92 def __delattr__( selfclass, name: str ) -> None:
93 if not _class__delattr__( selfclass, name ):
94 super( ).__delattr__( name )
96 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
97 if not _class__setattr__( selfclass, name ):
98 super( ).__setattr__( name, value )
100Class.__doc__ = __.generate_docstring(
101 Class,
102 'description of class factory class',
103 'class attributes immutability' )
106@__.typx.dataclass_transform( kw_only_default = True )
107class Dataclass( Class ):
108 ''' Immutable dataclass factory. '''
110 def __new__( # pylint: disable=too-many-arguments
111 clscls: type[ Dataclass ],
112 name: str,
113 bases: tuple[ type, ... ],
114 namespace: dict[ str, __.typx.Any ], *,
115 decorators: ClassDecorators = ( ),
116 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
117 mutables: __.cabc.Collection[ str ] = ( ),
118 **args: __.typx.Any
119 ) -> Dataclass:
120 decorators_ = (
121 __.dcls.dataclass( kw_only = True, slots = True ),
122 *decorators )
123 return Class.__new__( # pyright: ignore
124 clscls, name, bases, namespace,
125 decorators = decorators_,
126 docstring = docstring,
127 mutables = mutables,
128 **args )
130Dataclass.__doc__ = __.generate_docstring(
131 Dataclass,
132 'description of class factory class',
133 'class attributes immutability' )
136@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True )
137class CompleteDataclass( Class ):
138 ''' Immutable dataclass factory.
140 Dataclasses from this factory produce immutable instances. '''
141 def __new__( # pylint: disable=too-many-arguments
142 clscls: type[ CompleteDataclass ],
143 name: str,
144 bases: tuple[ type, ... ],
145 namespace: dict[ str, __.typx.Any ], *,
146 decorators: ClassDecorators = ( ),
147 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
148 mutables: __.cabc.Collection[ str ] = ( ),
149 **args: __.typx.Any
150 ) -> CompleteDataclass:
151 decorators_ = (
152 __.dcls.dataclass( frozen = True, kw_only = True, slots = True ),
153 *decorators )
154 return Class.__new__( # pyright: ignore
155 clscls, name, bases, namespace,
156 decorators = decorators_,
157 docstring = docstring,
158 mutables = mutables,
159 **args )
161CompleteDataclass.__doc__ = __.generate_docstring(
162 CompleteDataclass,
163 'description of class factory class',
164 'class attributes immutability' )
167class ABCFactory( __.abc.ABCMeta ):
168 ''' Immutable abstract base class factory. '''
170 def __new__( # pylint: disable=too-many-arguments
171 clscls: type[ ABCFactory ],
172 name: str,
173 bases: tuple[ type, ... ],
174 namespace: dict[ str, __.typx.Any ], *,
175 decorators: ClassDecorators = ( ),
176 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
177 mutables: __.cabc.Collection[ str ] = ( ),
178 **args: __.typx.Any
179 ) -> ABCFactory:
180 class_ = __.abc.ABCMeta.__new__(
181 clscls, name, bases, namespace, **args )
182 return _class__new__(
183 class_, decorators = decorators,
184 docstring = docstring,
185 mutables = mutables )
187 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
188 super( ).__init__( *posargs, **nomargs )
189 _class__init__( selfclass )
191 def __delattr__( selfclass, name: str ) -> None:
192 if not _class__delattr__( selfclass, name ):
193 super( ).__delattr__( name )
195 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
196 if not _class__setattr__( selfclass, name ):
197 super( ).__setattr__( name, value )
199ABCFactory.__doc__ = __.generate_docstring(
200 ABCFactory,
201 'description of class factory class',
202 'class attributes immutability' )
205# pylint: disable=bad-classmethod-argument,no-self-argument
206class ProtocolClass( type( __.typx.Protocol ) ):
207 ''' Immutable protocol class factory. '''
209 def __new__( # pylint: disable=too-many-arguments
210 clscls: type[ ProtocolClass ],
211 name: str,
212 bases: tuple[ type, ... ],
213 namespace: dict[ str, __.typx.Any ], *,
214 decorators: ClassDecorators = ( ),
215 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
216 mutables: __.cabc.Collection[ str ] = ( ),
217 **args: __.typx.Any
218 ) -> ProtocolClass:
219 class_ = super( ProtocolClass, clscls ).__new__( # pylint: disable=too-many-function-args
220 clscls, name, bases, namespace, **args )
221 return _class__new__(
222 class_,
223 decorators = decorators,
224 docstring = docstring,
225 mutables = mutables )
227 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
228 super( ).__init__( *posargs, **nomargs )
229 _class__init__( selfclass )
231 def __delattr__( selfclass, name: str ) -> None:
232 if not _class__delattr__( selfclass, name ):
233 super( ).__delattr__( name )
235 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
236 if not _class__setattr__( selfclass, name ):
237 super( ).__setattr__( name, value )
238# pylint: enable=bad-classmethod-argument,no-self-argument
240ProtocolClass.__doc__ = __.generate_docstring(
241 ProtocolClass,
242 'description of class factory class',
243 'class attributes immutability' )
246# pylint: disable=bad-classmethod-argument,no-self-argument
247@__.typx.dataclass_transform( kw_only_default = True )
248class ProtocolDataclass( ProtocolClass ):
249 ''' Immutable protocol dataclass factory. '''
250 def __new__( # pylint: disable=too-many-arguments
251 clscls: type[ ProtocolDataclass ],
252 name: str,
253 bases: tuple[ type, ... ],
254 namespace: dict[ str, __.typx.Any ], *,
255 decorators: ClassDecorators = ( ),
256 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
257 mutables: __.cabc.Collection[ str ] = ( ),
258 **args: __.typx.Any
259 ) -> ProtocolDataclass:
260 decorators_ = (
261 __.dcls.dataclass( kw_only = True, slots = True ),
262 *decorators )
263 return ProtocolClass.__new__( # pyright: ignore
264 clscls, name, bases, namespace,
265 decorators = decorators_,
266 docstring = docstring,
267 mutables = mutables,
268 **args )
269# pylint: enable=bad-classmethod-argument,no-self-argument
271ProtocolDataclass.__doc__ = __.generate_docstring(
272 ProtocolDataclass,
273 'description of class factory class',
274 'class attributes immutability' )
277# pylint: disable=bad-classmethod-argument,no-self-argument
278@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True )
279class CompleteProtocolDataclass( ProtocolClass ):
280 ''' Immutable protocol dataclass factory.
282 Dataclasses from this factory produce immutable instances. '''
283 def __new__( # pylint: disable=too-many-arguments
284 clscls: type[ CompleteProtocolDataclass ],
285 name: str,
286 bases: tuple[ type, ... ],
287 namespace: dict[ str, __.typx.Any ], *,
288 decorators: ClassDecorators = ( ),
289 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
290 mutables: __.cabc.Collection[ str ] = ( ),
291 **args: __.typx.Any
292 ) -> CompleteProtocolDataclass:
293 decorators_ = (
294 __.dcls.dataclass( frozen = True, kw_only = True, slots = True ),
295 *decorators )
296 return ProtocolClass.__new__( # pyright: ignore
297 clscls, name, bases, namespace,
298 decorators = decorators_,
299 docstring = docstring,
300 mutables = mutables,
301 **args )
302# pylint: enable=bad-classmethod-argument,no-self-argument
304CompleteProtocolDataclass.__doc__ = __.generate_docstring(
305 CompleteProtocolDataclass,
306 'description of class factory class',
307 'class attributes immutability' )
310def _accumulate_mutables(
311 class_: type, mutables: __.cabc.Collection[ str ]
312) -> frozenset[ str ]:
313 return frozenset( mutables ).union( *(
314 frozenset( base.__dict__.get( '_class_mutables_', ( ) ) )
315 for base in class_.__mro__ ) )
318# pylint: disable=protected-access
319def _class__new__(
320 original: type,
321 decorators: ClassDecorators = ( ),
322 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
323 mutables: __.cabc.Collection[ str ] = ( ),
324) -> type:
325 # Some decorators create new classes, which invokes this method again.
326 # Short-circuit to prevent recursive decoration and other tangles.
327 class_decorators_ = original.__dict__.get( '_class_decorators_', [ ] )
328 if class_decorators_: return original
329 if not __.is_absent( docstring ): original.__doc__ = docstring
330 original._class_mutables_ = _accumulate_mutables( original, mutables )
331 original._class_decorators_ = class_decorators_
332 reproduction = original
333 for decorator in decorators:
334 class_decorators_.append( decorator )
335 reproduction = decorator( original )
336 if original is not reproduction:
337 __.repair_class_reproduction( original, reproduction )
338 original = reproduction
339 class_decorators_.clear( ) # Flag '__init__' to enable immutability
340 return reproduction
341# pylint: enable=protected-access
344# pylint: disable=protected-access
345def _class__init__( class_: type ) -> None:
346 # Some metaclasses add class attributes in '__init__' method.
347 # So, we wait until last possible moment to set immutability.
348 # Consult class attributes dictionary to ignore immutable base classes.
349 cdict = class_.__dict__
350 if cdict.get( '_class_decorators_' ): return
351 del class_._class_decorators_
352 if ( class_behaviors := cdict.get( '_class_behaviors_' ) ):
353 class_behaviors.add( _behavior )
354 else: class_._class_behaviors_ = { _behavior }
355# pylint: enable=protected-access
358def _class__delattr__( class_: type, name: str ) -> bool:
359 # Consult class attributes dictionary to ignore immutable base classes.
360 cdict = class_.__dict__
361 if name in cdict.get( '_class_mutables_', ( ) ): return False
362 if _behavior not in cdict.get( '_class_behaviors_', ( ) ): return False
363 from .exceptions import AttributeImmutabilityError
364 raise AttributeImmutabilityError( name )
367def _class__setattr__( class_: type, name: str ) -> bool:
368 # Consult class attributes dictionary to ignore immutable base classes.
369 cdict = class_.__dict__
370 if name in cdict.get( '_class_mutables_', ( ) ): return False
371 if _behavior not in cdict.get( '_class_behaviors_', ( ) ): return False
372 from .exceptions import AttributeImmutabilityError
373 raise AttributeImmutabilityError( name )