Sphinx Extension

pyTooling.Documentation.Sphinx is a Sphinx extension providing the roles and directives pyTooling’s documentation uses - and any other project’s documentation can use. It is enabled in conf.py:

# doc/conf.py
extensions = [
  ...,
  "pyTooling.Documentation.Sphinx",
]

Attention

The extension needs Sphinx and docutils, which pyTooling itself does not depend on. Install it as pyTooling[sphinx], which requires Python 3.12 or newer.

Roles

The extension registers roles for styling inline text, one for inline Python code, and two for a line break and a horizontal rule - and a stylesheet for the styles, linked into every HTML page.

Style roles

Role

Renders

:bolditalic:

bold and italic

:underline:

underlined

:strike:

struck through

:xlarge:

extra large

:red:

red

:green:

green

:blue:

blue

:purple:

purple

:deletion:

deleted

:addition:

added

A :red:`warning` and a :deletion:`removed` word.

A style role puts CSS classes on the text; the stylesheet gives them their meaning. :deletion: and :addition: are the two a diff needs.

Every colour and size is a CSS custom property, so a project changes it without replacing the stylesheet - in a stylesheet of its own, listed in html_css_files:

:root {
  --pyTooling-color-red: #b00020;
}

The properties are --pyTooling-color-red, -green, -blue, -purple and --pyTooling-xlarge-size.

Inline Python code

:pycode: renders inline Python code, syntax-highlighted: isinstance(value, int).

:pycode:`isinstance(value, int)`

Line break and horizontal rule

|br| is a line break, |hr| a horizontal rule - in HTML and in LaTeX. They are substitutions, appended to rst_prolog, and delegate to the roles :br: and :hr:. |degree| writes a degree sign.

:param value: The first line. |br|
              The second line.

condensed-class

The condensed-class directive renders a class’ public interface as a code block: the class line, its class variables, its methods and its properties, each with the signature it is declared with, and ... for a body.

The source is parsed, not imported. Annotations appear as they are written - Nullable[str], not the Optional[str] an import resolves it to - members appear in the order of the file, and the file becomes a dependency of the page, so editing the class rebuilds the page.

Left out is what the surrounding text is for: bodies, doc-strings, private members (one leading underscore), and the annotated fields of a slotted class.

.. condensed-class:: pyTooling.Stopwatch.Stopwatch
   :members: Methods, Properties

This is how the example renders:

@export
class Stopwatch(SlottedObject):

  def Start(self) -> None:
    ...

  def Split(self) -> float:
    ...

  def Pause(self) -> float:
    ...

  def Resume(self) -> float:
    ...

  def Stop(self) -> float:
    ...

  @property
  def Digits(self) -> int:
    ...

  @Digits.setter
  def Digits(self, digits: int) -> None:
    ...

  @readonly
  def Name(self) -> Nullable[str]:
    ...

  @readonly
  def IsStarted(self) -> bool:
    ...

  @readonly
  def IsRunning(self) -> bool:
    ...

  @readonly
  def IsPaused(self) -> bool:
    ...

  @readonly
  def IsStopped(self) -> bool:
    ...

  @readonly
  def StartTime(self) -> Nullable[datetime]:
    ...

  @readonly
  def StopTime(self) -> Nullable[datetime]:
    ...

  @readonly
  def HasSplitTimes(self) -> bool:
    ...

  @readonly
  def SplitCount(self) -> int:
    ...

  @readonly
  def ActiveCount(self) -> int:
    ...

  @readonly
  def InactiveCount(self) -> int:
    ...

  @readonly
  def Activity(self) -> float:
    ...

  @readonly
  def Inactivity(self) -> float:
    ...

  @readonly
  def Duration(self) -> float:
    ...

  @readonly
  def DurationInNanoseconds(self) -> int:
    ...

  @readonly
  def Exclude(self) -> ExcludeContextManager:
    ...
.. condensed-class:: <dotted name of a class>

Renders the interface of the class the argument names. The longest prefix of the name that is a module is the module; the rest is the class, and may name a class nested in a class.

:members: <kinds>

The kinds of member to render, separated by commas, in any case: ClassVariables, Dunders, Methods, Properties or All. Default: All.

A property is a method decorated with @property, @readonly, @cached_property or as a setter or deleter; a dunder is a method named __<name>__.

