Change the docstring of a class method within a decorator based on class attribute

Viewed 1478

Let's start with this:

class Example(object):

    change_docstring = True

    @add_to_docstring(" (additional content!)")
    def example_method(self):
        """Example docstring."""
        pass

What I am trying to do is allow the @add_to_docstring decorator to append its parameter string to the docstring of the method only if the change_docstring attribute is True. I do not want to pass anything else into the decorator.

This solution works, but it is not exactly what I'm looking for.

def add_to_docstring(text):

    def decorator(original_method):

        def wrapper(self):
            """wrapper docstring."""
            wrapper.__doc__ = original_method.__doc__

            if self.change_docstring:
                wrapper.__doc__ += text

            return original_method(self)

        return wrapper

    return decorator

Let me explain.

The above solution only changes the docstring if example_method is executed. The docstring does not change when the class, method, etc. is loaded.

>>> Example.example_method.__doc__
"wrapper docstring."
>>>
>>> Example().example_method()
>>> Example.example_method.__doc__
"Example docstring. (additional content!)"

This is what I would like the output of the above command to be:

>>> Example.example_method.__doc__
"Example docstring. (additional content!)"

Again, I do not want to pass anything else into the decorator.

Update

For additional clarification, this is to allow for a decorator to change the docstring of a method, and to have that change be reflected in Sphinx generated documentation. Sphinx loads everything and gathers docstrings, but it does not do anything more.

Based on the selected solution, I added a module variable in the decorators module and exposed a method to disable the docstring change feature in the decorators. In order to disable the feature universally, I then called that disable function within my Sphinx conf.py files like so:

# import the decorators module
from some_modules import decorators
# disable the docstring change feature
decorators.disable_docstring_change()

Then the decorator can be used on any method in the project and the docstring changes will be either enabled or disabled.

2 Answers

As mentioned in Martijn Pieter's answer to "Accessing class variables from a list comprehension in the class definition" you cannot access class attributes if you're inside a new scope in the class. That answer mainly focuses on comprehensions and generator expressions in the class scope, but the same applies to ordinary functions, including decorators.

A simple way around this is to make change_docstring a global, and define it just before the class so that you can easily set it on a class by class basis. Another option is to make it an argument of the decorator, but you said you'd prefer not to do that. Here's a short demo that works on both Python 2 & 3.

def add_to_docstring(text):
    def decorator(original_method):
        def wrapper(self):
            return original_method(self)
        wrapper.__doc__ = original_method.__doc__
        if change_docstring:
            wrapper.__doc__ += text
        return wrapper
    return decorator

change_docstring = True
class Example(object):
    @add_to_docstring(" (additional content!)")
    def example_method(self):
        """Example docstring."""
        pass

change_docstring = False
class Other(object):
    @add_to_docstring(" (more content!)")
    def example_method(self):
        """Other docstring."""
        pass

print(Example.example_method.__doc__)
print(Other.example_method.__doc__)

output

Example docstring. (additional content!)
Other docstring.

Decorate and tag the methods

We don't need to care about much here for function signatures, whether it's bound or unbound - we just put an attribute with the additional text on the function object whatever it is.

def add_to_docstring(text):
    def func(f):
        f.__add_to_docstring = text
        return f
    return func

Decorate the class to indicate we want tagged methods to be honoured

By using a class decorator we can indicate we wish to honour the tagged methods and change the docstrings. We scan over the callable objects, check if they're decorated objects that contain something to add to the docstring and make the appropriate changes before returning a new type with different function docstrings.

def change_docstrings(cls):
    for obj in vars(cls).values():
        if callable(obj) and hasattr(obj, '__add_to_docstring'):
            obj.__doc__ = (obj.__doc__ or '') + obj.__add_to_docstring
            del obj.__add_to_docstring
    return cls

Putting that together

@change_docstrings
class Example:
    @add_to_docstring('(cabbage!)')
    def example(self):
        """ something here """
        pass

Checking Example.example.__doc__ we get - ' something here (cabbage!)' and if you remove the @change_docstrings class decorator - you get no changes.

Note that this moves the change_docstrings out of the class and to whether you decorate or not, however, it allows a construct such as:

unchanged_docstrings = Example
changed_docstrings = change_docstrings(Example)
Related