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

108 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-05 04:28 +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 is initialized, its attributes cannot be reassigned or deleted. 

26 However, it may still accrete new attribute assignments. 

27 

28 The implementation includes: 

29 

30 * ``Class``: Standard metaclass for accretive 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 accretive import Class 

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

41 ... x = 1 

42 >>> Example.y = 2 # Add new class attribute 

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

44 Traceback (most recent call last): 

45 ... 

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

47 

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

49 

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

51 ... name = 'MyApp' 

52 ... version = '1.0.0' 

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

54 >>> Config.version 

55 '1.0.1' 

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

57 Traceback (most recent call last): 

58 ... 

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

60''' 

61# pylint: enable=line-too-long 

62 

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

64 

65 

66from __future__ import annotations 

67 

68from . import __ 

69 

70 

71ClassDecorators: __.typx.TypeAlias = ( 

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

73 

74 

75_behavior = 'accretion' 

76 

77 

78class Class( type ): 

79 ''' Accretive class factory. ''' 

80 

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

82 clscls: type[ Class ], 

83 name: str, 

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

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

86 decorators: ClassDecorators = ( ), 

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

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

89 **args: __.typx.Any 

90 ) -> Class: 

91 class_ = type.__new__( 

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

93 return _class__new__( 

94 class_, 

95 decorators = decorators, 

96 docstring = docstring, 

97 mutables = mutables ) 

98 

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

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

101 _class__init__( selfclass ) 

102 

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

104 if not _class__delattr__( selfclass, name ): 

105 super( ).__delattr__( name ) 

106 

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

108 if not _class__setattr__( selfclass, name ): 

109 super( ).__setattr__( name, value ) 

110 

111Class.__doc__ = __.generate_docstring( 

112 Class, 

113 'description of class factory class', 

114 'class attributes accretion' ) 

115 

116 

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

118class Dataclass( Class ): 

119 ''' Accretive dataclass factory. ''' 

120 

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

122 clscls: type[ Dataclass ], 

123 name: str, 

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

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

126 decorators: ClassDecorators = ( ), 

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

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

129 **args: __.typx.Any 

130 ) -> Dataclass: 

131 decorators_ = ( 

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

133 *decorators ) 

134 return Class.__new__( # pyright: ignore 

135 clscls, name, bases, namespace, 

136 decorators = decorators_, 

137 docstring = docstring, 

138 mutables = mutables, 

139 **args ) 

140 

141Dataclass.__doc__ = __.generate_docstring( 

142 Dataclass, 

143 'description of class factory class', 

144 'class attributes accretion' ) 

145 

146 

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

148class CompleteDataclass( Class ): 

149 ''' Accretive dataclass factory. 

150 

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

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

153 clscls: type[ CompleteDataclass ], 

154 name: str, 

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

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

157 decorators: ClassDecorators = ( ), 

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

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

160 **args: __.typx.Any 

161 ) -> CompleteDataclass: 

162 decorators_ = ( 

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

164 *decorators ) 

165 return Class.__new__( # pyright: ignore 

166 clscls, name, bases, namespace, 

167 decorators = decorators_, 

168 docstring = docstring, 

169 mutables = mutables, 

170 **args ) 

171 

172CompleteDataclass.__doc__ = __.generate_docstring( 

173 CompleteDataclass, 

174 'description of class factory class', 

175 'class attributes accretion' ) 

176 

177 

178class ABCFactory( __.abc.ABCMeta ): 

179 ''' Accretive abstract base class factory. ''' 

180 

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

182 clscls: type[ ABCFactory ], 

183 name: str, 

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

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

186 decorators: ClassDecorators = ( ), 

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

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

189 **args: __.typx.Any 

190 ) -> ABCFactory: 

191 class_ = __.abc.ABCMeta.__new__( 

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

193 return _class__new__( 

194 class_, 

195 decorators = decorators, 

196 docstring = docstring, 

197 mutables = mutables ) 

198 

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

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

201 _class__init__( selfclass ) 

202 

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

204 if not _class__delattr__( selfclass, name ): 

205 super( ).__delattr__( name ) 

206 

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

208 if not _class__setattr__( selfclass, name ): 

209 super( ).__setattr__( name, value ) 

210 

211ABCFactory.__doc__ = __.generate_docstring( 

212 ABCFactory, 

213 'description of class factory class', 

214 'class attributes accretion' ) 

215 

216 

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

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

219 ''' Accretive protocol class factory. ''' 

220 

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

222 clscls: type[ ProtocolClass ], 

223 name: str, 

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

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

226 decorators: ClassDecorators = ( ), 

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

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

229 **args: __.typx.Any 

230 ) -> ProtocolClass: 

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

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

