58

I have an OO hierarchy with docstrings that take as much maintenance as the code itself. E.g.,

class Swallow(object):
    def airspeed(self):
        """Returns the airspeed (unladen)"""
        raise NotImplementedError

class AfricanSwallow(Swallow):
    def airspeed(self):
        # whatever

Now, the problem is that AfricanSwallow.airspeed does not inherit the superclass method's docstring. I know I can keep the docstring using the template method pattern, i.e.

class Swallow(object):
    def airspeed(self):
        """Returns the airspeed (unladen)"""
        return self._ask_arthur()

and implementing _ask_arthur in each subclass. However, I was wondering whether there's another way to have docstrings be inherited, perhaps some decorator that I hadn't discovered yet?

Fred Foo
  • 355,277
  • 75
  • 744
  • 836
  • 8
    The example alone would be worth +1 (you see far too few Python references outside the official documentation). Luckily, the remaining question also justifies an upvote ;) –  Nov 11 '11 at 21:30
  • Have a look at http://www.google.com/search?q=python+inherit+docstring -- there are lots of solutions. – Sven Marnach Nov 11 '11 at 21:36
  • 3
    It should be possible to write a class decorator that goes through all the methods to see if their `__doc__` is `None` and if so borrows the super `__doc__`. Don't have time to try it right now... See also http://stackoverflow.com/questions/2025562/inherit-docstrings-in-python-class-inheritance – wberry Nov 11 '11 at 23:07
  • @wberry: class decorators aren't an option, as I'm targeting Python 2.5. Sorry, I should have said that earlier. – Fred Foo Nov 11 '11 at 23:12

6 Answers6

24

This is a variation on Paul McGuire's DocStringInheritor metaclass.

  1. It inherits a parent member's docstring if the child member's docstring is empty.
  2. It inherits a parent class docstring if the child class docstring is empty.
  3. It can inherit the docstring from any class in any of the base classes's MROs, just like regular attribute inheritance.
  4. Unlike with a class decorator, the metaclass is inherited, so you only need to set the metaclass once in some top-level base class, and docstring inheritance will occur throughout your OOP hierarchy.

import unittest
import sys

class DocStringInheritor(type):
    """
    A variation on
    http://groups.google.com/group/comp.lang.python/msg/26f7b4fcb4d66c95
    by Paul McGuire
    """
    def __new__(meta, name, bases, clsdict):
        if not('__doc__' in clsdict and clsdict['__doc__']):
            for mro_cls in (mro_cls for base in bases for mro_cls in base.mro()):
                doc=mro_cls.__doc__
                if doc:
                    clsdict['__doc__']=doc
                    break
        for attr, attribute in clsdict.items():
            if not attribute.__doc__:
                for mro_cls in (mro_cls for base in bases for mro_cls in base.mro()
                                if hasattr(mro_cls, attr)):
                    doc=getattr(getattr(mro_cls,attr),'__doc__')
                    if doc:
                        if isinstance(attribute, property):
                            clsdict[attr] = property(attribute.fget, attribute.fset, 
                                                     attribute.fdel, doc)
                        else:
                            attribute.__doc__ = doc
                        break
        return type.__new__(meta, name, bases, clsdict)



