Наследование строк документации методов в Python - PullRequest
50 голосов
/ 12 ноября 2011

У меня есть иерархия OO со строками документов, которые требуют столько же обслуживания, сколько и сам код. Например.,

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

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

Теперь проблема в том, что AfricanSwallow.airspeed не наследует строку документации метода суперкласса. Я знаю, что могу сохранить строку документации, используя шаблонный метод, т. Е.

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

и реализации _ask_arthur в каждом подклассе. Однако мне было интересно, есть ли другой способ наследования строк документации, возможно, какой-нибудь декоратор, который я еще не обнаружил?

Ответы [ 5 ]

23 голосов
/ 12 ноября 2011

Это разновидность метакласса Пола Макгуайра DocStringInheritor .

  1. Он наследует строку документа родительского элемента, если дочерний элемент Строка документа пуста.
  2. Он наследует строку документации родительского класса, если строка документа дочернего класса пустой.
  3. Может наследовать строку документации от любого класса в любой из MRO базовых классов, как и обычное наследование атрибутов.
  4. В отличие от декоратора классов, метакласс наследуется, поэтому вам нужно только один раз установить метакласс в некотором базовом классе верхнего уровня, и наследование строк документации будет происходить во всей вашей иерархии ООП.

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)
22 голосов
/ 12 ноября 2011

Напишите функцию в стиле класса-декоратора, чтобы сделать копирование для вас. В Python2.5 вы можете применить его сразу после создания класса. В более поздних версиях вы можете подать заявление с пометкой @ decorator .

Вот первый пример того, как это сделать:

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__

В новых версиях Python последняя часть еще более проста и красива:

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

Это метод Pythonic, который точно соответствует дизайну существующих инструментов в стандартной библиотеке. Например, декоратор класса functools.total_ordering добавляет недостающие богатые методы сравнения к классам. А для другого примера, декоратор functools.wraps копирует метаданные из одной функции в другую.

13 голосов
/ 16 июля 2016

F.Y.I для людей, которые только сейчас спотыкаются на этой теме: Начиная с Python 3.5, inspect.getdoc автоматически получает строки документации из иерархии наследования.

Ответы выше, таким образом, полезны для Python 2, или если вы хотите быть более креативным, объединяя строки документов родителей и детей.

Я также создал несколько легких инструментов для наследования строк документации . Они поддерживают несколько хороших стилей документации по умолчанию (numpy, google, reST) из коробки. Вы также можете легко использовать свой собственный стиль документации

4 голосов
/ 31 мая 2014

Следующая адаптация также обрабатывает свойства и классы смешивания. Я также столкнулся с ситуацией, когда мне пришлось использовать func.__func__ (для «instancemethod»), но я не совсем уверен, почему другие решения не помогли решить эту проблему.

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
0 голосов
/ 07 июля 2016
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
...