Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to modify class docstrings using classmethods

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?

like image 374
Tymon Pitts Avatar asked Mar 28 '14 03:03

Tymon Pitts


People also ask

Which built in Python property can be used to manipulate docstrings of a Python object?

Docstrings are accessible from the doc attribute (__doc__) for any of the Python objects and also with the built-in help() function. An object's docstring is defined by including a string constant as the first statement in the object's definition.

Should classes have docstrings?

All modules should normally have docstrings, and all functions and classes exported by a module should also have docstrings. Public methods (including the __init__ constructor) should also have docstrings. A package may be documented in the module docstring of the __init__.py file in the package directory.

Where should I put docstring in a class?

Module docstrings are placed at the top of the file even before any imports. Module docstrings should include the following: A brief description of the module and its purpose. A list of any classes, exception, functions, and any other objects exported by the module.

Where does Python put docstrings?

Declaring Docstrings: The docstrings are declared using ”'triple single quotes”' or “””triple double quotes””” just below the class, method or function declaration. All functions should have a docstring.


2 Answers

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
like image 60
metatoaster Avatar answered Nov 20 '22 11:11

metatoaster


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.

like image 20
daouzli Avatar answered Nov 20 '22 12:11

daouzli