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

106 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-05 02:47 +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, new attributes may not be assigned to it and its 

26 existing attributes cannot be reassigned or deleted. 

27 

28 The implementation includes: 

29 

30 * ``Class``: Standard metaclass for immutable classes; derived from 

31 :py:class:`type`. 

32 * ``ABCFactory``: Metaclass for abstract base classes; derived from 

33 :py:class:`abc.ABCMeta`. 

34 * ``ProtocolClass``: Metaclass for protocol classes; derived from 

35 :py:class:`typing.Protocol`. 

36 

37 Additionally, metaclasses for dataclasses are provided as a convenience. 

38 

39 >>> from frigid import Class 

40 >>> class Example( metaclass = Class ): 

41 ... x = 1 

42 >>> Example.y = 2 # Attempt assignment 

43 Traceback (most recent call last): 

44 ... 

45 frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'y'. 

46 >>> Example.x = 3 # Attempt reassignment 

47 Traceback (most recent call last): 

48 ... 

49 frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'. 

50''' 

51# pylint: enable=line-too-long 

52 

53# TODO? Allow predicate functions and regex patterns as mutability checkers. 

54 

55 

56from __future__ import annotations 

57 

58from . import __ 

59 

60 

61ClassDecorators: __.typx.TypeAlias = ( 

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

63 

64 

65_behavior = 'immutability' 

66 

67 

68class Class( type ): 

69 ''' Immutable class factory. ''' 

70 

71 def __new__( # pylint: disable=too-many-arguments 

72 clscls: type[ Class ], 

73 name: str, 

74 bases: tuple[ type, ... ], 

75 namespace: dict[ str, __.typx.Any ], *, 

76 decorators: ClassDecorators = ( ), 

77 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent, 

78 mutables: __.cabc.Collection[ str ] = ( ), 

79 **args: __.typx.Any 

80 ) -> Class: 

81 class_ = type.__new__( clscls, name, bases, namespace, **args ) 

82 return _class__new__( 

83 class_, 

84 decorators = decorators, 

85 docstring = docstring, 

86 mutables = mutables ) 

87 

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

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

90 _class__init__( selfclass ) 

91 

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

93 if not _class__delattr__( selfclass, name ): 

94 super( ).__delattr__( name ) 

95 

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

97 if not _class__setattr__( selfclass, name ): 

98 super( ).__setattr__( name, value ) 

99 

100Class.__doc__ = __.generate_docstring( 

101 Class, 

102 'description of class factory class', 

103 'class attributes immutability' ) 

104 

105 

106@__.typx.dataclass_transform( kw_only_default = True ) 

107class Dataclass( Class ): 

108 ''' Immutable dataclass factory. ''' 

109 

110 def __new__( # pylint: disable=too-many-arguments 

111 clscls: type[ Dataclass ], 

112 name: str, 

113 bases: tuple[ type, ... ], 

114 namespace: dict[ str, __.typx.Any ], *, 

115 decorators: ClassDecorators = ( ), 

116 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent, 

117 mutables: __.cabc.Collection[ str ] = ( ), 

118 **args: __.typx.Any 

119 ) -> Dataclass: 

120 decorators_ = ( 

121 __.dcls.dataclass( kw_only = True, slots = True ), 

122 *decorators ) 

123 return Class.__new__( # pyright: ignore 

124 clscls, name, bases, namespace, 

125 decorators = decorators_, 

126 docstring = docstring, 

127 mutables = mutables, 

128 **args ) 

129 

130Dataclass.__doc__ = __.generate_docstring( 

131 Dataclass, 

132 'description of class factory class', 

133 'class attributes immutability' ) 

134 

135 

136@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) 

137class CompleteDataclass( Class ): 

138 ''' Immutable dataclass factory. 

139 