class Test(unittest.TestCase):

    def test_null(self):
        class Foo(object):

            def frobnicate(self): pass

        class Bar(Foo, metaclass=DocStringInheritor):
            pass

        self.assertEqual(Bar.__doc__, object.__doc__)
        self.assertEqual(Bar().__doc__, object.__doc__)
        self.assertEqual(Bar.frobnicate.__doc__, None)

    def test_inherit_from_parent(self):
        class Foo(object):
            'Foo'

            def frobnicate(self):
                'Frobnicate this gonk.'
        class Bar(Foo, metaclass=DocStringInheritor):
            pass
        self.assertEqual(Foo.__doc__, 'Foo')
        self.assertEqual(Foo().__doc__, 'Foo')
        self.assertEqual(Bar.__doc__, 'Foo')
        self.assertEqual(Bar().__doc__, 'Foo')
        self.assertEqual(Bar.frobnicate.__doc__, 'Frobnicate this gonk.')

    def test_inherit_from_mro(self):
        class Foo(object):
            'Foo'

            def frobnicate(self):
                'Frobnicate this gonk.'
        class Bar(Foo):
            pass

        class Baz(Bar, metaclass=DocStringInheritor):
            pass

        self.assertEqual(Baz.__doc__, 'Foo')
        self.assertEqual(Baz().__doc__, 'Foo')
        self.assertEqual(Baz.frobnicate.__doc__, 'Frobnicate this gonk.')

    def test_inherit_metaclass_(self):
        class Foo(object):
            'Foo'

            def frobnicate(self):
                'Frobnicate this gonk.'
        class Bar(Foo, metaclass=DocStringInheritor):
            pass

        class Baz(Bar):
            pass
        self.assertEqual(Baz.__doc__, 'Foo')
        self.assertEqual(Baz().__doc__, 'Foo')
        self.assertEqual(Baz.frobnicate.__doc__, 'Frobnicate this gonk.')

    def test_property(self):
        class Foo(object):
            @property
            def frobnicate(self): 
                'Frobnicate this gonk.'
        class Bar(Foo, metaclass=DocStringInheritor):
            @property
            def frobnicate(self): pass

        self.assertEqual(Bar.frobnicate.__doc__, 'Frobnicate this gonk.')


