Source code for sphinx_uml.uml_generate_directive

import argparse
from pathlib import Path
from docutils import nodes
from docutils.parsers.rst import directives
from pylint.pyreverse.main import writer
from pylint.pyreverse.diagrams import (
    ClassDiagram,
    PackageDiagram,
)
from sphinx.ext.graphviz import (
    figure_wrapper,
    graphviz,
)
from sphinx.util.docutils import SphinxDirective
from sphinx.util.typing import OptionSpec
from typing import ClassVar


[docs] class UmlNode(graphviz): """ Defines a UML ``docutils`` node. We populate its attribute so that we can rely on the default ``graphviz`` node. """
[docs] @classmethod def from_dot(cls, dotcode: str) -> "UmlNode": """ Builds a :py:class:`UmlNode` instance from a Graphviz dot string. Args: dotcode (str): A Graphviz dot string. *Example:* ``digraph G {0 -> 1}`` Returns: The resulting :py:class:`UmlNode` instance. """ node = cls() node["code"] = dotcode node["options"] = {"graphviz_dot": "dot"} # We rely on sphinx.ext.inheritance_diagram CSS classes # to be responsive to dark/light themes. # TODO: improve this! node["classes"] = ["inheritance"] return node
[docs] @classmethod def to_dot( cls, diagram: ClassDiagram | PackageDiagram, config: argparse.Namespace ) -> str: """ Exports a diagram definition obtained from ``pyreverse`` to a Graphviz dot string. Args: diagram (ClassDiagram | PackageDiagram): The diagram that must be exported. config (argparse.Namespace): The configuration obtained from the Sphinx configuration file. Returns: The resulting :py:class:`UmlNode` instance. """ from .pyreverse import DotPrinter, SphinxHtmlProxy dwriter = writer.DiagramWriter(config) dwriter.printer_class = DotPrinter # TODO Build xrefs as in # /usr/lib/python3/dist-packages/sphinx/ext/inheritance_diagram.py? dwriter.api_doc = SphinxHtmlProxy() dwriter.api_doc.sphinx_html_dir = config.sphinx_html_dir dwriter.write([diagram]) return "\n".join(dwriter.printer.lines)
[docs] @classmethod def from_pyreverse( cls, diagram: ClassDiagram | PackageDiagram, config: argparse.Namespace ) -> "UmlNode": """ Builds a :py:class:`UmlNode` from a diagram definition obtained from ``pyreverse``. Args: diagram (ClassDiagram | PackageDiagram): The diagram that must be exported. config (argparse.Namespace): The configuration obtained from the Sphinx configuration file. Returns: The resulting :py:class:`UmlNode` instance. """ return cls.from_dot(cls.to_dot(diagram, config))
[docs] def guess_svg_basename(options: dict, code: str) -> str: """ Infers the svg filename of an UML diagram See ``/usr/lib/python3/dist-packages/sphinx/ext/graphviz.py``. Args: options (dict): The options passed to the :py:func:`graphviz.ext.render_dot` function. code (str): A graphviz string, passed to the :py:func:`graphviz.ext.render_dot` function. Returns: The corresponding SVG basename """ from hashlib import sha1 graphviz_dot = options.get("graphviz_dot", None) hashkey = (code + str(options) + str(graphviz_dot) + "()").encode() svg_basename = ( f'graphviz-{sha1(hashkey, usedforsecurity=False).hexdigest()}.svg' ) return svg_basename
[docs] class UMLGenerateDirective(SphinxDirective): """ UML directive to generate a pyreverse diagram """ # Sphinx stuff to control argument passing has_content = False required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True option_spec: ClassVar[OptionSpec] = { "caption": directives.unchanged, "classes": directives.flag, "packages": directives.flag, } def _build_args(self) -> list[str]: args = list() config = self.config if config.uml_filter_mode: assert config.uml_filter_mode args.extend(("--filter-mode", config.uml_filter_mode)) if config.uml_class: args.extend(("--class", config.uml_class)) if config.uml_show_ancestors: args.extend(("--show-ancestors", config.uml_show_ancestors)) if config.uml_all_ancestors: args.append("--all-ancestors") if config.uml_show_associated: args.extend(("--show-associated", config.uml_show_associated)) if config.uml_all_associated: args.append("--all-associated") if config.uml_show_builtin: args.append("--show-builtin") if config.uml_module_names: args.extend(("--module-names", config.uml_module_names)) if config.uml_only_classnames: args.append("--only-classnames") if config.uml_ignore: args.extend(("--ignore", config.uml_ignore)) if config.uml_colorized: args.append("--colorized") return args
[docs] def html_root_dir(self) -> Path: """ Crafts the HTML prefix to move from the current HTML to the HTML root directory. Returns: The corresponding relative :py:class:`Path` instance. Example: Assume that: - the documentation is built in: ``"~/git/sphinx-uml/docs"``; - the current document is; ``"~/git/sphinx-uml/docs/users/examples.rst"``; Then, the returned value is ``"../"``. As the HTML hierarchy follows the RST hierarchy, we use this prefix to setup our :py:class:`SphinxHtmlProxy`. """ doc = self.state.document env = doc.settings.env base_dir = Path(env.srcdir).absolute() cur_path = Path(doc.current_source) rel_path = str(cur_path.relative_to(base_dir).parent) if rel_path == ".": return Path(rel_path) n = len(rel_path.split("/")) return Path("/".join([".."] * n))
[docs] def run(self): """ To test this extension, as a developer: .. shell make install-sphinx-custom clean-doc docs To test this `pyreverse2`, called by this extension: .. shell pyreverse2 \\ --output svg \\ --project example.a \\ --sphinx-html-dir docs/_html \\ --output-directory docs/ \\ -m y \\ example.a """ # ..uml: module_name module_name = self.arguments[0] # :classes:, :packages:, :caption: with_classes = "classes" in self.options with_packages = "packages" in self.options caption = self.options.get("caption") pyprocess_args = self._build_args() + [ "--sphinx-html-dir", str(self.html_root_dir()), module_name ] # make install-sphinx-custom clean-doc docs from .pyreverse import Run, ParsePyreverseArgs parser = ParsePyreverseArgs(pyprocess_args) runner = Run(parser.config) diadefs = runner.diadefs(parser.remaining_args) # Craft the list of nodes to be appended to the doctree's AST. ret = list() for diagram in diadefs: # :classes: and :packages: switches if isinstance(diagram, PackageDiagram): if not with_packages: continue elif isinstance(diagram, ClassDiagram): if not with_classes: continue else: raise ValueError(f"Invalid type {type(diagram)}") # Build graphviz node code = UmlNode.to_dot(diagram, runner.config) node = UmlNode.from_dot(code) svg_basename = guess_svg_basename(node.attributes["options"], code) # Appends a link 'Open in a new tab' paragraph = nodes.paragraph(text="") paragraph += nodes.reference( # See the HTMLTranslator.visit_reference function in # /usr/lib/python3/dist-packages/docutils/writers/_html_base.py text="Open in a new tab", refuri=str( self.html_root_dir() / "_images" / svg_basename ), ) # Add caption if caption: node = figure_wrapper(self, node, caption) ret += [node, paragraph] return ret