unx-immutable 1.0.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.
@@ -0,0 +1,21 @@
1
+ Copyright 2025 Paul <unixator unixator@proton.me>
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
14
+
15
+ ---
16
+ AI USE NOTICE
17
+ This project allows its code to be analyzed or used in machine-learning
18
+ and AI systems. However, any reproduction or redistribution of generated
19
+ code that substantially copies this source without attribution constitutes
20
+ a violation of the License’s attribution requirements.
21
+
@@ -0,0 +1,358 @@
1
+ Metadata-Version: 2.4
2
+ Name: unx-immutable
3
+ Version: 1.0.0
4
+ Summary: Allows to make class and instance attributes immutable, supports a few different modes.
5
+ Keywords: immutable,frozen,freezable,readonly
6
+ Author: Paul
7
+ Author-email: Paul <unixator@proton.me>
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Maintainer: Paul
16
+ Maintainer-email: Paul <unixator@proton.me>
17
+ Requires-Python: >=3.13
18
+ Project-URL: Repository, https://codeberg.org/unixator/immutable.py
19
+ Project-URL: Documentation, https://codeberg.org/unixator/immutable.py/src/branch/release/README.md
20
+ Project-URL: Issues, https://codeberg.org/unixator/immutable.py/issues
21
+ Project-URL: Changelog, https://codeberg.org/unixator/immutable.py/src/branch/release/CHANGELOG.md
22
+ Project-URL: Mirror, https://gitlab.com/unixator/immutable.py
23
+ Description-Content-Type: text/markdown
24
+
25
+ # python package: immutable
26
+ Package implements a few different modes of object immutability.
27
+
28
+
29
+ - [License](#license)
30
+ - [Repository](#repository)
31
+ - [Versioning](#versioning)
32
+ - [Branch strategy](#branch-strategy)
33
+ - [Quick introduction](#quick-introduction)
34
+ - [Immutability flags](#immutability-flags)
35
+ - [ImmutabilityMode](#immutabilitymode)
36
+ - [Immutability implementation](#objects)
37
+ - [ImmutableClass](#immutableclass)
38
+ - [ImmutableObject](#immutableobject)
39
+ - [Immutable](#immutable)
40
+
41
+
42
+
43
+ ## License
44
+ This project is licensed under the [Apache License 2.0](./LICENSE).
45
+
46
+ The author permits this code to be used for AI training, analysis, and
47
+ research. However, reproducing this source code or its derivatives without
48
+ proper attribution violates the Apache 2.0 License.
49
+
50
+
51
+ ## Repository
52
+ The main repository is: https://codeberg.org/unixator/immutable.py
53
+ Mirror on GitLab: https://gitlab.com/unixator/immutable.py
54
+
55
+
56
+ ### Versioning
57
+ The next versioning scheme `vX.Y.Z` is used, where:
58
+ - `X`: (major) reflects current stable version of interface.
59
+ - It must be increased in case of incompatible changes, when the code that use this package must be updated to use the new version.
60
+ - 0: means developing stage, so it can become incompatible without increasing major part of version.
61
+ - 1: is going to be the first stable release version.
62
+ - `Y`: (minor) it must be changed with new added functionalities which do not break compatibility.
63
+ - `Z`: (patch): for fixes/improvements which does not change anything to the end users (internal improvements).
64
+
65
+ > additional flags are not supported like +build or -rc, -beta, etc.
66
+
67
+
68
+
69
+ ### Branch strategy
70
+ Branch agreement:
71
+ - The `release` branch:
72
+ - It's the default branch which contain only stable versions of package (Merge request only for stable code).
73
+ - all stable versions have signed annotated tag to help distinguish commits pointing to the stable version.
74
+ - all tagged versions have releases (codeberg/gitlab/pypi)
75
+ - 0.x.x: Developing branch which will be removed after releasing 1.x.x.
76
+ - 1.x.x: Current LTS release.
77
+ - 1.x.x means <n>.x.x where "x.x" is just a str, not pointer to the acutal version. Only the first (major) number is going to be changed.
78
+ - Branch always point to the latest 1.x.x stable version.
79
+ - For now 1.x.x and release should be the same
80
+ - The `RC` branch:
81
+ - This branch contains the newest release candidate version of the package, which have not been released yet.
82
+ - Code in this branch should be tested and covered with unittests if applicable, it's like open-beta stage.
83
+ - When code is merged here, the version is already bumped.
84
+ - Any other branches should be threated as developing ones and are not recommended for using until one knows what they are doing.
85
+
86
+
87
+
88
+
89
+ ## Quick introduction
90
+ - [exceptions.py](src/unx/immutable/exceptions.py) represents exceptions used by the package
91
+ - [mode.py](src/unx/immutable/mode.py) defines different immutability modes.
92
+ - [obj.py](src/unx/immutable/obj.py) provides base clases/metaclasses to apply immutability modes to the objects.
93
+
94
+ All entities can be imported from the `__init__.py` directly, so there is no need to import all files.
95
+ Quick example how to create a custom class, where class attributes are frozen during class creating, and instance attributes are frozen after initialization:
96
+ ```python
97
+ from unx.immutable import ImmutableError, Immutable, IMMUTABLE
98
+
99
+ class A(Immutable, mode=IMMUTABLE):
100
+ cls_attr1: int = 15
101
+
102
+ def __init__(self):
103
+ self.instance_attr1 = "Attr"
104
+ self.immutability.freeze()
105
+
106
+ try:
107
+ A.cls_attr1 = 42
108
+ A.instance_attr1 = "New str"
109
+ except ImmutableError:
110
+ print("it happens on the class attribute but it's too late for both")
111
+ ```
112
+
113
+
114
+
115
+ ## Immutability flags
116
+
117
+ Instead of using one **"read-only"** flag to mark an object as immutable,
118
+ the package supports a few immutability flags for different object's parts to give more flexibility to freeze them independently.
119
+
120
+ Flags are defined as number constants but set of flags is represented as a single unsigned integer
121
+ where one flag takes one bit and **0** means no restrictions.
122
+
123
+ > Please, do not use numbers directly to choose a mode.
124
+ > In the [mode.py](src/unx/immutable/mode.py) file there are pre-defined constants for all supported flags and their naming consistency is garanteed.
125
+ > For instance the `IMMUTABLE` constant always means all flags are raised so the actual value depends on the amount of flags.
126
+
127
+
128
+ Next table represents list of supported constants, flag ids, and short description:
129
+ | ID | CONSTANT NAME | DESCRIPTION |
130
+ | -- | :-----------: | :---------- |
131
+ | 0 | `UNRESTRICTED` | No flags are raised, no restrictions are applied, The target class must not be restricted. |
132
+ | 1 | `NO_DEL` | The `ImmutableError` exception is raised to prevent any attribute/item deletion. |
133
+ | 2 | `NO_NEW` | The `ImmutableError` is raised if new attr/item is going to be added. |
134
+ | 3 | `SEALED` | The same as `NO_NEW \| NO_DEL`); no attr/item deletion or adding. |
135
+ | 4 | `IMMUTABLE_NONE` | The `None` values cannot be replaced with other. |
136
+ | 8 | `IMMUTABLE_EMPTY` | The **"EMPTY"** (`not bool(value)`) values cannot be modified. |
137
+ | 16 | `IMMUTABLE_EVALUATED` | Already defined values (`bool(value)`) cannot be modified. |
138
+ | 28 | `IMMUTABLE_EXISTED` | Combination of the `IMMUTABLE_NONE \| IMMUTABLE_EMPTY \| IMMUTABLE_EVALUATED`, all existing attrs/items are immutable. |
139
+ | 31 | `IMMUTABLE` | `SEALED \| IMMUTABLE_EXISTED` gives full immutability. |
140
+
141
+
142
+ > All constants can be imported directly: `from unx.immutable import IMMUTABLE, IMMUTABLE_EXISTED, SEALED`
143
+
144
+
145
+
146
+ ### ImmutabilityMode
147
+ The `ImmutabilityMode` class has been created to store immutability flags under one namespace,
148
+ provide methods to raise separate flags, and properties to read their state.
149
+
150
+ This class is only about storing and managing but not restricting anything by itself.
151
+
152
+
153
+ > **Invariant:** The state is monotonic — flags can only be raised, up to full immutability and never lowered.
154
+
155
+
156
+ All methods that raise flags return `self`.
157
+ So, method calls can be chained, for example:
158
+ ```python
159
+ fm = ImmutableMode().seal().freeze_none()
160
+ ```
161
+
162
+
163
+ Code example:
164
+ ```python
165
+ mode1 = ImmutabilityMode(11) # not recommended to use numbers directly, adding new flags in the future can break such code.
166
+ mode2 = ImmutabilityMode(IMMUTABLE_NONE | SEALED)
167
+ mode3 = ImmutabilityMode().freeze_none().seal()
168
+ assert mode1 == mode2 == mode3 # modes are comparable
169
+ assert ImmutabilityMode(SEALED) < ImmutabilityMode(IMMUTABLE_NONE)
170
+ assert ImmutabilityMode().seal() < ImmutabilityMode().freeze_none()
171
+
172
+ mode = ImmutabilityMode() # by default mode is UNRESTRICTED (0), no flags are raised.
173
+ assert bool(mode) is False # False means no flags have been raised.
174
+ assert mode.state == 0 # the current mode
175
+ assert bool(ImmutabilityMode().forbid_new()) is True # since NO_NEW flag has been raised.
176
+
177
+ assert mode.immutable_none is False # Shows if the IMMUTABLE_NONE flag is raised.
178
+ assert mode.immutable_empty is False
179
+ assert mode.immutable_evaluated is False
180
+ assert mode.immutable_existed is False
181
+ assert mode.sealed is False
182
+ assert mode.no_new is False
183
+ assert mode.no_del is False
184
+ assert mode.immutable is False # it's True when all flags are raised, so it's IMMUTABLE
185
+
186
+ mode.freeze_none() # Raise the IMMUTABLE_NONE flag
187
+ mode.freeze_empty()
188
+ mode.freeze_evaluated()
189
+ mode.freeze_existed()
190
+ mode.forbid_new_attrs()
191
+ mode.forbid_attrs_removing()
192
+ mode.seal()
193
+ mode.freeze() # Raise all flags
194
+ ```
195
+
196
+
197
+
198
+ ## Objects
199
+
200
+ `obj.py` file provides one metaclass and two classes to add read-only functionality for python objects.
201
+
202
+ All classes support all defined flags and use `ImmutabilityMode` as flag management point.
203
+ For any forbidden modification the `ImmutableError` exception is raised.
204
+
205
+
206
+ ### ImmutableClass
207
+ This metaclass should be used to add immutability at the class level to restrict **class** attributes.
208
+
209
+ When a custom class use `ImmutableClass` as metaclass,
210
+ it has `cls_immutability` attribute which is an instance of the `ImmutabilityMode` class.
211
+
212
+
213
+ > It's possible to freeze class at the definition stage by using the mode keyword in a class definition (see examples bellow).
214
+
215
+ ```python
216
+ from unx.immutable import ImmutableClass, ImmutabilityMode
217
+ class A(metaclass=ImmutableClass, mode=ImmutabilityMode().freeze()):
218
+ attr = "value"
219
+
220
+ assert A.attr == "value"
221
+ try:
222
+ A.attr = None
223
+ except ImmutableError:
224
+ print("Class is immutable and cannot be modified.")
225
+
226
+ #---------------------------------------------
227
+ # other ways to freeze at the definition level
228
+ class A(metaclass=ImmutableClass, mode=ImmutabilityMode().freeze_none().seal()):...
229
+ class A(metaclass=ImmutableClass, mode=ImmutabilityMode(IMMUTABLE_NONE | SEALED)):...
230
+ class A(metaclass=ImmutableClass, mode=IMMUTABLE_NONE | SEALED):...
231
+
232
+ class A(metaclass=ImmutableClass, mode=ImmutabilityMode().seal()):
233
+ attr = "value"
234
+
235
+ # this works because none of the modification flags have been raised.
236
+ # SEALED (NO_NEW actually) forbids only adding new attributes, but not modifying existing ones.
237
+ assert A.attr == "value"
238
+ A.attr = None
239
+ assert A.attr == None
240
+
241
+ try:
242
+ A.attr2 = True
243
+ except ImmutableError:
244
+ print("Class is sealed, so new attributes cannot be assigned.")
245
+
246
+
247
+ #---------------------------------------------
248
+ # For postponed freezing.
249
+ class A(metaclass=ImmutableClass):...
250
+
251
+ assert bool(A.cls_immutability) is False
252
+ # new attributes can be defined.
253
+ A.attr1 = None
254
+ A.attr2 = True
255
+ A.attr3 = 0
256
+
257
+ # None
258
+ A.cls_immutability.freeze_none()
259
+ assert A.cls_immutability.immutable_none is True
260
+ assert bool(A.cls_immutability) is True
261
+ A.attr3 = 1
262
+
263
+ try:
264
+ A.attr1 = 42
265
+ except ImmutableError:
266
+ print("Raised IMMUTABLE_NONE flag fobids any atribute modifications with the None value.")
267
+
268
+
269
+ # evaluated
270
+ A.cls_immutability.freeze_evaluated()
271
+ assert A.cls_immutability.immutable_evaluated is True
272
+ assert A.cls_immutability.immutable_existed is True
273
+ A.attr4 = "still work for now"
274
+ try:
275
+ A.attr3 = 2
276
+ except ImmutableError:
277
+ print("Raised IMMUTABLE_EVALUATED and IMMUTABLE_NONE flags fobid any atribute modifications.")
278
+
279
+
280
+ A.cls_immutability.seal()
281
+ assert A.cls_immutability.sealed is True
282
+
283
+
284
+ A.cls_immutability.freeze()
285
+ assert A.cls_immutability.immutable is True
286
+ ```
287
+
288
+
289
+
290
+
291
+
292
+
293
+ ### ImmutableObject
294
+
295
+ `ImmutableObject` class should be used as base for those custom classes which take care
296
+ to add read-only functionality for the new objects/instances of the class rather than class itself.
297
+
298
+ > `ImmutableObject` adds `immutability` attribute which is instance of the `ImmutabilityMode` class to manage read-only flags for objects.
299
+
300
+
301
+ Examples:
302
+ ```python
303
+ class SealedDataclass(ImmutableObject):
304
+ def __init__(self):
305
+ self.attr1 = 42
306
+ self.attr2 = True
307
+ self.immutability.seal()
308
+
309
+ obj = SealedDataclass()
310
+ obj.attr1 = 24
311
+ obj.attr2 = False
312
+
313
+ try:
314
+ A.attr3 = 2
315
+ except ImmutableError:
316
+ print("obj is sealed and cannot apply new attrs.")
317
+
318
+
319
+ #-----------------------------------
320
+ class CustomData(ImmutableObject):...
321
+ def __init__(self):
322
+ self.immutability.freeze_evaluated()
323
+
324
+ obj = CustomData()
325
+ d1 = {"a1": 1, "a2": 2}
326
+ d2 = {"a1": 5, "a3": 3}
327
+
328
+ for k, v in d1.items():
329
+ setattr(obj, k, v)
330
+
331
+ try:
332
+ for k, v in d2.items():
333
+ setattr(obj, k, v)
334
+ except ImmutableError:
335
+ print("failed on a1=5, because it's forbidden to override existing attributes with value other than None.")
336
+ ```
337
+
338
+
339
+
340
+
341
+ ### Immutable
342
+ The `Immutable` class is just a quick way to get read-only functionality on both (instance and class) levels.
343
+ Its definition is `class Immutable(ImmutableObject, metaclass=ImmutableClass)...`
344
+
345
+ Custom class based on this one, have both attributes: `cls_immutability` at the class level, and `immutability` at the instance one.
346
+
347
+ This is recommended way to use this module, since freezing both prevents some mistakes when instance have an attr with the same name as class have.
348
+
349
+
350
+
351
+ Next simplified example shows how to get instance which prevents overriding existing values:
352
+ ```python
353
+ class Template(Immutable, mode=IMMUTABLE):
354
+ def __init__(self, **kwargs):
355
+ self.immutability.freeze_existed()
356
+ for attr_name, val in kwargs.items():
357
+ setattr(self, attr_name, val)
358
+ ```