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

151 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-11-10 23:02 +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 ABCMeta as ABCFactory 

37from functools import partial as partial_function 

38from inspect import cleandoc as clean_docstring 

39from sys import modules 

40from types import ( 

41 MappingProxyType as DictionaryProxy, 

42 ModuleType as Module, 

43 NotImplementedType as TypeofNotImplemented, 

44 SimpleNamespace, 

45) 

46 

47from . import _annotations as a 

48 

49 

50H = a.TypeVar( 'H', bound = cabc.Hashable ) 

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

52 

53 

54ClassDecorators: a.TypeAlias = ( 

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

56ComparisonResult: a.TypeAlias = bool | TypeofNotImplemented 

57DictionaryNominativeArgument: a.TypeAlias = a.Annotation[ 

58 a.Any, 

59 a.Doc( 

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

61 'dictionary data.' ), 

62] 

63# TODO: Support taking our dictionaries, themselves, as arguments. 

64# Supposed to work via structural typing, but must match protocol. 

65# https://github.com/python/mypy/issues/2922 

66# https://github.com/python/mypy/issues/2922#issuecomment-1186587232 

67# https://github.com/python/typing/discussions/1127#discussioncomment-2538837 

68# https://mypy.readthedocs.io/en/latest/protocols.html 

69DictionaryPositionalArgument: a.TypeAlias = a.Annotation[ 

70 cabc.Mapping[ cabc.Hashable, a.Any ] 

71 | cabc.Iterable[ tuple[ cabc.Hashable, a.Any] ], 

72 a.Doc( 

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

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

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

76] 

77DictionaryProducer: a.TypeAlias = a.Annotation[ 

78 cabc.Callable[ [ ], a.Any ], 

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

80] 

81DictionaryValidator: a.TypeAlias = a.Annotation[ 

82 cabc.Callable[ [ cabc.Hashable, a.Any ], bool ], 

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

84] 

85ModuleReclassifier: a.TypeAlias = cabc.Callable[ 

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

87 

88 

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

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

91 from platform import python_implementation 

92 match python_implementation( ): 

93 case 'CPython': # pragma: no branch 

94 _repair_cpython_class_closures( original, reproduction ) 

95 

96 

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

98 original: type, reproduction: type 

99) -> None: 

100 def try_repair_closure( function: cabc.Callable ) -> bool: # type: ignore 

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

102 except ValueError: return False 

103 closure = function.__closure__[ index ] # type: ignore 

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__( # type: ignore 

140 class_, decorators = decorators ) 

141 

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

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

144 _immutable_class__init__( selfclass ) 

145 

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

147 return tuple( sorted( 

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

149 if not name.startswith( '_' ) 

150 or name in selfclass._class_attribute_visibility_includes_ ) ) 

151 

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

153 if not _immutable_class__delattr__( selfclass, name ): 

154 super( ).__delattr__( name ) 

155 

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

157 if not _immutable_class__setattr__( selfclass, name ): 

158 super( ).__setattr__( name, value ) 

159 

160 

161def _immutable_class__new__( 

162 original: type, 

163 decorators: ClassDecorators = ( ), 

164) -> type: 

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

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

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

168 if decorators_: return original 

169 setattr( original, '_class_decorators_', decorators_ ) 

170 reproduction = original 

171 for decorator in decorators: 

172 decorators_.append( decorator ) 

173 reproduction = decorator( original ) 

174 if original is not reproduction: 

175 repair_class_reproduction( original, reproduction ) 

176 original = reproduction 

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

178 return reproduction 

179 

180 

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

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

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

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

185 del class_._class_decorators_ 

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

187 class_behaviors.add( _immutability_label ) 

188 # TODO: accretive set 

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

190 

191 

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

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

194 if _immutability_label not in class_.__dict__.get( 

195 '_class_behaviors_', ( ) 

196 ): return False 

197 raise AttributeError( 

198 "Cannot delete attribute {name!r} " 

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

200 name = name, 

201 class_fqname = calculate_class_fqname( class_ ) ) ) 

202 

203 

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

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

206 if _immutability_label not in class_.__dict__.get( 

207 '_class_behaviors_', ( ) 

208 ): return False 

209 raise AttributeError( 

210 "Cannot assign attribute {name!r} " 

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

212 name = name, 

213 class_fqname = calculate_class_fqname( class_ ) ) ) 

