Coverage for sources/accretive/__.py: 100%
150 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-10 04:52 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-10 04:52 +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 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)
47from . import _annotations as a
50H = a.TypeVar( 'H', bound = cabc.Hashable )
51V = a.TypeVar( 'V' ) # Value
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 ]
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 )
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
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__( # type: ignore
140 class_, decorators = decorators )
142 def __init__( selfclass, *posargs: a.Any, **nomargs: a.Any ):
143 super( ).__init__( *posargs, **nomargs )
144 _immutable_class__init__( selfclass )
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_ ) )
152 def __delattr__( selfclass, name: str ) -> None:
153 if not _immutable_class__delattr__( selfclass, name ):
154 super( ).__delattr__( name )
156 def __setattr__( selfclass, name: str, value: a.Any ) -> None:
157 if not _immutable_class__setattr__( selfclass, name ):
158 super( ).__setattr__( name, value )
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
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 } )
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_ ) ) )
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_ ) ) )
216class ConcealerExtension:
217 ''' Conceals instance attributes according to some criteria.
219 By default, public attributes are displayed.
220 '''
222 _attribute_visibility_includes_: cabc.Collection[ str ] = frozenset( )
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_ ) )
231class InternalObject( ConcealerExtension, metaclass = InternalClass ):
232 ''' Concealment and immutability on instance attributes. '''
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 ) ) )
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 ) ) )
247class Falsifier( metaclass = InternalClass ): # pylint: disable=eq-without-hash
248 ''' Produces falsey objects.
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( ConcealerExtension, dict ): # type: ignore[type-arg]
286 ''' Accretive subclass of :py:class:`dict`.
288 Can be used as an instance dictionary.
290 Prevents attempts to mutate dictionary via inherited interface.
291 '''
293 def __init__(
294 self,
295 *iterables: DictionaryPositionalArgument,
296 **entries: DictionaryNominativeArgument
297 ):
298 super( ).__init__( )
299 self.update( *iterables, **entries )
301 def __delitem__( self, key: cabc.Hashable ) -> None:
302 from .exceptions import IndelibleEntryError
303 raise IndelibleEntryError( key )
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 )
310 def clear( self ) -> a.Never:
311 ''' Raises exception. Cannot clear indelible entries. '''
312 from .exceptions import InvalidOperationError
313 raise InvalidOperationError( 'clear' )
315 def copy( self ) -> a.Self:
316 ''' Provides fresh copy of dictionary. '''
317 return type( self )( self )
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' )
326 def popitem( self ) -> a.Never:
327 ''' Raises exception. Cannot pop indelible entry. '''
328 from .exceptions import InvalidOperationError
329 raise InvalidOperationError( 'popitem' )
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
350class Docstring( str ):
351 ''' Dedicated docstring container. '''
354def calculate_class_fqname( class_: type ) -> str:
355 ''' Calculates fully-qualified name for class. '''
356 return f"{class_.__module__}.{class_.__qualname__}"
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__}"
365def discover_public_attributes(
366 attributes: cabc.Mapping[ str, a.Any ]
367) -> tuple[ str, ... ]:
368 ''' Discovers public attributes of certain types from dictionary.
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 ) ) )
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 )