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

162 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2024-12-05 03:47 +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 

27from __future__ import annotations 

28 

29import collections.abc as cabc 

30 

31from abc import ( 

32 ABCMeta as ABCFactory, 

33 abstractmethod as abstract_member_function, 

34) 

35from functools import partial as partial_function 

36from inspect import cleandoc as clean_docstring 

37from sys import modules 

38from types import ( 

39 MappingProxyType as DictionaryProxy, 

40 ModuleType as Module, 

41 NotImplementedType as TypeofNotImplemented, 

42 SimpleNamespace, 

43) 

44 

45from . import _annotations as a 

46 

47 

48C = a.TypeVar( 'C' ) # Class 

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

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

51_H = a.TypeVar( '_H' ) 

52_V = a.TypeVar( '_V' ) 

53 

54ClassDecorators: a.TypeAlias = ( 

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

56ComparisonResult: a.TypeAlias = bool | TypeofNotImplemented 

57DictionaryNominativeArgument: a.TypeAlias = a.Annotation[ 

58 V, 

59 a.Doc( 

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

61 'dictionary data.' ), 

62] 

63DictionaryPositionalArgument: a.TypeAlias = a.Annotation[ 

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

65 a.Doc( 

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

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

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

69] 

70DictionaryValidator: a.TypeAlias = a.Annotation[ 

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

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

73] 

74ModuleReclassifier: a.TypeAlias = cabc.Callable[ 

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

76 

77 

78behavior_label = 'immutability' 

79 

80 

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

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

83 from platform import python_implementation 

84 match python_implementation( ): 

85 case 'CPython': # pragma: no branch 

86 _repair_cpython_class_closures( original, reproduction ) 

87 case _: pass # pragma: no cover 

88 

89 

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

91 original: type, reproduction: type 

92) -> None: 

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

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

95 except ValueError: return False 

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

97 closure = function.__closure__[ index ] 

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

99 closure.cell_contents = reproduction 

100 return True 

101 return False # pragma: no cover 

102 

103 from inspect import isfunction, unwrap 

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

105 attribute_ = unwrap( attribute ) 

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

107 return 

108 if isinstance( attribute_, property ): 

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

110 accessor = getattr( attribute_, aname ) 

111 if None is accessor: continue 

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

113 

114 

115class InternalClass( type ): 

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

117 

118 _class_attribute_visibility_includes_: cabc.Collection[ str ] = ( 

119 frozenset( ) ) 

120 

121 def __new__( 

122 factory: type[ type ], 

123 name: str, 

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

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

126 decorators: ClassDecorators = ( ), 

127 **args: a.Any 

128 ) -> InternalClass: 

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

130 return _immutable_class__new__( class_, decorators = decorators ) 

131 

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

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

134 _immutable_class__init__( selfclass ) 

135 

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

137 default: frozenset[ str ] = frozenset( ) 

138 includes: frozenset[ str ] = frozenset.union( *( # type: ignore 

139 getattr( class_, '_class_attribute_visibility_includes_', default ) 

140 for class_ in selfclass.__mro__ ) ) 

141 return tuple( sorted( 

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

143 if not name.startswith( '_' ) or name in includes ) ) 

144 

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

146 if not _immutable_class__delattr__( selfclass, name ): 

147 super( ).__delattr__( name ) 

148 

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

150 if not _immutable_class__setattr__( selfclass, name ): 

151 super( ).__setattr__( name, value ) 

152 

153 

154def _immutable_class__new__( 

155 original: type, 

156 decorators: ClassDecorators = ( ), 

157) -> type: 

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

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

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

161 if decorators_: return original 

162 setattr( original, '_class_decorators_', decorators_ ) 

163 reproduction = original 

164 for decorator in decorators: 

165 decorators_.append( decorator ) 

166 reproduction = decorator( original ) 

167 if original is not reproduction: 

168 repair_class_reproduction( original, reproduction ) 

169 original = reproduction 

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

171 return reproduction 

172 

173 

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

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

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

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

178 del class_._class_decorators_ 

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

180 class_behaviors.add( behavior_label ) 

181 else: setattr( class_, '_class_behaviors_', { behavior_label } ) 

182 

183 

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

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

186 if behavior_label not in class_.__dict__.get( 

187 '_class_behaviors_', ( ) 

188 ): return False 

189 raise AttributeError( 

190 "Cannot delete attribute {name!r} " 

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

192 name = name, 

193 class_fqname = calculate_class_fqname( class_ ) ) ) 

194 

195 

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

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

198 if behavior_label not in class_.__dict__.get( 

199 '_class_behaviors_', ( ) 

200 ): return False 

201 raise AttributeError( 

202 "Cannot assign attribute {name!r} " 

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

204 name = name, 

205 class_fqname = calculate_class_fqname( class_ ) ) ) 

