Coverage for sources/frigid/classes.py: 100%
82 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-24 04:09 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-24 04:09 +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, its attributes cannot be reassigned or deleted.
27 The implementation includes:
29 * ``Class``: Standard metaclass for immutable 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 frigid import Class
43 >>> class Example( metaclass = Class ):
44 ... x = 1
45 >>> Example.y = 2 # Attempt assignment
46 Traceback (most recent call last):
47 ...
48 frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'y'.
49 >>> Example.x = 3 # Attempt reassignment
50 Traceback (most recent call last):
51 ...
52 frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'.
53'''
54# pylint: enable=line-too-long
56# TODO? Allow predicate functions and regex patterns as mutability checkers.
59from __future__ import annotations
61from . import __
64ClassDecorators: __.typx.TypeAlias = (
65 __.cabc.Iterable[ __.cabc.Callable[ [ type ], type ] ] )
68_behavior = 'immutability'
71class Class( type ):
72 ''' Immutable class factory. '''
74 def __new__( # pylint: disable=too-many-arguments
75 clscls: type[ Class ],
76 name: str,
77 bases: tuple[ type, ... ],
78 namespace: dict[ str, __.typx.Any ], *,
79 decorators: ClassDecorators = ( ),
80 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
81 mutables: __.cabc.Collection[ str ] = ( ),
82 **args: __.typx.Any
83 ) -> Class:
84 class_ = type.__new__( clscls, name, bases, namespace, **args )
85 return _class__new__(
86 class_,
87 decorators = decorators,
88 docstring = docstring,
89 mutables = mutables )
91 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
92 super( ).__init__( *posargs, **nomargs )
93 _class__init__( selfclass )
95 def __delattr__( selfclass, name: str ) -> None:
96 if not _class__delattr__( selfclass, name ):
97 super( ).__delattr__( name )
99 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
100 if not _class__setattr__( selfclass, name ):
101 super( ).__setattr__( name, value )
103Class.__doc__ = __.generate_docstring(
104 Class,
105 'description of class factory class',
106 'class attributes immutability' )
109class ABCFactory( __.abc.ABCMeta ):
110 ''' Immutable abstract base class factory. '''
112 def __new__( # pylint: disable=too-many-arguments
113 clscls: type[ ABCFactory ],
114 name: str,
115 bases: tuple[ type, ... ],
116 namespace: dict[ str, __.typx.Any ], *,
117 decorators: ClassDecorators = ( ),
118 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
119 mutables: __.cabc.Collection[ str ] = ( ),
120 **args: __.typx.Any
121 ) -> ABCFactory:
122 class_ = __.abc.ABCMeta.__new__(
123 clscls, name, bases, namespace, **args )
124 return _class__new__(
125 class_, decorators = decorators,
126 docstring = docstring,
127 mutables = mutables )
129 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
130 super( ).__init__( *posargs, **nomargs )
131 _class__init__( selfclass )
133 def __delattr__( selfclass, name: str ) -> None:
134 if not _class__delattr__( selfclass, name ):
135 super( ).__delattr__( name )
137 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
138 if not _class__setattr__( selfclass, name ):
139 super( ).__setattr__( name, value )
141ABCFactory.__doc__ = __.generate_docstring(
142 ABCFactory,
143 'description of class factory class',
144 'class attributes immutability' )
147# pylint: disable=bad-classmethod-argument,no-self-argument
148class ProtocolClass( type( __.typx.Protocol ) ):
149 ''' Immutable protocol class factory. '''
151 def __new__( # pylint: disable=too-many-arguments
152 clscls: type[ ProtocolClass ],
153 name: str,
154 bases: tuple[ type, ... ],
155 namespace: dict[ str, __.typx.Any ], *,
156 decorators: ClassDecorators = ( ),
157 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
158 mutables: __.cabc.Collection[ str ] = ( ),
159 **args: __.typx.Any
160 ) -> ProtocolClass:
161 class_ = super( ProtocolClass, clscls ).__new__( # pylint: disable=too-many-function-args
162 clscls, name, bases, namespace, **args )
163 return _class__new__(
164 class_,
165 decorators = decorators,
166 docstring = docstring,
167 mutables = mutables )
169 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ):
170 super( ).__init__( *posargs, **nomargs )
171 _class__init__( selfclass )
173 def __delattr__( selfclass, name: str ) -> None:
174 if not _class__delattr__( selfclass, name ):
175 super( ).__delattr__( name )
177 def __setattr__( selfclass, name: str, value: __.typx.Any ) -> None:
178 if not _class__setattr__( selfclass, name ):
179 super( ).__setattr__( name, value )
180# pylint: enable=bad-classmethod-argument,no-self-argument
182ProtocolClass.__doc__ = __.generate_docstring(
183 ProtocolClass,
184 'description of class factory class',
185 'class attributes immutability' )
188def _accumulate_mutables(
189 class_: type, mutables: __.cabc.Collection[ str ]
190) -> frozenset[ str ]:
191 return frozenset( mutables ).union( *(
192 frozenset( base.__dict__.get( '_class_mutables_', ( ) ) )
193 for base in class_.__mro__ ) )
196# pylint: disable=protected-access
197def _class__new__(
198 original: type,
199 decorators: ClassDecorators = ( ),
200 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
201 mutables: __.cabc.Collection[ str ] = ( ),
202) -> type:
203 # Some decorators create new classes, which invokes this method again.
204 # Short-circuit to prevent recursive decoration and other tangles.
205 class_decorators_ = original.__dict__.get( '_class_decorators_', [ ] )
206 if class_decorators_: return original
207 if not __.is_absent( docstring ): original.__doc__ = docstring
208 original._class_mutables_ = _accumulate_mutables( original, mutables )
209 original._class_decorators_ = class_decorators_
210 reproduction = original
211 for decorator in decorators:
212 class_decorators_.append( decorator )
213 reproduction = decorator( original )
214 if original is not reproduction:
215 __.repair_class_reproduction( original, reproduction )
216 original = reproduction
217 class_decorators_.clear( ) # Flag '__init__' to enable immutability
218 return reproduction
219# pylint: enable=protected-access
222# pylint: disable=protected-access
223def _class__init__( class_: type ) -> None:
224 # Some metaclasses add class attributes in '__init__' method.
225 # So, we wait until last possible moment to set immutability.
226 # Consult class attributes dictionary to ignore immutable base classes.
227 cdict = class_.__dict__
228 if cdict.get( '_class_decorators_' ): return
229 del class_._class_decorators_
230 if ( class_behaviors := cdict.get( '_class_behaviors_' ) ):
231 class_behaviors.add( _behavior )
232 else: class_._class_behaviors_ = { _behavior }
233# pylint: enable=protected-access
236def _class__delattr__( class_: type, name: str ) -> bool:
237 # Consult class attributes dictionary to ignore immutable base classes.
238 cdict = class_.__dict__
239 if name in cdict.get( '_class_mutables_', ( ) ): return False
240 if _behavior not in cdict.get( '_class_behaviors_', ( ) ): return False
241 from .exceptions import AttributeImmutabilityError
242 raise AttributeImmutabilityError( name )
245def _class__setattr__( class_: type, name: str ) -> bool:
246 # Consult class attributes dictionary to ignore immutable base classes.
247 cdict = class_.__dict__
248 if name in cdict.get( '_class_mutables_', ( ) ): return False
249 if _behavior not in cdict.get( '_class_behaviors_', ( ) ): return False
250 from .exceptions import AttributeImmutabilityError
251 raise AttributeImmutabilityError( name )