Documentation Style#

Title Style#

Hand-authored page and section titles use Chicago Manual of Style headline style: capitalize the first and last words and all major words; lowercase articles, coordinating conjunctions, prepositions, and the infinitive “to”.

Preserve literal case for: code and API identifiers, filenames, config keys, CLI commands, and paths; project and library names in their own casing (matplotlib, numpy, pint, metpy, pixi, tephpy); and acronyms and scientific symbols (CAPE, CIN, LCL, WMO, SPEC 0). The rule does not apply to autoapi-generated API pages, numpydoc section headers, changelog entries, or anything that is a full sentence (captions, admonition text, docstring summaries), which use sentence case. Bibliography entries reproduce the source’s published title.

Glossary#

The glossary is written for software engineers, not meteorologists. Each entry gives the concept in one plain sentence, then how it appears in tephpy (the data, its units, the API type that carries it), and links deeper physics to the Explanation quadrant.

Cross-reference the first mention of a glossary term per page with :term:, in narrative prose only — never in titles, code blocks, API signatures, or admonition labels. Within a definition, link related terms but never the term itself. Keep one canonical spelling per concept.

When a definition names a documented API, cross-reference it with the matching Sphinx domain role — :class:, :func:, :meth:, :mod:, or :obj: — so the reader can follow the link straight into the API documentation, rather than quoting the name as a plain double-backtick literal. Keep the accessor idiom the entry reads in as the link’s display text, so calc.parcel_path and ax.shade_cape stay legible:

avoid:   ``calc.parcel_path(...)`` computes a parcel's ascent
prefer:  :func:`calc.parcel_path(...) <tephpy.calc.parcel_path>` computes a parcel's ascent

Third-party objects (matplotlib, metpy, numpy, pandas, pint, xarray) resolve the same way through intersphinx — see Cross-References below. Reserve plain double-backtick literals for names with no documentation target: private members, dataclass fields already reachable through their linked owner, external tools without an intersphinx inventory (pixi), option strings, and keyword arguments. This mirrors the changelog fragment convention documented in changelog/README.md.

Cross-References#

Third-party APIs resolve through intersphinx. Every third-party package the documentation names — matplotlib, metpy, numpy, pandas, pint, xarray — has its inventory registered in intersphinx_mapping in docs/src/conf.py, so a domain role such as metpy.calc.moist_lapse() or pint.Quantity links straight into that project’s documentation. When you first cite an API from a package that is not yet mapped, add its objects.inv location there in the same change, so the reference resolves rather than rendering as plain text.

Parameter and return types are cross-referenced automatically. With numpydoc_xref_param_type enabled, numpydoc turns each type in a Parameters/Returns block into a link: fully-qualified names (pint.Quantity, numpy.ndarray) resolve through intersphinx, tephpy’s own short type names (Sounding, Profile, the Tephpy*Error exceptions) are mapped to their targets in numpydoc_xref_aliases, and descriptive connective words (optional, of) are listed in numpydoc_xref_ignore. Write the type as its plain qualified name — pint.Quantity, not a hand-written role — and let the configuration link it.

nitpicky is enabled, so an unresolved cross-reference is a warning and — via the docs Makefile’s --fail-on-warning — fails the build. A reference that does not resolve is therefore caught automatically; a clean build is proof the links land. The only sanctioned exceptions live in nitpick_ignore in conf.py: annotation types autoapi emits as py:class xrefs while numpy publishes them as py:data/py:attribute (numpy.typing.ArrayLike, numpy.typing.NDArray, numpy.float64), the Ellipsis in variadic tuples, and parameter defaults from the private _constants module. Do not extend that list to silence a reference you can instead make resolve — add a numpydoc_xref_aliases entry or write the full dotted name.

Specification Citations#

Cite a design specification as plain text — spec §3.2, logo spec §1, docs spec §3.6 — and never as a hand-written role. The build turns each one into a link to the section it names, so writing the role yourself is not an improvement but a hazard: a role carries a second string that can disagree with its display text, and

:ref:`spec §3.2 <logo-spec-3-2>`

has the right text against the wrong document while resolving perfectly cleanly, so neither the citation checker nor a nitpicky build has anything to object to. Writing the citation once means the text and the target cannot disagree.

The prefix names the document and is load-bearing. A bare §N means this document’s §N, which makes it safe inside a specification and an error anywhere else — a docstring owns no sections. Where several sections are cited together, the prefix carries across the run, so spec §3.3, §10 and spec §3.1/§10 each name two sections of the parent specification. The run continues across a comma or a solidus, and across nothing else: writing and in place of either separator ends it, leaving the second citation bare. A bare §N opening the next sentence falls back to the containing document rather than inheriting, for the same reason.

