3

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.

mzjn
  • 48,958
  • 13
  • 128
  • 248
gliemezis
  • 778
  • 2
  • 6
  • 18
  • I don't think that's possible (without passing `change_docstring` to the decorator, or making it a global) due to the way class scope works. See [here](https://stackoverflow.com/a/13913933/4014959) for details. Also see https://stackoverflow.com/questions/47223764/python-3-class-variable-is-not-defined – PM 2Ring Nov 22 '17 at 16:54
  • In the very likely case that it is not possible, what would you suggest for making that `change_docstring` available to the decorator? Essentially it is a setting that should default to False. I was just looking to have the setting as an attribute on the class to make it easy to set. – gliemezis Nov 22 '17 at 17:01
  • 1
    Why can't you just leave out the decorator if `change_docstring` is unset or false? – user2357112 Nov 22 '17 at 17:01
  • In this case, the decorator doesn't do anything else, but in the real case, the docstring change will only be a piece of the decorator's logic. – gliemezis Nov 22 '17 at 17:05
  • The simple way is to make `change_docstring` global instead of a class attribute. So you write `change_docstring = True` immediately before the `class Example(object):` line. That still allows you to set it on a class by class basis. – PM 2Ring Nov 22 '17 at 17:14
  • Thank you. I hoping that somehow the class itself could be accessed itself with something like `original_method.__class__.change_docstring`, but from everything I've seen, that is not possible as well. – gliemezis Nov 22 '17 at 17:17
  • 1
    No, you can't access the class itself there because the class doesn't exist as an object until after its definition has been executed. – PM 2Ring Nov 22 '17 at 17:20

2 Answers2

1

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.
PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
  • I like both this answer and Jon Clements' answer, but this is ultimately the idea I ended up going with. In my specific case outside this example, I set it as a module variable and set up a function to change the default value. This way I could set the value once to apply to all classes whose methods are decorated. The difference related to this example is optionally adding `from module import decorators` and `decorators.disable_docstring_change()` lines above the class. – gliemezis Nov 22 '17 at 19:41
1

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)
Jon Clements
  • 138,671
  • 33
  • 247
  • 280