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

108 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-01 20: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''' Accretive classes. 

23 

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

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

26 

27 The 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 

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

47 Traceback (most recent call last): 

48 ... 

49 accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete attribute 'x'. 

50 

51 For cases where some attributes need to remain mutable, use the ``mutables`` parameter: 

52 

53 >>> class Config( metaclass = Class, mutables = ( 'version', ) ): 

54 ... name = 'MyApp' 

55 ... version = '1.0.0' 

56 >>> Config.version = '1.0.1' # Can modify designated mutable attributes 

57 >>> Config.version 

58 '1.0.1' 

59 >>> Config.name = 'NewApp' # Other attributes remain immutable 

60 Traceback (most recent call last): 

61 ... 

62 accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete attribute 'name'. 

63''' 

64# pylint: enable=line-too-long 

65 

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

67 

68 

69from __future__ import annotations 

70 

71from . import __ 

72 

73 

74ClassDecorators: __.typx.TypeAlias = ( 

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

76 

77 

78_behavior = 'accretion' 

79 

80 

81class Class( type ): 

82 ''' Accretive class factory. ''' 

83 

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

85 clscls: type[ Class ], 

86 name: str, 

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

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

89 decorators: ClassDecorators = ( ), 

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

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

92 **args: __.typx.Any 

93 ) -> Class: 

94 class_ = type.__new__( 

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

96 return _class__new__( 

97 class_, 

98 decorators = decorators, 

99 docstring = docstring, 

100 mutables = mutables ) 

101 

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

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

104 _class__init__( selfclass ) 

105 

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

107 if not _class__delattr__( selfclass, name ): 

108 super( ).__delattr__( name ) 

109 

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

111 if not _class__setattr__( selfclass, name ): 

112 super( ).__setattr__( name, value ) 

113 

114Class.__doc__ = __.generate_docstring( 

115 Class, 

116 'description of class factory class', 

117 'class attributes accretion' ) 

118 

119 

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

121class Dataclass( Class ): 

122 ''' Accretive dataclass factory. ''' 

123 

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

125 clscls: type[ Dataclass ], 

126 name: str, 

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

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

129 decorators: ClassDecorators = ( ), 

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

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

132 **args: __.typx.Any 

133 ) -> Dataclass: 

134 decorators_ = ( 

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

136 *decorators ) 

137 return Class.__new__( # pyright: ignore 

138 clscls, name, bases, namespace, 

139 decorators = decorators_, 

140 docstring = docstring, 

141 mutables = mutables, 

142 **args ) 

143 

144Dataclass.__doc__ = __.generate_docstring( 

145 Dataclass, 

146 'description of class factory class', 

147 'class attributes accretion' ) 

148 

149 

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

151class CompleteDataclass( Class ): 