A citation must also sit whole on one line, and so must a compound run — one wrapping after its comma or solidus strands the continuation, which falls back to the containing document instead of inheriting the prefix it was written under. Only horizontal whitespace joins a prefix to its section number, and the same holds of the gap after a separator, so a prefix stranded at the end of a line with its number wrapped onto the next is no longer part of the citation: what remains is a bare §N, rejected outside a specification and read as a local reference inside one — either way, not the citation that was written. The rule is what keeps the displayed text and the link target from disagreeing, because the hook reads one line at a time while the build reads a whole paragraph, and a citation able to span the wrap is one they can read differently.

Cite a section in body prose. Four other places will not carry a citation, and each fails the documentation build rather than rendering wrongly. A page title is linked like any other heading, but Sphinx copies the title into <title> with the markup stripped, and the theme repeats it in the breadcrumb without the anchor, so the citation reaches the reader as plain text in the browser tab and above the page. A toctree :caption: is a directive option rather than text the build can rewrite, and renders twice — once where the toctree sits and once in the sidebar. A .. raw:: html block and an API signature — a parameter’s default value included — are left alone deliberately, because the build rewrites neither raw output nor code. Name the section in the surrounding prose instead.

A section heading is worth avoiding for a second reason, and it is the one the build names. The theme rebuilds its “On this page” navigation out of the headings, keeping the text and dropping the anchor, and wraps the copy in the navigation’s own link — so the citation is a link, to the section it sits in rather than to the section it names, and the check on the built HTML cannot tell one anchor from another. The build therefore warns about a citation inside a heading, naming the heading, and --fail-on-warning turns that into a failure. Writing the link yourself does not avoid it: the navigation strips an author’s link the same way, so a heading citation is reported whether the build would link it or you already have. Cite the section in the prose below the heading instead.

A heading is worth avoiding for a third reason, which fails differently again. A .. contents:: directive links every heading it lists — in its own list and in the heading itself — and it does so after the citation has already become a link, so the page ends up with one anchor inside another. That is invalid HTML, which a browser restructures silently, and Sphinx reports nothing: only the check on the built HTML notices. Writing a citation inside a link yourself is the same collision from the other side, and in body prose it is not an error — the build leaves such a citation as plain text rather than nesting a link in a link, and your own link is the one the reader follows. In a heading it is reported, for the reason above.

A pre-commit hook checks that every citation names an anchor that exists, and the documentation build checks that every rendered citation became a link. Both are specified in the published specifications design: docs spec §3.6 covers the hook, and docs spec §3.7 covers the build.

GitHub References#

Refer to a tephpy issue or pull request with the matching extlink role, never as plain text and never by URL. Write :issue:`65` and :pull:`73` in reStructuredText and in docstrings; write {issue}`65` and {pull}`73` in the Markdown specifications. Each renders as a linked #65 or #73.

This is the opposite instruction to the one above, and the same reasoning decides both. An extlink generates its caption from its value, so there is one string and the text cannot disagree with the target; a hand-written :ref: carries two. Writing the role is what keeps them together here, and what would pull them apart there.

Keep the word that says which kind it is. PR :pull:`19` renders PR #19, and a reader who sees only #19 cannot tell what the link opens, because the caption is the same for both roles.

Two things follow. An issue in another project has no role — the two above are scoped to this repository — so write it as an ordinary link with its own URL. And a hexadecimal colour is not a reference: keep it in literal markup — #808080 — or inside a string, which is where a colour belongs anyway.

A pre-commit hook rejects both a bare #65 and a hand-written https://github.com/bjlittle/tephpy/issues/65; the documentation build rejects the second again through extlinks_detect_hardcoded_links, naming the role to write instead. Neither can tell :issue: from :pull: — GitHub redirects between them, so the wrong one of the two still reaches the right page. The rule is specified in docs spec §3.8.

Code Examples#

Every python block in the how-to, tutorial and explanation quadrants is executed by tests/test_docs_snippets.py, as one script per page and in document order, ending with a draw of every figure the page leaves open. Four rules follow from that, and the gate itself is specified in docs spec §3.9.

A page is a session, not a catalogue. A later block may rely on a name an earlier one bound — add_logo() with no argument brands the figure the block above it created — so the blocks of a page cannot be reordered freely, and a block that would not run after the ones above it is a page defect rather than a gate problem.

