Skip to main content

Provides a proxy to an object that prevents mutation at any depth

Project description

immutable-proxy

The main aim of this package is to define the class DeepImmutableProxy that prevents a user-defined class to be mutated to any level of depth.

DeepImmutableProxy takes an object as an argument and acts as a proxy to that object. Attributes and methods can be accessed normally unless they try to mutate any object that is accessible from the wrapped object.

Installation

This package is written in pure python. Simply install it using pip

pip install immutable-proxy

Basic usage

>>> from immutable import DeepImmutableProxy, ConstantAttributeError

We define a simple class:

>>> class Example():
...     def __init__(self, x):
...         self.x = x
...     def __repr__(self):
...         return f'{type(self).__qualname__}({self.x!r})'

>>> example = Example(42)
>>> example.x = 0
>>> print(example)
Example(0)

If we wrap an object with DeepImmutableProxy, then this object cannot be mutated.

>>> proxy = DeepImmutableProxy(Example(42))

>>> try:
...     proxy.x = 0
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Example]' object is immutable. Cannot change attribute 'x' after initialization.

>>> try:
...     del proxy.x
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Example]' object is immutable. Cannot delete attribute 'x' after initialization.

>>> print(proxy)
Example(42)

Nested attributes, indirect mutation, and inheritance

The following example demonstrates a more complicated class with properties and methods that set attributes.

>>> class Inner:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __repr__(self):
...         return f'{type(self).__qualname__}({self.x!r}, {self.y!r})'
...     def set_x(self, x):
...         "Demonstrates a way to set attribute 'x'."
...         self.x = x
...     def get_x(self) -> int:
...         return self.x
...     @property
...     def y(self):
...         "Demonstrates a property to handle attribute 'y'."
...         return self.__dict__['y']
...     @y.setter
...     def y(self, value):
...         self.__dict__['y'] = value

The objects of this class work as expected.

>>> inner = Inner(1, 2)
>>> inner.x, inner.y = -1, -2
>>> print(inner)
Inner(-1, -2)

>>> inner.set_x(3)
>>> print(inner)
Inner(3, -2)

Now we wrap an object of this class with DeepImmutableProxy. This object cannot be mutated.

>>> inner_proxy = DeepImmutableProxy(Inner(1, 2))

>>> try:
...     inner_proxy.x = -1
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Inner]' object is immutable. Cannot change attribute 'x' after initialization.

>>> try:
...     inner_proxy.y = -2
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Inner]' object is immutable. Cannot change attribute 'y' after initialization.

>>> try:
...     inner_proxy.set_x(3)
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Inner]' object is immutable. Cannot change attribute 'x' after initialization.

>>> try:
...     del inner_proxy.x
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Inner]' object is immutable. Cannot delete attribute 'x' after initialization.

We define another class whose attributes are of type Inner. It also contains an attribute referring to another object that refers back to an object of this class. The initial object cannot be modified indirectly trough this back reference.

>>> class Outer:
...     def __init__(self, a: Inner, b: Inner) -> None:
...         self.a = a
...         self.b = b
...         self.delegate = Modifier(self)
...     def __repr__(self):
...         return f'{type(self).__qualname__}({self.a!r}, {self.b!r})'
...     def set_a(self, a):
...         'Sets a.'
...         self.a = a
...     def get_a(self):
...         return self.a
...     @property
...     def b(self):
...         return self.__dict__['b']
...     @b.setter
...     def b(self, value) -> None:
...         self.__dict__['b'] = value

>>> class Modifier():
...     def __init__(self, obj):
...         self.back = obj
...     def reset(self, x):
...         self.back.x = x

>>> outer_proxy = DeepImmutableProxy(Outer(Inner(1, 2), Inner(3, 4)))

The object just created is wrapped with DeepImmutableProxy. This prevents mutation of attributes at all levels of depth.

>>> try:
...     outer_proxy.a = Inner(-1, -2)
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Outer]' object is immutable. Cannot change attribute 'a' after initialization.

>>> try:
...     outer_proxy.b = Inner(-3, -4)
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Outer]' object is immutable. Cannot change attribute 'b' after initialization.

>>> try:
...     outer_proxy.a.x = -1
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Inner]' object is immutable. Cannot change attribute 'x' after initialization.

>>> try:
...     outer_proxy.a.y = -2
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Inner]' object is immutable. Cannot change attribute 'y' after initialization.

>>> try:
...     outer_proxy.b.x = -3
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Inner]' object is immutable. Cannot change attribute 'x' after initialization.

>>> try:
...     outer_proxy.b.y = -4
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Inner]' object is immutable. Cannot change attribute 'y' after initialization.

>>> try:
...     outer_proxy.set_a(Inner(-1, -2))
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Outer]' object is immutable. Cannot change attribute 'a' after initialization.

>>> try:
...     outer_proxy.a.set_x(-1)
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Inner]' object is immutable. Cannot change attribute 'x' after initialization.

>>> try:
...     del outer_proxy.a
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Outer]' object is immutable. Cannot delete attribute 'a' after initialization.

