Skip to main content

Allows to make class and instance attributes immutable, supports a few different modes.

Project description

python package: immutable

Package implements a few different modes of object immutability.

License

This project is licensed under the Apache License 2.0.

The author permits this code to be used for AI training, analysis, and research. However, reproducing this source code or its derivatives without proper attribution violates the Apache 2.0 License.

Repository

Versioning

The next versioning scheme vX.Y.Z is used, where:

  • X: (major) reflects current stable version of interface.
    • It must be increased in case of incompatible changes, when the code that use this package must be updated to use the new version.
    • 0: means developing stage, so it can become incompatible without increasing major part of version.
    • 1: is going to be the first stable release version.
  • Y: (minor) it must be changed with new added functionalities which do not break compatibility.
  • Z: (patch): for fixes/improvements which does not change anything to the end users (internal improvements).

additional flags are not supported like +build or -rc, -beta, etc.

Branch strategy

Branch agreement:

  • The release branch:
    • It's default branch which is for the only stable code and points to the last release.
    • for every new official release vx.x.x signed annotated tag is created.
    • documentation update can be merged without tag creation, no need to bump version for README.
    • all tagged versions have created releases on codeberg/pypi
    • rc branch is used as a main source for releases.
    • fast_forward merge strategy is used for merging from the rc branch.
  • 1.x.x: Current LTS release.
    • 1.x.x means .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.
    • Branch always point to the latest 1.x.x stable version.
    • For now 1.x.x and release should be the same
    • it's already created to indicate that when 2.x.x come, this branch still be supported.
  • The rc branch:
    • This branch contains the newest release candidate version of the package, which have not been released yet.
    • Code in this branch should be ready to use, well tested and covered with unittests if applicable.
    • Documentation should be updated and describe new changes.
    • When code is merged to this branch, the version is already bumped.
    • rc branch is the main point to create feature/fix branches for contributing.
    • gpg signed merge commit with squashing is used for merging into the rc branch.
  • Any other branches should be threated as developing ones and are not recommended for using until one knows what they are doing.

For contribution use the rc branch as the main source to clone and create a pull request.

Quick introduction

  • mode.py defines different immutability modes.
  • obj.py provides base clases/metaclasses to apply immutability modes to the objects.

All entities can be imported from the __init__.py directly, so there is no need to import all files. Quick example how to create a custom class, where class attributes are immutable during class creating, and instance attributes become immutable after initialization:

from unx.immutable import ImmutableAttrError, Immutable, IMMUTABLE

class A(Immutable, mode=IMMUTABLE):
  cls_attr1: int = 15

  def __init__(self):
      super().__init__()
      self.instance_attr1 = "Attr"
      self.immutability.freeze()

# class attrs
try:
    A.cls_attr1 = 42
except ImmutableAttrError:
    print("it happens on the class attribute")

try:
    A.instance_attr1 = property(lambda x: 42)
except ImmutableAttrError:
    print("Class immutability also protects from such code as well since all changes is already prohibited.")

# instance attrs
a = A()
try:
    a.instance_attr1 = True
except ImmutableAttrError:
    print("This time instance_attr1 in instance of the class is frozen")

Immutability flags

Instead of using one "read-only" flag to mark an object as immutable, the package supports a few immutability flags for different object's parts to give more flexibility to freeze them independently.

Flags are defined as number constants but set of flags is represented as a single unsigned integer where one flag takes one bit and 0 means no restrictions.

Please, do not use numbers directly to choose a mode, they are not part of API and can be changed without bumping major part of version. In the mode.py file there are pre-defined constants for all supported flags and their naming consistency is garanteed. For instance the IMMUTABLE constant always means all flags are raised so the actual value depends on the amount of flags.

Next table represents list of supported constants, flag ids, and short description:

ID CONSTANT NAME DESCRIPTION
0 UNRESTRICTED No flags are raised, no restrictions are applied, The target class must not be restricted.
1 NO_DEL Attribute/Item deleting is forbidden.
2 NO_NEW Assigning new attribute/item is forbidden.
3 SEALED The same as NO_NEW | NO_DEL); no attr/item deletion or adding.
4 IMMUTABLE_NONE The None values cannot be replaced with other.
8 IMMUTABLE_EMPTY The "EMPTY" (not bool(value)) values cannot be modified.
16 IMMUTABLE_EVALUATED Already defined values (bool(value)) cannot be modified.
28 IMMUTABLE_EXISTED Combination of the IMMUTABLE_NONE | IMMUTABLE_EMPTY | IMMUTABLE_EVALUATED, all existing attrs/items are immutable.
31 IMMUTABLE SEALED | IMMUTABLE_EXISTED gives full immutability.

All constants can be imported directly: from unx.immutable import IMMUTABLE, IMMUTABLE_EXISTED, SEALED

ImmutabilityMode

The ImmutabilityMode class has been created to store immutability flags under one namespace, provide methods to raise separate flags, and properties to read their state.

This class is only about storing and managing but not restricting anything by itself.

Invariant: The state is monotonic — flags can only be raised, up to full immutability and never lowered.

