Question

My Problem:

I've created a series of nodes that each have a set of attribute objects associated with them. The attribute objects are initialized with descriptions and names for each attribute. I'd like these attributes, and their descriptions, to show up in my sphinx documentation without having to maintain the name/description in two places, once in the doc string of the class and once in the initialization of the attribute.

To illustrate the problem, consider the following code:

class Foo(object):
    """My doc string
    """
    @classmethod
    def default_attributes(cls):
        return {'foo':'description of foo attribute',
                'bar':'description of bar attribute'}

    @classmethod
    def attributes_string(cls):
        attributes = cls.default_attributes()
        result = '\nDefault Attributes:\n'
        for key, value in attributes.iteritems():
            result += '%s: %s\n' % (key, value)
        return result

print Foo.__doc__

I'd like the result of Foo.attributes_string to show up in the doc string of Foo so that I get this:

My doc string

Default Attributes:
foo: description of foo attribute
bar: description of bar attribute

My Solution Attempts:

First I thought "Hey that's easy! I'll just setup a class decorator!":

def my_decorator(cls):
    doc = getattr(cls, '__doc__', '')
    doc += cls.attributes_string()
    cls.__doc__ = doc
    return cls

@my_decorator
class Foo(object):
    """My doc string
    """

This failed miserably with the following error:

AttributeError: attribute '__doc__' of 'type' objects is not writable

So then I thought "Well then I'll just use a metaclass to set __doc__ before the class is created!". When I went to implement this I immediately ran into a problem: How do you call a classmethod for a class that hasn't been created yet?

I circumvented that problem with a very hacky workaround that makes me cringe: I create the class twice, once without modifying it so I can call it's classmethod, and then again to create the class with the proper __doc__:

class Meta(type):
    def __new__(meta_cls, name, bases, cls_dict):
        tmpcls = super(Meta, meta_cls).__new__(meta_cls, name, bases, cls_dict)

        doc = cls_dict.get('__doc__', '')
        doc += tmpcls.attributes_string()
        cls_dict['__doc__'] = doc

        return super(Meta, meta_cls).__new__(meta_cls, name, bases, cls_dict)

class Foo(object):
    """My doc string
    """
    __metaclass__ = Meta

This totally works and gives me the result I was looking for:

My doc string

Default Attributes:
foo: description of foo attribute
bar: description of bar attribute

However, isn't it terribly inefficient to create the class twice? Does it matter? Is there a better way? Is what I'm trying to do really dumb?

Was it helpful?

Solution

Did a bit of looking around for you, no dice because it's something fundamentally broken and wasn't fixed until Python 3.3. So if you plan to release your program for >3.3, __doc__ attributes will be mutable.

That however doesn't seem to help you, but there are ways to more cleverly beat this problem into submission by simply provide a __doc__ property in the metaclass.

class Meta(type):

    @property
    def __doc__(self):
        return self.attributes_string()

class Foo(object):
    """My doc string
    """

    __metaclass__ = Meta

    @classmethod
    def default_attributes(cls):
        return {'foo':'description of foo attribute',
                'bar':'description of bar attribute'}

    @classmethod
    def attributes_string(cls):
        attributes = cls.default_attributes()
        result = '\nDefault Attributes:\n'
        for key, value in attributes.items():
            result += '%s: %s\n' % (key, value)
        return result

No need to butcher that __new__ method, since attributes and properties in metaclasses will be available to subclasses and vice versa. Do a help(Foo) and it now gives this:

CLASSES
    __builtin__.object
        Foo
    __builtin__.type(__builtin__.object)
        Meta

    class Foo(__builtin__.object)
     |  Default Attributes:
     |  foo: description of foo attribute
     |  bar: description of bar attribute

Tested under Python 2.7.

Caveat: It will override the standard way of declaring docstrings, so you probably have to put the entire thing in there unless you also happen to override the __new__ method to put the original __doc__ out of harms way. Maybe this is what you want:

class Meta(type):

    def __new__(cls, name, bases, attrs):
        attrs['_doc'] = attrs.get('__doc__', '')
        return super(Meta, cls).__new__(cls, name, bases, attrs)

    @property
    def __doc__(self):
        return self._doc + self.attributes_string()

You result:

class Foo(__builtin__.object)
 |  My doc string
 |      
 |  Default Attributes:
 |  foo: description of foo attribute
 |  bar: description of bar attribute

OTHER TIPS

If you want to generate docstrings for uncommented functions, you can also use Pyment.

It won't provide your specific expected format but it currently generate patches adding (or converting) docstrings in formats Sphinxs, Numpydoc, or Google doc style.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top