vivri
vivri

Reputation: 905

Creating an automatic delegating wrapper class in Python 3

I created a delegating wrapper class in Python 3(.8), which allow me to fully replicate the base-class' API, delegate to the underlying instance, and swap the underlying instance when necessary.

I'm still not getting full API-parity, so I thought I'd share and ask for help.

I know this is an odd choice, but I need to support an existing API, which is (a) very extensive, and (b) not in my control (3rd party library), while being able to swap the underlying instance without the caller-code knowing. E.g.:

with MyResource() as r:  # MyResource is my class, while an `r` instance is an instance of a class in a 3rd party library.
  # r should be wrapped in the delegating instance
  r.do_x()
  # -- under the hood, call .__exit__ on the base instance of `r`, and regenerate it without the calling code knowing.
  r.do_y()  # this is already delegated to the new instance of base `r`

Here's the code I have thus far:

T = TypeVar("T")

class SwappableWrapper(Generic[T]):
    def __init__(self, base: T):
        self.__base: T = base
        for name in [_ for _ in dir(base) if _ not in ["__class__", "__dict__", "__weakref__"]]:
            class_attr = SwappableWrapper.__get_class_attr(type(base), name)
            if class_attr:
                is_property = isinstance(class_attr, property)
                is_function = not is_property and callable(getattr(base, name))
                if is_function:
                    setattr(self, name, self.__call_base_instance(name))
                elif is_property:
                    self.__set_property(name)
                else:  # make even the normal attributes into getter-style delegating properties.
                    self.__set_property(name)

    @staticmethod
    def __get_class_attr(clazz, attr_name: str):
        if hasattr(clazz, attr_name):
            return getattr(clazz, attr_name)
        else:
            for parent in clazz.__bases__:
                attr = SwappableWrapper.__get_class_attr(parent, attr_name)
                if attr:
                    return attr
        return None

    def __call_base_instance(self, func_name):
        def do_call(*args, **kwargs):
            return getattr(self.__get_base_instance(), func_name)(*args, **kwargs)
        return do_call

    def __set_property(self, name):
        setattr(self, name, property(fget=lambda: getattr(self.__get_base_instance(), name),
                                     fset=lambda v: setattr(self.__get_base_instance(), name, v),
                                     fdel=lambda: delattr(self.__get_base_instance(), name)))

    def __get_base_instance(self) -> T:
        return self.__base

    def swap_base_instance_(self, new_base: T) -> None:
        print(f"Swap old base: {self.__base} New base: {new_base}")
        self.__base = new_base

The calling code prints oddly - instead of calling the newly-minted delegating properties, it just prints property(...).

Would be happy for any help. Thanks!

Upvotes: 2

Views: 1053

Answers (1)

vivri
vivri

Reputation: 905

I just realized this can be easily solved using Python's dynamic dispatch. For posterity, you can achieve this by:

class SwappableWrapper(Generic[T]):
    def __init__(self, base: T):
        self.__base: T = base

    def __getattr__(self, item):
        return getattr(self.__base, item)

    def swap_base_instance_(self, new_base: T) -> None:
        self.__base = new_base

For a deeper explanation, see this SO question.

Upvotes: 2

Related Questions