saltstack/salt

View on GitHub
doc/_ext/saltdomain.py

Summary

Maintainability
A
1 hr
Test Coverage
import itertools
import os
import re

from docutils import nodes
from docutils.parsers.rst import Directive

from docutils.statemachine import ViewList
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType
from sphinx.domains.python import PyObject
from sphinx.locale import _
from sphinx.roles import XRefRole
from sphinx.util.nodes import make_refnode
from sphinx.util.nodes import nested_parse_with_titles
from sphinx.util.nodes import set_source_info

from sphinx.domains import python as python_domain

import salt


class Event(PyObject):
    '''
    Document Salt events
    '''
    domain = 'salt'


class LiterateCoding(Directive):
    '''
    Auto-doc SLS files using literate-style comment/code separation
    '''
    has_content = False
    required_arguments = 1
    optional_arguments = 0
    final_argument_whitespace = False

    def parse_file(self, fpath):
        '''
        Read a file on the file system (relative to salt's base project dir)

        :returns: A file-like object.
        :raises IOError: If the file cannot be found or read.
        '''
        sdir =  os.path.abspath(os.path.join(os.path.dirname(salt.__file__),
            os.pardir))
        with open(os.path.join(sdir, fpath), 'rb') as f:
            return f.readlines()

    def parse_lit(self, lines):
        '''
        Parse a string line-by-line delineating comments and code

        :returns: An tuple of boolean/list-of-string pairs. True designates a
            comment; False designates code.
        '''
        comment_char = '#' # TODO: move this into a directive option
        comment = re.compile(r'^\s*{0}[ \n]'.format(comment_char))
        section_test = lambda val: bool(comment.match(val))

        sections = []
        for is_doc, group in itertools.groupby(lines, section_test):
            if is_doc:
                text = [comment.sub('', i).rstrip('\r\n') for i in group]
            else:
                text = [i.rstrip('\r\n') for i in group]

            sections.append((is_doc, text))

        return sections

    def run(self):
        try:
            lines = self.parse_lit(self.parse_file(self.arguments[0]))
        except IOError as exc:
            document = self.state.document
            return [document.reporter.warning(str(exc), line=self.lineno)]

        node = nodes.container()
        node['classes'] = ['lit-container']
        node.document = self.state.document

        enum = nodes.enumerated_list()
        enum['classes'] = ['lit-docs']
        node.append(enum)

        # make first list item
        list_item = nodes.list_item()
        list_item['classes'] = ['lit-item']

        for is_doc, line in lines:
            if is_doc and line == ['']:
                continue

            section = nodes.section()

            if is_doc:
                section['classes'] = ['lit-annotation']

                nested_parse_with_titles(self.state, ViewList(line), section)
            else:
                section['classes'] = ['lit-content']

                code = '\n'.join(line)
                literal = nodes.literal_block(code, code)
                literal['language'] = 'yaml'
                set_source_info(self, literal)
                section.append(literal)

            list_item.append(section)

            # If we have a pair of annotation/content items, append the list
            # item and create a new list item
            if len(list_item.children) == 2:
                enum.append(list_item)
                list_item = nodes.list_item()
                list_item['classes'] = ['lit-item']

        # Non-semantic div for styling
        bg = nodes.container()
        bg['classes'] = ['lit-background']
        node.append(bg)

        return [node]


class LiterateFormula(LiterateCoding):
    '''
    Customizations to handle finding and parsing SLS files
    '''

    def parse_file(self, sls_path):
        '''
        Given a typical Salt SLS path (e.g.: apache.vhosts.standard), find the
        file on the file system and parse it
        '''
        config = self.state.document.settings.env.config
        formulas_dirs = config.formulas_dirs
        fpath = sls_path.replace('.', '/')

        name_options = (
            '{0}.sls'.format(fpath),
            os.path.join(fpath, 'init.sls')
        )

        paths = [os.path.join(fdir, fname)
                for fname in name_options
                    for fdir in formulas_dirs]

        for i in paths:
            try:
                with open(i, 'rb') as f:
                    return f.readlines()
            except IOError:
                pass

        raise IOError("Could not find sls file '{0}'".format(sls_path))


