14

There is a question about Inherit docstrings in Python class inheritance, but the answers there deal with method docstrings.

My question is how to inherit a docstring of a parent class as the __doc__ attribute. The usecase is that Django rest framework generates nice documentation in the html version of your API based on your view classes' docstrings. But when inheriting a base class (with a docstring) in a class without a docstring, the API doesn't show the docstring.

It might very well be that sphinx and other tools do the right thing and handle the docstring inheritance for me, but django rest framework looks at the (empty) .__doc__ attribute.

class ParentWithDocstring(object):
    """Parent docstring"""
    pass


class SubClassWithoutDoctring(ParentWithDocstring):
    pass


parent = ParentWithDocstring()
print parent.__doc__  # Prints "Parent docstring".
subclass = SubClassWithoutDoctring()
print subclass.__doc__  # Prints "None"

I've tried something like super(SubClassWithoutDocstring, self).__doc__, but that also only got me a None.

Community
  • 1
  • 1
Reinout van Rees
  • 13,486
  • 2
  • 36
  • 68

4 Answers4

14

Since you cannot assign a new __doc__ docstring to a class (in CPython at least), you'll have to use a metaclass:

import inspect

def inheritdocstring(name, bases, attrs):
    if not '__doc__' in attrs:
        # create a temporary 'parent' to (greatly) simplify the MRO search
        temp = type('temporaryclass', bases, {})
        for cls in inspect.getmro(temp):
            if cls.__doc__ is not None:
                attrs['__doc__'] = cls.__doc__
                break

    return type(name, bases, attrs)

Yes, we jump through an extra hoop or two, but the above metaclass will find the correct __doc__ however convoluted you make your inheritance graph.

Usage:

>>> class ParentWithDocstring(object):
...     """Parent docstring"""
... 
>>> class SubClassWithoutDocstring(ParentWithDocstring):
...     __metaclass__ = inheritdocstring
... 
>>> SubClassWithoutDocstring.__doc__
'Parent docstring'

The alternative is to set __doc__ in __init__, as an instance variable:

def __init__(self):
    try:
        self.__doc__ = next(cls.__doc__ for cls in inspect.getmro(type(self)) if cls.__doc__ is not None)
    except StopIteration:
        pass

Then at least your instances have a docstring:

>>> class SubClassWithoutDocstring(ParentWithDocstring):
...     def __init__(self):
...         try:
...             self.__doc__ = next(cls.__doc__ for cls in inspect.getmro(type(self)) if cls.__doc__ is not None)
...         except StopIteration:
...             pass
... 
>>> SubClassWithoutDocstring().__doc__
'Parent docstring'

As of Python 3.3 (which fixed issue 12773), you can finally just set the __doc__ attribute of custom classes, so then you can use a class decorator instead:

import inspect

def inheritdocstring(cls):
    for base in inspect.getmro(cls):
        if base.__doc__ is not None:
            cls.__doc__ = base.__doc__
            break
    return cls

which then can be applied thus:

