Наследовать родительский класс docstring как атрибут __doc__

Возникает вопрос о Наследовать docstrings в наследовании класса Python, но ответы там касаются метода docstrings.

Мой вопрос заключается в том, как наследовать docstring родительского класса как атрибут __doc__. Usecase заключается в том, что Django rest framework создает хорошую документацию в html-версии вашего API на основе docstrings ваших классов. Но при наследовании базового класса (с docstring) в классе без docstring API не показывает docstring.

Вполне возможно, что sphinx и другие инструменты делают правильные вещи и обрабатывают наследование docstring для меня, но django rest framework смотрит на (пустой) атрибут .__doc__.

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"

Я пробовал что-то вроде super(SubClassWithoutDocstring, self).__doc__, но это также дало мне None.

Ответ 1

Поскольку вы не можете назначить новую __doc__ docstring для класса (по крайней мере, в CPython), вам придется использовать метакласс:

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)

Да, мы перепрыгиваем через дополнительный обруч или два, но вышеупомянутый метакласс найдет правильный __doc__, но запутанный, вы делаете свой график наследования.

Использование:

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

Альтернативой является установка __doc__ в __init__ в качестве переменной экземпляра:

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

Тогда, по крайней мере, ваши экземпляры имеют 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'

Как и в случае с Python 3.3 (который исправил issue 12773), вы можете, наконец, просто установить атрибут __doc__ для пользовательских классов, так что тогда вы вместо этого можно использовать декоратор класса:

import inspect

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

который затем может быть применен таким образом:

>>> @inheritdocstring
... class SubClassWithoutDocstring(ParentWithDocstring):
...     pass
... 
>>> SubClassWithoutDocstring.__doc__
'Parent docstring'

Ответ 2

В этом конкретном случае вы также можете переопределить, как среда REST определяет имя, используемое для конечной точки, путем переопределения метода .get_name().

Если вы берете этот маршрут, вы, вероятно, обнаружите, что хотите определить набор базовых классов для своих представлений и переопределите метод во всех своих базовых представлениях с помощью простого класса mixin.

Например:

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

Обратите внимание также, что метод get_name считается закрытым и, вероятно, изменится в какой-то момент в будущем, поэтому вам нужно будет следить за примечаниями к выпуску при обновлении для любых изменений там.

Ответ 3

Самый простой способ - назначить его как переменную класса:

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__

Это руководство, к сожалению, но прямолинейное. Кстати, в то время как форматирование строк не работает обычным способом, оно работает по тому же методу:

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

A accepts the following types: (<type 'str'>, <type 'int'>)

Ответ 4

Вы также можете сделать это, используя @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

Вы даже можете перезаписать 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

Одно из предостережений, свойство не может быть унаследовано другими классами, очевидно, вам нужно добавить свойство в каждый класс, который вы хотите перезаписать 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

Облом! Но определенно проще, чем делать мета-класс!