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

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

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#============================================================================# 

19 

20 

21# pylint: disable=line-too-long 

22''' Accretive classes. 

23 

24Provides metaclasses for creating classes with accretive attributes. Once a 

25class attribute is set, it cannot be reassigned or deleted. 

26 

27The implementation includes: 

28 

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`. 

35 

36These metaclasses are particularly useful for: 

37 

38* Creating classes with constant class attributes 

39* Defining stable abstract base classes 

40* Building protocol classes with fixed interfaces 

41 

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 

52 

53 

54from __future__ import annotations 

55 

56from . import __ 

57 

58 

59ClassDecorators: __.a.TypeAlias = ( 

60 __.cabc.Iterable[ __.cabc.Callable[ [ type ], type ] ] ) 

61 

62 

63_behavior = 'accretion' 

64 

65 

66class Class( type ): 

67 ''' Accretive class factory. ''' 

68 

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 ) 

82 

83 def __init__( selfclass, *posargs: __.a.Any, **nomargs: __.a.Any ): 

84 super( ).__init__( *posargs, **nomargs ) 

85 _class__init__( selfclass ) 

86 

87 def __delattr__( selfclass, name: str ) -> None: 

88 if not _class__delattr__( selfclass, name ): 

89 super( ).__delattr__( name ) 

90 

91 def __setattr__( selfclass, name: str, value: __.a.Any ) -> None: 

92 if not _class__setattr__( selfclass, name ): 

93 super( ).__setattr__( name, value ) 

94 

95Class.__doc__ = __.generate_docstring( 

96 Class, 

97 'description of class factory class', 

98 'class attributes accretion' 

99) 

100 

101 

102class ABCFactory( __.ABCFactory ): # type: ignore 

103 ''' Accretive abstract base class factory. ''' 

104 

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 ) 

118 

119 def __init__( selfclass, *posargs: __.a.Any, **nomargs: __.a.Any ): 

120 super( ).__init__( *posargs, **nomargs ) 

121 _class__init__( selfclass ) 

122 

123 def __delattr__( selfclass, name: str ) -> None: 

124 if not _class__delattr__( selfclass, name ): 

125 super( ).__delattr__( name ) 

126 

127 def __setattr__( selfclass, name: str, value: __.a.Any ) -> None: 

128 if not _class__setattr__( selfclass, name ): 

129 super( ).__setattr__( name, value ) 

130 

131ABCFactory.__doc__ = __.generate_docstring( 

132 ABCFactory, 

133 'description of class factory class', 

134 'class attributes accretion' 

135) 

136 

137 

138# pylint: disable=bad-classmethod-argument,no-self-argument 

139class ProtocolClass( type( __.a.Protocol ) ): 

140 ''' Accretive protocol class factory. ''' 

141 

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 ) 

156 

157 def __init__( selfclass, *posargs: __.a.Any, **nomargs: __.a.Any ): 

158 super( ).__init__( *posargs, **nomargs ) 

159 _class__init__( selfclass ) 

160 

161 def __delattr__( selfclass, name: str ) -> None: 

162 if not _class__delattr__( selfclass, name ): 

163 super( ).__delattr__( name ) 

164 

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 

169 

170ProtocolClass.__doc__ = __.generate_docstring( 

171 ProtocolClass, 

172 'description of class factory class', 

173 'class attributes accretion' 

174) 

175 

176 

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 

198 

199 

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 } ) 

208 

209 

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 ) 

216 

217 

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