206 

207 

208class ConcealerExtension: 

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

210 

211 By default, public attributes are displayed. 

212 ''' 

213 

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

215 

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

217 return tuple( sorted( 

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

219 if not name.startswith( '_' ) 

220 or name in self._attribute_visibility_includes_ ) ) 

221 

222 

223class InternalObject( ConcealerExtension, metaclass = InternalClass ): 

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

225 

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

227 raise AttributeError( 

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

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

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

231 

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

233 raise AttributeError( 

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

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

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

237 

238 

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

240 ''' Produces falsey objects. 

241 

242 Why not something already in Python? 

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

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

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

246 ''' 

247 

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

249 

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

251 return self is other 

252 

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

254 return self is not other 

255 

256 

257class Absent( Falsifier, InternalObject ): 

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

259 

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

261 ''' Singleton. ''' 

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

263 if isinstance( absent_, selfclass ): return absent_ 

264 return super( ).__new__( selfclass ) 

265 

266 

267Optional: a.TypeAlias = V | Absent 

268absent: a.Annotation[ 

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

270] = Absent( ) 

271 

272 

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

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

275 return absent is value 

276 

277 

278class ImmutableDictionary( 

279 ConcealerExtension, 

280 dict[ _H, _V ], 

281 a.Generic[ _H, _V ], 

282): 

283 ''' Immutable subclass of :py:class:`dict`. 

284 

285 Can be used as an instance dictionary. 

286 

287 Prevents attempts to mutate dictionary via inherited interface. 

288 ''' 

289 

290 def __init__( 

291 self, 

292 *iterables: DictionaryPositionalArgument[ _H, _V ], 

293 **entries: DictionaryNominativeArgument[ _V ], 

294 ): 

295 self._behaviors_: set[ str ] = set( ) 

296 super( ).__init__( ) 

297 from itertools import chain 

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

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

300 lambda element: ( # type: ignore 

301 element.items( ) 

302 if isinstance( element, cabc.Mapping ) 

303 else element 

304 ), 

305 ( *iterables, entries ) 

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

307 self._behaviors_.add( behavior_label ) 

308 

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

310 from .exceptions import EntryImmutabilityError 

311 raise EntryImmutabilityError( key ) 

312 

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

314 from .exceptions import EntryImmutabilityError 

315 default: set[ str ] = set( ) 

316 if behavior_label in getattr( self, '_behaviors_', default ): 

317 raise EntryImmutabilityError( key ) 

318 if key in self: 

319 raise EntryImmutabilityError( key ) 

320 super( ).__setitem__( key, value ) 

321 

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

323 ''' Raises exception. Cannot clear immutable entries. ''' 

324 from .exceptions import OperationValidityError 

325 raise OperationValidityError( 'clear' ) 

326 

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

328 ''' Provides fresh copy of dictionary. ''' 

329 return type( self )( self ) 

330 

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

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

333 ) -> a.Never: 

334 ''' Raises exception. Cannot pop immutable entry. ''' 

335 from .exceptions import OperationValidityError 

336 raise OperationValidityError( 'pop' ) 

337 

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

339 ''' Raises exception. Cannot pop immutable entry. ''' 

340 from .exceptions import OperationValidityError 

341 raise OperationValidityError( 'popitem' ) 

342 

343 def update( # type: ignore 

344 self, # pylint: disable=unused-argument 

345 *iterables: DictionaryPositionalArgument[ _H, _V ], 

346 **entries: DictionaryNominativeArgument[ _V ], 

347 ) -> None: 

348 ''' Raises exception. Cannot perform mass update. ''' 

349 from .exceptions import OperationValidityError 

350 raise OperationValidityError( 'update' ) 

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__ = ( )