>>> @inheritdocstring
... class SubClassWithoutDocstring(ParentWithDocstring):
...     pass
... 
>>> SubClassWithoutDocstring.__doc__
'Parent docstring'
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    I'll just add from what I recall about discussions I've had regarding Python about this. It doesn't inherit docstrings by default because it's considered that since Python can't know whether the docstring would be meaningful anymore (although inheritance should be enough to mean the programmer is keeping in line with normal OOP to not completely change the meaning of an object), it was considered the case where the doc was blank was going to be less mis-leading. It gets more complex where the parent class is an ABC for instance... – Jon Clements Dec 18 '12 at 17:16
  • In 3.3 the `__doc__` `getset_descriptor` is now writable for heap types. Before it only had a `getter` defined; now it has the `setter` [`type_set_doc`](http://hg.python.org/cpython/file/bd8afb90ebf2/Objects/typeobject.c#l632). `check_set_special_type_attr` prevents deleting `__doc__`. – Eryk Sun Dec 18 '12 at 18:21
  • @eryksun: Confirmed; that's a fix that was long overdue! Resurrected my original class decorator idea for Python 3.3 only. – Martijn Pieters Dec 18 '12 at 18:23
  • Thanks! In the end I used the `__init__()` one. The metaclass requires me to have that metaclass in lots of places instead of only one __init__ in one (base) class. – Reinout van Rees Dec 18 '12 at 19:51
  • Actually, I used your `inspect.getmro(type(self))` solution from the `.__init__()` in django rest framework's `.get_description()` method, but the idea is the same. – Reinout van Rees Dec 18 '12 at 20:00
2

In this particular case you could also override how REST framework determines the name to use for the endpoint, by overriding the .get_name() method.

If you do take that route you'll probably find yourself wanting to define a set of base classes for your views, and override the method on all your base view using a simple mixin class.

For example:

class GetNameMixin(object):
    def get_name(self):
        # Your docstring-or-ancestor-docstring code here

class ListAPIView(GetNameMixin, generics.ListAPIView):
    pass

class RetrieveAPIView(GetNameMixin, generics.RetrieveAPIView):
    pass

Note also that the get_name method is considered private, and is likely to change at some point in the future, so you would need to keep tabs on the release notes when upgrading, for any changes there.

Tom Christie
  • 33,394
  • 7
  • 101
  • 86
  • You probably mean `.get_description()` instead of `.get_name()`? Yes, I've seen that one and I've got a base class where I'm trying to overwrite it. But I still can't lay my hands on my parents' docstrings :-) Well, at least, that was before I read the other answer. – Reinout van Rees Dec 18 '12 at 19:34
  • "You probably mean .get_description() instead of .get_name()" Indeed, yes I did. – Tom Christie Dec 19 '12 at 14:24
  • See http://reinout.vanrees.org/weblog/2012/12/19/docstring-inheritance-djangorestframework.html for how I did it in the end. – Reinout van Rees Dec 19 '12 at 16:12
2

The simplest way is to assign it as a class variable:

class ParentWithDocstring(object):
    """Parent docstring"""
    pass

class SubClassWithoutDoctring(ParentWithDocstring):
    __doc__ = ParentWithDocstring.__doc__

parent = ParentWithDocstring()
print parent.__doc__  # Prints "Parent docstring".
subclass = SubClassWithoutDoctring()
assert subclass.__doc__ == parent.__doc__

It's manual, unfortunately, but straightforward. Incidentally, while string formatting doesn't work the usual way, it does with the same method:

class A(object):
    _validTypes = (str, int)
    __doc__ = """A accepts the following types: %s""" % str(_validTypes)

A accepts the following types: (<type 'str'>, <type 'int'>)
Walter Nissen
  • 16,451
  • 4
  • 26
  • 27
  • Note: according to Martijn's answer (http://stackoverflow.com/a/13937525/27401), setting `.__doc__` only works in python 3.3. – Reinout van Rees Aug 12 '13 at 14:03
  • 2
    @ReinoutvanRees, I tried this in 2.3.4 (yes, two point three) and 2.7. You can't assign to .__doc__ after the class is defined, true, but you can do it at the time of definition. That's why I posted my answer. – Walter Nissen Aug 12 '13 at 16:08
  • Ah, you're right. So that would be an option, too, provided you remember to actually assign the `__doc__` at definition time. Quite OK and simple. – Reinout van Rees Aug 13 '13 at 00:37
0

You can also do it using @property

class ParentWithDocstring(object):
    """Parent docstring"""
    pass

class SubClassWithoutDocstring(ParentWithDocstring):
    @property
    def __doc__(self):
        return None

class SubClassWithCustomDocstring(ParentWithDocstring):
    def __init__(self, docstring, *args, **kwargs):
        super(SubClassWithCustomDocstring, self).__init__(*args, **kwargs)
        self.docstring = docstring
    @property
    def __doc__(self):
        return self.docstring

>>> parent = ParentWithDocstring()
>>> print parent.__doc__  # Prints "Parent docstring".
Parent docstring
>>> subclass = SubClassWithoutDocstring()
>>> print subclass.__doc__  # Prints "None"
None
>>> subclass = SubClassWithCustomDocstring('foobar')
>>> print subclass.__doc__  # Prints "foobar"
foobar

You can even overwrite a docstring.

class SubClassOverwriteDocstring(ParentWithDocstring):
    """Original docstring"""
    def __init__(self, docstring, *args, **kwargs):
        super(SubClassOverwriteDocstring, self).__init__(*args, **kwargs)
        self.docstring = docstring
    @property
    def __doc__(self):
        return self.docstring

>>> subclass = SubClassOverwriteDocstring('new docstring')
>>> print subclass.__doc__  # Prints "new docstring"
new docstring

One caveat, the property can't be inherited by other classes evidently, you have to add the property in each class that you want to overwrite the docstring.

class SubClassBrokenDocstring(SubClassOverwriteDocstring):
    """Broken docstring"""
    def __init__(self, docstring, *args, **kwargs):
        super(SubClassBrokenDocstring, self).__init__(docstring, *args, **kwargs)

>>> subclass = SubClassBrokenDocstring("doesn't work")
>>> print subclass.__doc__  # Prints "Broken docstring"
Broken docstring

Bummer! But definitely easier than doing the meta class thing!

Mark Mikofski
  • 19,398
  • 2
  • 57
  • 90