Package: immutable-proxy
immutable-proxyThe main aim of this package is to define the classDeepImmutableProxythat prevents a user-defined class to be mutated to any level of depth.DeepImmutableProxytakes 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.InstallationThis package is written in pure python. Simply install it using pippip install immutable-proxyBasic usage>>>fromimmutableimportDeepImmutableProxy,ConstantAttributeErrorWe define a simple class:>>>classExample():...def__init__(self,x):...self.x=x...def__repr__(self):...returnf'{type(self).__qualname__}({self.x!r})'>>>example=Example(42)>>>example.x=0>>>print(example)Example(0)If we wrap an object withDeepImmutableProxy, then this object cannot be mutated.>>>proxy=DeepImmutableProxy(Example(42))>>>try:...proxy.x=0...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Example]'objectisimmutable.Cannotchangeattribute'x'afterinitialization.>>>try:...delproxy.x...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Example]'objectisimmutable.Cannotdeleteattribute'x'afterinitialization.>>>print(proxy)Example(42)Nested attributes, indirect mutation, and inheritanceThe following example demonstrates a more complicated class with properties and methods that set attributes.>>>classInner:...def__init__(self,x,y):...self.x=x...self.y=y...def__repr__(self):...returnf'{type(self).__qualname__}({self.x!r},{self.y!r})'...defset_x(self,x):..."Demonstrates a way to set attribute 'x'."...self.x=x...defget_x(self)->int:...returnself.x...@property...defy(self):..."Demonstrates a property to handle attribute 'y'."...returnself.__dict__['y']...@y.setter...defy(self,value):...self.__dict__['y']=valueThe 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 withDeepImmutableProxy.
This object cannot be mutated.>>>inner_proxy=DeepImmutableProxy(Inner(1,2))>>>try:...inner_proxy.x=-1...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Inner]'objectisimmutable.Cannotchangeattribute'x'afterinitialization.>>>try:...inner_proxy.y=-2...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Inner]'objectisimmutable.Cannotchangeattribute'y'afterinitialization.>>>try:...inner_proxy.set_x(3)...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Inner]'objectisimmutable.Cannotchangeattribute'x'afterinitialization.>>>try:...delinner_proxy.x...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Inner]'objectisimmutable.Cannotdeleteattribute'x'afterinitialization.We define another class whose attributes are of typeInner.
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.>>>classOuter:...def__init__(self,a:Inner,b:Inner)->None:...self.a=a...self.b=b...self.delegate=Modifier(self)...def__repr__(self):...returnf'{type(self).__qualname__}({self.a!r},{self.b!r})'...defset_a(self,a):...'Sets a.'...self.a=a...defget_a(self):...returnself.a...@property...defb(self):...returnself.__dict__['b']...@b.setter...defb(self,value)->None:...self.__dict__['b']=value>>>classModifier():...def__init__(self,obj):...self.back=obj...defreset(self,x):...self.back.x=x>>>outer_proxy=DeepImmutableProxy(Outer(Inner(1,2),Inner(3,4)))The object just created is wrapped withDeepImmutableProxy.
This prevents mutation of attributes at all levels of depth.>>>try:...outer_proxy.a=Inner(-1,-2)...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Outer]'objectisimmutable.Cannotchangeattribute'a'afterinitialization.>>>try:...outer_proxy.b=Inner(-3,-4)...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Outer]'objectisimmutable.Cannotchangeattribute'b'afterinitialization.>>>try:...outer_proxy.a.x=-1...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Inner]'objectisimmutable.Cannotchangeattribute'x'afterinitialization.>>>try:...outer_proxy.a.y=-2...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Inner]'objectisimmutable.Cannotchangeattribute'y'afterinitialization.>>>try:...outer_proxy.b.x=-3...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Inner]'objectisimmutable.Cannotchangeattribute'x'afterinitialization.>>>try:...outer_proxy.b.y=-4...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Inner]'objectisimmutable.Cannotchangeattribute'y'afterinitialization.>>>try:...outer_proxy.set_a(Inner(-1,-2))...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Outer]'objectisimmutable.Cannotchangeattribute'a'afterinitialization.>>>try:...outer_proxy.a.set_x(-1)...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Inner]'objectisimmutable.Cannotchangeattribute'x'afterinitialization.>>>try:...delouter_proxy.a...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Outer]'objectisimmutable.Cannotdeleteattribute'a'afterinitialization.We recall that instances ofOuterhave an attribute that references an object of another class that has a reference to the first.  We show thatDeepImmutableProxystill prevents mutation.>>>try:...outer_proxy.delegate.back.x=5...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Outer]'objectisimmutable.Cannotchangeattribute'x'afterinitialization.Next we present an example where inheritance is involved andsuper()is used.>>>classDerived(Outer):...defset_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))...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Derived]'objectisimmutable.Cannotchangeattribute'a'afterinitialization.Built-in typesThe classDeepImmutableProxyworks with instances of user defined classes.
For classes that are not written inPythonthere might be problems, as the introspection methods used byDeepImmutableProxymay not work.
This problem is solved with the class methodsDeepImmutableProxy.register_immutable, which can be used to tellDeepImmutableProxythat certain class is already immutable (hence, wrapping it withDeepImmutableProxyhas no effect), andDeepImmutableProxy.register_proxy, that allows a user to register a subclass ofDeepImmutableProxythat defines an immutable proxy specific for the given type.For example, typesint,float,str,bytes, and some others, have already been registered to be immutable:>>>no_proxy=DeepImmutableProxy(3)>>>type(no_proxy)==intTrueThe class methodDeepImmutableProxy.get_registered_immutablereturns a list of the currently registered immutable classes:>>>[cls.__name__forclsinDeepImmutableProxy.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 applyingDeepImmutableProxyto 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 withDeepImmutableProxyit really becomes immutable.
This is achieved by returning a special wrapper for tuples.
The usualDeepImmutableProxyclass 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_proxyTupleImmutableProxy(('immutable',['mutable']))The first item of the tuple is astr, 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')...exceptAttributeErroraserror:...print(error)Objectoftype"ListImmutableProxy"doesnothaveattribute"append".CopyingDeepImmutableProxyobjects include a hook for__copy__and__deepcopy__that copies the underlying object.  The copy is mutable.>>>fromcopyimportdeepcopy>>>copied=deepcopy(outer_proxy)>>>print(copied)Outer(Inner(1,2),Inner(3,4))>>>copied.a.x=-1>>>try:...outer_proxy.a.x=-1...exceptConstantAttributeErroraserror:...print(error)'DeepImmutableProxy[Inner]'objectisimmutable.Cannotchangeattribute'x'afterinitialization.