Login

Auto-documenting Django Models with Sphinx

Author:
Voightkampff
Posted:
August 25, 2011
Language:
Python
Version:
1.3
Score:
5 (after 5 ratings)

In my sphinx documentation I really wanted a nice clean list of the fields on my Django models. The most obvious way of doing this is to add ":param blah:" tags to the docstring, but this takes a long time to implement and violates DRY principles when you already have lots of nice help text in the model definition. Another way is to use "#:" comments on attributes, but this approach suffers from the same issues as the previous method with the additional problem of Sphinx crapping out on file and custom fields.

So anyway, this is my solution. It uses the Sphinx docstring processing callback to intercept any objects that inherit from django.models.Model and creates a nice param list of all the fields on that model. Param descriptions come from the field's help text or verbose name if no help text is defined.

To use this, just add it to the end of your source/conf.py, filling out the project path as appropriate. You may be able to skip the Django environment setup lines if you're adding this to a Sphinx doc that already has Django models set up.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
THIS_DIR = os.path.dirname(__file__)
PROJECT_DIR = os.path.join(THIS_DIR, 'relative/path/to/your/project/')
sys.path.append(PROJECT_DIR)

import inspect
import settings
from django.core.management import setup_environ
from django.utils.html import strip_tags
from django.utils,encoding import force_unicode

setup_environ(settings)


def process_docstring(app, what, name, obj, options, lines):
    # This causes import errors if left outside the function
    from django.db import models
    
    # Only look at objects that inherit from Django's base model class
    if inspect.isclass(obj) and issubclass(obj, models.Model):
        # Grab the field list from the meta class
        fields = obj._meta._fields()
    
        for field in fields:
            # Decode and strip any html out of the field's help text
            help_text = strip_tags(force_unicode(field.help_text))
            
            # Decode and capitalize the verbose name, for use if there isn't
            # any help text
            verbose_name = force_unicode(field.verbose_name).capitalize()
            
            if help_text:
                # Add the model field to the end of the docstring as a param
                # using the help text as the description
                lines.append(u':param %s: %s' % (field.attname, help_text))
            else:
                # Add the model field to the end of the docstring as a param
                # using the verbose name as the description
                lines.append(u':param %s: %s' % (field.attname, verbose_name))
                
            # Add the field's type to the docstring
            lines.append(u':type %s: %s' % (field.attname, type(field).__name__))
    
    # Return the extended docstring
    return lines  
  
def setup(app):
    # Register the docstring processor with sphinx
    app.connect('autodoc-process-docstring', process_docstring)  

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 3 months ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 3 months, 1 week ago
  3. Serializer factory with Django Rest Framework by julio 10 months, 1 week ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 10 months, 3 weeks ago
  5. Help text hyperlinks by sa2812 11 months, 3 weeks ago

Comments

akaihola (on October 7, 2011):

This will fail if the help_text= kwargs for model fields are Unicode objects with non-ASCII characters, e.g.:

price = models.DecimalField(help_text=u'Price (in €)')

To fix this, add this import at the top:

from django.utils.encoding import force_unicode

and replace line 24 with:

help_text = strip_tags(force_unicode(field.help_text))

Also change the bytestrings on lines 33, 37 and 40 to Unicode literals, e.g. lines.append(u':param %s %s' ....

#

Voightkampff (on October 9, 2011):

Thanks for that! I've fixed the snippet.

#

vdboor (on July 9, 2012):

Thanks for the snippet! This works really sweet.

One addition to link to model classes to:

# Add the field's type to the docstring
if isinstance(field, models.ForeignKey):
    to = field.rel.to
    lines.append(u':type %s: %s to :class:`~%s.%s`' % (field.attname, type(field).__name__, to.__module__, to.__name__))
else:
    lines.append(u':type %s: %s' % (field.attname, type(field).__name__))

Secondly, it makes sense to keep the setup() in conf.py only, and place the rest in a docs/_ext folder. The Django docs do this, and it makes it really clean to implement.

#

[email protected] (on November 4, 2014):

in django>=1.6

_meta._fields() has been removed please change it to _meta.fields as an attribute

#

cgaspoz (on January 27, 2015):

If you try to use it with python 3 and django 1.7, you need to replace force_unicode() by force_text()

from django.utils.encoding import force_text
... 
help_text = strip_tags(force_text(field.help_text))
...
verbose_name = force_text(field.verbose_name).capitalize()'''

Thanks for the snippet

#

abulka (on March 17, 2016):

In >= django1.8 neither _meta.fields nor _meta.fields() works, its now _meta.get_fields()

Also there is a typo on line 9 of the original solution, from django.utils,encoding import force_unicode should be from django.utils.encoding import force_unicode

Thus the latest working solution, also incorporating the use of force_text() and links to model classes is:

import inspect
from django.utils.html import strip_tags
from django.utils.encoding import force_text

def process_docstring(app, what, name, obj, options, lines):
    # This causes import errors if left outside the function
    from django.db import models

    # Only look at objects that inherit from Django's base model class
    if inspect.isclass(obj) and issubclass(obj, models.Model):
        # Grab the field list from the meta class
        fields = obj._meta.get_fields()

        for field in fields:
            # Decode and strip any html out of the field's help text
            help_text = strip_tags(force_text(field.help_text))

            # Decode and capitalize the verbose name, for use if there isn't
            # any help text
            verbose_name = force_text(field.verbose_name).capitalize()

            if help_text:
                # Add the model field to the end of the docstring as a param
                # using the help text as the description
                lines.append(u':param %s: %s' % (field.attname, help_text))
            else:
                # Add the model field to the end of the docstring as a param
                # using the verbose name as the description
                lines.append(u':param %s: %s' % (field.attname, verbose_name))

            # Add the field's type to the docstring
            if isinstance(field, models.ForeignKey):
                to = field.rel.to
                lines.append(u':type %s: %s to :class:`~%s.%s`' % (field.attname, type(field).__name__, to.__module__, to.__name__))
            else:
                lines.append(u':type %s: %s' % (field.attname, type(field).__name__))

    # Return the extended docstring
    return lines

def setup(app):
    # Register the docstring processor with sphinx
    app.connect('autodoc-process-docstring', process_docstring)

P.S. The django 1.9 bootstrapping I use at the top of conf.py is:

import django
sys.path.insert(0, os.path.abspath('../../your_project_dir'))  # so sphinx can find modules, and also to allow django to set up
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_django_project.settings")
django.setup()

#

abulka (on March 18, 2016):

Also, you may need to add

        # Skip ManyToOneRel and ManyToManyRel fields which have no 'verbose_name'
        if not hasattr(field, 'verbose_name'):
            continue

before the line verbose_name = force_text(field.verbose_name).capitalize()

#

abulka (on March 19, 2016):

Sorry, actually place the 'continue' skipping code above to be the first thing in the for loop, since ManyToOneRel and ManyToManyRel fields have no 'help_text'either.

    for field in fields:
        # Skip ManyToOneRel and ManyToManyRel fields which have no 'verbose_name' or 'help_text'
        if not hasattr(field, 'verbose_name'):
            continue

#

abulka (on March 21, 2016):

Latest working solution for django 1.9 with all edits and corrections: https://gist.github.com/abulka/48b54ea4cbc7eb014308

#

Please login first before commenting.