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

150 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-11-10 04:11 +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 :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( ConcealerExtension, dict ): # type: ignore[type-arg] 

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

287 

288 Can be used as an instance dictionary. 

289 

290 Prevents attempts to mutate dictionary via inherited interface. 

291 ''' 

292 

293 def __init__( 

294 self, 

295 *iterables: DictionaryPositionalArgument, 

296 **entries: DictionaryNominativeArgument 

297 ): 

298 super( ).__init__( ) 

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

300 

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

302 from .exceptions import IndelibleEntryError 

303 raise IndelibleEntryError( key ) 

304 

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

306 from .exceptions import IndelibleEntryError 

307 if key in self: raise IndelibleEntryError( key ) 

308 super( ).__setitem__( key, value ) 

309 

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

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

312 from .exceptions import InvalidOperationError 

313 raise InvalidOperationError( 'clear' ) 

314 

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

316 ''' Provides fresh copy of dictionary. ''' 

317 return type( self )( self ) 

318 

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

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

321 ) -> a.Never: 

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

323 from .exceptions import InvalidOperationError 

324 raise InvalidOperationError( 'pop' ) 

325 

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

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

328 from .exceptions import InvalidOperationError 

329 raise InvalidOperationError( 'popitem' ) 

330 

331 def update( 

332 self, 

333 *iterables: DictionaryPositionalArgument, 

334 **entries: DictionaryNominativeArgument 

335 ) -> a.Self: 

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

337 from itertools import chain 

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

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

340 lambda element: ( 

341 element.items( ) 

342 if isinstance( element, cabc.Mapping ) 

343 else element 

344 ), 

345 ( *iterables, entries ) 

346 ) ): self[ indicator ] = value 

347 return self 

348 

349 

350class Docstring( str ): 

351 ''' Dedicated docstring container. ''' 

352 

353 

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

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

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

357 

358 

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

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

361 class_ = type( obj ) 

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

363 

364 

365def discover_public_attributes( 

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

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

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

369 

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

371 ''' 

372 return tuple( sorted( 

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

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

375 

376 

377def generate_docstring( 

378 *fragment_ids: type | Docstring | str 

379) -> str: 

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

381 from inspect import cleandoc, getdoc, isclass 

382 from ._docstrings import TABLE 

383 fragments = [ ] 

384 for fragment_id in fragment_ids: 

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

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

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

388 fragments.append( cleandoc( fragment ) ) 

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