214 

215 

216class ConcealerExtension: 

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

218 

219 By default, public attributes are displayed. 

220 ''' 

221 

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

223 

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

225 return tuple( sorted( 

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

227 if not name.startswith( '_' ) 

228 or name in self._attribute_visibility_includes_ ) ) 

229 

230 

231class InternalObject( ConcealerExtension, metaclass = InternalClass ): 

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

233 

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

235 raise AttributeError( 

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

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

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

239 

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

241 raise AttributeError( 

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

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

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

245 

246 

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

248 ''' Produces falsey objects. 

249 

250 Why not something already in Python? 

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

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

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

254 ''' 

255 

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

257 

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

259 return self is other 

260 

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

262 return self is not other 

263 

264 

265class Absent( Falsifier, InternalObject ): 

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

267 

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

269 ''' Singleton. ''' 

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

271 if isinstance( absent_, selfclass ): return absent_ 

272 return super( ).__new__( selfclass ) 

273 

274 

275Optional: a.TypeAlias = V | Absent 

276absent: a.Annotation[ 

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

278] = Absent( ) 

279 

280 

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

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

283 return absent is value 

284 

285 

286class CoreDictionary( ConcealerExtension, dict ): # type: ignore[type-arg] 

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

288 

289 Can be used as an instance dictionary. 

290 

291 Prevents attempts to mutate dictionary via inherited interface. 

292 ''' 

293 

294 def __init__( 

295 self, 

296 *iterables: DictionaryPositionalArgument, 

297 **entries: DictionaryNominativeArgument 

298 ): 

299 super( ).__init__( ) 

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

301 

302 def __delitem__( self, key: cabc.Hashable ) -> None: 

303 from .exceptions import IndelibleEntryError 

304 raise IndelibleEntryError( key ) 

305 

306 def __setitem__( self, key: cabc.Hashable, value: a.Any ) -> None: 

307 from .exceptions import IndelibleEntryError 

308 if key in self: raise IndelibleEntryError( key ) 

309 super( ).__setitem__( key, value ) 

310 

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

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

313 from .exceptions import InvalidOperationError 

314 raise InvalidOperationError( 'clear' ) 

315 

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

317 ''' Provides fresh copy of dictionary. ''' 

318 return type( self )( self ) 

319 

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

321 self, key: cabc.Hashable, default: Optional[ a.Any ] = absent 

322 ) -> a.Never: 

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

324 from .exceptions import InvalidOperationError 

325 raise InvalidOperationError( 'pop' ) 

326 

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

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

329 from .exceptions import InvalidOperationError 

330 raise InvalidOperationError( 'popitem' ) 

331 

332 def update( 

333 self, 

334 *iterables: DictionaryPositionalArgument, 

335 **entries: DictionaryNominativeArgument 

336 ) -> a.Self: 

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

338 from itertools import chain 

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

340 for indicator, value in chain.from_iterable( map( 

341 lambda element: ( 

342 element.items( ) 

343 if isinstance( element, cabc.Mapping ) 

344 else element 

345 ), 

346 ( *iterables, entries ) 

347 ) ): self[ indicator ] = value 

348 return self 

349 

350 

351class Docstring( str ): 

352 ''' Dedicated docstring container. ''' 

353 

354 

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

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

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

358 

359 

360def calculate_fqname( obj: a.Any ) -> str: 

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

362 class_ = type( obj ) 

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

364 

365 

366def discover_public_attributes( 

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

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

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

370 

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

372 ''' 

373 return tuple( sorted( 

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

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

376 

377 

378def generate_docstring( 

379 *fragment_ids: type | Docstring | str 

380) -> str: 

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

382 from inspect import cleandoc, getdoc, isclass 

383 from ._docstrings import TABLE 

384 fragments = [ ] 

385 for fragment_id in fragment_ids: 

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

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

388 else: fragment = TABLE[ fragment_id ] # type: ignore 

389 fragments.append( cleandoc( fragment ) ) 

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

391 

392 

393__all__ = ( )