:exclude-members: <names>

Names of methods not to render, separated by commas.

:indent: <columns>

Width of one indentation level. Default: 2.

:width: <columns>

Column a signature is wrapped at, one parameter per line. Default: 100.

:caption: <text>

A caption for the code block.

dependency-table

The dependency-table directive renders a project’s dependencies as a table - per package the version required, its license and what it requires in turn - from the requirements themselves rather than by hand. The data is fetched from the package index while the documentation is built.

Attention

The directive needs the pypi extra as well: pyTooling[sphinx,pypi].

Configuration

A table names an entrypoint, which conf.py declares:

# doc/conf.py
pyTooling_Dependency_Requirements = {
  "package":       {"file":    "../requirements.txt"},
  "documentation": {"file":    "requirements.txt"},
  "yaml":          {"package": "pyTooling[yaml]"},
}

Each entrypoint states exactly one of:

file / files

→ a requirements file, or several, read relative to conf.py with their -r includes followed. They are read while conf.py is processed, so a path that doesn’t exist ends the build with one message.

package / packages

→ a package, or several, as the package index publishes its latest release: pyTooling for its own requirements, pyTooling[yaml] for what the extra yaml adds.

Configuration values in conf.py

Name

Value

pyTooling_Dependency_Requirements

The entrypoints, by identifier.

pyTooling_Dependency_PackageOverrides

Optional, a YAML file stating the license of a package the index can’t answer for, relative to conf.py.

pyTooling_Dependency_IndexURL

Optional, the package index. Default: https://pypi.org.

pyTooling_Dependency_APIURL

Optional, the index’s JSON API. Default: https://pypi.org/pypi/.

The tables of a build share one view of the index, so a package several tables require is downloaded once. Each table logs what it cost, and the build ends with the total and the packages whose license couldn’t be resolved - the list the override file answers.

.. dependency-table:: <entrypoint>

Renders the dependencies of the entrypoint the argument names.

:depth: <levels>

Levels of sub-dependencies to expand. Default: 0, which expands until the tree ends.

:simplified-versions: yes | no

Whether a version constraint is reduced to its lower bound: ≥9.1 instead of ≥9.1, <10. Default: yes.

:version-format: Major | MajorMinor | MajorMinorPatch | All

How many parts of a version number are printed. Default: MajorMinor.

:dependency-format: Package | PackageVersion | PackageLicense | PackageVersionLicense

What a line of a dependency tree states. Default: PackageVersionLicense.

:caption: <text>

A caption for the table.

.. dependency-table:: package
   :caption: Mandatory dependencies of the pyTooling package.
   :depth: 4

Dependencies shows the tables pyTooling’s own documentation renders.

xsd-graph

The xsd-graph directive draws an XML schema as a Graphviz graph, from the schema file itself:

  • every complex type is a record of its name, its attributes, and its simple-typed child elements with their cardinality;

  • every complex-typed child element is an edge, labelled with its name and cardinality - so containment and recursion are edges rather than repeated type names;

  • an enumeration is a node of its own, listing its values;

  • a root element is a double circle.

The schema is read with xmlschema, part of the sphinx extra, and drawn by sphinx.ext.graphviz, which the extension sets up itself. The schema file becomes a dependency of the page.

.. xsd-graph:: ../../pyTooling/Resources/TestReport-v0.1.xsd
   :caption: The types of TestReport-v0.1.xsd.
.. xsd-graph:: <path of an XML schema>

Draws the schema the argument names, relative to the document.

:caption: <text>

A caption under the graph.

Overview shows the graphs of the schemas pyTooling ships.

Another schema language

SchemaGraph is the directive’s language-neutral base-class, and DotGraph assembles the graph. A directive for another schema language derives from the base-class, names itself, and overrides _RenderGraph():

class JSONSchemaGraph(SchemaGraph):
  directiveName: str = "json-schema-graph"

  @classmethod
  def _RenderGraph(cls, schemaFile: Path) -> str:
    graph = DotGraph()
    ...
    return str(graph)

shields

The shields directive renders a project’s badges from shields.io - where the project lives, how it is licensed, whether it builds, where it is published.

The options state the project’s coordinates: its GitHub repository, its PyPI package, its licenses, its workflow and its documentation’s URL. The content names the badges and is the layout: badges appear in the order written, and each line is a row.

