Coverage for sources/accretive/__.py: 100%
151 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-10 23:02 +0000
« 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 -*-
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 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 '''
256 def __bool__( self ) -> bool: return False
258 def __eq__( self, other: a.Any ) -> ComparisonResult:
259 return self is other
261 def __ne__( self, other: a.Any ) -> ComparisonResult:
262 return self is not other
265class Absent( Falsifier, InternalObject ):
266 ''' Type of the sentinel for option without default value. '''
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 )
275Optional: a.TypeAlias = V | Absent
276absent: a.Annotation[
277 Absent, a.Doc( ''' Sentinel for option with no default value. ''' )
278] = Absent( )
281def is_absent( value: object ) -> a.TypeIs[ Absent ]:
282 ''' Checks if a value is absent or not. '''
283 return absent is value
286class CoreDictionary( ConcealerExtension, dict ): # type: ignore[type-arg]
287 ''' Accretive subclass of :py:class:`dict`.
289 Can be used as an instance dictionary.
291 Prevents attempts to mutate dictionary via inherited interface.
292 '''
294 def __init__(
295 self,
296 *iterables: DictionaryPositionalArgument,
297 **entries: DictionaryNominativeArgument
298 ):
299 super( ).__init__( )
300 self.update( *iterables, **entries )
302 def __delitem__( self, key: cabc.Hashable ) -> None:
303 from .exceptions import IndelibleEntryError
304 raise IndelibleEntryError( key )
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 )
311 def clear( self ) -> a.Never:
312 ''' Raises exception. Cannot clear indelible entries. '''
313 from .exceptions import InvalidOperationError
314 raise InvalidOperationError( 'clear' )
316 def copy( self ) -> a.Self:
317 ''' Provides fresh copy of dictionary. '''
318 return type( self )( self )
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' )
327 def popitem( self ) -> a.Never:
328 ''' Raises exception. Cannot pop indelible entry. '''
329 from .exceptions import InvalidOperationError
330 raise InvalidOperationError( 'popitem' )
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
351class Docstring( str ):
352 ''' Dedicated docstring container. '''
355def calculate_class_fqname( class_: type ) -> str:
356 ''' Calculates fully-qualified name for class. '''
357 return f"{class_.__module__}.{class_.__qualname__}"
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__}"
366def discover_public_attributes(
367 attributes: cabc.Mapping[ str, a.Any ]
368) -> tuple[ str, ... ]:
369 ''' Discovers public attributes of certain types from dictionary.
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 ) ) )
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 )
393__all__ = ( )