Skip to content

Instantly share code, notes, and snippets.

@marshalc
Forked from codingjoe/django_docs.md
Last active November 11, 2016 15:27
Show Gist options
  • Save marshalc/327fc737ce0557a253c0c3d57f679292 to your computer and use it in GitHub Desktop.
Save marshalc/327fc737ce0557a253c0c3d57f679292 to your computer and use it in GitHub Desktop.
Build Django docs like a pro!

Build Django docs like a pro!

Revised edition to support autosummary, markdown, and other goodies

Requirements

Add the following to your development requirements file:

  • sphinx
  • sphinx_rtd_theme
  • pyenchant
  • sphinxcontrib-spelling
  • recommonmark

Sphinx config

docs/conf.py

import sys
import os
import importlib
import inspect
from recommonmark.parser import CommonMarkParser
from django.conf import settings
from django.db.models.fields.files import FileDescriptor
from django.db.models.manager import ManagerDescriptor
from django.db.models.query import QuerySet
from django.utils.translation import ugettext, get_language, activate, deactivate

try:
    import enchant  # NoQA
except ImportError:
    enchant = None

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
sys.path.append(os.path.abspath('../../lib/python3.5/site-packages/'))

# Import Django settings
settings.configure()

import django
django.setup()

# Fix Django's FileFields
FileDescriptor.__get__ = lambda self, *args, **kwargs: self
ManagerDescriptor.__get__ = lambda self, *args, **kwargs: self.manager

# Stop Django from executing DB queries
QuerySet.__repr__ = lambda self: self.__class__.__name__

GITHUB_USER = ''  # Name of your Github user or organisation
GITHUB_PROJECT = ''  # Name of your Github repository


def process_django_models(app, what, name, obj, options, lines):
    """Append params from fields to model documentation."""
    from django.utils.encoding import force_text
    from django.utils.html import strip_tags
    from django.db import models

    spelling_white_list = ['', '.. spelling::']

    if inspect.isclass(obj) and issubclass(obj, models.Model):
        for field in obj._meta.fields:
            help_text = strip_tags(force_text(field.help_text))
            verbose_name = force_text(field.verbose_name).capitalize()

            if help_text:
                lines.append(':param %s: %s - %s' % (field.attname, verbose_name,  help_text))
            else:
                lines.append(':param %s: %s' % (field.attname, verbose_name))

            if enchant is not None:
                from enchant.tokenize import basic_tokenize

                words = verbose_name.replace('-', '.').replace('_', '.').split('.')
                words = [s for s in words if s != '']
                for word in words:
                    spelling_white_list += ["    %s" % ''.join(i for i in word if not i.isdigit())]
                    spelling_white_list += ["    %s" % w[0] for w in basic_tokenize(word)]

            field_type = type(field)
            module = field_type.__module__
            if 'django.db.models' in module:
                # scope with django.db.models * imports
                module = 'django.db.models'
            lines.append(':type %s: %s.%s' % (field.attname, module, field_type.__name__))
        if enchant is not None:
            lines += spelling_white_list
    return lines


def process_modules(app, what, name, obj, options, lines):
    """Add module names to spelling white list."""
    if what != 'module':
        return lines
    from enchant.tokenize import basic_tokenize

    spelling_white_list = ['', '.. spelling::']
    words = name.replace('-', '.').replace('_', '.').split('.')
    words = [s for s in words if s != '']
    for word in words:
        spelling_white_list += ["    %s" % ''.join(i for i in word if not i.isdigit())]
        spelling_white_list += ["    %s" % w[0] for w in basic_tokenize(word)]
    lines += spelling_white_list
    return lines


def skip_queryset(app, what, name, obj, skip, options):
    """Skip queryset subclasses to avoid database queries."""
    from django.db import models
    if isinstance(obj, (models.QuerySet, models.manager.BaseManager)) or name.endswith('objects'):
        return True
    return skip


