Commits

Georg Brandl  committed d8b73a3 Merge
  • Participants
  • Parent commits a891f63, ae04348

Comments (0)

Files changed (7)

 - autorun: Execute code in a runblock directive.
 - blockdiag: embed block diagrams by using blockdiag_
 - cheeseshop: easily link to PyPI packages
+- coffeedomain: a domain for (auto)documenting CoffeeScript source code.
 - context: a builder for ConTeXt.
 - doxylink: Link to external Doxygen-generated HTML documentation
 - epydoc: cross-reference eypdoc generated documentation

File coffeedomain/README.rst

+===================
+CoffeeScript Domain
+===================
+
+:author: Stephen Sugden <glurgle@gmail.com>
+
+About
+=====
+
+This extension adds a CoffeeScript domain with autodoc support to Sphinx.
+
+This relies on coffeedoc_ to extract source comments from ``.coffee``
+files. To install coffeedoc globally::
+
+  npm install -g coffeedoc
+
+.. _coffeedoc: https://github.com/omarkhan/coffeedoc
+
+Usage
+=====
+
+First you must add the extension to your list of extensions in conf.py::
+
+  extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.coffeedomain']
+
+Because Python has no clue about where your sources are, you must tell it
+where to look in your conf.py like so::
+
+  coffee_src_dir = os.path.abspath('../')
+
+Finally, if your project is primarily CoffeeScript, you might want to
+define the primary domain as well::
+
+  primary_domain = 'coffee'
+
+Directives and Roles
+====================
+
+This domain provides module, function, class and method directives, as
+well as meth, class, and func roles for cross-referencing. In order to
+reference another object, use it's fully-qualified name: The module name,
+a double colon, and the dot separate path to the object within the module.
+
+For example, to reference the ``swizzle`` function in the file
+``lib/widgets.coffee``, write ``:coffee:func:`lib/widgets::swizzle``. You
+can drop the leading ``:coffee`` if your primary domain has been
+set to coffee, and you can drop the module name iff you are linking from
+within the ``lib/widgets`` module.
+
+Autodoc
+=======
+
+You can document entire modules easily with ``automodule``:
+
+.. sourcecode:: rst
+
+  .. automodule:: mymodule
+    :members:

File coffeedomain/setup.py

+from setuptools import setup, find_packages
+
+if __name__ == '__main__':
+    setup(name='sphinxcontrib-coffee',
+          version='0.1.0',
+          license='BSD',
+          author="Stephen Sugden",
+          author_email="glurgle@gmail.com",
+          description='Sphinx extension to add CoffeeScript support',
+          platforms='any',
+          packages=find_packages(),
+          namespace_packages=['sphinxcontrib'])

File coffeedomain/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 coffeedomain/sphinxcontrib/coffeedomain/__init__.py

+def setup(app):
+    app.add_config_value('coffee_src_dir', None, 'env')
+    from .domain import CoffeeDomain
+    app.add_domain(CoffeeDomain)
+    from . import documenters as doc
+    app.add_autodocumenter(doc.ModuleDocumenter)
+    app.add_autodocumenter(doc.ClassDocumenter)
+    app.add_autodocumenter(doc.FunctionDocumenter)
+    app.add_autodocumenter(doc.MethodDocumenter)

File coffeedomain/sphinxcontrib/coffeedomain/documenters.py

+from sphinx.util.docstrings import prepare_docstring
+from sphinx.ext.autodoc import Documenter, members_option, bool_option, ModuleDocumenter as PyModuleDocumenter
+from subprocess import Popen, PIPE
+import json
+ 
+class StubObject(object):
+    """
+    A python object that takes the place of the coffeescript object being
+    documented.
+    """
+    def __init__(self, type, data):
+        """
+        :param type: The type of CS object, possible types:
+            * module
+            * class
+            * function
+            * method
+            * staticmethod
+        :param data: Data for this object from the ``coffeedoc`` output.
+        """
+        self.type = type
+        self.data = data
+
+    def __getitem__(self, key):
+        return self.data[key]
+
+    @property
+    def __doc__(self):
+        return self['docstring']
+
+    def __repr__(self):
+        return "<StubObject for %s %s>" % (self.type, self['name'])
+
+class CoffeedocDocumenter(Documenter):
+    """
+    Base class for documenters that use the output of ``coffeedoc``
+    """
+    priority = 20
+    domain = 'coffee'
+
+    @classmethod
+    def can_document_member(cls, member, membername, isattr, parent):
+        return isinstance(member, StubObject) and member.type == cls.documents_type
+
+    def parse_name(self):
+        """
+        Determine what module to import and what attribute to document.
+        Returns True and sets *self.modname*, *self.objpath*, *self.fullname*,
+        *self.args* and *self.retann* if parsing and resolving was successful.
+        """
+        self.fullname = self.name
+        self.modname, path = self.name.split('::')
+        self.real_modname = self.modname
+        self.objpath = path.split('.')
+        self.args = None
+        self.retann = None
+        return True
+            
+    def format_name(self):
+        return self.modname + '::' + '.'.join(self.objpath)
+
+    def get_object_members(self, want_all=False):
+        members = []
+        for type in self.sub_member_keys:
+            for obj in self.object[type]:
+                members.append((obj['name'], StubObject(type, obj)))
+        return False, members
+
+    @property
+    def coffeedoc_module(self):
+        filename= self.modname + '.coffee'
+        modules = self.env.temp_data.setdefault('coffee:docgen', {})
+        basedir = self.env.config.coffee_src_dir
+        if filename not in modules:
+            docgen = Popen(['coffeedoc', '--stdout', '--renderer', 'json',
+                            filename],
+                           cwd=basedir, stdout=PIPE)
+            module_data = json.load(docgen.stdout)[0]['module']
+            print "ran coffeedoc --stdout --renderer json %s" % filename
+            modules[filename] = StubObject('module', module_data)
+        return modules[filename]
+
+    def import_object(self):
+        raise NotImplemented("")
+
+class ModuleDocumenter(CoffeedocDocumenter, PyModuleDocumenter):
+    objtype = 'module'
+
+    content_indent = u''
+    titles_allowed = True
+
+    documents_type = 'module'
+    sub_member_keys = ('classes', 'functions')
+
+    def import_object(self):
+        self.object = self.coffeedoc_module
+        return True
+
+    def parse_name(self):
+        """
+        Determine what module to import and what attribute to document.
+        Returns True and sets *self.modname*, *self.objpath*, *self.fullname*,
+        *self.args* and *self.retann* if parsing and resolving was successful.
+        """
+        self.objpath = []
+        self.fullname = self.modname = self.name
+        return True
+
+    def add_content(self, more_content, no_docstring=False):
+        super(ModuleDocumenter, self).add_content(more_content, no_docstring=False)
+        if not self.object['deps']:
+            return
+        self.add_line('*Dependencies:*', '<autodoc>')
+        for localname, module in self.object['deps'].items():
+            self.add_line('  * %s = require "%s"' % (localname, module), '<autodoc>')
+
+class SubMember(object):
+    option_spec = {
+        'members': members_option
+    }
+
+    def find_parent_object(self):
+        raise NotImplemented("")
+
+    def _import_candidates(self, parent):
+        return parent[self.documents_type]
+
+    def import_object(self):
+        parent = self.find_parent_object()
+        if not parent:
+            return None
+        for data in self._import_candidates(parent):
+            dpath = data['name'].split('.')
+            if dpath == self.objpath[-1 * len(dpath):]:
+                self.object = StubObject(self.documents_type, data)
+                return True
+        return False
+
+class ModuleMember(SubMember):
+    def find_parent_object(self):
+        return self.coffeedoc_module
+
+class ClassMember(SubMember):
+    def find_parent_object(self):
+        module = self.coffeedoc_module
+        for data in module['classes']:
+            cpath = data['name'].split('.')
+            if cpath == self.objpath[:len(cpath)]:
+                return data
+
+class ClassDocumenter(ModuleMember, CoffeedocDocumenter):
+    objtype = 'class'
+    documents_type = 'classes'
+    sub_member_keys = ('staticmethods', 'instancemethods')
+
+    def get_object_members(self, want_all=False):
+        members = []
+        for type in self.sub_member_keys:
+            for obj in self.object[type]:
+                members.append((obj['name'], StubObject(type, obj)))
+        return False, members
+
+class CodeDocumenter(CoffeedocDocumenter):
+    sub_member_keys = ()
+
+    def format_signature(self):
+        return '(%s)' % ', '.join(self.object['params'])
+
+class FunctionDocumenter(ModuleMember, CodeDocumenter):
+    objtype = 'function'
+    documents_type = 'functions'
+
+class MethodDocumenter(ClassMember, CodeDocumenter):
+    objtype = 'method'
+    documents_type = 'methods'
+
+    @property
+    def __doc__(self):
+        return self['docstring']
+
+    def _import_candidates(self, parent):
+        methods = []
+        for meth in parent['instancemethods']:
+            meth['static'] = False
+            methods.append(meth)
+        for meth in parent['staticmethods']:
+            meth['static'] = True
+            methods.append(meth)
+        return methods
+
+    @classmethod
+    def can_document_member(cls, member, membername, isattr, parent):
+        return isinstance(member, StubObject) and member.type.endswith('methods')

File coffeedomain/sphinxcontrib/coffeedomain/domain.py

+from docutils import nodes
+from docutils.parsers.rst import directives
+from sphinx import addnodes
+from sphinx.directives import ObjectDescription, Directive
+from sphinx.roles import XRefRole
+from sphinx.domains import Domain, ObjType
+from sphinx.domains.python import _pseudo_parse_arglist
+from sphinx.locale import l_, _
+from sphinx.util.docfields import DocFieldTransformer, TypedField, GroupedField
+from sphinx.util.nodes import make_refnode
+
+class CoffeeObj(ObjectDescription):
+    doc_field_types = [
+        TypedField('parameter', label='Parameters',
+                     names=('param', 'parameter', 'arg', 'argument'),
+                     typerolename='obj', typenames=('paramtype', 'type')),
+    ]
+
+    def handle_signature(self, sig, signode):
+        if self.display_prefix:
+            signode += addnodes.desc_annotation(self.display_prefix, self.display_prefix)
+
+        fullname, _, args = sig.partition('(')
+        modname, name = fullname.split('::')
+        args = args[:-1]
+        signode += addnodes.desc_name(name, name)
+        if isinstance(self, CoffeeFunction):
+            _pseudo_parse_arglist(signode, args)
+        return fullname
+
+    def add_target_and_index(self, fqn, sig, signode):
+        doc = self.state.document
+        if fqn not in self.state.document.ids:
+            signode['names'].append(fqn)
+            signode['ids'].append(fqn)
+            self.state.document.note_explicit_target(signode)
+        objects = self.env.domaindata['coffee']['objects']
+        objects[fqn] = (self.env.docname, self.objtype)
+
+        indextext = "%s (%s)" % (fqn, self.display_prefix.strip())
+        self.indexnode['entries'].append(('single', _(indextext), fqn, ''))
+
+# CoffeeModule inherits from Directive to allow it to output titles etc.
+class CoffeeModule(Directive):
+    domain = 'coffee'
+    required_arguments = 1
+    has_content = True
+
+    def run(self):
+        env = self.state.document.settings.env
+        modname = self.arguments[0].strip()
+        noindex = 'noindex' in self.options
+        env.temp_data['coffee:module'] = modname
+        if noindex:
+            return []
+        env.domaindata['coffee']['modules'][modname] = \
+            (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 module in CoffeeDomain.find_obj()
+        env.domaindata['coffee']['objects'][modname] = (env.docname, 'module')
+        targetnode = nodes.target('', '', ids=['module-' + modname],
+                                  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
+        indextext = _('%s (module)') % modname[:-2]
+        inode = addnodes.index(entries=[('single', indextext,
+                                         'module-' + modname, '')])
+        return [targetnode, inode]
+
+class CoffeeClass(CoffeeObj):
+    option_spec = {
+        'module': directives.unchanged,
+        'export_name': directives.unchanged,
+    }
+    display_prefix = 'class '
+
+class CoffeeFunction(CoffeeObj):
+    option_spec = {
+        'module': directives.unchanged,
+        'export_name': directives.unchanged,
+    }
+    display_prefix = 'function '
+
+class CoffeeMethod(CoffeeFunction):
+    option_spec = {
+        'module': directives.unchanged,
+        'class': directives.unchanged,
+        'static': directives.unchanged,
+    }
+    @property
+    def display_prefix(self):
+        return 'method '
+
+
+class CoffeeXRefRole(XRefRole):
+    def process_link(self, env, refnode, has_explicit_title, title, target):
+        """ Called after CoffeeDomain.resolve_xref """
+        if not has_explicit_title:
+            title = title.split('::').pop()
+        return title, target
+
+class CoffeeDomain(Domain):
+    label = 'CoffeeScript'
+    name = 'coffee'
+    object_types = {
+        'module':      ObjType(l_('module'), 'module'),
+        'function':  ObjType(l_('function'),  'func'),
+        'class':     ObjType(l_('class'),     'class'),
+        'method':    ObjType(l_('method'), 'method')
+    }
+    directives = {
+        'module': CoffeeModule,
+        'function': CoffeeFunction,
+        'class': CoffeeClass,
+        'method': CoffeeMethod,
+    }
+
+    roles = {
+       'meth': CoffeeXRefRole(),
+       'class': CoffeeXRefRole(),
+       'func': CoffeeXRefRole(),
+    }
+    data_version = 1
+    initial_data = {"modules": {}, "objects": {}}
+
+    def get_objects(self):
+        for fqn, obj in self.data['objects'].iteritems():
+            (docname, objtype) = obj
+            yield (fqn, fqn, objtype, docname, fqn, 1)
+            
+    def resolve_xref(self, env, fromdocname, builder, type, target, node, contnode):
+        if target[0] == '~':
+            target = target[1:]
+        doc, _ = self.data['objects'].get(target, (None, None))
+        if doc:
+            return make_refnode(builder, fromdocname, doc, target, contnode,
+                                target)