Implementing custom "autodoc" style objects

29 views
Skip to first unread message

Sean Leavey

unread,
Apr 4, 2020, 4:15:59 AM4/4/20
to sphinx-users
Hello Sphinx users,

I want to create a Sphinx directive that documents custom objects for a domain-specific language. I want to define an object like this...

My Custom Item
==============

..  dmn:component:: myitemcls

    This is some extra text!


and have Sphinx document it like it were a Python class, but not quite - something like this:


(That's a mock-up of what I'd like - so far I've not managed it. Note also I'd like the "This is some extra text!" text to appear somewhere there too...)

The idea is that the custom Sphinx directive would look up myitemcls in my library and introspect its parameters and then document them here.
I figure this is the same as the Python autodoc module, but I'm having trouble understanding how it works - I find it's horrendously complicated and makes calls to other modules all over the place, not all of which are documented or require calls to non-public APIs. I tried to get it to work instead using a custom directive class, but I had to do something hacky - in the run method I grab the output from the parent (ObjectDescription) and inject some parameters into the middle of the node list - the problem is that the ObjectDescription directive appears to want the :param A: notation to have already been injected in before instantiation, as I believe autodoc would do. In contrast, with my method I have to inject these parameter strings into the object after instantiation (in run()), and that seems to be my problem. I guess the solution is to subclass some part of autodoc, but as I said I find it hard to figure out how.

Here's what I've got so far:

import docutils
from docutils import nodes
import sphinx
from docutils.parsers import rst
from docutils.parsers.rst import directives
from docutils.statemachine import StringList
from sphinx.domains import Domain, Index
from sphinx.domains.std import StandardDomain
from sphinx.roles import XRefRole
from sphinx.directives import ObjectDescription
from sphinx.util.nodes import make_refnode
from sphinx.util.docfields import DocFieldTransformer, GroupedField
from sphinx import addnodes


class ComponentNode(ObjectDescription):
    """A custom node that describes a component."""

    required_arguments = 1

    option_spec = {}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._component_name = self.arguments[0]

        # Argument node list, filled later.
        self._arglist = None
        self._paramlist = None

        # This is my own function, not shown, that populates self._paramlist with a list of strings containing ":param name:" etc. It also builds self._arglist with addnodes.desc_parameter nodes.
        self._parse_parameters()

    def run(self, *args, **kwargs):
        nodes = super().run(*args, **kwargs)

        # The following is a bit of a hack. I first overwrite the field type map to set up the
        # "param" parser. I then create a list of strings containing ":param [x]: [description]"
        # entries and parse the contents into a new node. Finally I stitch this node into the
        # middle of the node list returned by the parent run() method.

        self._doc_field_type_map = {
            "param": (
                GroupedField("parameters", label="Parameters", names=('param',), can_collapse=True),
                False # is typed
            ),
        }

        # Add parameters to content.
        extra = StringList(
            self._paramlist,
            source=((self.content.parent, 0)*len(self._paramlist)),
            parent=self.content.parent,
            parent_offset=self.content.parent_offset
        )

        contentnode = addnodes.desc_content()
        self.state.nested_parse(extra, self.content_offset, contentnode)

        DocFieldTransformer(self).transform_all(contentnode)

        return nodes[0:2] + [contentnode] + nodes[2:]

I'm not happy with this, it's quite hacky and I figure I'm not doing it properly. It also generates documentation that looks different to that of class documentation - the "parameter" labels are blue not grey:


Can anyone help me figure out how to implement this custom directive, ideally using autodoc?

Daniel Scott

unread,
Apr 5, 2020, 9:45:42 PM4/5/20
to sphinx...@googlegroups.com


Sent from my Metro By T-Mobile 4G LTE Android Device

From: sphinx...@googlegroups.com <sphinx...@googlegroups.com> on behalf of Sean Leavey <sean....@gmail.com>
Sent: Saturday, April 4, 2020 4:15:59 AM
To: sphinx-users <sphinx...@googlegroups.com>
Subject: [sphinx-users] Implementing custom "autodoc" style objects
 
--
You received this message because you are subscribed to the Google Groups "sphinx-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to sphinx-users...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/sphinx-users/5f306f1b-a565-4d16-a515-8dd2b2861d51%40googlegroups.com.

Sean Leavey

unread,
Apr 6, 2020, 3:19:03 AM4/6/20
to sphinx-users
Hi Daniel, I didn't see anything in your reply - did you mean to say something or was that a mistake?

In any case, I got something to work by indeed subclassing from sphinx.ext.autodoc. Here is my working solution:

from sphinx.ext.autodoc import Documenter, ClassDocumenter, ModuleLevelDocumenter

__version__ = "0.1"


class MyDocumenter(ModuleLevelDocumenter):
    """Custom Sphinx autodocumeter.

    Note: this disables the hooks used by numpydoc to stop it from mangling the function signature
    and docstring. This is required because numpy assumes it is documenting a Python function and
    therefore that the arguments are contained in brackets.
    """

    objtype = "myobj"
    directivetype = "attribute"

    @classmethod
    def can_document_member(cls, member, membername, isattr, parent):
        return False

    def document_members(self, all_members=False):
        pass

    def process_doc(self, docstrings):
        # Stop extensions from preprocessing the docstrings.
        for docstringlines in docstrings:
            yield from docstringlines

    def get_doc(self, ignore=1):
        # Add your custom document fields here.
        lines.append([":My Section:"])
        lines.append([""])
        lines.append(["::"])
        lines.append([""])
        lines.append(["    Some paragraph..."])
        lines.append([""])

        return lines

    def format_name(self):
        # Ensure Python module is not added to name.
        return self.name

    def format_signature(self, **kwargs):
        return ""

    def resolve_name(self, modname, parents, path, base):
        # Needs something here for Documenter to generate output; this is also used for the index.
        return "fake", [base]

    def import_object(self):
        return True


def setup(app):
    app.add_autodocumenter(MyDocumenter)

    return {"version": __version__, "parallel_read_safe": True}

To unsubscribe from this group and stop receiving emails from it, send an email to sphinx...@googlegroups.com.

Komiya Takeshi

unread,
Apr 6, 2020, 10:28:13 AM4/6/20
to sphinx...@googlegroups.com
Hi,

Autodoc is one of complex extension in Sphinx, I think. It is hard to understand how it implemented. It adopted two-layered model. The first layer; python domain describes python object in reST level. It provides many directives for that purpose; `py:function`, `py:class`, `py:method` and so on. The second layer is autodoc itself. It generates reStructuredText source code based on the first layer internally.

And the `ObjectDescription` class you used is a base class of the first layer. It is a kind of mini framework to create object-description directives. It expects to derived classes to override `handle_signature()` and some methods. You can see examples of it on sphinx/domains/*.py.

If your goal is making autodoc by one directive, there are no example to do that. So I propose you to make the first layer at first.

Thanks,
Takeshi KOMIYA


2020年4月4日(土) 17:16 Sean Leavey <sean....@gmail.com>:
--
You received this message because you are subscribed to the Google Groups "sphinx-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to sphinx-users...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages