Inheriting methods' docstrings in Python
Asked Answered
Z

6

60

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?

Zyrian answered 11/11, 2011 at 21:26 Comment(4)
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 ;)Aurelio
Have a look at google.com/search?q=python+inherit+docstring -- there are lots of solutions.Zoospore
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 #2026062Epner
@wberry: class decorators aren't an option, as I'm targeting Python 2.5. Sorry, I should have said that earlier.Zyrian
B
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.

Booty answered 12/11, 2011 at 0:44 Comment(2)
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.Benedick
Here is a version that will also inherit the description of properties.Jigging
Z
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)
Zoraidazorana answered 11/11, 2011 at 23:17 Comment(6)
@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.Zoraidazorana
It's only overkill in case there is a simpler solution which has no drawbacks.Dextrorotation
This crashes if the derived classes have properties (Python 2.7.9). See: https://mcmap.net/q/176547/-python-inheriting-docstring-errors-39-read-only-39/551045Jigging
@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.Zoraidazorana
I would like to incorporate code derived from this in the MIT licensed typhon library. Would you be willing to relicense your code under an MIT-compatible license so that I can legally do so?Ethnogeny
@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.Zoraidazorana
B
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.

Booty answered 12/11, 2011 at 0:44 Comment(2)
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.Benedick
Here is a version that will also inherit the description of properties.Jigging
C
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

Calc answered 16/7, 2016 at 18:30 Comment(0)
C
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
Clamber answered 30/5, 2014 at 22:20 Comment(0)
N
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
Nadanadab answered 7/7, 2016 at 14:59 Comment(0)
A
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]
Askari answered 7/3, 2022 at 8:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.