Coverage for sources/frigid/__.py: 100%
162 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-05 01:36 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-05 01:36 +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
27from __future__ import annotations
29import collections.abc as cabc
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)
45from . import _annotations as a
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' )
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 ]
78behavior_label = 'immutability'
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
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
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
115class InternalClass( type ):
116 ''' Concealment and immutability on class attributes. '''
118 _class_attribute_visibility_includes_: cabc.Collection[ str ] = (
119 frozenset( ) )
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 )
132 def __init__( selfclass, *posargs: a.Any, **nomargs: a.Any ):
133 super( ).__init__( *posargs, **nomargs )
134 _immutable_class__init__( selfclass )
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 ) )
145 def __delattr__( selfclass, name: str ) -> None:
146 if not _immutable_class__delattr__( selfclass, name ):
147 super( ).__delattr__( name )
149 def __setattr__( selfclass, name: str, value: a.Any ) -> None:
150 if not _immutable_class__setattr__( selfclass, name ):
151 super( ).__setattr__( name, value )
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
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 } )
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_ ) ) )
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_ ) ) )
208class ConcealerExtension:
209 ''' Conceals instance attributes according to some criteria.
211 By default, public attributes are displayed.
212 '''
214 _attribute_visibility_includes_: cabc.Collection[ str ] = frozenset( )
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_ ) )
223class InternalObject( ConcealerExtension, metaclass = InternalClass ):
224 ''' Concealment and immutability on instance attributes. '''
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 ) ) )
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 ) ) )
239class Falsifier( metaclass = InternalClass ): # pylint: disable=eq-without-hash
240 ''' Produces falsey objects.
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 '''
248 def __bool__( self ) -> bool: return False
250 def __eq__( self, other: a.Any ) -> ComparisonResult:
251 return self is other
253 def __ne__( self, other: a.Any ) -> ComparisonResult:
254 return self is not other
257class Absent( Falsifier, InternalObject ):
258 ''' Type of the sentinel for option without default value. '''
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 )
267Optional: a.TypeAlias = V | Absent
268absent: a.Annotation[
269 Absent, a.Doc( ''' Sentinel for option with no default value. ''' )
270] = Absent( )
273def is_absent( value: object ) -> a.TypeIs[ Absent ]:
274 ''' Checks if a value is absent or not. '''
275 return absent is value
278class ImmutableDictionary(
279 ConcealerExtension,
280 dict[ _H, _V ],
281 a.Generic[ _H, _V ],
282):
283 ''' Immutable subclass of :py:class:`dict`.
285 Can be used as an instance dictionary.
287 Prevents attempts to mutate dictionary via inherited interface.
288 '''
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 )
309 def __delitem__( self, key: _H ) -> None:
310 from .exceptions import EntryImmutabilityError
311 raise EntryImmutabilityError( key )
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 )
322 def clear( self ) -> a.Never:
323 ''' Raises exception. Cannot clear immutable entries. '''
324 from .exceptions import OperationValidityError
325 raise OperationValidityError( 'clear' )
327 def copy( self ) -> a.Self:
328 ''' Provides fresh copy of dictionary. '''
329 return type( self )( self )
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' )
338 def popitem( self ) -> a.Never:
339 ''' Raises exception. Cannot pop immutable entry. '''
340 from .exceptions import OperationValidityError
341 raise OperationValidityError( 'popitem' )
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' )
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__ = ( )