233 return _class__new__( 

234 class_, 

235 decorators = decorators, 

236 docstring = docstring, 

237 mutables = mutables ) 

238 

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

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

241 _class__init__( selfclass ) 

242 

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

244 if not _class__delattr__( selfclass, name ): 

245 super( ).__delattr__( name ) 

246 

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

248 if not _class__setattr__( selfclass, name ): 

249 super( ).__setattr__( name, value ) 

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

251 

252ProtocolClass.__doc__ = __.generate_docstring( 

253 ProtocolClass, 

254 'description of class factory class', 

255 'class attributes accretion' ) 

256 

257 

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

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

260class ProtocolDataclass( ProtocolClass ): 

261 ''' Accretive protocol dataclass factory. ''' 

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

263 clscls: type[ ProtocolDataclass ], 

264 name: str, 

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

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

267 decorators: ClassDecorators = ( ), 

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

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

270 **args: __.typx.Any 

271 ) -> ProtocolDataclass: 

272 decorators_ = ( 

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

274 *decorators ) 

275 return ProtocolClass.__new__( # pyright: ignore 

276 clscls, name, bases, namespace, 

277 decorators = decorators_, 

278 docstring = docstring, 

279 mutables = mutables, 

280 **args ) 

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

282 

283ProtocolDataclass.__doc__ = __.generate_docstring( 

284 ProtocolDataclass, 

285 'description of class factory class', 

286 'class attributes accretion' ) 

287 

288 

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

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

291class CompleteProtocolDataclass( ProtocolClass ): 

292 ''' Accretive protocol dataclass factory. 

293 

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

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

296 clscls: type[ CompleteProtocolDataclass ], 

297 name: str, 

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

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

300 decorators: ClassDecorators = ( ), 

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

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

303 **args: __.typx.Any 

304 ) -> CompleteProtocolDataclass: 

305 decorators_ = ( 

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

307 *decorators ) 

308 return ProtocolClass.__new__( # pyright: ignore 

309 clscls, name, bases, namespace, 

310 decorators = decorators_, 

311 docstring = docstring, 

312 mutables = mutables, 

313 **args ) 

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

315 

316CompleteProtocolDataclass.__doc__ = __.generate_docstring( 

317 CompleteProtocolDataclass, 

318 'description of class factory class', 

319 'class attributes accretion' ) 

320 

321 

322def _accumulate_mutables( 

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

324) -> frozenset[ str ]: 

325 return frozenset( mutables ).union( *( 

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

327 for base in class_.__mro__ ) ) 

328 

329# pylint: disable=protected-access 

330def _class__new__( 

331 original: type, 

332 decorators: ClassDecorators = ( ), 

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

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

335) -> type: 

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

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

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

339 if class_decorators_: return original 

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

341 original._class_mutables_ = _accumulate_mutables( original, mutables ) 

342 original._class_decorators_ = class_decorators_ 

343 reproduction = original 

344 for decorator in decorators: 

345 class_decorators_.append( decorator ) 

346 reproduction = decorator( original ) 

347 if original is not reproduction: 

348 __.repair_class_reproduction( original, reproduction ) 

349 original = reproduction 

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

351 return reproduction 

352# pylint: enable=protected-access 

353 

354 

355# pylint: disable=protected-access 

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

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

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

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

360 cdict = class_.__dict__ 

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

362 del class_._class_decorators_ 

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

364 class_behaviors.add( _behavior ) 

365 else: class_._class_behaviors_ = { _behavior } 

366# pylint: enable=protected-access 

367 

368 

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

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

371 cdict = class_.__dict__ 

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

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

374 from .exceptions import AttributeImmutabilityError 

375 raise AttributeImmutabilityError( name ) 

376 

377 

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

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

380 cdict = class_.__dict__ 

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

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

383 if hasattr( class_, name ): 

384 from .exceptions import AttributeImmutabilityError 

385 raise AttributeImmutabilityError( name ) 

386 return False # Allow setting new attributes.