Source code for sphinx_c_autodoc.viewcode

"""
Handles viewcode for c.

The processing idea:

1. Walk through every node in the document finding out if it is a C
   construct. Then find out which file, if any it is associated with:

   a. Create a pending cross reference to the file.

   b. Add the file to the environment list of files to create source listings
      of, :attr:`app.env._viewcode_c_modules`

2. Walk through all of the files in the environment list,
   :attr:`app.env._viewcode_c_modules` and create a source listing for each
   one.

3. Process the pending cross references and link them up to the source
   listings.

.. note:: The environment attribute is namespaced with `c` to prevent
    populating the `viewcode`_ extension's variable for multi-language
    documentation purposes.

.. _viewcode: https://www.sphinx-doc.org/en/master/usage/extensions/viewcode.html

"""

from dataclasses import dataclass
from typing import Any, Dict, Iterator, Tuple, List, Optional

from docutils import nodes
from docutils.nodes import Node, Element

from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.util import logging
from sphinx.util.nodes import make_refnode

# Sphinx 7 moved the status iterator into the display module
try:
    from sphinx.util.display import status_iterator
except ImportError:  # pragma: no cover
    # pylint: disable=no-name-in-module
    from sphinx.util import status_iterator  # type: ignore

from sphinx_c_autodoc import ViewCodeListing

MODULES_DIRECTORY = "_modules"

# To work with the c domain from sphinx all C constructs will have this prefix.
C_DOMAIN_LINK_PREFIX = "c."

logger = logging.getLogger(__name__)