There is no way to mark a block as not for execution. A block a reader is invited to copy and which cannot run is the defect; the answer is to fix the snippet, or to stop presenting it as one. A REPL transcript is code too — write it as a script in a python block rather than as pycon, which the gate reports rather than skips.

Snippets carry no linter directives. # noqa and # type: ignore suppress nothing in a .rst file, and they ask a reader pasting the line to satisfy tooling they are not running. Where an import looks unused, say why it is there instead — import tephpy  # registers the "tephigram" projection.

Where a snippet’s surrounding prose makes a behavioural promise, a test pins the promise. Execution and truth fail independently: #113 fixed a passage whose snippet ran perfectly and whose prose was wrong, and the gate would have passed it. Name the test in the pull request that adds the prose, so the connection is on the record.

Published Figures#

A user page either publishes figures or it does not, and the two forms never mix. On a page that does, every python block is a .. plot::, which renders the block and shows its source. Leaving one plain code-block:: python behind is the defect the rule exists to stop: that block runs in the snippet gate and not in the documentation build, so the build’s namespace silently loses whatever it bound. The rules below are specified in plots spec §3.2 and asserted by tests/test_docs_snippets.py; the images themselves are pinned against docs/baseline by .github/scripts/check_docs_figures.py (plots spec §3.5).

One picture per point the prose makes, not one per block. A page is a session in which a later block supersedes an earlier one — three blocks of Emphasise a Reference Isopleth call ax.isotherms(...) on the same axes — so a picture after every block would sometimes show a state the surrounding prose has stopped describing. A section making two distinct points publishes two: Emphasise a Reference Isopleth’s “Configure It Once” section shows the context-manager idiom and then a diagram opting out of it, and Add the tephpy Logo’s “On the Plot or Around It” section shows the axes-and-figure targeting and then the no-argument call that brands whichever figure is current.

Each block carries its options by five rules:

  • The first block on the page carries :context: reset. Without it the page opens with whatever the previously built page left behind, and build order is not a property any page controls.

  • Every later block carries :context: or :context: close-figs. A block with no :context: at all runs in a fresh namespace, where the page’s imports never happened; close-figs is what opens a section that starts its own figure. The two values do not combine — the directive takes exactly one of nothing, reset or close-figs.

  • A block whose picture would add nothing, or should not be published, carries :nofigs:. It still runs, so the session is unbroken and the snippet gate still covers it. That is why a plain code-block:: python is not the answer for such a block.

  • Every figure-producing block carries a :filename-prefix:, unique across the documentation. Unnamed, the image takes a per-document counter, so inserting a section renumbers every image after it and every baseline with them. A name and a :nofigs: on the same block is a figure declared and never built, which the figure gate reports.

  • No file-argument form. .. plot:: script.py renders the figure from a file, and the code a reader is invited to copy has to be on the page.

Nothing a published block does may outlive it. Every block on every page executes in the Sphinx process, with sys.modules shared across the whole build, and :context: reset clears the namespace the blocks run in without touching module state. So demonstrate configuration with tephpy.config.context() rather than by assigning to tephpy.config: a bare assignment applies to every axes created afterwards, on that page and on every page built after it. A page whose subject is global, persistent configuration publishes no figures — Configure tephpy From a File is that page (plots spec §3.3).

When a figure is meant to change, re-bless it in the same change that caused it:

$ pixi run docs-figures

Read the diff before committing. That command approves whatever was rendered, a regression as readily as a correction.

Attribute Documentation#

The API reference is generated by sphinx-autoapi, which parses the source statically and therefore never reads comments. A Sphinx #: doc-comment — whether on the line above an assignment or trailing it inline — is silently dropped from the rendered page; only sphinx.ext.autodoc (which imports the module) honours #:. Document a rendered attribute one of two ways instead:

  • Prefer the numpydoc Attributes section of the owning class’s docstring. This is the established pattern for tephpy’s public dataclasses (Sounding, Profile, SoundingIndices) and keeps every field’s description in one place alongside its type.

  • When an attribute must carry its documentation at the point of definition, use a PEP 224 attribute docstring: a triple-quoted string on the line below the assignment. autoapi renders it; a #: comment in the same spot renders nothing.

Reserve #: comments for annotations on private members — the private _constants and _config modules, and _-prefixed module constants — which autoapi excludes from the reference regardless of comment style. There the choice is purely stylistic, and #: reads naturally above a constant.