51 votes

Héritage des docstrings des méthodes en Python

J'ai une hiérarchie OO avec des docstrings qui demandent autant de maintenance que le code lui-même. Par exemple,

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

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

Maintenant, le problème est que AfricanSwallow.airspeed n'hérite pas de la docstring de la méthode de la superclasse. Je sais que je peux conserver la docstring en utilisant le modèle de méthode template, c'est à dire

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

et de mettre en œuvre _ask_arthur dans chaque sous-classe. Cependant, je me demandais s'il y avait un autre moyen de faire hériter les docstrings, peut-être un décorateur que je n'ai pas encore découvert ?

23voto

unutbu Points 222216

Il s'agit d'une variante de La métaclasse DocStringInheritor de Paul McGuire .

  1. Il hérite de la docstring d'un membre parent si la docstring du membre enfant est vide. du membre enfant est vide.
  2. Elle hérite de la docstring d'une classe parent si la docstring de la classe enfant est vide.
  3. Elle peut hériter de la docstring de n'importe quelle classe dans n'importe quelle MRO de la classe de base, tout comme l'héritage d'un attribut ordinaire.
  4. Contrairement à un décorateur de classe, la métaclasse est héritée, de sorte que vous ne devez définir la métaclasse qu'une seule fois dans une classe de base de haut niveau, et l'héritage de la docstring se produira tout au long de votre hiérarchie OOP.

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)

22voto

Raymond Hettinger Points 50330

Écrivez une fonction dans un style de décorateur de classe pour faire la copie pour vous. Dans Python2.5, vous pouvez l'appliquer directement après la création de la classe. Dans les versions ultérieures, vous pouvez l'appliquer avec la fonction @decorator la notation.

Voici un premier aperçu de la façon de procéder :

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__

Dans les versions plus récentes de Python, la dernière partie est encore plus simple et plus belle :

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

Il s'agit d'une technique Python qui correspond exactement à la conception des outils existants dans la bibliothèque standard. Par exemple, l'outil functools.total_ordre Le décorateur de classe ajoute les méthodes de comparaison riches manquantes aux classes. Et pour un autre exemple, le functools.wraps Le décorateur copie les métadonnées d'une fonction à l'autre.

17voto

Ryan Soklaski Points 215

F.Y.I pour les personnes qui ne font que tomber sur ce sujet : A partir de Python 3.5, inspecter.getdoc récupère automatiquement les docstrings de la hiérarchie d'héritage.

Les réponses ci-dessus sont donc utiles pour Python 2, ou si vous voulez être plus créatif avec la fusion des docstrings des parents et des enfants.

J'ai aussi créé des outils légers pour l'héritage de la documentation . Ils prennent en charge certains styles de chaînes de caractères par défaut (numpy, google, reST). Vous pouvez aussi facilement utiliser votre propre style de docstring.

4voto

neo Points 586

L'adaptation suivante traite également les propriétés et les classes mixtes. J'ai également rencontré une situation où j'ai dû utiliser func.__func__ (pour les "instancemethod "s), mais je ne suis pas complètement sûr de la raison pour laquelle les autres solutions n'ont pas résolu ce problème.

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

0voto

marscher Points 195
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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X