[docs] @dataclass class DocumentationReference: """ Representation of the documentation of a C construct. Attributes: docname (str): The document which contains the documentation of the C construct. module (str): The c module, this is relative to a path in LINK_TO_SOURCE_ROOTS fullname (str): The name of the construct, i.e. function name, variable name etc. """ docname: str module: str fullname: str
[docs] def missing_reference( app: Sphinx, env: BuildEnvironment, node: Element, contnode: Node ) -> Optional[Node]: """ Adds a reference node to and c viewcode links which haven't been resolved yet. If the location to cross reference does not exist this will return an empty node. Args: app (Sphinx): The currently running sphinx application. env (BuildEnvironment): The current build environment. node (Element): The node which is a pending cross reference. contnode (Node): The _contents_ node of the created cross reference Returns: The cross refernce node to use. """ # pylint: disable=unused-argument if node["reftype"] == f"{C_DOMAIN_LINK_PREFIX}viewcode": assert app.builder is not None # We have to wait until here to see if the module actually exists as a # plain c directive could be reference a module before the auto c # directives are encountered. modules = getattr(app.builder.env, "_viewcode_c_modules", {}) module = modules.get(node["module"]) if module is None: return nodes.inline() construct = _find_construct(node["fullname"], module.ast) if construct is None: return nodes.inline() return make_refnode( app.builder, node["refdoc"], node["reftarget"], node["refid"], contnode ) return None
[docs] def add_source_listings(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: """ The idea is to create a code listing of each source file that has a pending cross reference to. The pending source files to create listings for are stored in :attr:`app.env._viewcode_c_modules`. Meant to be connected to the `html-collect-pages` event, https://www.sphinx-doc.org/en/master/extdev/appapi.html#event-html-collect-pages Args: app: The current sphinx app being run. Yields: Tuple[str, Dict[str, Any], str]: The name of the page, the contents of the page, the name of the template to use for generating the final page. """ assert app.builder is not None modules_to_list = getattr(app.builder.env, "_viewcode_c_modules", {}) iterator = status_iterator( sorted(modules_to_list.items()), "highlighting c module code... ", "blue", len(modules_to_list), app.verbosity, lambda x: x[0], ) for module, code_listing in iterator: highlighted_source = _get_highlighted_source(app, code_listing.raw_listing) _insert_line_anchors(app, highlighted_source, code_listing) context = { "title": module, "body": ( f"<h1>Source code for {module}</h1>" + "\n".join(highlighted_source) ), } yield (_get_source_page_name(module), context, "page.html")
def _get_source_page_name(module: str) -> str: """ Get the page name of the resultant source listing. Args: module (str): The module of the source listing. Returns: str: The page name of the resultant source listing. """ return f"{MODULES_DIRECTORY}/{module}" def _insert_documentation_backlinks( app: Sphinx, highlighted_code: List[str], code_listing: ViewCodeListing ) -> None: """ Insert links from `highlighted_code` to the places that documented `highlighted_code`. Args: app (Sphinx): The currently running sphinx application. highlighted_code (List[str]): The source listing to create links from. This will be modified in place. code_listing (ViewCodeListing): Contains the documentation locations which documented `highlighted_code`. """ assert app.builder is not None for doc in code_listing.doc_links.values(): construct = _find_construct(doc.fullname, code_listing.ast) # Can happen when documenting a non existent C construct. # TODO consider if this should be a warning. if construct is None: continue link_line = construct["start_line"] page_name = _get_source_page_name(doc.module) relative_link = app.builder.get_relative_uri(page_name, doc.docname) link_text = f"{relative_link}#{C_DOMAIN_LINK_PREFIX}{doc.fullname}" highlighted_code[link_line] = ( f'<a class="viewcode-back" href="{link_text}">[docs]</a>' + highlighted_code[link_line] ) def _find_construct(fullname: str, ast: Dict) -> Optional[Dict]: """ Find a C construct inside of the provided `ast`. Args: fullname (str): The full, dotted name, to the C construct. ast (dict): A dictionary like representation of the code constructs. See :ref:`developer_notes:Common Terms`. Returns: Dict: The C construct if it is found in the `ast`. None otherwise. """ parts = fullname.split(".", 1) parent = parts[0] child = {} for child in ast["children"]: if child["name"] == parent: break else: return None if len(parts) > 1: child_path = parts[1] return _find_construct(child_path, child) return child def _insert_line_anchors( app: Sphinx, highlighted_source: List[str], code_listing: ViewCodeListing ) -> None: """ Insert line anchors into `highlighted_source` which can be pointed to from other locations in the documentation. Also adds in links back to the documentation locations. Args: app (Sphinx): The sphinx app currently doing the processing. highlighted_source (List[str]): The source to populate with anchors. This should already highlighted in html format. code_listing (ViewCodeListing): The code listing to retrieve anchors from. """ _insert_documentation_backlinks(app, highlighted_source, code_listing) # The root file needs no anchor so only document it's children for child in code_listing.ast["children"]: _insert_construct_anchor(app, highlighted_source, child) def _insert_construct_anchor( app: Sphinx, highlighted_source: List[str], construct: Dict, prefix: Optional[str] = None, ) -> None: """ Recursivley insert an anchor for a c construct and anchors for all of it's children. Args: app (Sphinx): The sphinx app currently doing the processing. highlighted_source (List[str]): The source to populate with anchors. This should already highlighted in html format. construct (Dict): The construct to create an anchor for. See :ref:`c_construct` prefix (Optional[str]): The prefix representing the dotted path of the current """ start = construct["start_line"] end = construct["end_line"] name = construct["name"] if prefix: prefix = ".".join((prefix, name)) else: prefix = name highlighted_source[start] = ( f'<div class="viewcode-block" id="{C_DOMAIN_LINK_PREFIX}{prefix}">' + highlighted_source[start] ) highlighted_source[end] += "</div>" for child in construct.get("children", []): _insert_construct_anchor(app, highlighted_source, child, prefix) def _get_highlighted_source(app: Sphinx, code: str) -> List[str]: """ Turn the code into a highlighted source file. Args: app (Sphinx): The sphinx app currently doing the processing. code (str): The code to turn into highlighted source. Returns: List[str]: The code lines with necessary markup to be highlighted. The first line of the `code` will be in index ``1`` of the list. """ highlighter = app.builder.highlighter # type: ignore highlighted_code = highlighter.highlight_block(code, "c", linenos=False) lines = highlighted_code.splitlines() return _align_code_lines(lines) def _align_code_lines(highlighted_code: List[str]) -> List[str]: """ Source lines are 1 based indices, while actual lists are 0 based. This funciton ensures that the first code line ends up at index 1 in the list of lines. For whatever reason pygments html, the actual backend formatter, places the termination on a separate trailing line, but doesn't put the initial surround on a preceding line. .. code_block:: highlighted_code = ["<div><pre><span></span>{first line of file}", "{second line of flie}", ... "{last line of flie}", "</pre></div>"] So we'll break the first line into two so that the ``div`` and ``pre`` tags go into index 0 and the first line ends up in index 1. Args: highlighted_code (List[str]): The code to align correctly for line numbers. Returns: List[str]: The code properly aligned with ``1`` based line numbering. """ *leading_html, first_line = highlighted_code[0].partition("<pre>") highlighted_code[0:1] = ["".join(leading_html), first_line] return highlighted_code
[docs] def doctree_read(app: Sphinx, doctree: Node) -> None: """ Go through the entire document looking for C signature nodes to create cross references to the actual source listings. Args: app (Sphinx): The sphinx app currently doing the processing. doctree (Node): The root node of the document to walk through. This will be modified in place, by modifiying signature nodes of C constructs. """ c_nodes = (n for n in doctree.findall(addnodes.desc) if n.get("domain") == "c") for node in c_nodes: signature_nodes = (n for n in node if isinstance(n, addnodes.desc_signature)) # I really dislike negative flags. Anyway it's ok to link to source # listing from anywhere, but it's undesirable to link from the source # listing back to any documentation location. So we leverage the # :noindex: option and assume that only the entries which allowed the # indices are the consolidated locations of documentation. use_back_refs = not node.get("noindex", False) for signature in signature_nodes: fullname = signature.get("fullname") if use_back_refs: _add_pending_back_reference(app, signature, fullname) _add_pending_source_cross_reference(app, signature, fullname)
def _add_pending_source_cross_reference( app: Sphinx, signode: addnodes.desc_signature, fullname: str ) -> None: """ Adds a pending source cross reference to the signature in the doctree, `signode`. The viewcode and linkcode extensions walk the doctree once parsed and then add this node, however since sphinx_c_autodoc already has to add logic, the `module` option to the directives it seems more practical to just create the full pending cross reference here, and then viewcode is an extension which will populate this cross refernce. Args: app (Sphinx): The sphinx app currently doing the processing. signode (Node): The signature node to apply the source code cross reference to. fullname (str): The dotted fullname of the C construct. """ module = signode.get("module") if module is None: return assert app.builder is not None assert app.builder.env is not None source_page = _get_source_page_name(module) # Using the `viewcode-link` to be consistent with the python versions in # case someone else wants to walk the tree and do other links inline = nodes.inline("", "[source]", classes=["viewcode-link"]) # Limit this cross referencing only to html html_node = addnodes.only(expr="html") html_node += addnodes.pending_xref( "", inline, reftype=f"{C_DOMAIN_LINK_PREFIX}viewcode", refdomain="std", refexplicit=False, reftarget=source_page, refid=f"{C_DOMAIN_LINK_PREFIX}{fullname}", refdoc=app.builder.env.docname, module=module, fullname=fullname, ) signode += html_node def _add_pending_back_reference( app: Sphinx, signode: addnodes.desc_signature, fullname: str ) -> None: """ Updates the ``doc_links`` entry of the modules stored in ``_viewcode_c_modules`` so that they can later be added to the source code listings as links to the documentation locations. Args: app (Sphinx): The sphinx app currently doing the processing. signode (Node): The signature node to create the back reference to. fullname (str): The name of the construct, i.e. function name, variable name etc. """ module = signode.get("module") if module is None: return assert app.builder is not None assert app.builder.env is not None env = app.builder.env code_listing = env._viewcode_c_modules.get(module) # type: ignore if code_listing is None: return doc_links = code_listing.doc_links doc_links.setdefault( fullname, DocumentationReference(env.docname, module, fullname) )
[docs] def setup(app: Sphinx) -> None: """ Setup function for registering this with sphinx Args: app (Sphinx): The application for the current run of sphinx. """ app.connect("doctree-read", doctree_read) app.connect("missing-reference", missing_reference) app.connect("html-collect-pages", add_source_listings)