Coverage for sources/accretive/classes.py: 100%
78 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-20 02:16 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-20 02:16 +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.
24Provides metaclasses for creating classes with accretive attributes. Once a
25class attribute is set, it cannot be reassigned or deleted.
27The 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`.
36These 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
47Traceback (most recent call last):
48 ...
49accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete attribute 'x'.
50'''
51# pylint: enable=line-too-long
54from __future__ import annotations
56from . import __
59ClassDecorators: __.a.TypeAlias = (
60 __.cabc.Iterable[ __.cabc.Callable[ [ type ], type ] ] )
63_behavior = 'accretion'
66class Class( type ):
67 ''' Accretive class factory. '''
69 def __new__( # pylint: disable=too-many-arguments
70 factory: type[ type ],
71 name: str,
72 bases: tuple[ type, ... ],
73 namespace: dict[ str, __.a.Any ], *,
74 decorators: ClassDecorators = ( ),
75 docstring: __.Optional[ __.a.Nullable[ str ] ] = __.absent,
76 **args: __.a.Any
77 ) -> Class:
78 class_ = type.__new__(
79 factory, name, bases, namespace, **args )
80 return _class__new__( # type: ignore
81 class_, decorators = decorators, docstring = docstring )
83 def __init__( selfclass, *posargs: __.a.Any, **nomargs: __.a.Any ):
84 super( ).__init__( *posargs, **nomargs )
85 _class__init__( selfclass )
87 def __delattr__( selfclass, name: str ) -> None:
88 if not _class__delattr__( selfclass, name ):
89 super( ).__delattr__( name )
91 def __setattr__( selfclass, name: str, value: __.a.Any ) -> None:
92 if not _class__setattr__( selfclass, name ):
93 super( ).__setattr__( name, value )
95Class.__doc__ = __.generate_docstring(
96 Class,
97 'description of class factory class',
98 'class attributes accretion'
99)
102class ABCFactory( __.ABCFactory ): # type: ignore
103 ''' Accretive abstract base class factory. '''
105 def __new__( # pylint: disable=too-many-arguments
106 factory: type[ type ],
107 name: str,
108 bases: tuple[ type, ... ],
109 namespace: dict[ str, __.a.Any ], *,
110 decorators: ClassDecorators = ( ),
111 docstring: __.Optional[ __.a.Nullable[ str ] ] = __.absent,
112 **args: __.a.Any
113 ) -> ABCFactory:
114 class_ = __.ABCFactory.__new__(
115 factory, name, bases, namespace, **args )
116 return _class__new__( # type: ignore
117 class_, decorators = decorators, docstring = docstring )
119 def __init__( selfclass, *posargs: __.a.Any, **nomargs: __.a.Any ):
120 super( ).__init__( *posargs, **nomargs )
121 _class__init__( selfclass )
123 def __delattr__( selfclass, name: str ) -> None:
124 if not _class__delattr__( selfclass, name ):
125 super( ).__delattr__( name )
127 def __setattr__( selfclass, name: str, value: __.a.Any ) -> None:
128 if not _class__setattr__( selfclass, name ):
129 super( ).__setattr__( name, value )
131ABCFactory.__doc__ = __.generate_docstring(
132 ABCFactory,
133 'description of class factory class',
134 'class attributes accretion'
135)
138# pylint: disable=bad-classmethod-argument,no-self-argument
139class ProtocolClass( type( __.a.Protocol ) ):
140 ''' Accretive protocol class factory. '''
142 def __new__( # pylint: disable=too-many-arguments
143 factory: type[ type ],
144 name: str,
145 bases: tuple[ type, ... ],
146 namespace: dict[ str, __.a.Any ], *,
147 decorators: ClassDecorators = ( ),
148 docstring: __.Optional[ __.a.Nullable[ str ] ] = __.absent,
149 **args: __.a.Any
150 ) -> ProtocolClass:
151 class_ = __.a.Protocol.__class__.__new__( # type: ignore
152 factory, name, bases, namespace, **args ) # type: ignore
153 return _class__new__(
154 class_, # type: ignore
155 decorators = decorators, docstring = docstring )
157 def __init__( selfclass, *posargs: __.a.Any, **nomargs: __.a.Any ):
158 super( ).__init__( *posargs, **nomargs )
159 _class__init__( selfclass )
161 def __delattr__( selfclass, name: str ) -> None:
162 if not _class__delattr__( selfclass, name ):
163 super( ).__delattr__( name )
165 def __setattr__( selfclass, name: str, value: __.a.Any ) -> None:
166 if not _class__setattr__( selfclass, name ):
167 super( ).__setattr__( name, value )
168# pylint: enable=bad-classmethod-argument,no-self-argument
170ProtocolClass.__doc__ = __.generate_docstring(
171 ProtocolClass,
172 'description of class factory class',
173 'class attributes accretion'
174)
177def _class__new__(
178 original: type,
179 decorators: ClassDecorators = ( ),
180 docstring: __.Optional[ __.a.Nullable[ str ] ] = __.absent,
181) -> type:
182 # Handle decorators similar to immutable implementation.
183 # Some decorators create new classes, which invokes this method again.
184 # Short-circuit to prevent recursive decoration and other tangles.
185 class_decorators_ = original.__dict__.get( '_class_decorators_', [ ] )
186 if class_decorators_: return original
187 if not __.is_absent( docstring ): original.__doc__ = docstring
188 setattr( original, '_class_decorators_', class_decorators_ )
189 reproduction = original
190 for decorator in decorators:
191 class_decorators_.append( decorator )
192 reproduction = decorator( original )
193 if original is not reproduction:
194 __.repair_class_reproduction( original, reproduction )
195 original = reproduction
196 class_decorators_.clear( ) # Flag '__init__' to enable accretion
197 return reproduction
200def _class__init__( class_: type ) -> None:
201 # Some metaclasses add class attributes in '__init__' method.
202 # So, we wait until last possible moment to set accretion.
203 if class_.__dict__.get( '_class_decorators_' ): return
204 del class_._class_decorators_
205 if ( class_behaviors := class_.__dict__.get( '_class_behaviors_' ) ):
206 class_behaviors.add( _behavior )
207 else: setattr( class_, '_class_behaviors_', { _behavior } )
210def _class__delattr__( class_: type, name: str ) -> bool:
211 # Consult class attributes dictionary to ignore accretive base classes.
212 if _behavior not in class_.__dict__.get( '_class_behaviors_', ( ) ):
213 return False
214 from .exceptions import AttributeImmutabilityError
215 raise AttributeImmutabilityError( name )
218def _class__setattr__( class_: type, name: str ) -> bool:
219 # Consult class attributes dictionary to ignore accretive base classes.
220 if _behavior not in class_.__dict__.get( '_class_behaviors_', ( ) ):
221 return False
222 if hasattr( class_, name ):
223 from .exceptions import AttributeImmutabilityError
224 raise AttributeImmutabilityError( name )
225 return False # Allow setting new attributes