13

When we decorate function, we use functools.wraps to make decorated function look like original.

Is there any wat to do same, when we want to decorate class?

def some_class_decorator(cls_to_decorate):
    class Wrapper(cls_to_decorate):
        """Some Wrapper not important doc."""
        pass
    return Wrapper


@some_class_decorator
class MainClass:
    """MainClass important doc."""
    pass


help(MainClass)

Output:

class Wrapper(MainClass)
 |  Some Wrapper not important doc.
 |  
 |  Method resolution order:
 |      Wrapper
 |      MainClass
 |      builtins.object
 |  
 # ... no MainClass important doc below.

I tried to write wraps equivalent for class decorator based on functools.wraps source code, but my implementation doesn't work correct:

import functools


def wraps_cls(original_cls):
    def wrapper(wrapper_cls):
        """Update wrapper_cls to look like original_cls."""
        for attr in functools.WRAPPER_ASSIGNMENTS:
            try:
                value = getattr(original_cls, attr)
            except AttributeError:
                pass
            else:
                setattr(wrapper_cls, attr, value)
        return wrapper_cls
    return wrapper


def some_class_decorator(cls_to_decorate):
    @wraps_cls(cls_to_decorate)
    class Wrapper(cls_to_decorate):
        """Some Wrapper not important doc."""
        pass
    return Wrapper


@some_class_decorator
class MainClass:
    """MainClass important doc."""
    pass


help(MainClass)

Output:

class MainClass(MainClass)
 |  MainClass important doc.
 |  
 |  Method resolution order:
 |      MainClass
 |      MainClass
 |      builtins.object
 |  
 # ... MainClass doc is here but "Method resolution order" is broken.

Is there anyway to completely replace decorated MainClass help output with not decorated MainClass help output?

Community
  • 1
  • 1
Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
  • 1
    Your decorator returns a new class that is a subclass of the original (wrapped) class. There's no way you can get that to have the same method resolution order, because you added an additional class to the hierarchy. – BrenBarn Feb 20 '15 at 05:59
  • Thanks, your wraps_cls method help me wrap a class with parameters, while preserving Class name. – xlash Oct 24 '18 at 21:27
  • Using `@functools.wraps(cls, updated=())` would have saved you from writing your own wrapper. It suffers from the same problem though. – Adrien H Feb 19 '20 at 11:45

2 Answers2

5

No, there isn't, assuming your decorator really subclasses the wrapped class like some_class_decorator does. The output of help is defined by the pydoc.Helper class, which for classes ultimately calls pydoc.text.docclass, which contains this code:

# List the mro, if non-trivial.
mro = deque(inspect.getmro(object))
if len(mro) > 2:
    push("Method resolution order:")
    for base in mro:
        push('    ' + makename(base))
    push('')

You can see that it is hard-coded to display the class's real MRO. This is as it should be. The MRO displayed in your last example is not "broken", it is correct. By making your wrapper class inherit from the wrapped class, you added an additional class to the inheritance hierarchy. It would be misleading to show an MRO that left that out, because there really is a class there. In your example, this wrapper class doesn't provide any behavior of its own, but a realistic wrapper class would (or else why would you be doing the wrapping at all?), and you would want to know which behavior came from the wrapper class and which from the wrapped class.

If you wanted, you could make a decorator that dynamically renamed the wrapper class with some name derived from the original, so the MRO would show something like DecoratorWrapper_of_MainClass in the appropriate position. Whether this would be more readable than just having Wrapper there is debatable.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • "and you would want to know which behavior came from the wrapper class and which from the wrapped class" - yes, that's true, but as in case with decorated function (functools.wraps), original MainClass's doc is more important then Wrapper's doc. It turns that if I decorate class, I can't see MainClass's doc with help(). But I understood your explanation, thanks. May be best way would be just to add MainClass's doc to Wrapper's doc in wraps_cls. – Mikhail Gerasimov Feb 20 '15 at 06:31
  • 2
    @GerasimovMikhail: But that's a separate issue. It's not hard to write a wrapper that shows you MainClass's `__doc__`; your existing solution already does that, as you saw. But `help` shows you more than just the `__doc__`, and you can't interfere with that other output; you only control the `__doc__`. – BrenBarn Feb 20 '15 at 06:35
1

Oh, I guess now I understand what are your trying to achieve.

You want to attach new methods from a "wrapper" using a class decorator.

Here is a workng example:

class Wrapper(object):
    """Some Wrapper not important doc."""
    def another_method(self):
        """Another method."""
        print 'Another method'


def some_class_decorator(cls_to_decorate):
    return type(cls_to_decorate.__name__, cls_to_decorate.__bases__, dict
        (cls_to_decorate.__dict__, another_method=Wrapper.__dict__['another_method']))


class MainClass(object):
    """MainClass important doc."""
    def method(self):
        """A method."""
        print "A method"


help(MainClass)
_MainClass = some_class_decorator(MainClass)
help(_MainClass)
_MainClass().another_method()
MainClass().another_method()

This example creates a new class not modifying the old class.

But I guess you could also just inject the given methods in the old class, modifying it in place.

warvariuc
  • 57,116
  • 41
  • 173
  • 227
  • Thanks. This way can be solution. But I think BrenBarn is right that help() should show real MRO. I think that modify Wrapper's class name (and may be doc) would be more correct, than try to fully replace help() output as I wanted first. – Mikhail Gerasimov Feb 20 '15 at 06:57
  • This solution shows real MRO, because you don't inherit the "wrapped" class from the wrapper. You just take several methods form the "donor". But why do you need this? Why not making a mixin class instead of hacking with decorators? – warvariuc Feb 20 '15 at 07:10