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.
Documentation Links#
A few tracked files link into the documentation by absolute URL, because they are
outside the Sphinx project and have no role to write instead: README.md, the
repository’s landing page, and a script that sends a contributor to the page
explaining why it failed them. Such a link is invisible to everything that checks
the rest — nitpicky sees only the references the build resolved.
Write the URL as https://tephpy.readthedocs.io/en/latest/<page>.html,
optionally with a fragment. A per-pull-request preview host
(tephpy--<pr>.org.readthedocs.build) is where a documentation change is
verified rather than where a link belongs — Read the Docs deletes the preview when
the pull request closes — and latest is the only version published, so
en/stable and a path that drops the version alike resolve nowhere. A URL whose
path never reaches .html names no page the gate can look up, so it is passed
over rather than judged — as the Read the Docs badge at the top of the README is,
pointing at the base with a query string and no path.
In README.md, write the link as a Markdown reference — [CAPE][cape] in the
prose, with the target defined in the block at the foot of the file — so the prose
stays readable and each URL is stated once:
[cape]: https://tephpy.readthedocs.io/en/latest/reference/glossary.html#term-CAPE
Link the first mention of a glossary term in the README and no more, as on a
documentation page. Take the fragment from the built page rather than deriving it:
a glossary anchor is term- followed by the term with its case preserved and
each run of non-alphanumeric characters collapsed to a single hyphen, so CAPE
gives term-CAPE and Normand's point gives term-Normand-s-point. Label
the reference in lower case — Markdown labels are case-insensitive, and a lowercase
label is hard to mistake for the fragment, which is not.
The documentation build checks these links. check_documentation_links.py reads
each URL out of every file named in its SOURCES constant and looks it up in the
HTML just built, failing when a URL naming a page is written some other way, when
the page is absent, or when the fragment names no id. Renaming a glossary term
or moving a page therefore fails the build, rather than leaving a link pointing
into a 404 that nobody notices.
A new file that writes such a URL is checked only once it is added to SOURCES;
the gate reads that list and not the repository, so that a URL quoted in a test
fixture or frozen into an implementation plan is left alone. A file that stops
carrying a documentation link fails the gate rather than dropping out of it in
silence, so removing the last link means removing the entry too — and emptying
SOURCES entirely fails the same way, rather than passing on a search of
nothing.
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.
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
Attributessection 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.