1

I am attempting to wrap a class from a third-party package in such a way that my new class looks exactly like a subclass of the third-party class. The third-party class does not support inheritance, and it has nontrivial features, such as functions that have a __getitem__ method. I can wrap almost every attribute and method using a solution based on Wrapping a class whose methods return instances of that class and How can I intercept calls to python's "magic" methods in new style classes?. However, I still need to override the __init__ method of the third-party class. How can I do that? Note: I am using new-style classes.

Code so far:

import copy

class WrapperMetaclass(type):
    """
    Works with the `Wrapper` class to create proxies for the wrapped object's magic methods.
    """
    def __init__(cls, name, bases, dct):

        def make_proxy(name):
            def proxy(self, *args):
                return getattr(self._obj, name)
            return proxy

        type.__init__(cls, name, bases, dct)
        if cls.__wraps__:
            ignore = set("__%s__" % n for n in cls.__ignore__.split())
            for name in dir(cls.__wraps__):
                if name.startswith("__"):
                    if name not in ignore and name not in dct:
                        setattr(cls, name, property(make_proxy(name)))

class Wrapper(object):
    """
    Used to provide a (nearly) seamless inheritance-like interface for classes that do not support direct inheritance.
    """

    __metaclass__ = WrapperMetaclass
    __wraps__  = None
    # note that the __init__ method will be ignored by WrapperMetaclass
    __ignore__ = "class mro new init setattr getattr getattribute dict"

    def __init__(self, obj):
        if self.__wraps__ is None:
            raise TypeError("base class Wrapper may not be instantiated")
        elif isinstance(obj, self.__wraps__):
            self._obj = obj
        else:
            raise ValueError("wrapped object must be of %s" % self.__wraps__)

    def __getattr__(self, name):
        if name is '_obj':
            zot = 1
        orig_attr = self._obj.__getattribute__(name)
        if callable(orig_attr) and not hasattr(orig_attr, '__getitem__'):
            def hooked(*args, **kwargs):
                result = orig_attr(*args, **kwargs)
                if result is self._obj:
                    return self
                elif isinstance(result, self.__wraps__):
                    return self.__class__(result)
                else:
                    return result
            return hooked
        else:
            return orig_attr

    def __setattr__(self, attr, val):
        object.__setattr__(self, attr, val)
        if getattr(self._obj, attr, self._obj) is not self._obj: # update _obj's member if it exists
            setattr(self._obj, attr, getattr(self, attr))

class ClassToWrap(object):
    def __init__(self, data):
        self.data = data

    def theirfun(self):
        new_obj = copy.deepcopy(self)
        new_obj.data += 1
        return new_obj

    def __str__(self):
        return str(self.data)

class Wrapped(Wrapper):
    __wraps__ = ClassToWrap

    def myfun(self):
        new_obj = copy.deepcopy(self)
        new_obj.data += 1
        return new_obj

# can't instantiate Wrapped directly! This is the problem!
obj = ClassToWrap(0)
wr0 = Wrapped(obj)
print wr0
>> 0
print wr0.theirfun()
>> 1

This works, but for truly seamless inheritance-like behavior, I need to instantiate Wrapped directly, e.g.

wr0 = Wrapped(0)

which currently throws

ValueError: wrapped object must be of <class '__main__.ClassToWrap'>

I attempted to override by defining a new proxy for __init__ in WrapperMetaclass, but rapidly ran into infinite recursions.

My codebase is complex with users at different skill levels, so I can't afford to use monkey-patching or solutions that modify the definition of the example classes ClassToWrap or Wrapped. I am really hoping for an extension to the code above that overrides Wrapped.__init__.

Please note that this question is not simply a duplicate of e.g. Can I exactly mimic inheritance behavior with delegation by composition in Python?. That post does not have any answer that is nearly as detailed as what I'm already providing here.

Dave Kielpinski
  • 1,152
  • 1
  • 9
  • 20
  • I'm not quite sure I understand what issue you're having currently. Do you just want `Wrapper.__init__` to handle creating the instance of `__wraps__` for you? Why not have it accept `*args` and `**kwargs` and do `self._obj = self.__wraps__(*args, **kwargs)`? – Blckknght Aug 14 '17 at 20:17

1 Answers1

1

It sounds like you just want Wrapper.__init__ method to work differently that it currently does. Rather than taking an already existing instance of the __wraps__ class, it should take the arguments that the other class expects in its constructor and built the instance for you. Try something like this:

def __init__(self, *args, **kwargs):
    if self.__wraps__ is None:
        raise TypeError("base class Wrapper may not be instantiated")
    else:
        self._obj = self.__wraps__(*args, **kwargs)

If you want Wrapper to remain the same for some reason, you could put the logic in a new Wrapped.__init__ method instead:

def __init__(self, data): # I'm explicitly naming the argument here, but you could use *args
    super(self, Wrapped).__init__(self.__wraps__(data)) # and **kwargs to make it extensible
Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Awesome! I've ended up with something that does the job a little inelegantly, I'd love to do better: `def __init__(self, *args, **kwargs): if self.__wraps__ is None: raise TypeError("base class Wrapper may not be instantiated") elif args[0].__class__ is self.__wraps__: self._obj = args[0] else: self._obj = self.__wraps__(*args, **kwargs)` – Dave Kielpinski Aug 18 '17 at 00:50
  • I'm not sure there's any better approach that lets you do what that code does. It's probably a bad design to support both instances of the wrapped class and arguments for its constructor. Perhaps you should move one version to an alternative constructor, implemented as a `classmethod`? – Blckknght Aug 18 '17 at 01:33