We recall that instances of Outer have an attribute that references an object of another class that has a reference to the first. We show that DeepImmutableProxy still prevents mutation.

>>> try:
...     outer_proxy.delegate.back.x = 5
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Outer]' object is immutable. Cannot change attribute 'x' after initialization.

Next we present an example where inheritance is involved and super() is used.

>>> class Derived(Outer):
...     def set_a(self, a):
...         modified = Inner(2*a.x, 2*a.y)
...         super().set_a(modified)
    
>>> derived = Derived(Inner(1, 2), Inner(3, 4))
>>> derived.set_a(Inner(10, 20))
>>> print(derived)
Derived(Inner(20, 40), Inner(3, 4))

>>> derived_proxy = DeepImmutableProxy(Derived(Inner(1, 2), Inner(3, 4)))

>>> try:
...     derived_proxy.set_a(Inner(10, 20))
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Derived]' object is immutable. Cannot change attribute 'a' after initialization.

Built-in types

The class DeepImmutableProxy works with instances of user defined classes. For classes that are not written in Python there might be problems, as the introspection methods used by DeepImmutableProxy may not work. This problem is solved with the class methods DeepImmutableProxy.register_immutable, which can be used to tell DeepImmutableProxy that certain class is already immutable (hence, wrapping it with DeepImmutableProxy has no effect), and DeepImmutableProxy.register_proxy, that allows a user to register a subclass of DeepImmutableProxy that defines an immutable proxy specific for the given type.

For example, types int, float, str, bytes, and some others, have already been registered to be immutable:

>>> no_proxy = DeepImmutableProxy(3)
>>> type(no_proxy) == int
True

The class method DeepImmutableProxy.get_registered_immutable returns a list of the currently registered immutable classes:

>>> [cls.__name__ for cls in DeepImmutableProxy.get_registered_immutable()] # doctest: +ELLIPSIS
['bytes', 'int', 'float', 'complex', 'str', 'NoneType', ...]

Some containers like tuples are immutable only if their content is immutable.
Thus, when applying DeepImmutableProxy to a container like a tuple, we need a specific wrapper. This wrapper is already defined in this package.

For example, the second element of the next tuple is mutable, so the resulting object is mutable. However, when wrapped with DeepImmutableProxy it really becomes immutable. This is achieved by returning a special wrapper for tuples. The usual DeepImmutableProxy class does not work because attribute access for built-in types does not follow the standard Python rules.

>>> mutable_tuple_proxy = DeepImmutableProxy(('immutable', ['mutable']))
>>> mutable_tuple_proxy
TupleImmutableProxy(('immutable', ['mutable']))

The first item of the tuple is a str, which is immutable, so it is returned as is.

>>> mutable_tuple_proxy[0]
'immutable'

The second item is mutable, so it is wrapped and cannot be modified either.

>>> mutable_tuple_proxy[1]
ListImmutableProxy(['mutable'])

This second item is of type list, so it also needs a special wrapper. Methods that do not mutate the content can be used normally.

>>> len(mutable_tuple_proxy[1]), mutable_tuple_proxy[1].index('mutable')
(1, 0)

However, methods that do mutate content are forbidden.

>>> try:
...     mutable_tuple_proxy[1].append('another')
... except AttributeError as error:
...     print(error)
Object of type "ListImmutableProxy" does not have attribute "append".
    

Copying

DeepImmutableProxy objects include a hook for __copy__ and __deepcopy__ that copies the underlying object. The copy is mutable.

>>> from copy import deepcopy

>>> copied = deepcopy(outer_proxy)
>>> print(copied)
Outer(Inner(1, 2), Inner(3, 4))

>>> copied.a.x = -1

>>> try:
...     outer_proxy.a.x = -1
... except ConstantAttributeError as error:
...     print(error)
'DeepImmutableProxy[Inner]' object is immutable. Cannot change attribute 'x' after initialization.

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

immutable-proxy-0.1.0.tar.gz (19.3 kB view details)

Uploaded Source

Built Distribution

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

immutable_proxy-0.1.0-py3-none-any.whl (12.8 kB view details)

Uploaded Python 3

File details

Details for the file immutable-proxy-0.1.0.tar.gz.

File metadata

  • Download URL: immutable-proxy-0.1.0.tar.gz
  • Upload date:
  • Size: 19.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.9.6

File hashes

Hashes for immutable-proxy-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8da0dfc6c043e0bf5120c51b59f20ac9f1d8c77d313fcd2ebc6dbb2a13b33b76
MD5 c6dad32150a54af92dad2594b5c7dae7
BLAKE2b-256 67b981703d715a337365b5832cf2807469bd1f3a8cea9ea801d35f6c4da26f78

See more details on using hashes here.

File details

Details for the file immutable_proxy-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for immutable_proxy-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a9e8b3354948841549c6b467f18a78187172b58cdd4818e08f877cbdfbde793f
MD5 15f668ab1f09bfcdb716117174a086ed
BLAKE2b-256 3b668d4e4f4fe030245cadd1a832a26b710ffc22e15253f470e0ececf7fe0d01

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