lauren.marietta
lauren.marietta

Reputation: 2554

Using class attributes to modify a docstring with a decorator in Python

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).

Upvotes: 1

Views: 1227

Answers (3)

lauren.marietta
lauren.marietta

Reputation: 2554

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"

Upvotes: 1

blhsing
blhsing

Reputation: 106543

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

Upvotes: 1

chepner
chepner

Reputation: 531075

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)

Upvotes: 2

Related Questions