140 Dataclasses from this factory produce immutable instances. ''' 

141 def __new__( # pylint: disable=too-many-arguments 

142 clscls: type[ CompleteDataclass ], 

143 name: str, 

144 bases: tuple[ type, ... ], 

145 namespace: dict[ str, __.typx.Any ], *, 

146 decorators: ClassDecorators = ( ), 

147 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent, 

148 mutables: __.cabc.Collection[ str ] = ( ), 

149 **args: __.typx.Any 

150 ) -> CompleteDataclass: 

151 decorators_ = ( 

152 __.dcls.dataclass( frozen = True, kw_only = True, slots = True ), 

153 *decorators ) 

154 return Class.__new__( # pyright: ignore 

155 clscls, name, bases, namespace, 

156 decorators = decorators_, 

157 docstring = docstring, 

158 mutables = mutables, 

159 **args ) 

160 

161CompleteDataclass.__doc__ = __.generate_docstring( 

162 CompleteDataclass, 

163 'description of class factory class', 

164 'class attributes immutability' ) 

165 

166 

167class ABCFactory( __.abc.ABCMeta ): 

168 ''' Immutable abstract base class factory. ''' 

169 

170 def __new__( # pylint: disable=too-many-arguments 

171 clscls: type[ ABCFactory ], 

172 name: str, 

173 bases: tuple[ type, ... ], 

174 namespace: dict[ str, __.typx.Any ], *, 

175 decorators: ClassDecorators = ( ), 

176 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent, 

177 mutables: __.cabc.Collection[ str ] = ( ), 

178 **args: __.typx.Any 

179 ) -> ABCFactory: 

180 class_ = __.abc.ABCMeta.__new__( 

181 clscls, name, bases, namespace, **args ) 

182 return _class__new__( 

183 class_, decorators = decorators, 

184 docstring = docstring, 

185 mutables = mutables ) 

186 

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

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

189 _class__init__( selfclass ) 

190 

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

192 if not _class__delattr__( selfclass, name ): 

193 super( ).__delattr__( name ) 

194 

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

196 if not _class__setattr__( selfclass, name ): 

197 super( ).__setattr__( name, value ) 

198 

199ABCFactory.__doc__ = __.generate_docstring( 

200 ABCFactory, 

201 'description of class factory class', 

202 'class attributes immutability' ) 

203 

204 

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

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

207 ''' Immutable protocol class factory. ''' 

208 

209 def __new__( # pylint: disable=too-many-arguments 

210 clscls: type[ ProtocolClass ], 

211 name: str, 

212 bases: tuple[ type, ... ], 

213 namespace: dict[ str, __.typx.Any ], *, 

214 decorators: ClassDecorators = ( ), 

215 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent, 

216 mutables: __.cabc.Collection[ str ] = ( ), 

217 **args: __.typx.Any 

218 ) -> ProtocolClass: 

219 class_ = super( ProtocolClass, clscls ).__new__( # pylint: disable=too-many-function-args 

220 clscls, name, bases, namespace, **args ) 

221 return _class__new__( 

222 class_, 

223 decorators = decorators, 

224 docstring = docstring, 

225 mutables = mutables ) 

226 

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

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

229 _class__init__( selfclass ) 

230 

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

232 if not _class__delattr__( selfclass, name ): 

233 super( ).__delattr__( name ) 

234 

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

236 if not _class__setattr__( selfclass, name ): 

237 super( ).__setattr__( name, value ) 

238# pylint: enable=bad-classmethod-argument,no-self-argument 

239 

240ProtocolClass.__doc__ = __.generate_docstring( 

241 ProtocolClass, 

242 'description of class factory class', 

243 'class attributes immutability' ) 

244 

245 

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

247@__.typx.dataclass_transform( kw_only_default = True ) 

248class ProtocolDataclass( ProtocolClass ): 

249 ''' Immutable protocol dataclass factory. ''' 

250 def __new__( # pylint: disable=too-many-arguments 

251 clscls: type[ ProtocolDataclass ], 

252 name: str, 

253 bases: tuple[ type, ... ], 

254 namespace: dict[ str, __.typx.Any ], *, 

255 decorators: ClassDecorators = ( ), 

256 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent, 

257 mutables: __.cabc.Collection[ str ] = ( ), 

258 **args: __.typx.Any 

259 ) -> ProtocolDataclass: 

260 decorators_ = ( 

261 __.dcls.dataclass( kw_only = True, slots = True ), 

262 *decorators ) 

263 return ProtocolClass.__new__( # pyright: ignore 

264 clscls, name, bases, namespace, 

265 decorators = decorators_, 

266 docstring = docstring, 

267 mutables = mutables, 

268 **args ) 

269# pylint: enable=bad-classmethod-argument,no-self-argument 

270 

271ProtocolDataclass.__doc__ = __.generate_docstring( 

272 ProtocolDataclass, 

273 'description of class factory class', 

274 'class attributes immutability' ) 

275 

276 

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

278@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) 

279class CompleteProtocolDataclass( ProtocolClass ): 

280 ''' Immutable protocol dataclass factory. 

