Commits

togakushi  committed 0b7384d

Operation domain: 0.0.1

  • Participants

Comments (0)

Files changed (9)

+.. -*- restructuredtext -*-
+
+This file describes user-visible changes between the extension versions.
+
+Version 0.0.1 (2012-10-02)
+--------------------------
+
+* Initial version.
+If not otherwise noted, the extensions in this package are licensed
+under the following license.
+
+Copyright (c) 2010 by the contributors (see AUTHORS file).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include README
+include LICENSE
+include CHANGES.*
+================
+Operation Domain
+================
+
+:author: togakushi <nina.togakushi at gmail.com>
+
+About
+=====
+
+This is the Operation domain for Sphinx 1.0.
+Sphinx 1.0 will deliver new feature -- Domain.
+
+This extension provides directives and roles to write Operation documents.
+
+
+Quick Sample
+============
+
+::
+
+   .. op:setting:: xxxx
+
+
+   :op:setting:`xxxx`
+
+::
+
+   .. op:command:: zzzz
+
+   :op:command:`zzzz`
+
+
+Install
+=======
+[egg_info]
+tag_build = dev
+tag_date = true
+
+[aliases]
+release = egg_info -RDb ''
+# -*- coding: utf-8 -*-
+
+from setuptools import setup, find_packages
+
+long_desc = '''
+This package contains the sphinxcontrib-erlangdomain Sphinx extension.
+
+This extension adds Ruby Domain to Sphinx.
+It needs Sphinx 1.0 or newer.
+
+Detail document: http://packages.python.org/sphinxcontrib-erlangdomain/
+'''
+
+requires = ['Sphinx>=1.0']
+
+setup(
+    name='sphinxcontrib-operationdomain',
+    version='0.1',
+    url='http://bitbucket.org/togakushi/sphinx-contrib',
+    download_url='http://pypi.python.org/pypi/sphinxcontrib-erlangdomain',
+    license='BSD',
+    author='togakushi',
+    author_email='nina.togakushi at gmail.com',
+    description='Sphinx extension sphinxcontrib-operationdomain',
+    long_description=long_desc,
+    zip_safe=False,
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Environment :: Console',
+        'Environment :: Web Environment',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Topic :: Documentation',
+        'Topic :: Utilities',
+    ],
+    platforms='any',
+    packages=find_packages(),
+    include_package_data=True,
+    install_requires=requires,
+    #namespace_packages=['sphinxcontrib'],
+)

File sphinxcontrib/__init__.py

+# -*- coding: utf-8 -*-
+"""
+    sphinxcontrib
+    ~~~~~~~~~~~~~
+
+    This package is a namespace package that contains all extensions
+    distributed in the ``sphinx-contrib`` distribution.
+
+    :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+__import__('pkg_resources').declare_namespace(__name__)

File sphinxcontrib/op.py

+# -*- coding: utf-8 -*-
+"""
+    sphinx.domains.operation
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    The Operation domain.
+
+    :copyright: Copyright 2012 by togakushi
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from docutils import nodes
+from docutils.parsers.rst import directives
+
+from sphinx import addnodes
+from sphinx.roles import XRefRole
+from sphinx.locale import l_, _
+from sphinx.domains import Domain, ObjType, Index
+from sphinx.directives import ObjectDescription
+from sphinx.util.nodes import make_refnode
+from sphinx.util.compat import Directive
+from sphinx.util.docfields import Field, TypedField
+
+
+# REs for Operation signatures
+op_sig_re = re.compile(
+    r'''^ ([\w.]*\.)?            # class name(s)
+          (\w+)  \s*             # thing name
+          (?: \((.*)\)           # optional: arguments
+           (?:\s* -> \s* (.*))?  #           return annotation
+          )? $                   # and nothing more
+          ''', re.VERBOSE)
+
+
+class OperationObject(ObjectDescription):
+    """
+    Description of a general Operation object.
+    """
+    option_spec = {
+        'noindex': directives.flag,
+        'command': directives.unchanged,
+        'setting': directives.unchanged,
+        'annotation': directives.unchanged,
+    }
+
+    doc_field_types = [
+        TypedField('parameter', label=l_('Parameters'),
+                   names=('param', 'parameter', 'arg', 'argument',
+                          'keyword', 'kwarg', 'kwparam'),
+                   typerolename='obj', typenames=('paramtype', 'type'),
+                   can_collapse=True),
+        TypedField('variable', label=l_('Variables'), rolename='obj',
+                   names=('var', 'ivar', 'cvar'),
+                   typerolename='obj', typenames=('vartype',),
+                   can_collapse=True),
+        Field('returnvalue', label=l_('Returns'), has_arg=False,
+              names=('returns', 'return')),
+        Field('returntype', label=l_('Return type'), has_arg=False,
+              names=('rtype',)),
+    ]
+
+    def get_signature_prefix(self, sig):
+        """May return a prefix to put before the object name in the
+        signature.
+        """
+        return ''
+
+    def needs_arglist(self):
+        """May return true if an empty argument list is to be generated even if
+        the document contains none.
+        """
+        return False
+
+    def handle_signature(self, sig, signode):
+        """Transform a Operation signature into RST nodes.
+
+        Return (fully qualified name of the thing if any).
+
+        If inside a class, the current class name is handled intelligently:
+        * it is stripped from the displayed name if present
+        * it is added to the full name (return value) if not present
+        """
+        m = op_sig_re.match(sig)
+        if m is None:
+            raise ValueError
+        name_prefix, name, arglist, retann = m.groups()
+
+        # determine command and class name (if applicable), as well as full name
+        modname = self.options.get('command', self.env.temp_data.get('op:command'))
+        setname = self.options.get('setting', self.env.temp_data.get('op:setting'))
+
+        add_command = True
+        if name_prefix:
+            fullname = name_prefix + name
+        else:
+            fullname = name
+
+        signode['command'] = modname
+        signode['setting'] = setname
+        signode['fullname'] = fullname
+
+        sig_prefix = self.get_signature_prefix(sig)
+        if sig_prefix:
+            signode += addnodes.desc_annotation(sig_prefix, sig_prefix)
+
+        if name_prefix:
+            signode += addnodes.desc_addname(name_prefix, name_prefix)
+        # exceptions are a special case, since they are documented in the
+        # 'exceptions' command.
+        elif add_command and self.env.config.add_command_names:
+            modname = self.options.get(
+                'command', self.env.temp_data.get('op:command'))
+            setname = self.options.get(
+                'setting', self.env.temp_data.get('op:setting'))
+            nodetext = modname + '.'
+            signode += addnodes.desc_addname(nodetext, nodetext)
+
+        anno = self.options.get('annotation')
+
+        signode += addnodes.desc_name(name, name)
+        if not arglist:
+            if self.needs_arglist():
+                # for callables, add an empty parameter list
+                signode += addnodes.desc_parameterlist()
+            if retann:
+                signode += addnodes.desc_returns(retann, retann)
+            if anno:
+                signode += addnodes.desc_annotation(' ' + anno, ' ' + anno)
+            return fullname, name_prefix
+
+        if retann:
+            signode += addnodes.desc_returns(retann, retann)
+        if anno:
+            signode += addnodes.desc_annotation(' ' + anno, ' ' + anno)
+        return fullname, name_prefix
+
+    def get_index_text(self, modname, name):
+        """Return the text for the index entry of the object."""
+        raise NotImplementedError('must be implemented in subclasses')
+
+    def add_target_and_index(self, name_cls, sig, signode):
+        modname = self.options.get(
+            'command', self.env.temp_data.get('op:command'))
+        setname = self.options.get(
+            'setting', self.env.temp_data.get('op:setting'))
+        fullname = (modname and modname + '.' or '') + name_cls[0]
+        # note target
+        if fullname not in self.state.document.ids:
+            signode['names'].append(fullname)
+            signode['ids'].append(fullname)
+            signode['first'] = (not self.names)
+            self.state.document.note_explicit_target(signode)
+            objects = self.env.domaindata['op']['objects']
+            if fullname in objects:
+                self.state_machine.reporter.warning(
+                    'duplicate object description of %s, ' % fullname +
+                    'other instance in ' +
+                    self.env.doc2path(objects[fullname][0]) +
+                    ', use :noindex: for one of them',
+                    line=self.lineno)
+            objects[fullname] = (self.env.docname, self.objtype)
+
+        indextext = self.get_index_text(modname, name_cls)
+        if indextext:
+            self.indexnode['entries'].append(('single', indextext,
+                                              fullname, ''))
+
+
+class OperationCommand(Directive):
+    """
+    Directive to mark description of a new command.
+    """
+
+    has_content = False
+    required_arguments = 1
+    optional_arguments = 0
+    final_argument_whitespace = True
+    option_spec = {
+        'platform': lambda x: x,
+        'synopsis': lambda x: x,
+        'noindex': directives.flag,
+        'deprecated': directives.flag,
+    }
+
+    def run(self):
+        env = self.state.document.settings.env
+        comname = self.arguments[0].strip()
+        noindex = 'noindex' in self.options
+        env.temp_data['op:command'] = comname
+        ret = []
+        if not noindex:
+            env.domaindata['op']['commands'][comname] = \
+                (env.docname, self.options.get('synopsis', ''),
+                 self.options.get('platform', ''), 'deprecated' in self.options)
+            # make a duplicate entry in 'objects' to facilitate searching for
+            # the command in OperationDomain.find_obj()
+            env.domaindata['op']['objects'][comname] = (env.docname, 'command')
+            targetnode = nodes.target('', '', ids=['command-' + comname],
+                                      ismod=True)
+            self.state.document.note_explicit_target(targetnode)
+            # the platform and synopsis aren't printed; in fact, they are only
+            # used in the modindex currently
+            ret.append(targetnode)
+            indextext = _('%s (command)') % comname
+            inode = addnodes.index(entries=[('single', indextext,
+                                             'command-' + comname, '')])
+            ret.append(inode)
+        return ret
+
+
+class OperationSetting(Directive):
+    """
+    Directive to mark description of a new setting.
+    """
+
+    has_content = False
+    required_arguments = 1
+    optional_arguments = 0
+    final_argument_whitespace = True
+    option_spec = {
+        'platform': lambda x: x,
+        'synopsis': lambda x: x,
+        'noindex': directives.flag,
+        'deprecated': directives.flag,
+    }
+
+    def run(self):
+        env = self.state.document.settings.env
+        setname = self.arguments[0].strip()
+        noindex = 'noindex' in self.options
+        env.temp_data['op:setting'] = setname
+        ret = []
+        if not noindex:
+            env.domaindata['op']['settings'][setname] = \
+                (env.docname, self.options.get('synopsis', ''),
+                 self.options.get('platform', ''), 'deprecated' in self.options)
+            # make a duplicate entry in 'objects' to facilitate searching for
+            # the command in OperationDomain.find_obj()
+            env.domaindata['op']['objects'][setname] = (env.docname, 'setting')
+            targetnode = nodes.target('', '', ids=['setting-' + setname],
+                                      ismod=True)
+            self.state.document.note_explicit_target(targetnode)
+            # the platform and synopsis aren't printed; in fact, they are only
+            # used in the modindex currently
+            ret.append(targetnode)
+            indextext = _('%s (setting)') % setname
+            inode = addnodes.index(entries=[('single', indextext,
+                                             'setting-' + setname, '')])
+            ret.append(inode)
+        return ret
+
+
+class OperationXRefRole(XRefRole):
+    def process_link(self, env, refnode, has_explicit_title, title, target):
+        refnode['op:command'] = env.temp_data.get('op:command')
+        refnode['op:setting'] = env.temp_data.get('op:setting')
+        if not has_explicit_title:
+            title = title.lstrip('.')   # only has a meaning for the target
+            target = target.lstrip('~') # only has a meaning for the title
+            # if the first character is a tilde, don't display the command/class
+            # parts of the contents
+            if title[0:1] == '~':
+                title = title[1:]
+                dot = title.rfind('.')
+                if dot != -1:
+                    title = title[dot+1:]
+        # if the first character is a dot, search more specific namespaces first
+        # else search builtins first
+        if target[0:1] == '.':
+            target = target[1:]
+            refnode['refspecific'] = True
+        return title, target
+
+
+class OperationCommandIndex(Index):
+    """
+    Index subclass to provide the Operation command index.
+    """
+
+    name = 'commandindex'
+    localname = l_('Command Index')
+    shortname = l_('Command')
+
+    def generate(self, docnames=None):
+        content = {}
+        # list of prefixes to ignore
+        ignores = self.domain.env.config['modindex_common_prefix']
+        ignores = sorted(ignores, key=len, reverse=True)
+        # list of all commands, sorted by command name
+        commands = sorted(self.domain.data['commands'].iteritems(),
+                         key=lambda x: x[0].lower())
+        # sort out collapsable commands
+        prev_comname = ''
+        num_toplevels = 0
+        for comname, (docname, synopsis, platforms, deprecated) in commands:
+            if docnames and docname not in docnames:
+                continue
+
+            for ignore in ignores:
+                if comname.startswith(ignore):
+                    comname = comname[len(ignore):]
+                    stripped = ignore
+                    break
+            else:
+                stripped = ''
+
+            # we stripped the whole command name?
+            if not comname:
+                comname, stripped = stripped, ''
+
+            entries = content.setdefault(comname[0].lower(), [])
+
+            package = comname.split('.')[0]
+            if package != comname:
+                # it's a subcommand
+                if prev_comname == package:
+                    # first subcommand - make parent a group head
+                    if entries:
+                        entries[-1][1] = 1
+                elif not prev_comname.startswith(package):
+                    # subcommand without parent in list, add dummy entry
+                    entries.append([stripped + package, 1, '', '', '', '', ''])
+                subtype = 2
+            else:
+                num_toplevels += 1
+                subtype = 0
+
+            qualifier = deprecated and _('Deprecated') or ''
+            entries.append([stripped + comname, subtype, docname,
+                            'command-' + stripped + comname, platforms,
+                            qualifier, synopsis])
+            prev_comname = comname
+
+        # apply heuristics when to collapse modindex at page load:
+        # only collapse if number of toplevel commands is larger than
+        # number of submodules
+        collapse = len(commands) - num_toplevels < num_toplevels
+
+        # sort by first letter
+        content = sorted(content.iteritems())
+
+        return content, collapse
+
+
+class OperationSettingIndex(Index):
+    """
+    Index subclass to provide the Operation command index.
+    """
+
+    name = 'settingindex'
+    localname = l_('Setting Index')
+    shortname = l_('Setting')
+
+    def generate(self, docnames=None):
+        content = {}
+        # list of prefixes to ignore
+        ignores = self.domain.env.config['modindex_common_prefix']
+        ignores = sorted(ignores, key=len, reverse=True)
+        # list of all setting, sorted by command name
+        settings = sorted(self.domain.data['settings'].iteritems(),
+                         key=lambda x: x[0].lower())
+        # sort out collapsable setting
+        prev_setname = ''
+        num_toplevels = 0
+        for setname, (docname, synopsis, platforms, deprecated) in settings:
+            if docnames and docname not in docnames:
+                continue
+
+            for ignore in ignores:
+                if setname.startswith(ignore):
+                    setname = setname[len(ignore):]
+                    stripped = ignore
+                    break
+            else:
+                stripped = ''
+
+            # we stripped the whole command name?
+            if not setname:
+                setname, stripped = stripped, ''
+
+            entries = content.setdefault(setname[0].lower(), [])
+
+            package = setname.split('.')[0]
+            if package != setname:
+                # it's a subcommand
+                if prev_setname == package:
+                    # first subcommand - make parent a group head
+                    if entries:
+                        entries[-1][1] = 1
+                elif not prev_setname.startswith(package):
+                    # subcommand without parent in list, add dummy entry
+                    entries.append([stripped + package, 1, '', '', '', '', ''])
+                subtype = 2
+            else:
+                num_toplevels += 1
+                subtype = 0
+
+            qualifier = deprecated and _('Deprecated') or ''
+            entries.append([stripped + setname, subtype, docname,
+                            'setting-' + stripped + setname, platforms,
+                            qualifier, synopsis])
+            prev_setname = setname
+
+        # apply heuristics when to collapse modindex at page load:
+        # only collapse if number of toplevel setting is larger than
+        # number of subsetting
+        collapse = len(settings) - num_toplevels < num_toplevels
+
+        # sort by first letter
+        content = sorted(content.iteritems())
+
+        return content, collapse
+
+
+class OperationDomain(Domain):
+    """Operation domain."""
+    name = 'op'
+    label = 'Operation'
+    object_types = {
+        'command':       ObjType(l_('command'),        'command'),
+        'setting':       ObjType(l_('setting'),        'setting'),
+    }
+
+    directives = {
+        'command':         OperationCommand,
+        'setting':         OperationSetting,
+    }
+    roles = {
+        'command':   OperationXRefRole(),
+        'setting':   OperationXRefRole(),
+    }
+    initial_data = {
+        'objects': {},  # fullname -> docname, objtype
+        'commands': {},  # comname -> docname, synopsis, platform, deprecated
+        'settings': {},  # setname -> docname, synopsis, platform, deprecated
+    }
+    indices = [
+        OperationCommandIndex,
+        OperationSettingIndex,
+    ]
+
+    def clear_doc(self, docname):
+        for fullname, (fn, _) in self.data['objects'].items():
+            if fn == docname:
+                del self.data['objects'][fullname]
+        for modname, (fn, _, _, _) in self.data['commands'].items():
+            if fn == docname:
+                del self.data['commands'][modname]
+        for setname, (fn, _, _, _) in self.data['settings'].items():
+            if fn == docname:
+                del self.data['settings'][setname]
+
+    def find_obj(self, env, modname, setname, name, type, searchmode=0):
+        """Find a Operation object for "name", perhaps using the given command
+           Returns a list of (name, object entry) tuples.
+        """
+
+        if not name:
+            return []
+
+        objects = self.data['objects']
+        matches = []
+
+        newname = None
+        if searchmode == 1:
+            objtypes = self.objtypes_for_role(type)
+            if objtypes is not None:
+                if not newname:
+                    if modname and modname + '.' + name in objects and \
+                       objects[modname + '.' + name][1] in objtypes:
+                        newname = modname + '.' + name
+                    elif name in objects and objects[name][1] in objtypes:
+                        newname = name
+                    else:
+                        # "fuzzy" searching mode
+                        searchname = '.' + name
+                        matches = [(oname, objects[oname]) for oname in objects
+                                   if oname.endswith(searchname)
+                                   and objects[oname][1] in objtypes]
+        else:
+            # NOTE: searching for exact match, object type is not considered
+            if name in objects:
+                newname = name
+            elif modname and modname + '.' + name in objects:
+                newname = modname + '.' + name
+        if newname is not None:
+            matches.append((newname, objects[newname]))
+        return matches
+
+    def resolve_xref(self, env, fromdocname, builder,
+                     type, target, node, contnode):
+        modname = node.get('op:command')
+        setname = node.get('op:setting')
+        searchmode = node.hasattr('refspecific') and 1 or 0
+        matches = self.find_obj(env, modname, setname, target,
+                                type, searchmode)
+        if not matches:
+            return None
+        elif len(matches) > 1:
+            env.warn_node(
+                'more than one target found for cross-reference '
+                '%r: %s' % (target, ', '.join(match[0] for match in matches)),
+                node)
+        name, obj = matches[0]
+
+        if obj[1] == 'command':
+            # get additional info for commands
+            docname, synopsis, platform, deprecated = self.data['commands'][name]
+            assert docname == obj[0]
+            title = name
+            if synopsis:
+                title += ': ' + synopsis
+            if deprecated:
+                title += _(' (deprecated)')
+            if platform:
+                title += ' (' + platform + ')'
+            return make_refnode(builder, fromdocname, docname,
+                                'command-' + name, contnode, title)
+        elif obj[1] == 'setting':
+            # get additional info for settings
+            docname, synopsis, platform, deprecated = self.data['settings'][name]
+            assert docname == obj[0]
+            title = name
+            if synopsis:
+                title += ': ' + synopsis
+            if deprecated:
+                title += _(' (deprecated)')
+            if platform:
+                title += ' (' + platform + ')'
+            return make_refnode(builder, fromdocname, docname,
+                                'setting-' + name, contnode, title)
+        else:
+            return make_refnode(builder, fromdocname, obj[0], name,
+                                contnode, name)
+
+    def get_objects(self):
+        for modname, info in self.data['commands'].iteritems():
+            yield (modname, modname, 'command', info[0], 'command-' + modname, 0)
+        for setname, info in self.data['settings'].iteritems():
+            yield (setname, setname, 'setting', info[0], 'setting-' + modname, 0)
+        for refname, (docname, type) in self.data['objects'].iteritems():
+            yield (refname, refname, type, docname, refname, 1)
+
+def setup(app):
+    app.add_domain(OperationDomain)

File test/test_doc.rst

+===============
+Acceptance Test
+===============
+
+If all links are valid, test is done successfully.
+
+Test Case - setting Directiv
+============================
+
+.. op:setting:: Test Case 1
+
+
+Test Case - command Directiv
+============================
+
+.. op:command:: Test Case 2
+
+
+Test Case - Role
+================
+
+:op:setting:`Test Case 1`
+
+:op:command:`Test Case 2`