Christian Baumann
Christian Baumann

Reputation: 159

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?

Upvotes: 0

Views: 140

Answers (2)

Serge Ballesta
Serge Ballesta

Reputation: 148910

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

Upvotes: 1

juanpa.arrivillaga
juanpa.arrivillaga

Reputation: 95948

Don't use inheritance, use composition. Take advantage of __slots__:

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

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

class ReadOnlyWrapper:
    __slots__ = ('_foo',)
    def __init__(self, foo: Foo):
        self._foo = foo
    def __getattr__(self, name: str):
        if "evil" in name:
            raise PermissionError()
        else:
            return getattr(self._foo, name)


wrapper = ReadOnlyWrapper(Foo())

Upvotes: 1

Related Questions