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

153 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2024-11-20 01:33 +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''' Common constants, imports, and utilities. ''' 

22 

23# ruff: noqa: F401 

24# pylint: disable=unused-import 

25 

26 

27# Note: Immutability and absence machinery is vendored from elsewhere. 

28# This keeps the number of dependencies small, which is desirable for a 

29# fundamental library. 

30 

31 

32from __future__ import annotations 

33 

34import collections.abc as cabc 

35 

36from abc import ( 

37 ABCMeta as ABCFactory, 

38 abstractmethod as abstract_member_function, 

39) 

40from functools import partial as partial_function 

41from inspect import cleandoc as clean_docstring 

42from sys import modules 

43from types import ( 

44 MappingProxyType as DictionaryProxy, 

45 ModuleType as Module, 

46 NotImplementedType as TypeofNotImplemented, 

47 SimpleNamespace, 

48) 

49 

50from . import _annotations as a 

51 

52 

53H = a.TypeVar( 'H', bound = cabc.Hashable ) # Hash Key 

54V = a.TypeVar( 'V' ) # Value 

55_H = a.TypeVar( '_H' ) 

56_V = a.TypeVar( '_V' ) 

57 

58 

59ClassDecorators: a.TypeAlias = ( 

60 cabc.Iterable[ cabc.Callable[ [ type ], type ] ] ) 

61ComparisonResult: a.TypeAlias = bool | TypeofNotImplemented 

62DictionaryNominativeArgument: a.TypeAlias = a.Annotation[ 

63 V, 

64 a.Doc( 

65 'Zero or more keyword arguments from which to initialize ' 

66 'dictionary data.' ), 

67] 

68DictionaryPositionalArgument: a.TypeAlias = a.Annotation[ 

69 cabc.Mapping[ H, V ] | cabc.Iterable[ tuple[ H, V ] ], 

70 a.Doc( 

71 'Zero or more iterables from which to initialize dictionary data. ' 

72 'Each iterable must be dictionary or sequence of key-value pairs. ' 

73 'Duplicate keys will result in an error.' ), 

74] 

75DictionaryProducer: a.TypeAlias = a.Annotation[ 

76 cabc.Callable[ [ ], V ], 

77 a.Doc( 'Callable which produces values for absent dictionary entries.' ), 

78] 

79DictionaryValidator: a.TypeAlias = a.Annotation[ 

80 cabc.Callable[ [ H, V ], bool ], 

81 a.Doc( 'Callable which validates entries before addition to dictionary.' ), 

82] 

83ModuleReclassifier: a.TypeAlias = cabc.Callable[ 

84 [ cabc.Mapping[ str, a.Any ] ], None ] 

85 

86 

87def repair_class_reproduction( original: type, reproduction: type ) -> None: 

88 ''' Repairs a class reproduction, if necessary. ''' 

89 from platform import python_implementation 

90 match python_implementation( ): 

91 case 'CPython': # pragma: no branch 

92 _repair_cpython_class_closures( original, reproduction ) 

93 case _: pass # pragma: no cover 

94 

95 

96def _repair_cpython_class_closures( # pylint: disable=too-complex 

97 original: type, reproduction: type 

98) -> None: 

99 def try_repair_closure( function: cabc.Callable[ ..., a.Any ] ) -> bool: 

100 try: index = function.__code__.co_freevars.index( '__class__' ) 

101 except ValueError: return False 

102 if not function.__closure__: return False # pragma: no branch 

103 closure = function.__closure__[ index ] 

104 if original is closure.cell_contents: # pragma: no branch 

105 closure.cell_contents = reproduction 

106 return True 

107 return False # pragma: no cover 

108 

109 from inspect import isfunction, unwrap 

110 for attribute in reproduction.__dict__.values( ): # pylint: disable=too-many-nested-blocks 

111 attribute_ = unwrap( attribute ) 

112 if isfunction( attribute_ ) and try_repair_closure( attribute_ ): 

113 return 

114 if isinstance( attribute_, property ): 

115 for aname in ( 'fget', 'fset', 'fdel' ): 

116 accessor = getattr( attribute_, aname ) 

117 if None is accessor: continue 

118 if try_repair_closure( accessor ): return # pragma: no branch 

119 

120 

121_immutability_label = 'immutability' 

122 

123 

124class InternalClass( type ): 

125 ''' Concealment and immutability on class attributes. ''' 

126 

127 _class_attribute_visibility_includes_: cabc.Collection[ str ] = ( 

128 frozenset( ) ) 

129 

130 def __new__( 

131 factory: type[ type ], 

132 name: str, 

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

134 namespace: dict[ str, a.Any ], *, 

135 decorators: ClassDecorators = ( ), 

136 **args: a.Any 

137 ) -> InternalClass: 

138 class_ = type.__new__( factory, name, bases, namespace, **args ) 

139 return _immutable_class__new__( class_, decorators = decorators ) 