A badge needs only the options it is made of, so a project states what its badges use and nothing else.

.. shields::
   :github:                pyTooling/pyTooling
   :pypi:                  pyTooling
   :codacy:                08ef744c0b70490289712b02a7a4cebe
   :source-license:        github:LICENSE.md
   :documentation-license: CC-BY-4.0 github:doc/Doc-License.rst
   :github-action:         Pipeline.yml@main
   :documentation:         github-pages

   github, src-license, ghp-doc, doc-license
   pypi-tag, pypi-status, pypi-python
   github-action, lib-status, codacy-quality, codacy-coverage, codecov-coverage

This is how the example renders:

Sourcecode on GitHubCode licenseDocumentation - Read Now!Documentation License
PyPI - TagPyPI - StatusPyPI - Python Version
GitHub Workflow - Build and Test StatusLibraries.io status for latest releaseCodacy - QualityCodacy - Line CoverageCodecov - Branch Coverage

Options

.. shields::

Renders a project’s badges, in rows. Each line of the content is a row of badge identifiers, separated by commas.

:github: <organization>/<repository>

The GitHub repository, for the badges showing it and for github: links.

:pypi: <package>

The package’s name on PyPI.

:codacy: <project ID>

The Codacy project ID, as shown in Codacy’s badge settings.

:gitter: <room>

The Gitter room, e.g. hdl/community.

:source-license: [<SPDX expression> ]<link>

Where the source code’s license is written. Without a license before the link, the badge shows what PyPI reports for the package when shields:pypi is stated, and what GitHub reports for the repository otherwise.

:documentation-license: <SPDX expression> <link>

The documentation’s license and where it is written. The license is required: nothing reports a documentation’s license.

:github-action: <workflow file>[@<branch>]

The workflow whose status is shown. Without a branch, the badge shows the workflow’s latest run on any branch.

:documentation: github-pages | <URL>

Where the documentation is published. github-pages is the GitHub Pages site of shields:github; any other value is an http:// or https:// URL.

:class: <CSS classes>

Additional CSS classes on the rows.

Links

shields:source-license and shields:documentation-license end in a link, which is one of:

github:<path>

→ the file at <path> on the default branch of the repository shields:github names.

http://… or https://…

→ used as written.

Licenses

A license is an SPDX license expression, parsed by LicenseExpression.Parse(): Apache-2.0, CC-BY-4.0 or MIT OR Apache-2.0. A misspelt identifier is reported. A license that isn’t on the SPDX License List is written LicenseRef-<name>.

Badges

Identifier

Shows

Options

github

the repository

github

src-license

the source code’s license

source-license

doc-license

the documentation’s license

documentation-license

ghp-doc

whether the documentation is online

documentation

tag

the latest tag, including pre-releases

github

date

the date of the latest release

github

github-action

the workflow’s status

github, github-action

codacy-quality

Codacy’s code quality grade

github, codacy

codacy-coverage

Codacy’s line coverage

github, codacy

codecov-coverage

Codecov’s branch coverage

github

pypi-tag

the latest version on PyPI

pypi

pypi-status

the development status on PyPI

pypi

pypi-python

the Python versions on PyPI

pypi

lib-status

whether the dependencies are up to date, by Libraries.io

pypi

lib-rank

the SourceRank, by Libraries.io

pypi

lib-dep

how many repositories depend on the package

github, pypi

gitter

a link to the Gitter room

gitter

A github: link, a license GitHub reports, and github-pages need shields:github as well.

HTML and LaTeX

The directive emits both variants, each wrapped in an only node: HTML embeds the SVG from img.shields.io, LaTeX the PNG from raster.shields.io - a PDF cannot embed an SVG.

Errors

A mistake is reported on the page, where the badges would be, and in the build’s log:

shields: 'gha-test' is not a known badge. Known are: codacy-coverage, codacy-quality, codecov-coverage, ...
shields: Badge 'pypi-tag' needs option ':pypi:'.
shields: Option ':github:' is 'pyTooling', not '<organization>/<repository>'.
shields: Option ':source-license:' links to 'LICENSE.md', neither 'github:<path>' nor a URL.
shields: Option ':documentation-license:' states 'CC-BY-5.0', which isn't an SPDX license expression.