Coverage for sources/frigid/classes.py: 100%
76 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-05 03:26 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-05 03:26 +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
57from __future__ import annotations
59from . import __
62ClassDecorators: __.a.TypeAlias = (
63 __.cabc.Iterable[ __.cabc.Callable[ [ type ], type ] ] )
66_behavior = 'immutability'
69class Class( type ):
70 ''' Immutable class factory. '''
72 def __new__( # pylint: disable=too-many-arguments
73 factory: type[ type ],
74 name: str,
75 bases: tuple[ type, ... ],
76 namespace: dict[ str, __.a.Any ], *,
77 decorators: ClassDecorators = ( ),
78 docstring: __.Optional[ __.a.Nullable[ str ] ] = __.absent,
79 **args: __.a.Any
80 ) -> Class:
81 class_ = type.__new__(
82 factory, name, bases, namespace, **args )
83 return _class__new__( # type: ignore
84 class_, decorators = decorators, docstring = docstring )
86 def __init__( selfclass, *posargs: __.a.Any, **nomargs: __.a.Any ):
87 super( ).__init__( *posargs, **nomargs )
88 _class__init__( selfclass )
90 def __delattr__( selfclass, name: str ) -> None:
91 if not _class__delattr__( selfclass, name ):
92 super( ).__delattr__( name )
94 def __setattr__( selfclass, name: str, value: __.a.Any ) -> None:
95 if not _class__setattr__( selfclass, name ):
96 super( ).__setattr__( name, value )
98Class.__doc__ = __.generate_docstring(
99 Class,
100 'description of class factory class',
101 'class attributes immutability'
102)
105class ABCFactory( __.ABCFactory ): # type: ignore
106 ''' Immutable abstract base class factory. '''
108 def __new__( # pylint: disable=too-many-arguments
109 factory: type[ type ],
110 name: str,
111 bases: tuple[ type, ... ],
112 namespace: dict[ str, __.a.Any ], *,
113 decorators: ClassDecorators = ( ),
114 docstring: __.Optional[ __.a.Nullable[ str ] ] = __.absent,
115 **args: __.a.Any
116 ) -> ABCFactory:
117 class_ = __.ABCFactory.__new__(
118 factory, name, bases, namespace, **args )
119 return _class__new__( # type: ignore
120 class_, decorators = decorators, docstring = docstring )
122 def __init__( selfclass, *posargs: __.a.Any, **nomargs: __.a.Any ):
123 super( ).__init__( *posargs, **nomargs )
124 _class__init__( selfclass )
126 def __delattr__( selfclass, name: str ) -> None:
127 if not _class__delattr__( selfclass, name ):
128 super( ).__delattr__( name )
130 def __setattr__( selfclass, name: str, value: __.a.Any ) -> None:
131 if not _class__setattr__( selfclass, name ):
132 super( ).__setattr__( name, value )
134ABCFactory.__doc__ = __.generate_docstring(
135 ABCFactory,
136 'description of class factory class',
137 'class attributes immutability'
138)
141# pylint: disable=bad-classmethod-argument,no-self-argument
142class ProtocolClass( type( __.a.Protocol ) ):
143 ''' Immutable protocol class factory. '''
145 def __new__( # pylint: disable=too-many-arguments
146 factory: type[ type ],
147 name: str,
148 bases: tuple[ type, ... ],
149 namespace: dict[ str, __.a.Any ], *,
150 decorators: ClassDecorators = ( ),
151 docstring: __.Optional[ __.a.Nullable[ str ] ] = __.absent,
152 **args: __.a.Any
153 ) -> ProtocolClass:
154 class_ = __.a.Protocol.__class__.__new__( # type: ignore
155 factory, name, bases, namespace, **args ) # type: ignore
156 return _class__new__(
157 class_, # type: ignore
158 decorators = decorators, docstring = docstring )
160 def __init__( selfclass, *posargs: __.a.Any, **nomargs: __.a.Any ):
161 super( ).__init__( *posargs, **nomargs )
162 _class__init__( selfclass )
164 def __delattr__( selfclass, name: str ) -> None:
165 if not _class__delattr__( selfclass, name ):
166 super( ).__delattr__( name )
168 def __setattr__( selfclass, name: str, value: __.a.Any ) -> None:
169 if not _class__setattr__( selfclass, name ):
170 super( ).__setattr__( name, value )
171# pylint: enable=bad-classmethod-argument,no-self-argument
173ProtocolClass.__doc__ = __.generate_docstring(
174 ProtocolClass,
175 'description of class factory class',
176 'class attributes immutability'
177)
180def _class__new__(
181 original: type,
182 decorators: ClassDecorators = ( ),
183 docstring: __.Optional[ __.a.Nullable[ str ] ] = __.absent,
184) -> type:
185 # Handle decorators similar to accretive implementation.
186 # Some decorators create new classes, which invokes this method again.
187 # Short-circuit to prevent recursive decoration and other tangles.
188 class_decorators_ = original.__dict__.get( '_class_decorators_', [ ] )
189 if class_decorators_: return original
190 if not __.is_absent( docstring ): original.__doc__ = docstring
191 setattr( original, '_class_decorators_', class_decorators_ )
192 reproduction = original
193 for decorator in decorators:
194 class_decorators_.append( decorator )
195 reproduction = decorator( original )
196 if original is not reproduction:
197 __.repair_class_reproduction( original, reproduction )
198 original = reproduction
199 class_decorators_.clear( ) # Flag '__init__' to enable immutability
200 return reproduction
203def _class__init__( class_: type ) -> None:
204 # Some metaclasses add class attributes in '__init__' method.
205 # So, we wait until last possible moment to set immutability.
206 if class_.__dict__.get( '_class_decorators_' ): return
207 del class_._class_decorators_
208 if ( class_behaviors := class_.__dict__.get( '_class_behaviors_' ) ):
209 class_behaviors.add( _behavior )
210 else: setattr( class_, '_class_behaviors_', { _behavior } )
213def _class__delattr__( class_: type, name: str ) -> bool:
214 # Consult class attributes dictionary to ignore immutable base classes.
215 if _behavior not in class_.__dict__.get( '_class_behaviors_', ( ) ):
216 return False
217 from .exceptions import AttributeImmutabilityError
218 raise AttributeImmutabilityError( name )
221def _class__setattr__( class_: type, name: str ) -> bool:
222 # Consult class attributes dictionary to ignore immutable base classes.
223 if _behavior not in class_.__dict__.get( '_class_behaviors_', ( ) ):
224 return False
225 from .exceptions import AttributeImmutabilityError
226 raise AttributeImmutabilityError( name )