140 

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

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

143 _immutable_class__init__( selfclass ) 

144 

145 def __dir__( selfclass ) -> tuple[ str, ... ]: 

146 return tuple( sorted( 

147 name for name in super( ).__dir__( ) 

148 if not name.startswith( '_' ) 

149 or name in selfclass._class_attribute_visibility_includes_ ) ) 

150 

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

152 if not _immutable_class__delattr__( selfclass, name ): 

153 super( ).__delattr__( name ) 

154 

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

156 if not _immutable_class__setattr__( selfclass, name ): 

157 super( ).__setattr__( name, value ) 

158 

159 

160def _immutable_class__new__( 

161 original: type, 

162 decorators: ClassDecorators = ( ), 

163) -> type: 

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

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

166 decorators_ = original.__dict__.get( '_class_decorators_', [ ] ) 

167 if decorators_: return original 

168 setattr( original, '_class_decorators_', decorators_ ) 

169 reproduction = original 

170 for decorator in decorators: 

171 decorators_.append( decorator ) 

172 reproduction = decorator( original ) 

173 if original is not reproduction: 

174 repair_class_reproduction( original, reproduction ) 

175 original = reproduction 

176 decorators_.clear( ) # Flag '__init__' to enable immutability. 

177 return reproduction 

178 

179 

180def _immutable_class__init__( class_: type ) -> None: 

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

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

183 if class_.__dict__.get( '_class_decorators_' ): return 

184 del class_._class_decorators_ 

185 if ( class_behaviors := class_.__dict__.get( '_class_behaviors_' ) ): 

186 class_behaviors.add( _immutability_label ) 

187 # TODO: accretive set 

188 else: setattr( class_, '_class_behaviors_', { _immutability_label } ) 

189 

190 

191def _immutable_class__delattr__( class_: type, name: str ) -> bool: 

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

193 if _immutability_label not in class_.__dict__.get( 

194 '_class_behaviors_', ( ) 

195 ): return False 

196 raise AttributeError( 

197 "Cannot delete attribute {name!r} " 

198 "on class {class_fqname!r}.".format( 

199 name = name, 

200 class_fqname = calculate_class_fqname( class_ ) ) ) 

201 

202 

203def _immutable_class__setattr__( class_: type, name: str ) -> bool: 

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

205 if _immutability_label not in class_.__dict__.get( 

206 '_class_behaviors_', ( ) 

207 ): return False 

208 raise AttributeError( 

209 "Cannot assign attribute {name!r} " 

210 "on class {class_fqname!r}.".format( 

211 name = name, 

212 class_fqname = calculate_class_fqname( class_ ) ) ) 

213 

214 

215class ConcealerExtension: 

216 ''' Conceals instance attributes according to some criteria. 

217 

218 By default, public attributes are displayed. 

219 ''' 

220 

221 _attribute_visibility_includes_: cabc.Collection[ str ] = frozenset( ) 

222 

223 def __dir__( self ) -> tuple[ str, ... ]: 

224 return tuple( sorted( 

225 name for name in super( ).__dir__( ) 

226 if not name.startswith( '_' ) 

227 or name in self._attribute_visibility_includes_ ) ) 

228 

229 

230class InternalObject( ConcealerExtension, metaclass = InternalClass ): 

231 ''' Concealment and immutability on instance attributes. ''' 

232 

233 def __delattr__( self, name: str ) -> None: 

234 raise AttributeError( 

235 "Cannot delete attribute {name!r} on instance " 

236 "of class {class_fqname!r}.".format( 

237 name = name, class_fqname = calculate_fqname( self ) ) ) 

238 

239 def __setattr__( self, name: str, value: a.Any ) -> None: 

240 raise AttributeError( 

241 "Cannot assign attribute {name!r} on instance " 

242 "of class {class_fqname!r}.".format( 

243 name = name, class_fqname = calculate_fqname( self ) ) ) 

244 

245 

246class Falsifier( metaclass = InternalClass ): # pylint: disable=eq-without-hash 

247 ''' Produces falsey objects. 

248 

249 Why not something already in Python? 

250 :py:class:`object` produces truthy objects. 

251 :py:class:`types.NoneType` "produces" falsey ``None`` singleton. 

252 :py:class:`typing_extensions.NoDefault` is truthy singleton. 

253 ''' 

254 

255 def __bool__( self ) -> bool: return False 

256 

257 def __eq__( self, other: a.Any ) -> ComparisonResult: 

258 return self is other 

259 

260 def __ne__( self, other: a.Any ) -> ComparisonResult: 

261 return self is not other 

262 

263 

264class Absent( Falsifier, InternalObject ): 

265 ''' Type of the sentinel for option without default value. ''' 

266 

267 def __new__( selfclass ) -> a.Self: 

268 ''' Singleton. ''' 

269 absent_ = globals( ).get( 'absent' ) 