All methods that raise flags return self. So, method calls can be chained, for example:

fm = ImmutableMode().seal().freeze_none()

Code example:

    mode1 = ImmutabilityMode(11)  # not recommended to use numbers directly, adding new flags in the future refactoring will break such code.
    mode2 = ImmutabilityMode(IMMUTABLE_NONE | SEALED)
    mode3 = ImmutabilityMode().freeze_none().seal()
    assert mode1 == mode2 == mode3  # modes are comparable
    assert str(mode2) == "SEALED|IMMUTABLE_NONE"
    assert ImmutabilityMode(SEALED) < ImmutabilityMode(IMMUTABLE_NONE)
    assert ImmutabilityMode().seal() < ImmutabilityMode().freeze_none()

    mode = ImmutabilityMode()  # by default mode is UNRESTRICTED (0), no flags are raised.
    assert bool(mode) is False  # False means no flags have been raised.
    assert mode.state == 0  # the current mode
    assert bool(ImmutabilityMode().forbid_new()) is True  # since NO_NEW flag has been raised.

    assert mode.immutable_none is False  # Shows if the IMMUTABLE_NONE flag is raised.
    assert mode.immutable_empty is False
    assert mode.immutable_evaluated is False
    assert mode.immutable_existed is False
    assert mode.sealed is False
    assert mode.no_new is False
    assert mode.no_del is False
    assert mode.immutable is False  # it's True when all flags are raised, so it's IMMUTABLE

    mode.freeze_none()  # Raise the IMMUTABLE_NONE flag
    mode.freeze_empty()
    mode.freeze_evaluated()
    mode.freeze_existed()
    mode.forbid_new_attrs()
    mode.forbid_attrs_removing()
    mode.seal()
    mode.freeze()  # Raise all flags

    assert str(ImmutabilityMode(IMMUTABLE)) == "IMMUTABLE"
    assert str(ImmutabilityMode(SEALED)) == "SEALED"
    assert str(ImmutabilityMode(NO_NEW | NO_DEL)) == "SEALED"
    assert str(ImmutabilityMode(NO_NEW | IMMUTABLE_NONE)) == "NO_NEW|IMMUTABLE_NONE"

    assert int(ImmutabilityMode(SEALED)) == SEALED
    assert int(ImmutabilityMode(IMMUTABLE_EMPTY|NO_NEW)) == IMMUTABLE_EMPTY|NO_NEW

    assert (ImmutabilityMode(NO_NEW) | ImmutabilityMode(NO_DEL)).state == NO_NEW | NO_DEL
    assert (ImmutabilityMode(NO_NEW) | NO_DEL).state == NO_NEW | NO_DEL
    assert (ImmutabilityMode(NO_NEW) |= ImmutabilityMode(NO_DEL)).state == NO_NEW | NO_DEL == ImmutabilityMode(SEALED).state
    assert (ImmutabilityMode(NO_NEW) |= NO_DEL).state == NO_NEW | NO_DEL == ImmutabilityMode(SEALED).state

Objects

obj.py file provides one metaclass and two classes to add read-only functionality for python objects.

All classes support all defined flags and use ImmutabilityMode as flag management point. For any forbidden modification the ImmutableAttrError exception is raised.

ImmutableClass

This metaclass should be used to add immutability at the class level to restrict class attributes.

When a custom class use ImmutableClass as metaclass, it has cls_immutability attribute which is an instance of the ImmutabilityMode class.

It's possible to freeze class at the definition stage by using the mode keyword in a class definition (see examples bellow).

from unx.immutable import ImmutableClass, ImmutabilityMode
class A(metaclass=ImmutableClass, mode=ImmutabilityMode().freeze()):
    attr = "value"

assert A.attr == "value"
try:
    A.attr = None
except ImmutableAttrError:
    print("Class is immutable and cannot be modified.")

#---------------------------------------------
# other ways to freeze at the definition level
class A(metaclass=ImmutableClass, mode=ImmutabilityMode().freeze_none().seal()):...
class A(metaclass=ImmutableClass, mode=ImmutabilityMode(IMMUTABLE_NONE | SEALED)):...
class A(metaclass=ImmutableClass, mode=IMMUTABLE_NONE | SEALED):...

class A(metaclass=ImmutableClass, mode=ImmutabilityMode().seal()):
    attr = "value"

# this works because none of the modification flags have been raised.
# SEALED (NO_NEW actually) forbids only adding new attributes, but not modifying existing ones.
assert A.attr == "value"
A.attr = None
assert A.attr == None

try:
    A.attr2 = True
except ImmutableAttrError:
    print("Class is sealed, so new attributes cannot be assigned.")


#---------------------------------------------
# For postponed freezing.
class A(metaclass=ImmutableClass):...

assert bool(A.cls_immutability) is False
# new attributes can be defined.
A.attr1 = None
A.attr2 = True
A.attr3 = 0

# None
A.cls_immutability.freeze_none()
assert A.cls_immutability.immutable_none is True
assert bool(A.cls_immutability) is True
A.attr3 = 1