if __name__ == '__main__':
    sys.argv.insert(1, '--verbose')
    unittest.main(argv=sys.argv)
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • 2
    @NeilG: I updated the code to be compatible with Python3. The only change necessary (now) is to define `Baz` with `class Baz(Bar,metaclass=DocStringInheritor)` instead of `__metaclass__ = DocStringInheritor` in the class body. – unutbu Nov 12 '11 at 00:41
  • 7
    It's only overkill in case there is a simpler solution which has no drawbacks. – Alfe May 24 '13 at 10:08
  • This crashes if the derived classes have properties (Python 2.7.9). See: http://stackoverflow.com/q/38600186/551045 – RedX Jul 26 '16 at 22:09
  • @RedX: Thanks for bringing this to my attention. Since property `__doc__` attributes can not be modified by reassignment, I modified the code above to handle properties by reassigning the class attribute to a new property with the old getter, setter, deleter but a new docstring. – unutbu Jul 27 '16 at 02:33
  • I would like to incorporate code derived from this in the MIT licensed [typhon](https://pypi.python.org/pypi/typhon) library. Would you be willing to relicense your code under an MIT-compatible license so that I can legally do so? – gerrit Jul 07 '17 at 17:01
  • @gerrit: Provided that Paul McGuire also agrees, I am willing to grant permission for the use of the code in this post under the MIT license. – unutbu Jul 07 '17 at 19:38
23

Write a function in a class-decorator style to do the copying for you. In Python2.5, you can apply it directly after the class is created. In later versions, you can apply with the @decorator notation.

Here's a first cut at how to do it:

import types

def fix_docs(cls):
    for name, func in vars(cls).items():
        if isinstance(func, types.FunctionType) and not func.__doc__:
            print func, 'needs doc'
            for parent in cls.__bases__:
                parfunc = getattr(parent, name, None)
                if parfunc and getattr(parfunc, '__doc__', None):
                    func.__doc__ = parfunc.__doc__
                    break
    return cls


class Animal(object):
    def walk(self):
        'Walk like a duck'

class Dog(Animal):
    def walk(self):
        pass

Dog = fix_docs(Dog)
print Dog.walk.__doc__

In newer Python versions, the last part is even more simple and beautiful:

@fix_docs
class Dog(Animal):
    def walk(self):
        pass

This is a Pythonic technique that exactly matches the design of existing tools in the standard library. For example, the functools.total_ordering class decorator add missing rich comparison methods to classes. And for another example, the functools.wraps decorator copies metadata from one function to another.

Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • This answer contains an error. `vars(cls)` contains the pair `'__doc__': None` from the class, which raises an `AttributeError` in `if func.__doc__`. That item should be either skipped or special cased. – Bakuriu Jul 26 '16 at 21:48
  • [Here](http://stackoverflow.com/a/38601305/551045) is a version that will also inherit the description of properties. – RedX Jul 27 '16 at 05:46
19

F.Y.I for people just now stumbling on this topic: As of Python 3.5, inspect.getdoc automatically retrieves docstrings from the inheritance hierarchy.

The responses above are thus useful for Python 2, or if you want to be more creative with merging the docstrings of parents and children.

I've also created some lightweight tools for docstring inheritance. These support some nice default docstring styles (numpy, google, reST) out of the box. You can easily use your own docstring style as well

Ryan Soklaski
  • 540
  • 5
  • 8
4

The following adaptation also handles properties and mixin classes. I also came across a situation where I had to use func.__func__ (for "instancemethod"s), but I'm not completely sure why the other solutions didn't encouter that problem.

def inherit_docs(cls):
    for name in dir(cls):
        func = getattr(cls, name)
        if func.__doc__: 
            continue
        for parent in cls.mro()[1:]:
            if not hasattr(parent, name):
                continue
            doc = getattr(parent, name).__doc__
            if not doc: 
                continue
            try:
                # __doc__'s of properties are read-only.
                # The work-around below wraps the property into a new property.
                if isinstance(func, property):
                    # We don't want to introduce new properties, therefore check
                    # if cls owns it or search where it's coming from.
                    # With that approach (using dir(cls) instead of var(cls))
                    # we also handle the mix-in class case.
                    wrapped = property(func.fget, func.fset, func.fdel, doc)
                    clss = filter(lambda c: name in vars(c).keys() and not getattr(c, name).__doc__, cls.mro())
                    setattr(clss[0], name, wrapped)
                else:
                    try:
                        func = func.__func__ # for instancemethod's
                    except:
                        pass
                    func.__doc__ = doc
            except: # some __doc__'s are not writable
                pass
            break
    return cls
letmaik
  • 3,348
  • 1
  • 36
  • 43
0
def fix_docs(cls):
    """ copies docstrings of derived attributes (methods, properties, attrs) from parent classes."""
    public_undocumented_members = {name: func for name, func in vars(cls).items()
                                   if not name.startswith('_') and not func.__doc__}

    for name, func in public_undocumented_members.iteritems():
        for parent in cls.mro()[1:]:
            parfunc = getattr(parent, name, None)
            if parfunc and getattr(parfunc, '__doc__', None):
                if isinstance(func, property):
                    # copy property, since its doc attribute is read-only
                    new_prop = property(fget=func.fget, fset=func.fset,
                                        fdel=func.fdel, doc=parfunc.__doc__)
                    cls.func = new_prop
                else:
                    func.__doc__ = parfunc.__doc__
                break
    return cls
marscher
  • 800
  • 1
  • 5
  • 22
0

It is a very old thread. But If anyone is looking for a simple way, you can do this with __init_subclass__ which is called whenever you inherit that class, if you have access to parent class to make a change.

def __init_subclass__(cls, **kwargs):
    super().__init_subclass__(**kwargs)
    parent_method_docstr = {}
    for i, v in ParentClass.__dict__.items():
        if v and callable(v) and v.__doc__ is not None:
            parent_method_docstr[i] = v.__doc__

    for i, v in cls.__dict__.items():
        if v and callable(v) and v.__doc__ is None and i in parent_method_docstr:
            v.__doc__ = parent_method_docstr[i]
Durai
  • 505
  • 4
  • 12