1

I’m trying to create a decorator that is called within a class, which would pull attributes from that class, and use those class attributes to edit the function’s docstring.

My problem is that I have found examples of decorators that edit the docstring of the function (setting the function's __doc__ attribute equal to a new string), and I have also found examples of decorators that pull attributes from the parent class (by passing self into the decorator), but I haven’t been able to find an example of a decorator that is able to do both.

I have tried to combine these two examples, but it isn't working:

def my_decorator(func):
    def wrapper(self, *args, **kwargs):
        name = func.__name__  # pull function name
        cls = self.__class__.__name__ # pull class name
        func.__doc__ = "{} is new for the function {} in class {}".format(
            str(func.__doc__), name, cls) # set them to docstring
        return func(self, *args, **kwargs)
    return wrapper

class Test():
    @my_decorator
    def example(self, examplearg=1):
        """Docstring"""
        pass

With this, I would hope that the following would return "Docstring is now new for the function: example":

Test().example.__doc__

Instead it returns None.

Edit: Note that I am not interested in how to access the name of the class specifically, so much as how to access the class attributes in general (where here self.__class__.__name__ is used as an example).

lauren.marietta
  • 2,125
  • 1
  • 11
  • 19

3 Answers3

2

example is replaced with wrapper; the decoration is equivalent to

def example(self, examplearg=1):
    """Docstring"""
    pass

 example = my_decorator(example)

so you need to set wrapper.__doc__, not func.__doc__.

def my_decorator(func):
    def wrapper(self, *args, **kwargs):
        return func(self, *args, **kwargs)
    wrapper.__doc__ = "{} is new for the function {}".format(
        str(func.__doc__),
        func.__name__) 
    return wrapper

Note that at the time you call my_decorator, you don't have any information about what class the decorated function/method belongs to. You would have to pass its name explicitly:

def my_decorator(cls_name):
    def _decorator(func):
        def wrapper(self, *args, **kwargs):
            return func(self, *args, **kwargs)
        wrapper.__doc__ = "{} is new for function {} in class {}".format(
            func.__doc__, 
            func.__name__,
            cls_name)
       return wrapper
    return _decorator

class Test():
    @my_decorator("Test")
    def example(self, examplearg=1):
        """Docstring"""

    # or
    # def example(self, examplearg=1):
    #     """Docstring"""
    #
    # example = my_decorator("Test")(example)
chepner
  • 497,756
  • 71
  • 530
  • 681
  • 1
    Was about to point that about not having access to the class. [This related question](https://stackoverflow.com/q/2366713) has some discussion about it. – jdehesa Sep 18 '18 at 14:30
  • This works great for the specific example of accessing the class name, where the name is a string you can pass as a static argument to the decorator. However, how would you actually access the class attributes? In reality, I need `self.__class__`, in order to access `self.__class__.__base__` (a class object) and get the docstring from a method within a base class -- not only accessing the string of `self.__class__.__name__`. – lauren.marietta Sep 18 '18 at 14:48
  • The class attributes, at the time your decorator is called, aren't class attributes: they are simply local variables. They aren't attached to the class until the entire body of the `class` statement has been executed and the resulting `dict` passed to the metaclass. `class Foo: ...` is roughly equivalent to `d = {...}; Foo = type('Foo', (object,), d)`. – chepner Sep 18 '18 at 15:02
1

You can simply modify the __doc__ attribute when the decorator is called instead, and use the first token of the dot-delimited __qualname__ attribute of the function to obtain the class name:

def my_decorator(func):
    func.__doc__ = "{} is new for the function {} in class {}".format(
            str(func.__doc__), func.__name__, func.__qualname__.split('.')[0])
    return func

so that:

class Test():
    @my_decorator
    def example(self, examplearg=1):
        """Docstring"""
        pass
print(Test().example.__doc__)

would output:

Docstring is new for the function example in class Test
blhsing
  • 91,368
  • 6
  • 71
  • 106
1

Turns out that accessing class attributes from within a class is impossible, as the class has yet to be executed when the decorator is called. So the original goal - using a decorator within a class to access class attributes - does not seem to be possible.

However, thanks to jdehesa for pointing me to a workaround that allows access to the class attributes using a class decorator, here: Can a Python decorator of an instance method access the class?.

I was able to use the class decorator to alter the specific method's docstring using class attributes like so:

def class_decorator(cls):
    for name, method in cls.__dict__.items():
        if name == 'example':
            # do something with the method
            method.__doc__ = "{} is new for function {} in class {}".format(method.__doc__, name, cls.__name__)
            # Note that other class attributes such as cls.__base__ 
            # can also be accessed in this way
    return cls

@class_decorator
class Test():
    def example(self, examplearg=1):
        """Docstring"""

print(Test().example.__doc__)
# Returns "Docstring is new for function example in class Test"
lauren.marietta
  • 2,125
  • 1
  • 11
  • 19
  • It isn't impossible to access the class name from a method decorator. Did you try my answer? – blhsing Sep 18 '18 at 15:36
  • Yes, however `__qualname__` returns a string of the class name, whereas I need the class object in order to access class attributes (beyond just the name). It isn't possible to access /all/ of the class attributes from a method decorator. Please see the edit to the original post - getting the class name was just an example of accessing a class attribute. – lauren.marietta Sep 18 '18 at 18:59