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

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# TODO? Allow predicate functions and regex patterns as mutability checkers. 

57 

58 

59from __future__ import annotations 

60 

61from . import __ 

62 

63 

64ClassDecorators: __.typx.TypeAlias = ( 

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

66 

67 

68_behavior = 'immutability' 

69 

70 

71class Class( type ): 

72 ''' Immutable class factory. ''' 

73 

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 ) 

90 

91 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ): 

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

93 _class__init__( selfclass ) 

94 

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

96 if not _class__delattr__( selfclass, name ): 

97 super( ).__delattr__( name ) 

98 

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

100 if not _class__setattr__( selfclass, name ): 

101 super( ).__setattr__( name, value ) 

102 

103Class.__doc__ = __.generate_docstring( 

104 Class, 

105 'description of class factory class', 

106 'class attributes immutability' ) 

107 

108 

109class ABCFactory( __.abc.ABCMeta ): 

110 ''' Immutable abstract base class factory. ''' 

111 

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 ) 

128 

129 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ): 

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

131 _class__init__( selfclass ) 

132 

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

134 if not _class__delattr__( selfclass, name ): 

135 super( ).__delattr__( name ) 

136 

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

138 if not _class__setattr__( selfclass, name ): 

139 super( ).__setattr__( name, value ) 

140 

141ABCFactory.__doc__ = __.generate_docstring( 

142 ABCFactory, 

143 'description of class factory class', 

144 'class attributes immutability' ) 

145 

146 

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

148class ProtocolClass( type( __.typx.Protocol ) ): 

149 ''' Immutable protocol class factory. ''' 

150 

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 ) 

168 

169 def __init__( selfclass, *posargs: __.typx.Any, **nomargs: __.typx.Any ): 

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

171 _class__init__( selfclass ) 

172 

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

174 if not _class__delattr__( selfclass, name ): 

175 super( ).__delattr__( name ) 

176 

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 

181 

182ProtocolClass.__doc__ = __.generate_docstring( 

183 ProtocolClass, 

184 'description of class factory class', 

185 'class attributes immutability' ) 

186 

187 

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

194 

195 

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 

220 

221 

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 

234 

235 

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 ) 

243 

244 

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 )