frigid 1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
frigid/__.py ADDED
@@ -0,0 +1,395 @@
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
+
27
+ from __future__ import annotations
28
+
29
+ import collections.abc as cabc
30
+
31
+ from abc import (
32
+ ABCMeta as ABCFactory,
33
+ abstractmethod as abstract_member_function,
34
+ )
35
+ from functools import partial as partial_function
36
+ from inspect import cleandoc as clean_docstring
37
+ from sys import modules
38
+ from types import (
39
+ MappingProxyType as DictionaryProxy,
40
+ ModuleType as Module,
41
+ NotImplementedType as TypeofNotImplemented,
42
+ SimpleNamespace,
43
+ )
44
+
45
+ from . import _annotations as a
46
+
47
+
48
+ C = a.TypeVar( 'C' ) # Class
49
+ H = a.TypeVar( 'H', bound = cabc.Hashable ) # Hash Key
50
+ V = a.TypeVar( 'V' ) # Value
51
+ _H = a.TypeVar( '_H' )
52
+ _V = a.TypeVar( '_V' )
53
+
54
+ ClassDecorators: a.TypeAlias = (
55
+ cabc.Iterable[ cabc.Callable[ [ type ], type ] ] )
56
+ ComparisonResult: a.TypeAlias = bool | TypeofNotImplemented
57
+ DictionaryNominativeArgument: a.TypeAlias = a.Annotation[
58
+ V,
59
+ a.Doc(
60
+ 'Zero or more keyword arguments from which to initialize '
61
+ 'dictionary data.' ),
62
+ ]
63
+ DictionaryPositionalArgument: 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
+ ]
70
+ DictionaryValidator: a.TypeAlias = a.Annotation[
71
+ cabc.Callable[ [ H, V ], bool ],
72
+ a.Doc( 'Callable which validates entries before addition to dictionary.' ),
73
+ ]
74
+ ModuleReclassifier: a.TypeAlias = cabc.Callable[
75
+ [ cabc.Mapping[ str, a.Any ] ], None ]
76
+
77
+
78
+ behavior_label = 'immutability'
79
+
80
+
81
+ def 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
+
90
+ def _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
+
115
+ class 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
+
154
+ def _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
+
174
+ def _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
+
184
+ def _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
+
196
+ def _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
+
208
+ class 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
+
223
+ class 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
+
239
+ class 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
+
257
+ class 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
+
267
+ Optional: a.TypeAlias = V | Absent
268
+ absent: a.Annotation[
269
+ Absent, a.Doc( ''' Sentinel for option with no default value. ''' )
270
+ ] = Absent( )
271
+
272
+
273
+ def is_absent( value: object ) -> a.TypeIs[ Absent ]:
274
+ ''' Checks if a value is absent or not. '''
275
+ return absent is value
276
+
277
+
278
+ class 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
+
353
+ class Docstring( str ):
354
+ ''' Dedicated docstring container. '''
355
+
356
+
357
+ def calculate_class_fqname( class_: type ) -> str:
358
+ ''' Calculates fully-qualified name for class. '''
359
+ return f"{class_.__module__}.{class_.__qualname__}"
360
+
361
+
362
+ def 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
+
368
+ def 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
+
380
+ def 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__ = ( )
frigid/__init__.py ADDED
@@ -0,0 +1,59 @@
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
+ ''' Immutable data structures. '''
22
+
23
+ # ruff: noqa: F401,F403
24
+
25
+
26
+ from . import __
27
+ from . import classes
28
+ from . import dictionaries
29
+ from . import exceptions
30
+ from . import modules
31
+ from . import namespaces
32
+ from . import objects
33
+ from . import qaliases
34
+
35
+ from .classes import *
36
+ from .dictionaries import *
37
+ from .modules import *
38
+ from .namespaces import *
39
+ from .objects import *
40
+
41
+
42
+ __version__ = '1.0'
43
+
44
+
45
+ _attribute_visibility_includes_ = frozenset( ( '__version__', ) )
46
+
47
+ class _InternalModule( __.InternalObject, __.Module ): pass # type: ignore
48
+ def _reclassify_modules(
49
+ attributes: __.cabc.Mapping[ str, __.a.Any ]
50
+ ) -> None:
51
+ from inspect import ismodule
52
+ for attribute in attributes.values( ):
53
+ if not ismodule( attribute ): continue
54
+ if isinstance( attribute, _InternalModule ):
55
+ continue # pragma: no coverage
56
+ attribute.__class__ = _InternalModule
57
+
58
+ _reclassify_modules( globals( ) )
59
+ __.modules[ __name__ ].__class__ = _InternalModule
frigid/_annotations.py ADDED
@@ -0,0 +1,42 @@
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
+ ''' Standard annotations across Python versions. '''
22
+
23
+ # ruff: noqa: F401
24
+ # pylint: disable=unused-import
25
+
26
+
27
+ from typing_extensions import (
28
+ Annotated as Annotation,
29
+ Any,
30
+ Doc,
31
+ Generic,
32
+ Never,
33
+ Optional as Nullable,
34
+ Protocol,
35
+ Self,
36
+ TypeAlias,
37
+ TypeIs,
38
+ TypeVar,
39
+ )
40
+
41
+
42
+ __all__ = ( )
frigid/_docstrings.py ADDED
@@ -0,0 +1,72 @@
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
+ ''' Docstrings table for reuse across subpackages. '''
22
+
23
+
24
+ from types import MappingProxyType as _DictionaryProxy
25
+
26
+
27
+ TABLE: _DictionaryProxy[ str, str ] = _DictionaryProxy( {
28
+
29
+ 'class attributes immutability': '''
30
+ Prevents assignment or deletion of class attributes after class creation.
31
+ ''',
32
+
33
+ 'description of class factory class': '''
34
+ Derived from :py:class:`type`, this is a metaclass. A metaclass is a class
35
+ factory class. I.e., it is a class that produces other classes as its
36
+ instances.
37
+ ''',
38
+
39
+ 'description of module': '''
40
+ Derived from :py:class:`types.ModuleType`, this class is suitable for use as a
41
+ Python module class.
42
+ ''',
43
+
44
+ 'description of namespace': '''
45
+ A namespace is an object, whose attributes can be determined from iterables and
46
+ keyword arguments, at initialization time. The string representation of the
47
+ namespace object reflects its current instance attributes. Modeled after
48
+ :py:class:`types.SimpleNamespace`.
49
+ ''',
50
+
51
+ 'dictionary entries immutability': '''
52
+ Prevents addition, alteration, or removal of dictionary entries after creation.
53
+ ''',
54
+
55
+ 'dictionary entries validation': '''
56
+ When an attempt to create a dictionary with entries, each entry is validated
57
+ against supplied criteria. If validation fails for any entry, then the
58
+ dictionary creation is rejected.
59
+ ''',
60
+
61
+ 'instance attributes immutability': '''
62
+ Prevents assignment or deletion of instance attributes after instance creation.
63
+ ''',
64
+
65
+ 'module attributes immutability': '''
66
+ Prevents assignment or deletion of module attributes after module creation.
67
+
68
+ This behavior helps ensure that module-level constants remain constant and that
69
+ module interfaces remain stable during runtime.
70
+ ''',
71
+
72
+ } )
@@ -0,0 +1,28 @@
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
+ ''' Stubs for additional "builtin" types. '''
22
+
23
+ # https://github.com/microsoft/pyright/blob/main/docs/builtins.md
24
+
25
+
26
+ from icecream import IceCreamDebugger # type: ignore
27
+
28
+ ic: IceCreamDebugger