def setup(app):
    # Register the docstring processor with sphinx
    app.connect('autodoc-process-docstring', process_django_models)
    app.connect('autodoc-skip-member', skip_queryset)
    if enchant is not None:
        app.connect('autodoc-process-docstring', process_modules)


intersphinx_mapping = {
    'python': ('https://docs.python.org/2.7', None),
    'sphinx': ('http://sphinx.pocoo.org/', None),
    'django': ('https://docs.djangoproject.com/en/1.9/', 'https://docs.djangoproject.com/en/1.9/_objects/'),
    'djangoextensions': ('https://django-extensions.readthedocs.org/en/latest/', None),
    # 'geoposition': ('https://django-geoposition.readthedocs.org/en/latest/', None),
    'braces': ('https://django-braces.readthedocs.org/en/latest/', None),
    # 'select2': ('https://django-select2.readthedocs.org/en/latest/', None),
    # 'celery': ('https://celery.readthedocs.org/en/latest/', None),
}


def linkcode_resolve(domain, info):
    """Link source code to GitHub."""
    project = GITHUB_PROJECT
    github_user = GITHUB_USER
    head = 'master'

    if domain != 'py' or not info['module']:
        return None
    filename = info['module'].replace('.', '/')
    mod = importlib.import_module(info['module'])
    basename = os.path.splitext(mod.__file__)[0]
    if basename.endswith('__init__'):
        filename += '/__init__'
    item = mod
    lineno = ''
    for piece in info['fullname'].split('.'):
        item = getattr(item, piece)
        try:
            lineno = '#L%d' % inspect.getsourcelines(item)[1]
        except (TypeError, IOError):
            pass
    return ("https://github.com/%s/%s/blob/%s/%s.py%s" %
            (github_user, project, head, filename, lineno))

autodoc_member_order = 'bysource'
autodoc_default_flags = ['members', 'undoc-members']
autosummary_generate = True

# spell checking
spelling_lang = 'en_GB'
spelling_word_list_filename = 'spelling_wordlist.txt'
spelling_show_suggestions = True
spelling_ignore_pypi_package_names = True


# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.autosummary',
    'sphinx.ext.graphviz',
    'sphinx.ext.napoleon',
    'sphinx.ext.linkcode',
    'sphinx.ext.inheritance_diagram',
    'sphinx.ext.doctest',
    'sphinx.ext.todo',
    'sphinx.ext.coverage',
    'sphinx.ext.imgmath',
    'sphinx.ext.ifconfig',
    'sphinx.ext.viewcode',
    'sphinx.ext.intersphinx'
]

if enchant is not None:
    extensions.append('sphinxcontrib.spelling')

# Here goes all your other config.

Sphinx - autosummary patch

In lib/pythonX.Y/site-packages/sphinx/ext/autosummary/__init__.py, comment out lines 561-562 and add the two lines below. They attempt to add a file extension onto files that shouldn't exist with a file extension on. Without this patch, when source_suffix = ['.rst', '.md', '.txt'] in conf.py, this method is broken because it tries to add .rst onto .md and other files and thus generates a list of files that don't exist

# genfiles = [genfile + (not genfile.endswith(ext) and ext or '')
#             for genfile in genfiles]
# Filter files for ReST files only
genfiles = [genfile for genfile in genfiles if genfile.endswith('.rst')]
@marshalc
Copy link
Author

@marshalc
Copy link
Author

marshalc commented Jun 3, 2016

Also, if getting TypeErrors from pyenchant, remember this issue pyenchant/pyenchant#45 and the workaround.

@marshalc
Copy link
Author

marshalc commented Aug 3, 2016

Same fix applies to the python3.5 current iteration of sphinx

@marshalc
Copy link
Author

AbstractManagerDescriptor has been removed from Django 1.10, thus we can remove from this code

@marshalc
Copy link
Author

Updated for the current settings arrangement based on what is working in https://github.com/ouh-churchill/diakonia/tree/master/config/settings where common.py is responsible for loading and defining which environment to load settings from via the .env file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment