Coverage for sources/accretive/__.py: 100%
153 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-20 01:33 +0000
« 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 -*-
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#============================================================================#
21''' Common constants, imports, and utilities. '''
23# ruff: noqa: F401
24# pylint: disable=unused-import
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.
32from __future__ import annotations
34import collections.abc as cabc
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)
50from . import _annotations as a
53H = a.TypeVar( 'H', bound = cabc.Hashable ) # Hash Key
54V = a.TypeVar( 'V' ) # Value
55_H = a.TypeVar( '_H' )
56_V = a.TypeVar( '_V' )
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 ]
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
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
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
121_immutability_label = 'immutability'
124class InternalClass( type ):
125 ''' Concealment and immutability on class attributes. '''
127 _class_attribute_visibility_includes_: cabc.Collection[ str ] = (
128 frozenset( ) )
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 )
141 def __init__( selfclass, *posargs: a.Any, **nomargs: a.Any ):
142 super( ).__init__( *posargs, **nomargs )
143 _immutable_class__init__( selfclass )
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_ ) )
151 def __delattr__( selfclass, name: str ) -> None:
152 if not _immutable_class__delattr__( selfclass, name ):
153 super( ).__delattr__( name )
155 def __setattr__( selfclass, name: str, value: a.Any ) -> None:
156 if not _immutable_class__setattr__( selfclass, name ):
157 super( ).__setattr__( name, value )
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
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 } )
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_ ) ) )
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_ ) ) )
215class ConcealerExtension:
216 ''' Conceals instance attributes according to some criteria.
218 By default, public attributes are displayed.
219 '''
221 _attribute_visibility_includes_: cabc.Collection[ str ] = frozenset( )
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_ ) )
230class InternalObject( ConcealerExtension, metaclass = InternalClass ):
231 ''' Concealment and immutability on instance attributes. '''
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 ) ) )
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 ) ) )
246class Falsifier( metaclass = InternalClass ): # pylint: disable=eq-without-hash
247 ''' Produces falsey objects.
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 '''
255 def __bool__( self ) -> bool: return False
257 def __eq__( self, other: a.Any ) -> ComparisonResult:
258 return self is other
260 def __ne__( self, other: a.Any ) -> ComparisonResult:
261 return self is not other
264class Absent( Falsifier, InternalObject ):
265 ''' Type of the sentinel for option without default value. '''
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 )
274Optional: a.TypeAlias = V | Absent
275absent: a.Annotation[
276 Absent, a.Doc( ''' Sentinel for option with no default value. ''' )
277] = Absent( )
280def is_absent( value: object ) -> a.TypeIs[ Absent ]:
281 ''' Checks if a value is absent or not. '''
282 return absent is value
285class CoreDictionary(
286 ConcealerExtension,
287 dict[ _H, _V ],
288 a.Generic[ _H, _V ],
289):
290 ''' Accretive subclass of :py:class:`dict`.
292 Can be used as an instance dictionary.
294 Prevents attempts to mutate dictionary via inherited interface.
295 '''
297 def __init__(
298 self,
299 *iterables: DictionaryPositionalArgument[ _H, _V ],
300 **entries: DictionaryNominativeArgument[ _V ],
301 ):
302 super( ).__init__( )
303 self.update( *iterables, **entries )
305 def __delitem__( self, key: _H ) -> None:
306 from .exceptions import EntryImmutabilityError
307 raise EntryImmutabilityError( key )
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 )
314 def clear( self ) -> a.Never:
315 ''' Raises exception. Cannot clear indelible entries. '''
316 from .exceptions import OperationValidityError
317 raise OperationValidityError( 'clear' )
319 def copy( self ) -> a.Self:
320 ''' Provides fresh copy of dictionary. '''
321 return type( self )( self )
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' )
330 def popitem( self ) -> a.Never:
331 ''' Raises exception. Cannot pop indelible entry. '''
332 from .exceptions import OperationValidityError
333 raise OperationValidityError( 'popitem' )
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
353class Docstring( str ):
354 ''' Dedicated docstring container. '''
357def calculate_class_fqname( class_: type ) -> str:
358 ''' Calculates fully-qualified name for class. '''
359 return f"{class_.__module__}.{class_.__qualname__}"
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__}"
368def discover_public_attributes(
369 attributes: cabc.Mapping[ str, a.Any ]
370) -> tuple[ str, ... ]:
371 ''' Discovers public attributes of certain types from dictionary.
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 ) ) )
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 )
395__all__ = ( )