try:
    A.attr1 = 42
except ImmutableAttrError:
    print("Raised IMMUTABLE_NONE flag fobids any atribute modifications with the None value.")


# evaluated
A.cls_immutability.freeze_evaluated()
assert A.cls_immutability.immutable_evaluated is True
assert A.cls_immutability.immutable_existed is True
A.attr4 = "still work for now"
try:
    A.attr3 = 2
except ImmutableAttrError:
    print("Raised IMMUTABLE_EVALUATED and IMMUTABLE_NONE flags fobid any atribute modifications.")


A.cls_immutability.seal()
assert A.cls_immutability.sealed is True


A.cls_immutability.freeze()
assert A.cls_immutability.immutable is True

ImmutableObject

ImmutableObject class should be used as base for those custom classes which take care to add read-only functionality for the new objects/instances of the class rather than class itself.

ImmutableObject adds immutability attribute which is instance of the ImmutabilityMode class to manage read-only flags for objects.

Examples:

class SealedDataclass(ImmutableObject):
    def __init__(self):
        super().__init__()
        self.attr1 = 42
        self.attr2 = True
        self.immutability.seal()

obj = SealedDataclass()
obj.attr1 = 24
obj.attr2 = False

try:
    A.attr3 = 2
except ImmutableAttrError:
    print("obj is sealed and cannot apply new attrs.")


#-----------------------------------
class CustomData(ImmutableObject):...
    def __init__(self):
        super().__init__()
        self.immutability.freeze_evaluated()

obj = CustomData()
d1 = {"a1": 1, "a2": 2}
d2 = {"a1": 5, "a3": 3}

for k, v in d1.items():
    setattr(obj, k, v)

try:
    for k, v in d2.items():
        setattr(obj, k, v)
except ImmutableAttrError:
    print("failed on a1=5, because it's forbidden to override existing attributes with value other than None.")

Immutable

The Immutable class is just a quick way to get read-only functionality on both (instance and class) levels. Its definition is class Immutable(ImmutableObject, metaclass=ImmutableClass)...

Custom class based on this one, have both attributes: cls_immutability at the class level, and immutability at the instance one.

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.

Next simplified example shows how to get instance which prevents overriding existing values:

class Template(Immutable, mode=IMMUTABLE):
    def __init__(self, **kwargs):
        super().__init__()
        self.immutability.freeze_existed()
        for attr_name, val in kwargs.items():
            setattr(self, attr_name, val)

ImmutableDict

ImmutableDict class supports all immutability modes for items.

Example

from unx.immutable import ImmutableDict, ImmutableItemError

idict = ImmutableDict(a=1, b=2)
idict.immutability.freeze()
print(idict)
try:
    idict["a"] = 3
except ImmutableItemError:
    print("Fully immutable!!!!")


idict = ImmutableDict(a=1, b=2)
idict.immutability.freeze_evaluated()
try:
    idict["a"] = 3
except ImmutableItemError:
    print("immutable evaluated!!!!")

idict["c"] = 3
print(idict)
assert idict == {"a": 1, "b": 2, "c": 3}

List of changes comparing to the standard dict:

  • __init__ all arguments are passed directly to the dict.init, so it's not possible to specify immutability mode via __init__ method.
  • class supports serialization with preserving the current immutability mode (pickle.loads(pickle.dumps(ImmutabilityDict())))
  • attribute removing is forbided.
  • immutability attribute is protected from being overrided
  • __setitem__, __delitem__, clear(), pop(), popitem(), setdefault(), update() respect immutability mode.
  • copy() and __or__ return the ImmutableDict shalow copy but with unrestricted immutability mode, since if a copy is made it suppposed to be modified.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

unx_immutable-1.2.5.tar.gz (15.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

unx_immutable-1.2.5-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

File details

Details for the file unx_immutable-1.2.5.tar.gz.

File metadata

  • Download URL: unx_immutable-1.2.5.tar.gz
  • Upload date:
  • Size: 15.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for unx_immutable-1.2.5.tar.gz
Algorithm Hash digest
SHA256 5cea311dc2f162c01da6383fc6b380434ffafaf7687cc8c5a3e3b3830a642f8e
MD5 784bfd2e9b2b88bd7e2d1923da80338c
BLAKE2b-256 2b23c6616024143d9f33091305bf1d7faceb0a7d7ed5041de3bd17c834b1ed09

See more details on using hashes here.

File details

Details for the file unx_immutable-1.2.5-py3-none-any.whl.

File metadata

  • Download URL: unx_immutable-1.2.5-py3-none-any.whl
  • Upload date:
  • Size: 17.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for unx_immutable-1.2.5-py3-none-any.whl
Algorithm Hash digest
SHA256 11755555ac3d5c0b84c1ae15c93ab3e766edbc687fa703f8a76d19cd0c1b2efe
MD5 e300077a728bb51106d80948dd1c50d7
BLAKE2b-256 b34d04ce9c2bfb0f5896c210a506a85764b721d5344a44fcd53cbb179cc5ba18

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page