270 if isinstance( absent_, selfclass ): return absent_ 

271 return super( ).__new__( selfclass ) 

272 

273 

274Optional: a.TypeAlias = V | Absent 

275absent: a.Annotation[ 

276 Absent, a.Doc( ''' Sentinel for option with no default value. ''' ) 

277] = Absent( ) 

278 

279 

280def is_absent( value: object ) -> a.TypeIs[ Absent ]: 

281 ''' Checks if a value is absent or not. ''' 

282 return absent is value 

283 

284 

285class CoreDictionary( 

286 ConcealerExtension, 

287 dict[ _H, _V ], 

288 a.Generic[ _H, _V ], 

289): 

290 ''' Accretive subclass of :py:class:`dict`. 

291 

292 Can be used as an instance dictionary. 

293 

294 Prevents attempts to mutate dictionary via inherited interface. 

295 ''' 

296 

297 def __init__( 

298 self, 

299 *iterables: DictionaryPositionalArgument[ _H, _V ], 

300 **entries: DictionaryNominativeArgument[ _V ], 

301 ): 

302 super( ).__init__( ) 

303 self.update( *iterables, **entries ) 

304 

305 def __delitem__( self, key: _H ) -> None: 

306 from .exceptions import EntryImmutabilityError 

307 raise EntryImmutabilityError( key ) 

308 

309 def __setitem__( self, key: _H, value: _V ) -> None: 

310 from .exceptions import EntryImmutabilityError 

311 if key in self: raise EntryImmutabilityError( key ) 

312 super( ).__setitem__( key, value ) 

313 

314 def clear( self ) -> a.Never: 

315 ''' Raises exception. Cannot clear indelible entries. ''' 

316 from .exceptions import OperationValidityError 

317 raise OperationValidityError( 'clear' ) 

318 

319 def copy( self ) -> a.Self: 

320 ''' Provides fresh copy of dictionary. ''' 

321 return type( self )( self ) 

322 

323 def pop( # pylint: disable=unused-argument 

324 self, key: _H, default: Optional[ _V ] = absent 

325 ) -> a.Never: 

326 ''' Raises exception. Cannot pop indelible entry. ''' 

327 from .exceptions import OperationValidityError 

328 raise OperationValidityError( 'pop' ) 

329 

330 def popitem( self ) -> a.Never: 

331 ''' Raises exception. Cannot pop indelible entry. ''' 

332 from .exceptions import OperationValidityError 

333 raise OperationValidityError( 'popitem' ) 

334 

335 def update( # type: ignore 

336 self, 

337 *iterables: DictionaryPositionalArgument[ _H, _V ], 

338 **entries: DictionaryNominativeArgument[ _V ], 

339 ) -> None: 

340 ''' Adds new entries as a batch. ''' 

341 from itertools import chain 

342 # Add values in order received, enforcing no alteration. 

343 for indicator, value in chain.from_iterable( map( # type: ignore 

344 lambda element: ( # type: ignore 

345 element.items( ) 

346 if isinstance( element, cabc.Mapping ) 

347 else element 

348 ), 

349 ( *iterables, entries ) 

350 ) ): self[ indicator ] = value # type: ignore 

351 

352 

353class Docstring( str ): 

354 ''' Dedicated docstring container. ''' 

355 

356 

357def calculate_class_fqname( class_: type ) -> str: 

358 ''' Calculates fully-qualified name for class. ''' 

359 return f"{class_.__module__}.{class_.__qualname__}" 

360 

361 

362def calculate_fqname( obj: object ) -> str: 

363 ''' Calculates fully-qualified name for class of object. ''' 

364 class_ = type( obj ) 

365 return f"{class_.__module__}.{class_.__qualname__}" 

366 

367 

368def discover_public_attributes( 

369 attributes: cabc.Mapping[ str, a.Any ] 

370) -> tuple[ str, ... ]: 

371 ''' Discovers public attributes of certain types from dictionary. 

372 

373 By default, callables, including classes, are discovered. 

374 ''' 

375 return tuple( sorted( 

376 name for name, attribute in attributes.items( ) 

377 if not name.startswith( '_' ) and callable( attribute ) ) ) 

378 

379 

380def generate_docstring( 

381 *fragment_ids: type | Docstring | str 

382) -> str: 

383 ''' Sews together docstring fragments into clean docstring. ''' 

384 from inspect import cleandoc, getdoc, isclass 

385 from ._docstrings import TABLE 

386 fragments: list[ str ] = [ ] 

387 for fragment_id in fragment_ids: 

388 if isclass( fragment_id ): fragment = getdoc( fragment_id ) or '' 

389 elif isinstance( fragment_id, Docstring ): fragment = fragment_id 

390 else: fragment = TABLE[ fragment_id ] 

391 fragments.append( cleandoc( fragment ) ) 

392 return '\n\n'.join( fragments ) 

393 

394 

395__all__ = ( )