152 ''' Accretive dataclass factory. 

153 

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

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

156 clscls: type[ CompleteDataclass ], 

157 name: str, 

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

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

160 decorators: ClassDecorators = ( ), 

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

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

163 **args: __.typx.Any 

164 ) -> CompleteDataclass: 

165 decorators_ = ( 

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

167 *decorators ) 

168 return Class.__new__( # pyright: ignore 

169 clscls, name, bases, namespace, 

170 decorators = decorators_, 

171 docstring = docstring, 

172 mutables = mutables, 

173 **args ) 

174 

175CompleteDataclass.__doc__ = __.generate_docstring( 

176 CompleteDataclass, 

177 'description of class factory class', 

178 'class attributes accretion' ) 

179 

180 

181class ABCFactory( __.abc.ABCMeta ): 

182 ''' Accretive abstract base class factory. ''' 

183 

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

185 clscls: type[ ABCFactory ], 

186 name: str, 

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

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

189 decorators: ClassDecorators = ( ), 

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

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

192 **args: __.typx.Any 

193 ) -> ABCFactory: 

194 class_ = __.abc.ABCMeta.__new__( 

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

196 return _class__new__( 

197 class_, 

198 decorators = decorators, 

199 docstring = docstring, 

200 mutables = mutables ) 

201 

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

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

204 _class__init__( selfclass ) 

205 

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

207 if not _class__delattr__( selfclass, name ): 

208 super( ).__delattr__( name ) 

209 

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

211 if not _class__setattr__( selfclass, name ): 

212 super( ).__setattr__( name, value ) 

213 

214ABCFactory.__doc__ = __.generate_docstring( 

215 ABCFactory, 

216 'description of class factory class', 

217 'class attributes accretion' ) 

218 

219 

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

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

222 ''' Accretive protocol class factory. ''' 

223 

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

225 clscls: type[ ProtocolClass ], 

226 name: str, 

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

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

229 decorators: ClassDecorators = ( ), 

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

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

232 **args: __.typx.Any 

233 ) -> ProtocolClass: 

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

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

236 return _class__new__( 

237 class_, 

238 decorators = decorators, 

239 docstring = docstring, 

240 mutables = mutables ) 

241 

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

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

244 _class__init__( selfclass ) 

245 

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

247 if not _class__delattr__( selfclass, name ): 

248 super( ).__delattr__( name ) 

249 

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

251 if not _class__setattr__( selfclass, name ): 

252 super( ).__setattr__( name, value ) 

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

254 

255ProtocolClass.__doc__ = __.generate_docstring( 

256 ProtocolClass, 

257 'description of class factory class', 

258 'class attributes accretion' ) 

259 

260 

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

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

263class ProtocolDataclass( ProtocolClass ): 

264 ''' Accretive protocol dataclass factory. ''' 

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

266 clscls: type[ ProtocolDataclass ], 

267 name: str, 

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

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

270 decorators: ClassDecorators = ( ), 

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

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

273 **args: __.typx.Any 

274 ) -> ProtocolDataclass: 

275 decorators_ = ( 

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

277 *decorators ) 

278 return ProtocolClass.__new__( # pyright: ignore 

279 clscls, name, bases, namespace, 

280 decorators = decorators_, 

281 docstring = docstring, 

282 mutables = mutables, 

283 **args ) 

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

285 

286ProtocolDataclass.__doc__ = __.generate_docstring( 

287 ProtocolDataclass, 

288 'description of class factory class', 

289 'class attributes accretion' ) 

290 

291 

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

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

294class CompleteProtocolDataclass( ProtocolClass ): 

295 ''' Accretive protocol dataclass factory. 

296 

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

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

299 clscls: type[ CompleteProtocolDataclass ], 

300 name: str, 

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

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

303 decorators: ClassDecorators = ( ), 

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

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

306 **args: __.typx.Any 

307 ) -> CompleteProtocolDataclass: 

308 decorators_ = ( 

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

310 *decorators ) 

311 return ProtocolClass.__new__( # pyright: ignore 

312 clscls, name, bases, namespace, 

313 decorators = decorators_, 

314 docstring = docstring, 

315 mutables = mutables, 

316 **args ) 

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

318 

319CompleteProtocolDataclass.__doc__ = __.generate_docstring( 

320 CompleteProtocolDataclass, 

321 'description of class factory class', 

322 'class attributes accretion' ) 

323 

324 

325def _accumulate_mutables( 

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

327) -> frozenset[ str ]: 

328 return frozenset( mutables ).union( *( 

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

330 for base in class_.__mro__ ) ) 

331 

332# pylint: disable=protected-access 

333def _class__new__( 

334 original: type, 

335 decorators: ClassDecorators = ( ), 

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

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

338) -> type: 

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

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

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

342 if class_decorators_: return original 

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

344 original._class_mutables_ = _accumulate_mutables( original, mutables ) 

345 original._class_decorators_ = class_decorators_ 

346 reproduction = original 

347 for decorator in decorators: 

348 class_decorators_.append( decorator ) 

349 reproduction = decorator( original ) 

350 if original is not reproduction: 

351 __.repair_class_reproduction( original, reproduction ) 

352 original = reproduction 

353 class_decorators_.clear( ) # Flag '__init__' to enable accretion 

354 return reproduction 

355# pylint: enable=protected-access 

356 

357 

358# pylint: disable=protected-access 

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

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

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

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

363 cdict = class_.__dict__ 

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

365 del class_._class_decorators_ 

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

367 class_behaviors.add( _behavior ) 

368 else: class_._class_behaviors_ = { _behavior } 

369# pylint: enable=protected-access 

370 

371 

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

373 # Consult class attributes dictionary to ignore accretive base classes. 

374 cdict = class_.__dict__ 

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

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

377 from .exceptions import AttributeImmutabilityError 

378 raise AttributeImmutabilityError( name ) 

379 

380 

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

382 # Consult class attributes dictionary to ignore accretive base classes. 

383 cdict = class_.__dict__ 

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

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

386 if hasattr( class_, name ): 

387 from .exceptions import AttributeImmutabilityError 

388 raise AttributeImmutabilityError( name ) 

389 return False # Allow setting new attributes