281 

282 Dataclasses from this factory produce immutable instances. ''' 

283 def __new__( # pylint: disable=too-many-arguments 

284 clscls: type[ CompleteProtocolDataclass ], 

285 name: str, 

286 bases: tuple[ type, ... ], 

287 namespace: dict[ str, __.typx.Any ], *, 

288 decorators: ClassDecorators = ( ), 

289 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent, 

290 mutables: __.cabc.Collection[ str ] = ( ), 

291 **args: __.typx.Any 

292 ) -> CompleteProtocolDataclass: 

293 decorators_ = ( 

294 __.dcls.dataclass( frozen = True, kw_only = True, slots = True ), 

295 *decorators ) 

296 return ProtocolClass.__new__( # pyright: ignore 

297 clscls, name, bases, namespace, 

298 decorators = decorators_, 

299 docstring = docstring, 

300 mutables = mutables, 

301 **args ) 

302# pylint: enable=bad-classmethod-argument,no-self-argument 

303 

304CompleteProtocolDataclass.__doc__ = __.generate_docstring( 

305 CompleteProtocolDataclass, 

306 'description of class factory class', 

307 'class attributes immutability' ) 

308 

309 

310def _accumulate_mutables( 

311 class_: type, mutables: __.cabc.Collection[ str ] 

312) -> frozenset[ str ]: 

313 return frozenset( mutables ).union( *( 

314 frozenset( base.__dict__.get( '_class_mutables_', ( ) ) ) 

315 for base in class_.__mro__ ) ) 

316 

317 

318# pylint: disable=protected-access 

319def _class__new__( 

320 original: type, 

321 decorators: ClassDecorators = ( ), 

322 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent, 

323 mutables: __.cabc.Collection[ str ] = ( ), 

324) -> type: 

325 # Some decorators create new classes, which invokes this method again. 

326 # Short-circuit to prevent recursive decoration and other tangles. 

327 class_decorators_ = original.__dict__.get( '_class_decorators_', [ ] ) 

328 if class_decorators_: return original 

329 if not __.is_absent( docstring ): original.__doc__ = docstring 

330 original._class_mutables_ = _accumulate_mutables( original, mutables ) 

331 original._class_decorators_ = class_decorators_ 

332 reproduction = original 

333 for decorator in decorators: 

334 class_decorators_.append( decorator ) 

335 reproduction = decorator( original ) 

336 if original is not reproduction: 

337 __.repair_class_reproduction( original, reproduction ) 

338 original = reproduction 

339 class_decorators_.clear( ) # Flag '__init__' to enable immutability 

340 return reproduction 

341# pylint: enable=protected-access 

342 

343 

344# pylint: disable=protected-access 

345def _class__init__( class_: type ) -> None: 

346 # Some metaclasses add class attributes in '__init__' method. 

347 # So, we wait until last possible moment to set immutability. 

348 # Consult class attributes dictionary to ignore immutable base classes. 

349 cdict = class_.__dict__ 

350 if cdict.get( '_class_decorators_' ): return 

351 del class_._class_decorators_ 

352 if ( class_behaviors := cdict.get( '_class_behaviors_' ) ): 

353 class_behaviors.add( _behavior ) 

354 else: class_._class_behaviors_ = { _behavior } 

355# pylint: enable=protected-access 

356 

357 

358def _class__delattr__( class_: type, name: str ) -> bool: 

359 # Consult class attributes dictionary to ignore immutable base classes. 

360 cdict = class_.__dict__ 

361 if name in cdict.get( '_class_mutables_', ( ) ): return False 

362 if _behavior not in cdict.get( '_class_behaviors_', ( ) ): return False 

363 from .exceptions import AttributeImmutabilityError 

364 raise AttributeImmutabilityError( name ) 

365 

366 

367def _class__setattr__( class_: type, name: str ) -> bool: 

368 # Consult class attributes dictionary to ignore immutable base classes. 

369 cdict = class_.__dict__ 

370 if name in cdict.get( '_class_mutables_', ( ) ): return False 

371 if _behavior not in cdict.get( '_class_behaviors_', ( ) ): return False 

372 from .exceptions import AttributeImmutabilityError 

373 raise AttributeImmutabilityError( name )