frigid 2.0__tar.gz → 3.0__tar.gz
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-2.0 → frigid-3.0}/PKG-INFO +32 -10
- {frigid-2.0 → frigid-3.0}/README.rst +31 -9
- {frigid-2.0 → frigid-3.0}/pyproject.toml +43 -10
- {frigid-2.0 → frigid-3.0}/sources/frigid/__/dictionaries.py +10 -6
- frigid-3.0/sources/frigid/__/exceptions.py +53 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/__/imports.py +5 -1
- {frigid-2.0 → frigid-3.0}/sources/frigid/__init__.py +4 -11
- {frigid-2.0 → frigid-3.0}/sources/frigid/classes.py +128 -6
- {frigid-2.0 → frigid-3.0}/sources/frigid/dictionaries.py +4 -6
- {frigid-2.0 → frigid-3.0}/sources/frigid/exceptions.py +2 -14
- {frigid-2.0 → frigid-3.0}/sources/frigid/modules.py +4 -7
- frigid-3.0/sources/frigid/objects.py +190 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/qaliases.py +8 -4
- frigid-2.0/sources/frigid/objects.py +0 -129
- {frigid-2.0 → frigid-3.0}/.gitignore +0 -0
- {frigid-2.0 → frigid-3.0}/LICENSE.txt +0 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/__/__init__.py +0 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/__/docstrings.py +0 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/__/doctab.py +0 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/__/immutables.py +0 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/_typedecls/__builtins__.pyi +0 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/installers.py +0 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/namespaces.py +0 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/py.typed +0 -0
- {frigid-2.0 → frigid-3.0}/sources/frigid/sequences.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frigid
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0
|
|
4
4
|
Summary: Immutable data structures.
|
|
5
5
|
Project-URL: Homepage, https://github.com/emcd/python-frigid
|
|
6
6
|
Project-URL: Documentation, https://emcd.github.io/python-frigid
|
|
@@ -87,7 +87,7 @@ Key Features ⭐
|
|
|
87
87
|
be modified or removed. Also has variant for validation on initialization.
|
|
88
88
|
And provides set operations not found on `MappingProxyType
|
|
89
89
|
<https://docs.python.org/3/library/types.html#types.MappingProxyType>`_.
|
|
90
|
-
*
|
|
90
|
+
* 🗃️ **Immutable Namespace**: Similar to `SimpleNamespace
|
|
91
91
|
<https://docs.python.org/3/library/types.html#types.SimpleNamespace>`_, but
|
|
92
92
|
attributes are immutable from creation.
|
|
93
93
|
* 🧱 **Additional Types**: Classes (including abstract base classes), modules,
|
|
@@ -124,12 +124,12 @@ Examples 💡
|
|
|
124
124
|
===============================================================================
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
Immutable
|
|
127
|
+
Immutable Namespaces 🗃️
|
|
128
128
|
-------------------------------------------------------------------------------
|
|
129
129
|
|
|
130
130
|
An immutable namespace, similar to ``types.SimpleNamespace``, is available.
|
|
131
|
-
This namespace
|
|
132
|
-
|
|
131
|
+
This namespace can be initialized from multiple iterables and from keyword
|
|
132
|
+
arguments. (Keyword arguments shown below; see documentation for additional
|
|
133
133
|
forms of initialization.)
|
|
134
134
|
|
|
135
135
|
>>> from frigid import Namespace
|
|
@@ -146,13 +146,13 @@ frigid.exceptions.AttributeImmutabilityError: Cannot delete attribute 'apples'.
|
|
|
146
146
|
frigid.namespaces.Namespace( apples = 12, bananas = 6 )
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
Immutable
|
|
149
|
+
Immutable Dictionaries 📖
|
|
150
150
|
-------------------------------------------------------------------------------
|
|
151
151
|
|
|
152
|
-
An immutable dictionary, similar to ``dict``, is available. This dictionary
|
|
153
|
-
initialized from iterables and keyword arguments
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
An immutable dictionary, similar to ``dict``, is available. This dictionary can
|
|
153
|
+
be initialized from multiple iterables and from keyword arguments. (Keyword
|
|
154
|
+
arguments shown below; see documentation for additional forms of
|
|
155
|
+
initialization.)
|
|
156
156
|
|
|
157
157
|
>>> from frigid import Dictionary
|
|
158
158
|
>>> dct = Dictionary( apples = 12, bananas = 6)
|
|
@@ -168,6 +168,28 @@ frigid.exceptions.EntryImmutabilityError: Cannot delete entry for 'bananas'.
|
|
|
168
168
|
frigid.dictionaries.Dictionary( {'apples': 12, 'bananas': 6} )
|
|
169
169
|
|
|
170
170
|
|
|
171
|
+
Immutable Objects 🧱
|
|
172
|
+
-------------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
The ``immutable`` decorator can be applied to any class to make its instances fully immutable after initialization.
|
|
175
|
+
|
|
176
|
+
>>> from frigid import immutable
|
|
177
|
+
>>> @immutable
|
|
178
|
+
... class Config:
|
|
179
|
+
... def __init__( self, debug = False ):
|
|
180
|
+
... self.debug = debug
|
|
181
|
+
...
|
|
182
|
+
>>> config = Config( debug = True )
|
|
183
|
+
>>> config.verbose = True # ❌ Attempted addition raises error
|
|
184
|
+
Traceback (most recent call last):
|
|
185
|
+
...
|
|
186
|
+
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'verbose'.
|
|
187
|
+
>>> config.debug = False # ❌ Attempted reassignment raises error
|
|
188
|
+
Traceback (most recent call last):
|
|
189
|
+
...
|
|
190
|
+
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'debug'.
|
|
191
|
+
|
|
192
|
+
|
|
171
193
|
Use Cases 🎯
|
|
172
194
|
===============================================================================
|
|
173
195
|
|
|
@@ -58,7 +58,7 @@ Key Features ⭐
|
|
|
58
58
|
be modified or removed. Also has variant for validation on initialization.
|
|
59
59
|
And provides set operations not found on `MappingProxyType
|
|
60
60
|
<https://docs.python.org/3/library/types.html#types.MappingProxyType>`_.
|
|
61
|
-
*
|
|
61
|
+
* 🗃️ **Immutable Namespace**: Similar to `SimpleNamespace
|
|
62
62
|
<https://docs.python.org/3/library/types.html#types.SimpleNamespace>`_, but
|
|
63
63
|
attributes are immutable from creation.
|
|
64
64
|
* 🧱 **Additional Types**: Classes (including abstract base classes), modules,
|
|
@@ -95,12 +95,12 @@ Examples 💡
|
|
|
95
95
|
===============================================================================
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
Immutable
|
|
98
|
+
Immutable Namespaces 🗃️
|
|
99
99
|
-------------------------------------------------------------------------------
|
|
100
100
|
|
|
101
101
|
An immutable namespace, similar to ``types.SimpleNamespace``, is available.
|
|
102
|
-
This namespace
|
|
103
|
-
|
|
102
|
+
This namespace can be initialized from multiple iterables and from keyword
|
|
103
|
+
arguments. (Keyword arguments shown below; see documentation for additional
|
|
104
104
|
forms of initialization.)
|
|
105
105
|
|
|
106
106
|
>>> from frigid import Namespace
|
|
@@ -117,13 +117,13 @@ frigid.exceptions.AttributeImmutabilityError: Cannot delete attribute 'apples'.
|
|
|
117
117
|
frigid.namespaces.Namespace( apples = 12, bananas = 6 )
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
Immutable
|
|
120
|
+
Immutable Dictionaries 📖
|
|
121
121
|
-------------------------------------------------------------------------------
|
|
122
122
|
|
|
123
|
-
An immutable dictionary, similar to ``dict``, is available. This dictionary
|
|
124
|
-
initialized from iterables and keyword arguments
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
An immutable dictionary, similar to ``dict``, is available. This dictionary can
|
|
124
|
+
be initialized from multiple iterables and from keyword arguments. (Keyword
|
|
125
|
+
arguments shown below; see documentation for additional forms of
|
|
126
|
+
initialization.)
|
|
127
127
|
|
|
128
128
|
>>> from frigid import Dictionary
|
|
129
129
|
>>> dct = Dictionary( apples = 12, bananas = 6)
|
|
@@ -139,6 +139,28 @@ frigid.exceptions.EntryImmutabilityError: Cannot delete entry for 'bananas'.
|
|
|
139
139
|
frigid.dictionaries.Dictionary( {'apples': 12, 'bananas': 6} )
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
Immutable Objects 🧱
|
|
143
|
+
-------------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
The ``immutable`` decorator can be applied to any class to make its instances fully immutable after initialization.
|
|
146
|
+
|
|
147
|
+
>>> from frigid import immutable
|
|
148
|
+
>>> @immutable
|
|
149
|
+
... class Config:
|
|
150
|
+
... def __init__( self, debug = False ):
|
|
151
|
+
... self.debug = debug
|
|
152
|
+
...
|
|
153
|
+
>>> config = Config( debug = True )
|
|
154
|
+
>>> config.verbose = True # ❌ Attempted addition raises error
|
|
155
|
+
Traceback (most recent call last):
|
|
156
|
+
...
|
|
157
|
+
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'verbose'.
|
|
158
|
+
>>> config.debug = False # ❌ Attempted reassignment raises error
|
|
159
|
+
Traceback (most recent call last):
|
|
160
|
+
...
|
|
161
|
+
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'debug'.
|
|
162
|
+
|
|
163
|
+
|
|
142
164
|
Use Cases 🎯
|
|
143
165
|
===============================================================================
|
|
144
166
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# vim: set filetype=toml fileencoding=utf-8:
|
|
2
|
+
# -*- mode: toml ; coding: utf-8 -*-
|
|
3
|
+
|
|
1
4
|
[build-system]
|
|
2
5
|
requires = [
|
|
3
6
|
'hatchling',
|
|
@@ -14,6 +17,9 @@ requires-python = '>= 3.10'
|
|
|
14
17
|
dependencies = [
|
|
15
18
|
'absence',
|
|
16
19
|
'typing-extensions',
|
|
20
|
+
# --- BEGIN: Injected by Copier ---
|
|
21
|
+
|
|
22
|
+
# --- END: Injected by Copier ---
|
|
17
23
|
]
|
|
18
24
|
classifiers = [ # https://pypi.org/classifiers
|
|
19
25
|
'Development Status :: 5 - Production/Stable',
|
|
@@ -52,7 +58,7 @@ branch = true
|
|
|
52
58
|
command_line = '-m pytest' # TODO? '--fail-under'
|
|
53
59
|
data_file = '.auxiliary/caches/pytest/coverage.sqlite3'
|
|
54
60
|
parallel = true
|
|
55
|
-
source = [ 'sources'
|
|
61
|
+
source = [ 'sources' ]
|
|
56
62
|
[tool.coverage.html]
|
|
57
63
|
directory = '.auxiliary/artifacts/coverage-pytest'
|
|
58
64
|
[tool.coverage.xml]
|
|
@@ -62,10 +68,20 @@ output = '.auxiliary/artifacts/coverage-pytest/coverage.xml'
|
|
|
62
68
|
[tool.hatch.build]
|
|
63
69
|
directory = '.auxiliary/artifacts/hatch-build'
|
|
64
70
|
[tool.hatch.build.targets.sdist]
|
|
65
|
-
only-include = [
|
|
71
|
+
only-include = [
|
|
72
|
+
'sources/frigid',
|
|
73
|
+
# --- BEGIN: Injected by Copier ---
|
|
74
|
+
|
|
75
|
+
# --- END: Injected by Copier ---
|
|
76
|
+
]
|
|
66
77
|
strict-naming = false
|
|
67
78
|
[tool.hatch.build.targets.wheel]
|
|
68
|
-
only-include = [
|
|
79
|
+
only-include = [
|
|
80
|
+
'sources/frigid',
|
|
81
|
+
# --- BEGIN: Injected by Copier ---
|
|
82
|
+
|
|
83
|
+
# --- END: Injected by Copier ---
|
|
84
|
+
]
|
|
69
85
|
strict-naming = false
|
|
70
86
|
[tool.hatch.build.targets.wheel.sources]
|
|
71
87
|
'sources/frigid' = 'frigid'
|
|
@@ -79,6 +95,7 @@ dependencies = [
|
|
|
79
95
|
'coverage[toml]',
|
|
80
96
|
'furo',
|
|
81
97
|
'icecream',
|
|
98
|
+
'isort',
|
|
82
99
|
'packaging',
|
|
83
100
|
'pre-commit',
|
|
84
101
|
'pylint',
|
|
@@ -93,8 +110,16 @@ dependencies = [
|
|
|
93
110
|
'towncrier',
|
|
94
111
|
'tryceratops',
|
|
95
112
|
'yapf',
|
|
113
|
+
# --- BEGIN: Injected by Copier ---
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# --- END: Injected by Copier ---
|
|
96
118
|
]
|
|
97
119
|
post-install-commands = [
|
|
120
|
+
# --- BEGIN: Injected by Copier ---
|
|
121
|
+
|
|
122
|
+
# --- END: Injected by Copier ---
|
|
98
123
|
]
|
|
99
124
|
[tool.hatch.envs.develop.env-vars]
|
|
100
125
|
PYTHONUNBUFFERED = 'TRUE' # TODO: Only for coverage/pytest.
|
|
@@ -117,8 +142,10 @@ linters = [
|
|
|
117
142
|
sources/frigid''',
|
|
118
143
|
]
|
|
119
144
|
packagers = [
|
|
120
|
-
'hatch build',
|
|
121
|
-
#
|
|
145
|
+
'''hatch build''',
|
|
146
|
+
# --- BEGIN: Injected by Copier ---
|
|
147
|
+
|
|
148
|
+
# --- END: Injected by Copier ---
|
|
122
149
|
]
|
|
123
150
|
testers = [
|
|
124
151
|
'coverage erase',
|
|
@@ -242,9 +269,12 @@ ignored-parents = [
|
|
|
242
269
|
'builtins.TypeError',
|
|
243
270
|
'builtins.ValueError',
|
|
244
271
|
'builtins.type',
|
|
245
|
-
'frigid.__.
|
|
272
|
+
'frigid.__.exceptions.Omnierror',
|
|
273
|
+
'frigid.__.exceptions.Omniexception',
|
|
246
274
|
'frigid.objects.Class',
|
|
247
275
|
'frigid.objects.Object',
|
|
276
|
+
'typing.Protocol',
|
|
277
|
+
'typing_extensions.Protocol',
|
|
248
278
|
# --- BEGIN: Injected by Copier ---
|
|
249
279
|
'frigid.exceptions.Omnierror',
|
|
250
280
|
'frigid.exceptions.Omniexception',
|
|
@@ -296,7 +326,9 @@ disable = [
|
|
|
296
326
|
'multiple-statements',
|
|
297
327
|
'reimported',
|
|
298
328
|
'too-few-public-methods',
|
|
329
|
+
'too-many-positional-arguments', # 'too-many-arguments' is good enough
|
|
299
330
|
'ungrouped-imports',
|
|
331
|
+
'unused-wildcard-import',
|
|
300
332
|
'use-dict-literal',
|
|
301
333
|
'using-exception-groups-in-unsupported-version',
|
|
302
334
|
'wrong-import-order',
|
|
@@ -304,7 +336,7 @@ disable = [
|
|
|
304
336
|
]
|
|
305
337
|
# TODO: Latest 'per-file-ignores' code may supports dicts and lists in addition to strings.
|
|
306
338
|
per-file-ignores = '''
|
|
307
|
-
/tests/:attribute-defined-outside-init,comparison-with-itself,magic-value-comparison,missing-class-docstring,missing-function-docstring,protected-access,singleton-comparison,superfluous-parens,unexpected-keyword-arg
|
|
339
|
+
/tests/:attribute-defined-outside-init,comparison-with-itself,magic-value-comparison,missing-class-docstring,missing-function-docstring,protected-access,singleton-comparison,superfluous-parens,too-many-try-statements,unexpected-keyword-arg
|
|
308
340
|
__init__\.py:unused-import
|
|
309
341
|
''' # Note: Paths are regexes.
|
|
310
342
|
[tool.pylint.refactoring]
|
|
@@ -355,6 +387,7 @@ reportMatchNotExhaustive = true
|
|
|
355
387
|
reportMissingImports = true
|
|
356
388
|
reportMissingTypeStubs = true
|
|
357
389
|
reportMissingTypeArgument = true
|
|
390
|
+
reportPossiblyUnboundVariable = false # Covered by other linters.
|
|
358
391
|
reportPrivateImportUsage = false # Covered by other linters.
|
|
359
392
|
reportPrivateUsage = false # Covered by other linters.
|
|
360
393
|
reportSelfClsParameterName = false # Too opinionated.
|
|
@@ -380,8 +413,8 @@ stubPath = 'sources/frigid/_typedecls'
|
|
|
380
413
|
# attribute on protected modules. Instead, we use Sphinx to run doctests.
|
|
381
414
|
minversion = '8.1'
|
|
382
415
|
addopts = '--capture=no --exitfirst -rfE'
|
|
383
|
-
testpaths = [ 'tests'
|
|
384
|
-
python_files = [ '*.py' ]
|
|
416
|
+
testpaths = [ 'tests' ]
|
|
417
|
+
python_files = [ 'test_*.py' ]
|
|
385
418
|
python_functions = [ 'test_[0-9][0-9][0-9]_*' ]
|
|
386
419
|
cache_dir = '.auxiliary/caches/pytest'
|
|
387
420
|
|
|
@@ -480,4 +513,4 @@ split_penalty_after_opening_bracket = 100 # default: 300
|
|
|
480
513
|
#split_penalty_excess_character = 7000
|
|
481
514
|
#split_penalty_for_added_line_split = 30
|
|
482
515
|
#split_penalty_import_names = 0
|
|
483
|
-
#split_penalty_logical_operator = 300
|
|
516
|
+
#split_penalty_logical_operator = 300
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
# ruff: noqa: F401
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
# TODO: Consider a dictionary factory to allow 'mutables' closure
|
|
28
|
+
# to be referenced in the '__setitem__' and '__delitem__' methods.
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
from __future__ import annotations
|
|
28
32
|
|
|
29
33
|
from . import imports as __
|
|
@@ -66,11 +70,11 @@ class ImmutableDictionary(
|
|
|
66
70
|
self._behaviors_.add( _immutables.behavior_label )
|
|
67
71
|
|
|
68
72
|
def __delitem__( self, key: _H ) -> None:
|
|
69
|
-
from
|
|
73
|
+
from .exceptions import EntryImmutabilityError
|
|
70
74
|
raise EntryImmutabilityError( key )
|
|
71
75
|
|
|
72
76
|
def __setitem__( self, key: _H, value: _V ) -> None:
|
|
73
|
-
from
|
|
77
|
+
from .exceptions import EntryImmutabilityError
|
|
74
78
|
default: set[ str ] = set( )
|
|
75
79
|
if _immutables.behavior_label in getattr(
|
|
76
80
|
self, '_behaviors_', default
|
|
@@ -80,7 +84,7 @@ class ImmutableDictionary(
|
|
|
80
84
|
|
|
81
85
|
def clear( self ) -> __.typx.Never:
|
|
82
86
|
''' Raises exception. Cannot clear immutable entries. '''
|
|
83
|
-
from
|
|
87
|
+
from .exceptions import OperationInvalidity
|
|
84
88
|
raise OperationInvalidity( 'clear' )
|
|
85
89
|
|
|
86
90
|
def copy( self ) -> __.typx.Self:
|
|
@@ -91,12 +95,12 @@ class ImmutableDictionary(
|
|
|
91
95
|
self, key: _H, default: __.Absential[ _V ] = __.absent
|
|
92
96
|
) -> __.typx.Never:
|
|
93
97
|
''' Raises exception. Cannot pop immutable entry. '''
|
|
94
|
-
from
|
|
98
|
+
from .exceptions import OperationInvalidity
|
|
95
99
|
raise OperationInvalidity( 'pop' )
|
|
96
100
|
|
|
97
101
|
def popitem( self ) -> __.typx.Never:
|
|
98
102
|
''' Raises exception. Cannot pop immutable entry. '''
|
|
99
|
-
from
|
|
103
|
+
from .exceptions import OperationInvalidity
|
|
100
104
|
raise OperationInvalidity( 'popitem' )
|
|
101
105
|
|
|
102
106
|
def update( # type: ignore
|
|
@@ -105,5 +109,5 @@ class ImmutableDictionary(
|
|
|
105
109
|
**entries: __.DictionaryNominativeArgument[ _V ],
|
|
106
110
|
) -> None:
|
|
107
111
|
''' Raises exception. Cannot perform mass update. '''
|
|
108
|
-
from
|
|
112
|
+
from .exceptions import OperationInvalidity
|
|
109
113
|
raise OperationInvalidity( 'update' )
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
''' Family of exceptions for package internals. '''
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from . import imports as __
|
|
27
|
+
from . import immutables as _immutables
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Omniexception( _immutables.ImmutableObject, BaseException ):
|
|
31
|
+
''' Base for all exceptions raised internally. '''
|
|
32
|
+
|
|
33
|
+
_attribute_visibility_includes_: __.cabc.Collection[ str ] = (
|
|
34
|
+
frozenset( ( '__cause__', '__context__', ) ) )
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Omnierror( Omniexception, Exception ):
|
|
38
|
+
''' Base for error exceptions raised internally. '''
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EntryImmutabilityError( Omnierror, TypeError ):
|
|
42
|
+
''' Attempt to update or remove immutable dictionary entry. '''
|
|
43
|
+
|
|
44
|
+
def __init__( self, indicator: __.cabc.Hashable ) -> None:
|
|
45
|
+
super( ).__init__(
|
|
46
|
+
f"Cannot alter or remove existing entry for {indicator!r}." )
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class OperationInvalidity( Omnierror, RuntimeError, TypeError ):
|
|
50
|
+
''' Attempt to perform invalid operation. '''
|
|
51
|
+
|
|
52
|
+
def __init__( self, name: str ) -> None:
|
|
53
|
+
super( ).__init__( f"Operation {name!r} is not valid on this object." )
|
|
@@ -28,12 +28,16 @@ from __future__ import annotations
|
|
|
28
28
|
|
|
29
29
|
import abc
|
|
30
30
|
import collections.abc as cabc
|
|
31
|
+
import dataclasses as dcls
|
|
31
32
|
import types
|
|
32
33
|
|
|
33
34
|
import typing_extensions as typx
|
|
35
|
+
# --- BEGIN: Injected by Copier ---
|
|
34
36
|
|
|
37
|
+
# --- END: Injected by Copier ---
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
|
|
40
|
+
from absence import AbsentSingleton, Absential, absent, is_absent
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
C = typx.TypeVar( 'C' ) # Class
|
|
@@ -20,19 +20,12 @@
|
|
|
20
20
|
|
|
21
21
|
''' Immutable data structures. '''
|
|
22
22
|
|
|
23
|
-
# ruff: noqa: F401,F403
|
|
24
|
-
|
|
25
23
|
|
|
26
24
|
from . import __
|
|
27
|
-
from . import classes
|
|
28
|
-
from . import dictionaries
|
|
29
|
-
from . import exceptions
|
|
30
|
-
from . import installers
|
|
31
|
-
from . import modules
|
|
32
|
-
from . import namespaces
|
|
33
|
-
from . import objects
|
|
34
25
|
from . import qaliases
|
|
35
|
-
|
|
26
|
+
# --- BEGIN: Injected by Copier ---
|
|
27
|
+
from . import exceptions
|
|
28
|
+
# --- END: Injected by Copier ---
|
|
36
29
|
|
|
37
30
|
from .classes import *
|
|
38
31
|
from .dictionaries import *
|
|
@@ -43,7 +36,7 @@ from .objects import *
|
|
|
43
36
|
from .sequences import *
|
|
44
37
|
|
|
45
38
|
|
|
46
|
-
__version__ = '
|
|
39
|
+
__version__ = '3.0'
|
|
47
40
|
|
|
48
41
|
|
|
49
42
|
_attribute_visibility_includes_ = frozenset( ( '__version__', ) )
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
''' Immutable classes.
|
|
23
23
|
|
|
24
24
|
Provides metaclasses for creating classes with immutable attributes. Once a
|
|
25
|
-
class is initialized,
|
|
25
|
+
class is initialized, new attributes may not be assigned to it and its
|
|
26
|
+
existing attributes cannot be reassigned or deleted.
|
|
26
27
|
|
|
27
28
|
The implementation includes:
|
|
28
29
|
|
|
@@ -33,11 +34,7 @@
|
|
|
33
34
|
* ``ProtocolClass``: Metaclass for protocol classes; derived from
|
|
34
35
|
:py:class:`typing.Protocol`.
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* Creating classes with constant class attributes
|
|
39
|
-
* Defining stable abstract base classes
|
|
40
|
-
* Building protocol classes with fixed interfaces
|
|
37
|
+
Additionally, metaclasses for dataclasses are provided as a convenience.
|
|
41
38
|
|
|
42
39
|
>>> from frigid import Class
|
|
43
40
|
>>> class Example( metaclass = Class ):
|
|
@@ -106,6 +103,67 @@ Class.__doc__ = __.generate_docstring(
|
|
|
106
103
|
'class attributes immutability' )
|
|
107
104
|
|
|
108
105
|
|
|
106
|
+
@__.typx.dataclass_transform( kw_only_default = True )
|
|
107
|
+
class Dataclass( Class ):
|
|
108
|
+
''' Immutable dataclass factory. '''
|
|
109
|
+
|
|
110
|
+
def __new__( # pylint: disable=too-many-arguments
|
|
111
|
+
clscls: type[ Dataclass ],
|
|
112
|
+
name: str,
|
|
113
|
+
bases: tuple[ type, ... ],
|
|
114
|
+
namespace: dict[ str, __.typx.Any ], *,
|
|
115
|
+
decorators: ClassDecorators = ( ),
|
|
116
|
+
docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
|
|
117
|
+
mutables: __.cabc.Collection[ str ] = ( ),
|
|
118
|
+
**args: __.typx.Any
|
|
119
|
+
) -> Dataclass:
|
|
120
|
+
decorators_ = (
|
|
121
|
+
__.dcls.dataclass( kw_only = True, slots = True ),
|
|
122
|
+
*decorators )
|
|
123
|
+
return Class.__new__( # pyright: ignore
|
|
124
|
+
clscls, name, bases, namespace,
|
|
125
|
+
decorators = decorators_,
|
|
126
|
+
docstring = docstring,
|
|
127
|
+
mutables = mutables,
|
|
128
|
+
**args )
|
|
129
|
+
|
|
130
|
+
Dataclass.__doc__ = __.generate_docstring(
|
|
131
|
+
Dataclass,
|
|
132
|
+
'description of class factory class',
|
|
133
|
+
'class attributes immutability' )
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True )
|
|
137
|
+
class CompleteDataclass( Class ):
|
|
138
|
+
''' Immutable dataclass factory.
|
|
139
|
+
|
|
140
|
+
Dataclasses from this factory produce immutable instances. '''
|
|
141
|
+
def __new__( # pylint: disable=too-many-arguments
|
|
142
|
+
clscls: type[ CompleteDataclass ],
|
|
143
|
+
name: str,
|
|
144
|
+
bases: tuple[ type, ... ],
|
|
145
|
+
namespace: dict[ str, __.typx.Any ], *,
|
|
146
|
+
decorators: ClassDecorators = ( ),
|
|
147
|
+
docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
|
|
148
|
+
mutables: __.cabc.Collection[ str ] = ( ),
|
|
149
|
+
**args: __.typx.Any
|
|
150
|
+
) -> CompleteDataclass:
|
|
151
|
+
decorators_ = (
|
|
152
|
+
__.dcls.dataclass( frozen = True, kw_only = True, slots = True ),
|
|
153
|
+
*decorators )
|
|
154
|
+
return Class.__new__( # pyright: ignore
|
|
155
|
+
clscls, name, bases, namespace,
|
|
156
|
+
decorators = decorators_,
|
|
157
|
+
docstring = docstring,
|
|
158
|
+
mutables = mutables,
|
|
159
|
+
**args )
|
|
160
|
+
|
|
161
|
+
CompleteDataclass.__doc__ = __.generate_docstring(
|
|
162
|
+
CompleteDataclass,
|
|
163
|
+
'description of class factory class',
|
|
164
|
+
'class attributes immutability' )
|
|
165
|
+
|
|
166
|
+
|
|
109
167
|
class ABCFactory( __.abc.ABCMeta ):
|
|
110
168
|
''' Immutable abstract base class factory. '''
|
|
111
169
|
|
|
@@ -185,6 +243,70 @@ ProtocolClass.__doc__ = __.generate_docstring(
|
|
|
185
243
|
'class attributes immutability' )
|
|
186
244
|
|
|
187
245
|
|
|
246
|
+
# pylint: disable=bad-classmethod-argument,no-self-argument
|
|
247
|
+
@__.typx.dataclass_transform( kw_only_default = True )
|
|
248
|
+
class ProtocolDataclass( ProtocolClass ):
|
|
249
|
+
''' Immutable protocol dataclass factory. '''
|
|
250
|
+
def __new__( # pylint: disable=too-many-arguments
|
|
251
|
+
clscls: type[ ProtocolDataclass ],
|
|
252
|
+
name: str,
|
|
253
|
+
bases: tuple[ type, ... ],
|
|
254
|
+
namespace: dict[ str, __.typx.Any ], *,
|
|
255
|
+
decorators: ClassDecorators = ( ),
|
|
256
|
+
docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
|
|
257
|
+
mutables: __.cabc.Collection[ str ] = ( ),
|
|
258
|
+
**args: __.typx.Any
|
|
259
|
+
) -> ProtocolDataclass:
|
|
260
|
+
decorators_ = (
|
|
261
|
+
__.dcls.dataclass( kw_only = True, slots = True ),
|
|
262
|
+
*decorators )
|
|
263
|
+
return ProtocolClass.__new__( # pyright: ignore
|
|
264
|
+
clscls, name, bases, namespace,
|
|
265
|
+
decorators = decorators_,
|
|
266
|
+
docstring = docstring,
|
|
267
|
+
mutables = mutables,
|
|
268
|
+
**args )
|
|
269
|
+
# pylint: enable=bad-classmethod-argument,no-self-argument
|
|
270
|
+
|
|
271
|
+
ProtocolDataclass.__doc__ = __.generate_docstring(
|
|
272
|
+
ProtocolDataclass,
|
|
273
|
+
'description of class factory class',
|
|
274
|
+
'class attributes immutability' )
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# pylint: disable=bad-classmethod-argument,no-self-argument
|
|
278
|
+
@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True )
|
|
279
|
+
class CompleteProtocolDataclass( ProtocolClass ):
|
|
280
|
+
''' Immutable protocol dataclass factory.
|
|
281
|
+
|
|
282
|
+
Dataclasses from this factory produce immutable instances. '''
|
|
283
|
+
def __new__( # pylint: disable=too-many-arguments
|
|
284
|
+
clscls: type[ CompleteProtocolDataclass ],
|
|
285
|
+
name: str,
|
|
286
|
+
bases: tuple[ type, ... ],
|
|
287
|
+
namespace: dict[ str, __.typx.Any ], *,
|
|
288
|
+
decorators: ClassDecorators = ( ),
|
|
289
|
+
docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
|
|
290
|
+
mutables: __.cabc.Collection[ str ] = ( ),
|
|
291
|
+
**args: __.typx.Any
|
|
292
|
+
) -> CompleteProtocolDataclass:
|
|
293
|
+
decorators_ = (
|
|
294
|
+
__.dcls.dataclass( frozen = True, kw_only = True, slots = True ),
|
|
295
|
+
*decorators )
|
|
296
|
+
return ProtocolClass.__new__( # pyright: ignore
|
|
297
|
+
clscls, name, bases, namespace,
|
|
298
|
+
decorators = decorators_,
|
|
299
|
+
docstring = docstring,
|
|
300
|
+
mutables = mutables,
|
|
301
|
+
**args )
|
|
302
|
+
# pylint: enable=bad-classmethod-argument,no-self-argument
|
|
303
|
+
|
|
304
|
+
CompleteProtocolDataclass.__doc__ = __.generate_docstring(
|
|
305
|
+
CompleteProtocolDataclass,
|
|
306
|
+
'description of class factory class',
|
|
307
|
+
'class attributes immutability' )
|
|
308
|
+
|
|
309
|
+
|
|
188
310
|
def _accumulate_mutables(
|
|
189
311
|
class_: type, mutables: __.cabc.Collection[ str ]
|
|
190
312
|
) -> frozenset[ str ]:
|
|
@@ -82,8 +82,7 @@ class AbstractDictionary( __.cabc.Mapping[ __.H, __.V ] ):
|
|
|
82
82
|
after creation. This provides a clean interface for dictionaries
|
|
83
83
|
that should never change.
|
|
84
84
|
|
|
85
|
-
Implementations must provide
|
|
86
|
-
- __getitem__, __iter__, __len__
|
|
85
|
+
Implementations must provide __getitem__, __iter__, __len__.
|
|
87
86
|
'''
|
|
88
87
|
|
|
89
88
|
@__.abc.abstractmethod
|
|
@@ -110,6 +109,8 @@ class AbstractDictionary( __.cabc.Mapping[ __.H, __.V ] ):
|
|
|
110
109
|
class _DictionaryOperations( AbstractDictionary[ __.H, __.V ] ):
|
|
111
110
|
''' Mix-in providing additional dictionary operations. '''
|
|
112
111
|
|
|
112
|
+
# TODO? Common __init__.
|
|
113
|
+
|
|
113
114
|
def __or__( self, other: __.cabc.Mapping[ __.H, __.V ] ) -> __.typx.Self:
|
|
114
115
|
if not isinstance( other, __.cabc.Mapping ): return NotImplemented
|
|
115
116
|
conflicts = set( self.keys( ) ) & set( other.keys( ) )
|
|
@@ -170,6 +171,7 @@ class Dictionary( # pylint: disable=eq-without-hash
|
|
|
170
171
|
_objects.Object, _DictionaryOperations[ __.H, __.V ]
|
|
171
172
|
):
|
|
172
173
|
''' Immutable dictionary. '''
|
|
174
|
+
# TODO: version 2.0: Do not subclass from 'Object'.
|
|
173
175
|
|
|
174
176
|
__slots__ = ( '_data_', )
|
|
175
177
|
|
|
@@ -251,10 +253,6 @@ class Dictionary( # pylint: disable=eq-without-hash
|
|
|
251
253
|
|
|
252
254
|
Dictionary.__doc__ = __.generate_docstring(
|
|
253
255
|
Dictionary, 'dictionary entries immutability' )
|
|
254
|
-
# Register as subclass of Mapping rather than use it as mixin.
|
|
255
|
-
# We directly implement, for the sake of efficiency, the methods which the
|
|
256
|
-
# mixin would provide.
|
|
257
|
-
__.cabc.Mapping.register( Dictionary ) # type: ignore
|
|
258
256
|
|
|
259
257
|
|
|
260
258
|
class ValidatorDictionary( Dictionary[ __.H, __.V ] ):
|
|
@@ -23,11 +23,6 @@
|
|
|
23
23
|
Provides a hierarchy of exceptions that are raised when immutability is
|
|
24
24
|
violated. The hierarchy is designed to allow both specific and general
|
|
25
25
|
exception handling.
|
|
26
|
-
|
|
27
|
-
* ``Omniexception``: Base for all package exceptions
|
|
28
|
-
* ``Omnierror``: Base for all package errors
|
|
29
|
-
* ``AttributeImmutabilityError``: Raised for attribute modification
|
|
30
|
-
* ``EntryImmutabilityError``: Raised for dictionary entry modification
|
|
31
26
|
'''
|
|
32
27
|
|
|
33
28
|
|
|
@@ -59,7 +54,7 @@ class DecoratorCompatibilityError( Omnierror, TypeError ):
|
|
|
59
54
|
def __init__( self, class_name: str, method_name: str ) -> None:
|
|
60
55
|
# TODO: Use helper function to extract class name from class.
|
|
61
56
|
super( ).__init__(
|
|
62
|
-
f"Cannot apply
|
|
57
|
+
f"Cannot apply immutability decorator to {class_name!r} "
|
|
63
58
|
f"because it defines {method_name!r}.")
|
|
64
59
|
|
|
65
60
|
|
|
@@ -68,7 +63,7 @@ class EntryImmutabilityError( Omnierror, TypeError ):
|
|
|
68
63
|
|
|
69
64
|
def __init__( self, key: __.cabc.Hashable ) -> None:
|
|
70
65
|
super( ).__init__(
|
|
71
|
-
f"Cannot
|
|
66
|
+
f"Cannot add, alter, or remove entry for {key!r}." )
|
|
72
67
|
|
|
73
68
|
|
|
74
69
|
class EntryValidityError( Omnierror, ValueError ):
|
|
@@ -80,10 +75,3 @@ class EntryValidityError( Omnierror, ValueError ):
|
|
|
80
75
|
super( ).__init__(
|
|
81
76
|
f"Cannot add invalid entry with key, {indicator!r}, "
|
|
82
77
|
f"and value, {value!r}, to dictionary." )
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class OperationInvalidity( Omnierror, RuntimeError, TypeError ):
|
|
86
|
-
''' Attempt to perform invalid operation. '''
|
|
87
|
-
|
|
88
|
-
def __init__( self, name: str ) -> None:
|
|
89
|
-
super( ).__init__( f"Operation {name!r} is not valid on this object." )
|
|
@@ -66,18 +66,15 @@ def reclassify_modules(
|
|
|
66
66
|
) -> None:
|
|
67
67
|
''' Reclassifies modules to be immutable.
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
attribute immutability after conversion. It can operate on individual
|
|
71
|
-
modules or entire package hierarchies.
|
|
69
|
+
Can operate on individual modules or entire package hierarchies.
|
|
72
70
|
|
|
73
71
|
Notes
|
|
74
72
|
-----
|
|
75
73
|
* Only converts modules within the same package to prevent unintended
|
|
76
|
-
modifications to external modules
|
|
74
|
+
modifications to external modules.
|
|
77
75
|
* When used with a dictionary, converts any module objects found as
|
|
78
|
-
values if they belong to the same package
|
|
79
|
-
*
|
|
80
|
-
* Has no effect on already-immutable modules
|
|
76
|
+
values if they belong to the same package.
|
|
77
|
+
* Has no effect on already-immutable modules.
|
|
81
78
|
'''
|
|
82
79
|
from inspect import ismodule
|
|
83
80
|
from sys import modules
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
# ruff: noqa: F811
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# pylint: disable=line-too-long
|
|
25
|
+
''' Immutable objects.
|
|
26
|
+
|
|
27
|
+
Provides a base class and decorator for creating objects with immutable
|
|
28
|
+
attributes. Once an object is initialized, its attributes cannot be modified
|
|
29
|
+
or deleted.
|
|
30
|
+
|
|
31
|
+
>>> from frigid import Object
|
|
32
|
+
>>> class Point( Object ):
|
|
33
|
+
... def __init__( self, x, y ):
|
|
34
|
+
... self.x = x
|
|
35
|
+
... self.y = y
|
|
36
|
+
... super( ).__init__( )
|
|
37
|
+
...
|
|
38
|
+
>>> obj = Point( 1, 2 ) # Initialize with attributes
|
|
39
|
+
>>> obj.z = 3 # Attempt to add attribute
|
|
40
|
+
Traceback (most recent call last):
|
|
41
|
+
...
|
|
42
|
+
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'z'.
|
|
43
|
+
>>> obj.x = 4 # Attempt modification
|
|
44
|
+
Traceback (most recent call last):
|
|
45
|
+
...
|
|
46
|
+
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'.
|
|
47
|
+
|
|
48
|
+
The `immutable` decorator can be used to make any class immutable:
|
|
49
|
+
|
|
50
|
+
>>> from frigid import immutable
|
|
51
|
+
>>> @immutable
|
|
52
|
+
... class Config:
|
|
53
|
+
... def __init__( self, verbose = False ):
|
|
54
|
+
... self.verbose = verbose
|
|
55
|
+
...
|
|
56
|
+
>>> config = Config( verbose = True )
|
|
57
|
+
>>> config.verbose = False # Attempt to modify attribute
|
|
58
|
+
Traceback (most recent call last):
|
|
59
|
+
...
|
|
60
|
+
frigid.exceptions.AttributeImmutabilityError: ...
|
|
61
|
+
'''
|
|
62
|
+
# pylint: enable=line-too-long
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
from . import __
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_behavior = 'immutability'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _check_behavior( obj: object ) -> bool:
|
|
72
|
+
behaviors: __.cabc.MutableSet[ str ]
|
|
73
|
+
if _check_dict( obj ):
|
|
74
|
+
attributes = getattr( obj, '__dict__' )
|
|
75
|
+
behaviors = attributes.get( '_behaviors_', set( ) )
|
|
76
|
+
else: behaviors = getattr( obj, '_behaviors_', set( ) )
|
|
77
|
+
return _behavior in behaviors
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _check_dict( obj: object ) -> bool:
|
|
81
|
+
# Return False even if '__dict__' in '__slots__'.
|
|
82
|
+
if hasattr( obj, '__slots__' ): return False
|
|
83
|
+
return hasattr( obj, '__dict__' )
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@__.typx.overload
|
|
87
|
+
def immutable( # pragma: no branch
|
|
88
|
+
class_: type[ __.C ], *,
|
|
89
|
+
docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
|
|
90
|
+
mutables: __.cabc.Collection[ str ] = ( )
|
|
91
|
+
) -> type[ __.C ]: ...
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@__.typx.overload
|
|
95
|
+
def immutable( # pragma: no branch
|
|
96
|
+
class_: __.AbsentSingleton, *,
|
|
97
|
+
docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
|
|
98
|
+
mutables: __.cabc.Collection[ str ] = ( )
|
|
99
|
+
) -> __.typx.Callable[ [ type[ __.C ] ], type[ __.C ] ]: ...
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def immutable( # pylint: disable=too-complex,too-many-statements
|
|
103
|
+
class_: __.Absential[ type[ __.C ] ] = __.absent, *,
|
|
104
|
+
docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
|
|
105
|
+
mutables: __.cabc.Collection[ str ] = ( )
|
|
106
|
+
) -> __.typx.Union[
|
|
107
|
+
type[ __.C ], __.typx.Callable[ [ type[ __.C ] ], type[ __.C ] ]
|
|
108
|
+
]:
|
|
109
|
+
''' Decorator which makes class immutable after initialization.
|
|
110
|
+
|
|
111
|
+
Cannot be applied to classes which define their own __setattr__
|
|
112
|
+
or __delattr__ methods.
|
|
113
|
+
|
|
114
|
+
This decorator can be used in different ways:
|
|
115
|
+
|
|
116
|
+
1. Simple decorator:
|
|
117
|
+
|
|
118
|
+
>>> @immutable
|
|
119
|
+
... class Config:
|
|
120
|
+
... pass
|
|
121
|
+
|
|
122
|
+
2. With parameters:
|
|
123
|
+
|
|
124
|
+
>>> @immutable( mutables = ( 'version', ) )
|
|
125
|
+
... class Config:
|
|
126
|
+
... pass
|
|
127
|
+
'''
|
|
128
|
+
def decorator( cls: type[ __.C ] ) -> type[ __.C ]: # pylint: disable=too-many-statements
|
|
129
|
+
if not __.is_absent( docstring ): cls.__doc__ = docstring
|
|
130
|
+
for method in ( '__setattr__', '__delattr__' ):
|
|
131
|
+
if method in cls.__dict__:
|
|
132
|
+
from .exceptions import DecoratorCompatibilityError
|
|
133
|
+
raise DecoratorCompatibilityError( cls.__name__, method )
|
|
134
|
+
original_init = next(
|
|
135
|
+
base.__dict__[ '__init__' ] for base in cls.__mro__
|
|
136
|
+
if '__init__' in base.__dict__ ) # pylint: disable=magic-value-comparison
|
|
137
|
+
mutables_ = frozenset( mutables )
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any
|
|
141
|
+
) -> None:
|
|
142
|
+
original_init( self, *posargs, **nomargs )
|
|
143
|
+
behaviors: __.cabc.MutableSet[ str ]
|
|
144
|
+
if _check_dict( self ):
|
|
145
|
+
attributes = getattr( self, '__dict__' )
|
|
146
|
+
behaviors = attributes.get( '_behaviors_', set( ) )
|
|
147
|
+
if not behaviors: attributes[ '_behaviors_' ] = behaviors
|
|
148
|
+
else:
|
|
149
|
+
behaviors = getattr( self, '_behaviors_', set( ) )
|
|
150
|
+
if not behaviors: setattr( self, '_behaviors_', behaviors )
|
|
151
|
+
behaviors.add( _behavior )
|
|
152
|
+
|
|
153
|
+
def __delattr__( self: object, name: str ) -> None:
|
|
154
|
+
if name in mutables_:
|
|
155
|
+
super( cls, self ).__delattr__( name )
|
|
156
|
+
return
|
|
157
|
+
if _check_behavior( self ): # pragma: no branch
|
|
158
|
+
from .exceptions import AttributeImmutabilityError
|
|
159
|
+
raise AttributeImmutabilityError( name )
|
|
160
|
+
super( cls, self ).__delattr__( name ) # pragma: no cover
|
|
161
|
+
|
|
162
|
+
def __setattr__( self: object, name: str, value: __.typx.Any ) -> None:
|
|
163
|
+
if name in mutables_:
|
|
164
|
+
super( cls, self ).__setattr__( name, value )
|
|
165
|
+
return
|
|
166
|
+
if _check_behavior( self ):
|
|
167
|
+
from .exceptions import AttributeImmutabilityError
|
|
168
|
+
raise AttributeImmutabilityError( name )
|
|
169
|
+
super( cls, self ).__setattr__( name, value )
|
|
170
|
+
|
|
171
|
+
cls.__init__ = __init__
|
|
172
|
+
cls.__delattr__ = __delattr__
|
|
173
|
+
cls.__setattr__ = __setattr__
|
|
174
|
+
return cls
|
|
175
|
+
|
|
176
|
+
if not __.is_absent( class_ ): return decorator( class_ )
|
|
177
|
+
return decorator # No class to decorate; keyword arguments only.
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@immutable
|
|
181
|
+
class Object:
|
|
182
|
+
''' Immutable objects. '''
|
|
183
|
+
|
|
184
|
+
__slots__ = ( '__dict__', '_behaviors_' )
|
|
185
|
+
|
|
186
|
+
def __repr__( self ) -> str:
|
|
187
|
+
return "{fqname}( )".format( fqname = __.calculate_fqname( self ) )
|
|
188
|
+
|
|
189
|
+
Object.__doc__ = __.generate_docstring(
|
|
190
|
+
Object, 'instance attributes immutability' )
|
|
@@ -42,9 +42,13 @@
|
|
|
42
42
|
|
|
43
43
|
from . import __
|
|
44
44
|
from .classes import (
|
|
45
|
-
ABCFactory as
|
|
46
|
-
Class as
|
|
47
|
-
|
|
45
|
+
ABCFactory as ImmutableABCFactory,
|
|
46
|
+
Class as ImmutableClass,
|
|
47
|
+
CompleteDataclass as ImmutableCompleteDataclass,
|
|
48
|
+
CompleteProtocolDataclass as ImmutableCompleteProtocolDataclass,
|
|
49
|
+
Dataclass as ImmutableDataclass,
|
|
50
|
+
ProtocolClass as ImmutableProtocolClass,
|
|
51
|
+
ProtocolDataclass as ImmutableProtocolDataclass,
|
|
48
52
|
)
|
|
49
53
|
from .dictionaries import (
|
|
50
54
|
AbstractDictionary as AbstractImmutableDictionary,
|
|
@@ -60,5 +64,5 @@ from .namespaces import (
|
|
|
60
64
|
)
|
|
61
65
|
from .objects import (
|
|
62
66
|
Object as ImmutableObject,
|
|
63
|
-
|
|
67
|
+
immutable,
|
|
64
68
|
)
|
|
@@ -1,129 +0,0 @@
|
|
|
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
|
-
# pylint: disable=line-too-long
|
|
22
|
-
''' Immutable objects.
|
|
23
|
-
|
|
24
|
-
Provides a base class and decorator for creating objects with immutable
|
|
25
|
-
attributes. Once an object is initialized, its attributes cannot be modified
|
|
26
|
-
or deleted.
|
|
27
|
-
|
|
28
|
-
The implementation uses a special dictionary type for attribute storage that
|
|
29
|
-
enforces immutability. This makes it suitable for:
|
|
30
|
-
|
|
31
|
-
* Configuration objects
|
|
32
|
-
* Value objects
|
|
33
|
-
* Immutable data containers
|
|
34
|
-
* Objects requiring attribute stability
|
|
35
|
-
|
|
36
|
-
>>> from frigid import Object
|
|
37
|
-
>>> class Point( Object ):
|
|
38
|
-
... def __init__( self, x, y ):
|
|
39
|
-
... self.x = x
|
|
40
|
-
... self.y = y
|
|
41
|
-
... super( ).__init__( )
|
|
42
|
-
...
|
|
43
|
-
>>> obj = Point( 1, 2 ) # Initialize with attributes
|
|
44
|
-
>>> obj.z = 3 # Attempt to add attribute
|
|
45
|
-
Traceback (most recent call last):
|
|
46
|
-
...
|
|
47
|
-
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'z'.
|
|
48
|
-
>>> obj.x = 4 # Attempt modification
|
|
49
|
-
Traceback (most recent call last):
|
|
50
|
-
...
|
|
51
|
-
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'.
|
|
52
|
-
'''
|
|
53
|
-
# pylint: enable=line-too-long
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
from . import __
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _check_behavior( obj: object ) -> bool:
|
|
60
|
-
behaviors: __.cabc.MutableSet[ str ]
|
|
61
|
-
if _check_dict( obj ):
|
|
62
|
-
attributes = getattr( obj, '__dict__' )
|
|
63
|
-
behaviors = attributes.get( '_behaviors_', set( ) )
|
|
64
|
-
else: behaviors = getattr( obj, '_behaviors_', set( ) )
|
|
65
|
-
return __.behavior_label in behaviors
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _check_dict( obj: object ) -> bool:
|
|
69
|
-
# Return False even if '__dict__' in '__slots__'.
|
|
70
|
-
if hasattr( obj, '__slots__' ): return False
|
|
71
|
-
return hasattr( obj, '__dict__' )
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def immutable( class_: type[ __.C ] ) -> type[ __.C ]: # pylint: disable=too-complex
|
|
75
|
-
''' Decorator which makes class immutable after initialization.
|
|
76
|
-
|
|
77
|
-
Cannot be applied to classes which define their own __setattr__
|
|
78
|
-
or __delattr__ methods.
|
|
79
|
-
'''
|
|
80
|
-
for method in ( '__setattr__', '__delattr__' ):
|
|
81
|
-
if method in class_.__dict__:
|
|
82
|
-
from .exceptions import DecoratorCompatibilityError
|
|
83
|
-
raise DecoratorCompatibilityError( class_.__name__, method )
|
|
84
|
-
original_init = next(
|
|
85
|
-
base.__dict__[ '__init__' ] for base in class_.__mro__
|
|
86
|
-
if '__init__' in base.__dict__ ) # pylint: disable=magic-value-comparison
|
|
87
|
-
|
|
88
|
-
def __init__(
|
|
89
|
-
self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any
|
|
90
|
-
) -> None:
|
|
91
|
-
# TODO: Use accretive set for behaviors.
|
|
92
|
-
original_init( self, *posargs, **nomargs )
|
|
93
|
-
behaviors: __.cabc.MutableSet[ str ]
|
|
94
|
-
if _check_dict( self ):
|
|
95
|
-
attributes = getattr( self, '__dict__' )
|
|
96
|
-
behaviors = attributes.get( '_behaviors_', set( ) )
|
|
97
|
-
if not behaviors: attributes[ '_behaviors_' ] = behaviors
|
|
98
|
-
setattr( self, '__dict__', __.ImmutableDictionary( attributes ) )
|
|
99
|
-
else:
|
|
100
|
-
behaviors = getattr( self, '_behaviors_', set( ) )
|
|
101
|
-
if not behaviors: setattr( self, '_behaviors_', behaviors )
|
|
102
|
-
behaviors.add( __.behavior_label )
|
|
103
|
-
|
|
104
|
-
def __delattr__( self: object, name: str ) -> None:
|
|
105
|
-
if _check_behavior( self ):
|
|
106
|
-
from .exceptions import AttributeImmutabilityError
|
|
107
|
-
raise AttributeImmutabilityError( name )
|
|
108
|
-
super( class_, self ).__delattr__( name )
|
|
109
|
-
|
|
110
|
-
def __setattr__( self: object, name: str, value: __.typx.Any ) -> None:
|
|
111
|
-
if _check_behavior( self ):
|
|
112
|
-
from .exceptions import AttributeImmutabilityError
|
|
113
|
-
raise AttributeImmutabilityError( name )
|
|
114
|
-
super( class_, self ).__setattr__( name, value )
|
|
115
|
-
|
|
116
|
-
class_.__init__ = __init__
|
|
117
|
-
class_.__delattr__ = __delattr__
|
|
118
|
-
class_.__setattr__ = __setattr__
|
|
119
|
-
return class_
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
@immutable
|
|
123
|
-
class Object:
|
|
124
|
-
''' Immutable objects. '''
|
|
125
|
-
|
|
126
|
-
__slots__ = ( '__dict__', '_behaviors_' )
|
|
127
|
-
|
|
128
|
-
Object.__doc__ = __.generate_docstring(
|
|
129
|
-
Object, 'instance attributes immutability' )
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|