python documentation generator
Heredar métodos ''docstrings en Python (5)
Tengo una jerarquía OO con docstrings que requieren tanto mantenimiento como el código en sí. P.ej,
class Swallow(object):
def airspeed(self):
"""Returns the airspeed (unladen)"""
raise NotImplementedError
class AfricanSwallow(Swallow):
def airspeed(self):
# whatever
Ahora, el problema es que AfricanSwallow.airspeed
no hereda el docstring del método de la superclase. Sé que puedo guardar la docstring usando el patrón de método de plantilla, es decir,
class Swallow(object):
def airspeed(self):
"""Returns the airspeed (unladen)"""
return self._ask_arthur()
e implementando _ask_arthur
en cada subclase. Sin embargo, me preguntaba si existe otra forma de heredar docstrings, ¿quizás algún decorador que aún no había descubierto?
Escriba una función en un estilo de decorador de clases para hacer la copia por usted. En Python2.5, puede aplicarlo directamente después de crear la clase. En versiones posteriores, puede aplicar con la notación @decorator .
Aquí hay un primer corte sobre cómo hacerlo:
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__
En las versiones más nuevas de Python, la última parte es aún más simple y hermosa:
@fix_docs
class Dog(Animal):
def walk(self):
pass
Esta es una técnica Pythonic que coincide exactamente con el diseño de las herramientas existentes en la biblioteca estándar. Por ejemplo, el decorador de clases functools.total_ordering agrega métodos de comparación ricos faltantes a las clases. Y para otro ejemplo, el decorador functools.wraps copia los metadatos de una función a otra.
Esta es una variación de la metaclass DocStringInheritor de Paul McGuire .
- Hereda la docstring de un miembro padre si la docstring del miembro del niño está vacía.
- Hereda una docstring de clase primaria si la docstring de clase secundaria está vacía.
- Puede heredar la docstring de cualquier clase en cualquiera de las MRO de las clases base, al igual que la herencia de atributos regulares.
- A diferencia de un decorador de clases, la metaclase se hereda, por lo que solo necesita establecer la metaclase una vez en alguna clase base de nivel superior, y la herencia de docstring se producirá en toda su jerarquía de 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)
La siguiente adaptación también maneja propiedades y clases mixin. También me encontré con una situación en la que tuve que usar func.__func__
(para " func.__func__
de instancias"), pero no estoy completamente seguro de por qué las otras soluciones no abordaron ese problema.
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
Para su información, la gente acaba de tropezar con este tema: a partir de Python 3.5, inspect.getdoc recupera automáticamente docstrings de la jerarquía de herencia.
Por lo tanto, las respuestas anteriores son útiles para Python 2, o si desea ser más creativo con la fusión de las cadenas de documentos de padres y niños.
También he creado algunas herramientas livianas para la herencia de docstring . Estos admiten algunos buenos estilos predeterminados de docstring (numpy, google, reST) listos para usar. También puede usar fácilmente su propio estilo de docstring
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