2012-12-18 5 views
10

Il existe une question à propos de Inherit docstrings in Python class inheritance, mais les réponses à cette question portent sur les méthodes docstrings.Hériter d'une classe parent docstring en tant qu'attribut __doc__

Ma question est de savoir comment hériter une docstring d'une classe parente en tant qu'attribut __doc__. L'utilisation est que Django rest framework génère une belle documentation dans la version html de votre API basée sur les docstrings de vos classes d'affichage. Mais lors de l'héritage d'une classe de base (avec une docstring) dans une classe sans docstring, l'API n'affiche pas la docstring.

Il se pourrait très bien que sphinx et d'autres outils fassent la bonne chose et traitent l'héritage de docstring pour moi, mais le cadre de repos de django regarde l'attribut (vide) .__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" 

J'ai essayé quelque chose comme super(SubClassWithoutDocstring, self).__doc__, mais aussi ne me suis un None.

Répondre

11

Puisque vous ne pouvez pas attribuer un nouveau __doc__ docstring à une classe (en CPython au moins), vous devez utiliser un métaclasse:

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) 

Oui, nous sauter à travers un cerceau ou deux, mais la métaclasse ci-dessus va trouver le bon __doc__ cependant compliqué vous faites votre graphe d'héritage.

Utilisation:

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

L'alternative est de mettre __doc__ en __init__, comme une variable d'instance:

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 

Puis au moins vos instances ont une 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' 

À partir de Python 3.3 (qui a fixé issue 12773), vous peut enfin juste définir l'attribut __doc__ des classes personnalisées, donc vous pouvez utiliser un décorateur de classe à la place:

import inspect 

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

qui peut ensuite être appliquée ainsi:

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

Je vais juste ajouter à partir de ce que je me souviens des discussions que j'ai eues concernant Python à ce sujet. Il n'hérite pas de docstrings par défaut car il est considéré que Python ne peut pas savoir si la docstring aura plus de sens (bien que l'héritage devrait être suffisant pour que le programmeur reste en ligne avec la POO normale pour ne pas changer complètement la signification d'un objet), il a été considéré que le cas où le document était vierge allait être moins trompeur. Cela devient plus complexe là où la classe parente est un ABC par exemple ... –

+0

Dans 3.3 le 'getsd_descriptor'' __doc__' 'est maintenant accessible en écriture pour les types de tas. Avant il avait seulement un «getter» défini; maintenant il a 'setter' [' type_set_doc'] (http://hg.python.org/cpython/file/bd8afb90ebf2/Objects/typeobject.c#l632). 'check_set_special_type_attr' empêche la suppression de' __doc__'. – eryksun

+0

@eryksun: Confirmé; c'est une solution qui était attendue depuis longtemps! Ressuscité mon idée originale de décorateur de classe pour Python 3.3 seulement. –

2

Dans ce cas particulier que vous pourriez ignorez également comment l'infrastructure REST détermine le nom à utiliser pour le point de terminaison en remplaçant la méthode .get_name(). Si vous suivez cette route, vous aurez probablement besoin de définir un ensemble de classes de base pour vos vues, et de surcharger la méthode sur toute votre vue de base en utilisant une simple classe mixin.

Par exemple:

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 

Notez également que la méthode get_name est considérée comme privée, et est susceptible de changer à un moment donné dans l'avenir, donc vous devez garder un œil sur les notes de version lors de la mise à niveau, pour tout changement là-bas.

+0

Vous voulez probablement dire '.get_description()' au lieu de '.get_name()'? Oui, j'ai vu celui-là et j'ai une classe de base où j'essaye de l'écraser. Mais je ne peux toujours pas mettre la main sur les docstrings de mes parents :-) Eh bien, au moins, c'était avant de lire l'autre réponse. –

+0

"Vous voulez probablement dire .get_description() au lieu de .get_name()" En effet, oui je l'ai fait. –

+0

Voir http://reinout.vanrees.org/weblog/2012/12/19/docstring-inheritance-djangorestframework.html pour savoir comment je l'ai fait à la fin. –

1

La façon la plus simple est d'assigner comme une variable de classe:

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__ 

Il est manuel, malheureusement, mais simple. Soit dit en passant, alors que le formatage de ne fonctionne pas comme d'habitude, il le fait avec la même méthode:

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

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

Remarque: selon la réponse de Martijn (http://stackoverflow.com/a/13937525/27401), la définition de '.__ doc__' ne fonctionne que dans Python 3.3. –

+2

@ReinoutvanRees, j'ai essayé cela en 2.3.4 (oui, deux point trois) et 2.7. Vous ne pouvez pas affecter à .__ doc__ après que la classe soit définie, true, mais vous pouvez le faire au moment de la définition. C'est pourquoi j'ai posté ma réponse. –

+0

Ah, vous avez raison. Donc, ce serait aussi une option, à condition de ne pas oublier d'assigner le '__doc__' au moment de la définition. Tout à fait OK et simple. –

0

Vous pouvez aussi le faire en utilisant @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 

Vous pouvez même écraser un 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 

Une mise en garde, la propriété ne peut pas être héritée par d'autres classes évidemment, vous devez ajouter la propriété dans chaque classe que vous voulez écraser le 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 

Bummer! Mais définitivement plus facile que de faire la méta classe!

Questions connexes