class CurrentFormula(Directive):
    domain = 'salt'
    has_content = False
    required_arguments = 1
    optional_arguments = 0
    final_argument_whitespace = False
    option_spec = {}

    def run(self):
        env = self.state.document.settings.env
        modname = self.arguments[0].strip()
        if modname == 'None':
            env.temp_data['salt:formula'] = None
        else:
            env.temp_data['salt:formula'] = modname
        return []


class Formula(Directive):
    domain = 'salt'
    has_content = True
    required_arguments = 1

    def run(self):
        env = self.state.document.settings.env
        formname = self.arguments[0].strip()

        env.temp_data['salt:formula'] = formname

        if 'noindex' in self.options:
            return []

        env.domaindata['salt']['formulas'][formname] = (
                env.docname,
                self.options.get('synopsis', ''),
                self.options.get('platform', ''),
                'deprecated' in self.options)

        targetnode = nodes.target('', '', ids=['module-' + formname],
                ismod=True)
        self.state.document.note_explicit_target(targetnode)

        indextext = u'{0}-formula)'.format(formname)
        inode = addnodes.index(entries=[('single', indextext,
                                         'module-' + formname, '')])

        return [targetnode, inode]


class State(Directive):
    domain = 'salt'
    has_content = True
    required_arguments = 1

    def run(self):
        env = self.state.document.settings.env
        statename = self.arguments[0].strip()

        if 'noindex' in self.options:
            return []

        targetnode = nodes.target('', '', ids=['module-' + statename],
                ismod=True)
        self.state.document.note_explicit_target(targetnode)

        formula = env.temp_data.get('salt:formula')

        indextext = u'{1} ({0}-formula)'.format(formula, statename)
        inode = addnodes.index(entries=[
            ('single', indextext, 'module-{0}'.format(statename), ''),
        ])

        return [targetnode, inode]


class SLSXRefRole(XRefRole):
    pass


class SaltModuleIndex(python_domain.PythonModuleIndex):
    name = 'modindex'
    localname = _('Salt Module Index')
    shortname = _('all salt modules')


class SaltDomain(python_domain.PythonDomain):
    name = 'salt'
    label = 'Salt'
    data_version = 2

    object_types = python_domain.PythonDomain.object_types
    object_types.update({
        'state': ObjType(_('state'), 'state'),
    })

    directives = python_domain.PythonDomain.directives
    directives.update({
        'event': Event,
        'state': State,
        'formula': LiterateFormula,
        'currentformula': CurrentFormula,
        'saltconfig': LiterateCoding,
    })


    roles = python_domain.PythonDomain.roles
    roles.update({
        'formula': SLSXRefRole(),
    })

    initial_data = python_domain.PythonDomain.initial_data
    initial_data.update({
        'formulas': {},
    })

    indices = [
        SaltModuleIndex,
    ]

    def resolve_xref(self, env, fromdocname, builder, type, target, node,
            contnode):
        if type == 'formula' and target in self.data['formulas']:
            doc, _, _, _ = self.data['formulas'].get(target, (None, None))
            if doc:
                return make_refnode(builder, fromdocname, doc, target,
                        contnode, target)
        else:
            super(SaltDomain, self).resolve_xref(env, fromdocname, builder,
                    type, target, node, contnode)

# Monkey-patch the Python domain remove the python module index
python_domain.PythonDomain.indices = [SaltModuleIndex]


def setup(app):
    app.add_domain(SaltDomain)

    formulas_path = 'templates/formulas'
    formulas_dir = os.path.join(os.path.abspath(os.path.dirname(salt.__file__)),
            formulas_path)
    app.add_config_value('formulas_dirs', [formulas_dir], 'env')

    app.add_crossref_type(directivename="conf_master", rolename="conf_master",
            indextemplate="pair: %s; conf/master")
    app.add_crossref_type(directivename="conf_minion", rolename="conf_minion",
            indextemplate="pair: %s; conf/minion")
    app.add_crossref_type(directivename="conf_proxy", rolename="conf_proxy",
            indextemplate="pair: %s; conf/proxy")
    app.add_crossref_type(directivename="conf_log", rolename="conf_log",
            indextemplate="pair: %s; conf/logging")
    app.add_crossref_type(directivename="jinja_ref", rolename="jinja_ref",
            indextemplate="pair: %s; jinja filters")