Search code examples
pythonpython-3.xinheritancewrappersetattr

Python read-only wrapper class that denies access to certain methods and all attributes


I have the following base class.

class BaseWithMethod:
    def __init__(self, prop=None):
        self.prop = prop

    def evil_method(self):
        print(f"%@#&ç? {self.prop}")

I want to create a wrapper class ReadonlyWrapperSelectedMethods that shows the same functionality as the base class but does not allow certain methods (evil_method in this example) to be called. Further, wrapped instances should be read-only, as discussed in my other SO question here. That means that calls to __setattr__ should raise an error once the instance is initialized. The behavior is demonstrated in the following code:

# Instantiate the wrapper class
readonly_instance = ReadonlyWrapperSelectedMethods()

# I can access properties
prop = readonly_instance.prop

# This should raise a PermissionError
readonly_instance.prop = 23

# This should also raise a PermissionError
readonly_instance.evil_method()

Is there a way to implement this behavior without modifying the base class? See below how it can be done when the base class may be changed.

Attempt 1: Modifying the base class

So far I have tried the following. I added an attribute _initialized to the base class and set it to True at the end of __init__:

class BaseWithMethodModified:
    _initialized = False

    def __init__(self, prop=None):
        self.prop = prop
        self._initialized = True

    def evil_method(self):
        print(f"%@#&ç? {self.prop}")

In this case the following wrapper class should do the job. It overrides the __getattribute__ method and delegates calls to methods that are allowed to the super class.

class ReadonlyWrapperSelectedMethods(BaseWithMethodModified):
    """Read-only wrapper class."""

    def __getattribute__(self, name: str):
        if "evil" in name:
            raise PermissionError()
        else:
            return super().__getattribute__(name)

    def __setattr__(self, key, value) -> None:
        if self.__getattribute__("_initialized"):
            raise PermissionError()
        else:
            super().__setattr__(key, value)

The issue with this attempt is that I do not want to modify the base class and if the attribute _initialized is defined in the wrapper class, it cannot be accessed since all attribute accesses are delegated to the base class through __getattribute__. Maybe this can be circumvented in some way?


Solution

  • You could simply override the __init__ method:

    class ReadonlyWrapperSelectedMethods(BaseWithMethod):
        """Read-only wrapper class."""
    
        def __init__(self, prop=None):
            super().__init__(prop)
            self._initialized = True
    
        def __getattribute__(self, name: str):
            if "evil" in name:
                raise PermissionError()
            else:
                return super().__getattribute__(name)
    
        def __setattr__(self, key, value) -> None:
            if hasattr(self, "_initialized"):
                raise PermissionError()
            else:
                super().__setattr__(key, value)
    

    After __init__ returns, the object is readonly:

    >>> readonly_instance = ReadonlyWrapperSelectedMethods()
    >>> vars(readonly_instance)
    {'prop': None, '_initialized': True}
    >>> prop = readonly_instance.prop
    >>> readonly_instance.prop = 23
    Traceback (most recent call last):
      File "<pyshell#126>", line 1, in <module>
        readonly_instance.prop = 23
      File "<pyshell#121>", line 16, in __setattr__
        raise PermissionError()
    PermissionError
    >>> readonly_instance.evil_method()
    Traceback (most recent call last):
      File "<pyshell#127>", line 1, in <module>
        readonly_instance.evil_method()
      File "<pyshell#121>", line 10, in __getattribute__
        raise PermissionError()
    PermissionError