Coverage for sources/frigid/classes.py: 100%

76 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2024-12-05 03:04 +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''' Immutable classes. 

23 

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

25 class is initialized, its attributes cannot be reassigned or deleted. 

26 

27 The implementation includes: 

28 

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

35 

36 These 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 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 

55 

56 

57from __future__ import annotations 

58 

59from . import __ 

60 

61 

62ClassDecorators: __.a.TypeAlias = ( 

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

64 

65 

66_behavior = 'immutability' 

67 

68 

69class Class( type ): 

70 ''' Immutable class factory. ''' 

71 

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 ) 

85 

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

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

88 _class__init__( selfclass ) 

89 

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

91 if not _class__delattr__( selfclass, name ): 

92 super( ).__delattr__( name ) 

93 

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

95 if not _class__setattr__( selfclass, name ): 

96 super( ).__setattr__( name, value ) 

97 

98Class.__doc__ = __.generate_docstring( 

99 Class, 

100 'description of class factory class', 

101 'class attributes immutability' 

102) 

103 

104 

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

106 ''' Immutable abstract base class factory. ''' 

107 

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 ) 

121 

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

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

124 _class__init__( selfclass ) 

125 

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

127 if not _class__delattr__( selfclass, name ): 

128 super( ).__delattr__( name ) 

129 

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

131 if not _class__setattr__( selfclass, name ): 

132 super( ).__setattr__( name, value ) 

133 

134ABCFactory.__doc__ = __.generate_docstring( 

135 ABCFactory, 

136 'description of class factory class', 

137 'class attributes immutability' 

138) 

139 

140 

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

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

143 ''' Immutable protocol class factory. ''' 

144 

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 ) 

159 

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

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

162 _class__init__( selfclass ) 

163 

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

165 if not _class__delattr__( selfclass, name ): 

166 super( ).__delattr__( name ) 

167 

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 

172 

173ProtocolClass.__doc__ = __.generate_docstring( 

174 ProtocolClass, 

175 'description of class factory class', 

176 'class attributes immutability' 

177) 

178 

179 

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 

201 

202 

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

211 

212 

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 ) 

219 

220 

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 )