Documentation Style#
Estimated reading time: 49 minutes
Title Style#
Hand-authored page and section titles use Chicago Manual of Style headline style with one deliberate deviation: capitalize the first and last words, all major words, and prepositions of four letters or more; lowercase articles, coordinating conjunctions, the infinitive “to”, and prepositions of three letters or fewer.
The deviation is the length threshold. CMOS lowercases a preposition however
long it is, and tephpy capitalizes the longer ones —
Read a Sounding From an Archive’s “Read a Sounding From an Archive”,
Work With Units’s “Work With Units”, Add the tephpy Logo’s “On the Plot or
Around It” — while keeping the short ones down, as in
Frame the View’s “Fix the View by Ranges”. The rule is written this way
because it is what the corpus already did. Every title carrying a medial
preposition of four letters or more capitalized it bar one, and every title
carrying a shorter one lowercased it bar an adverbial particle that is not a
preposition at all (#217). Nothing had ever enforced the stricter
reading, so the rule is matched to the pages rather than imposed on them.
A preposition doing another job keeps that job’s case. In
Work With Units’s “Quantities Go In Directly”, In is an adverbial
particle of “go in” rather than a preposition, and CMOS capitalizes it whatever
its length.
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. A title is a claim like any other, and Reviewing Claims is the review that reads it as one.
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. The
documentation build is fail-on-warning, so a :term: whose entry does not
exist yet breaks it: a pull request that reaches for a new term adds the entry
in the same change.
The first-mention half is checked by .github/scripts/check_glossary_links.py,
a pre-commit hook over the user quadrants. It reads the glossary into alias
groups, so linking CAPE satisfies convective available potential energy
too, and it reports the line rather than only the page. What it deliberately
passes over is what this rule already excludes — titles, directive bodies and
options, role targets, inline literals — and emphasis spans besides, because a
page quoting another page’s title in italics is citing it rather than using the
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 Fragments.
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. Where no test can pin the promise — a claim about an external tool, or one that covers a set rather than a case — Reviewing Claims carries the questions that stand in for one.
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-figsis what opens a section that starts its own figure. The two values do not combine — the directive takes exactly one of nothing,resetorclose-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 plaincode-block:: pythonis 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. Spell the option in lowercase, indent it with spaces, and keep the value to one run of letters, digits, dots and dashes: Sphinx accepts more than that, and the gate reports anything else as a near miss rather than reading it as a declaration.No file-argument form.
.. plot:: script.pyrenders 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.
It refuses outright, without touching a file, when the gate cannot read a declaration — a near miss as above, or a page the gate expects to publish that has stopped declaring anything. Fix the declaration and run it again. The baseline that declaration named is a live pin, and to a scan that cannot read the declaration it looks exactly like the orphan of a renamed section, which this command removes.
Reading Time#
Every page a reader reads start to finish opens with a reading-time banner, placed after the title and before the first section:
.. readingtime::
In a specification, which is Markdown, it is the equivalent fence.
The estimate is counted from the parsed page rather than from its source, so directive options, comment blocks and front matter are not counted as words and a body another directive generated is. The rate is 150 words per minute [Bry19] [Car82] — below the 175 wpm floor Brysbaert reports for ordinary non-fiction prose, because these pages alternate argument with code the reader parses line by line.
Two per-page overrides exist, and both are deliberate marks in the source rather than silent adjustments. A literal duration is quoted instead of counted:
.. readingtime:: 45
and a rate replaces the default for that page alone:
.. readingtime:: 200wpm
Anything else is a build error. There is no way to spell an argument the directive half-understands.
A page carries exactly one banner. Two is a copy-paste, not a decision, and
test_no_page_carries_more_than_one_reading_time fails the build over it.
A page that is navigated rather than read carries no banner: the four Diátaxis
landing pages, the developer and specification indexes, the site root, the
glossary, the topic index, the four reference pages whose body a directive
generates, and a reference page whose body includes another page that already
carries the banner. These are named in EXEMPT in tests/test_docs_readingtime.py, with the reason
beside each, and test_every_page_a_reader_reads_carries_a_reading_time
fails for any other page that omits one. Adding a page means adding the banner
or adding the reason.
Landing Pages#
A section landing page is navigation rather than prose, which is the rule above read
forwards: it carries no reading-time banner because nobody reads it through. Six
sections take one of two shapes, and both end in a hidden toctree.
A table, in getting started, the tutorials, how-to and
explanation quadrants, and this developer guide. The page carries, in order, an
introduction, one two-column list-table, and the toctree.
The introduction says what the section is for, who it assumes the reader is, what it guarantees of every page in it, and where to go if this is the wrong section. It says nothing about an individual page. A paragraph that summarises the section page by page is a list that has to track a directory, and the how-to page’s grew from six clauses to nine by hand before this rule existed.
Each row is a :doc: link against one sentence. The sentence is editorial rather
than descriptive: it is there so a reader can tell this page from its siblings, and
it is deliberately not the page’s opening line, which a hover already shows. Write
:widths: auto and no header row, which is the shape the API reference’s own
summary tables already take.
A grid of cards, in the reference quadrant, whose pages are looked up by name
rather than chosen between. The page carries, in order, a one-sentence
introduction, a .. grid:: 1 2 2 2 of cards, the guidance paragraphs, and the
toctree. The API card comes first and spans the row with :columns: 12. One
column below 576px is not a matter of taste: two columns at that width split words
mid-word, which narrative spec §3.9 records measuring.
Each card takes its page’s title — the API card excepted, because that page is
titled by the package name — a light and a dark icon from
_static/cards/reference/, and one sentence that tells it from the card beside
it. A card raises no hover tooltip, so its sentence may say what the page opens
with where that is clearest. Draw an icon in the root page’s vocabulary; its dark
file differs from the light one only in the navy, #8FB8E8 for #1B3A6B, and
the knock-out halo, #14181e for #FFFFFF.
The index and the toctree carry the same pages in the same order, and it is the
order a reader needs rather than the alphabet. Hiding a toctree hides it from the
page body only: the sidebar, the breadcrumb and the previous and next footer all
read its order. tests/test_docs_landing_pages.py fails when the two disagree,
in membership or in order; when an entry links outside its section; when the index
omits a page the section holds, which an :orphan: page would otherwise do
silently, since the build’s own toctree check never sees one; and when the toctree
is not hidden, which would publish the same list twice. It reads a row’s first
cell and a card’s :link:, and derives the API card’s page from conf.py,
since that page exists only while a build runs.
Glossary terms stay out of the cells and the cards. Both are directives, and
the first-mention rule already passes over a directive’s
body, so a :term: in either neither satisfies that rule nor breaks it. Write
first mentions in the prose, and let a cell or a card take the plain word.
Gallery Examples#
The gallery is scraped from src/tephpy/examples, which ships in the wheel:
every entry is a module a reader can download, and also one an installed tephpy
can run with tephpy examples run <name>. The rules below are specified in
gallery spec §3.2, §3.3, §3.5, §3.6. Every one a test can read off a file — the
registry, the main() shape and its guard, the figure size, the tag
vocabulary and how many tags — is asserted by
tests/examples/test_examples.py. What belongs in the gallery at all, and
that an example reaches no network and writes no file, are left to review.
The gallery shows what the package draws. Everything else is a how-to. An example whose subject is not a picture — getting data in, configuring the package, installing it — belongs in the how-to quadrant, however much code it carries (gallery spec §5). An example that happens to load data is fine; the subject is what is tested, not the API surface touched.
Every module is named plot_*.py, and the prefix is load-bearing.
sphinx-gallery’s filename_pattern defaults to /plot, and only a matching
file is executed: a file outside the pattern is still rendered, silently, with
no figure and no error.
Every module defines main(), which builds the figure and returns it, and
closes with the guard that shows it:
def main() -> Figure:
...
return fig
if __name__ == "__main__":
main()
plt.show()
One construction then serves four consumers — sphinx-gallery, which executes the
file as __main__; tephpy examples run; pytest-mpl, which decorates a
function returning a figure; and the reader running the downloaded script.
Showing inside main would cost the third of those, and the pinned figure
would then be a claim about the test rather than about what was published.
An example takes any data it needs from tephpy.samples, reaches no
network, and writes no file. The documentation build executes it, so a
savefig call would leave an artefact in the generated tree on every build;
the vector-output line appears in an example’s prose instead, shown and not
run.
Add a new example to REGISTRY in src/tephpy/examples/__init__.py, in the
position it should occupy. Registry order is gallery order is
examples run --all order, and the tests read it: an unregistered
plot_*.py fails them rather than disappearing quietly. Pass
figsize=(8.0, 4.0) at the example’s own subplots or figure call —
sphinx-gallery calls plt.rcdefaults() before every example, so a configured
default is discarded before the first line runs.
Tags come from a closed vocabulary of seventeen terms, shared with the site-wide topic index, two to four per example, declared in the flag sphinx-gallery reads:
# sphinx_gallery_tags = ["analysis", "shading", "indices", "sounding"]
Each term is defined by what it covers and what it excludes in topics spec §3.3,
and that table is the authority when two people would tag a page differently — the
gate cannot see a disagreement, because both spellings are legal. They render on
the page and drive the index’s filter buttons, so a barb beside a barbs
splits the very index the feature exists to build. Widening the vocabulary means
adding the term to VOCABULARY in docs/src/_ext/tephpy_topics_data.py and
its definition to that table, together. Spell the flag exactly: sphinx-gallery
parses sphinx_gallery_tag into a differently-keyed entry and discards it in
silence, with no warning to fail the build on — which is why the test reads the
flag out of the source text rather than asking the parser.
Write the flag flush under the imports, with no blank line above it — the
removal preserves the blank lines around what it strips, so a blank line there
is one the reader is left looking at, three before def main where PEP 8
wrote two. tests/examples reproduces the removal and asserts the spacing it
leaves, so any flag written the other way fails there.
The flag itself does not reach the page.
remove_config_comments strips # sphinx_gallery_* lines from the rendered
code once sphinx-gallery has read them, so the tags still render and still drive
the index filter, and the script the reader downloads still carries the flag.
Nothing else is hidden — the pattern matches sphinx-gallery’s own flags and no
other comment, which is why this replaced sphinx_gallery_start_ignore, the
mechanism that would have hidden source along with it (gallery spec §3.6).
A module’s docstring is not a docstring — it is the page’s title and opening
prose — so the glossary rule applies to it in full:
cross-reference the first mention of a term per example with :term:, and
seed the entry in the same pull request when the term is new. The # %%
block comments and main()’s own numpydoc are code and documentation of
code, and take no :term:.
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.
API Version Stamps#
Every published API object records the version it arrived in, as a numpydoc
Notes section carrying the Sphinx versionadded directive. It is the
docstring’s last section, because numpydoc’s section order places Notes
after Raises. Sphinx would render the directive anywhere in the docstring;
the gate accepts it only inside Notes, so the form stays one form:
Notes
-----
.. versionadded:: 0.1.0
“Published” means what the API reference publishes: every module under
src/tephpy with no underscore-prefixed path component except the gallery,
and the objects those modules define. Attributes are outside it — a dataclass
field is documented in its class’s Attributes section, so it has nowhere of
its own to carry a directive — and so is the gallery, whose module docstring is
sphinx-gallery’s rendered title block rather than a numpydoc docstring.
Write versionadded, not Sphinx 9’s version-added. The two are the same
directive and render identically, but the documentation floor is
sphinx>=8.0, where the hyphenated spelling does not exist; numpydoc’s
GL10 two-colon check does not fire for it either, because it is not in
numpydoc’s directive list.
.github/scripts/check_api_docstrings.py is the gate. Run it directly to see
what is missing; tests/test_api_docstrings.py is what enforces it, and
check_api_inventory.py holds its idea of the published surface to the
inventory a real build writes. It is a test rather than a pre-commit hook
because it imports tephpy: the other local hooks are pure-stdlib text
scanners and run in the isolated environment pre-commit builds, whereas
declaring this one’s additional_dependencies would restate
requirements/pypi-core.txt and drift from it.
The same gate checks that a published object documents what it raises. The
rule is deliberately narrow: only an exception raised in the object’s own body,
with no propagation through the calls it makes. A propagating analysis cannot
follow dynamic dispatch, cannot see an exception a third party raises —
datetime.fromisoformat’s ValueError is correctly documented and
invisible to it — and reports internal guards no caller can reach. Each of
those is a false positive, and a gate that cries wolf gets switched off. So
documented but not found is never an error; only the missing direction is
checked.
A class is read through __post_init__ or __init__ rather than its own
body, because that is where a dataclass validates and the class docstring is
the only one the API reference shows. Documenting the raise on the private
validator instead would put it where no reader looks.
Raises entries are listed alphabetically, which the gate also checks.
The rule earns its place where one failure is reachable from many entry
points: thirteen docstrings across plotting restate the same
configuration-driven raise set by hand, and nothing else keeps those copies in
step (#226).
numpydoc enforces none of this. Its checks stop at RT05 and SA04,
with no RS family and no rule for a version directive. Any statement to the
contrary — including in the frozen implementation plans — is wrong.
Bibliography#
A published source is cited with :cite: against a key in docs/src/refs.bib,
and renders in References, which lists the file with :all:. That is the
fifth referencing mechanism this page describes, and the narrowest: it is for
sources outside this project that are published — a printed chart, a textbook, a
standards document. A web page with no edition is a
documentation link or an inline URL; another tephpy
page is a cross-reference; a specification section is a
citation.
Reach for it where a convention needs an authority. tephpy emphasises the 0 °C
isotherm because Met Office Factsheet 13 draws it distinctively, and a reader who
doubts that is entitled to the edition and the page rather than to a sentence
saying somebody checked.
An entry reproduces the source’s published title, which means bracing it —
title = {{Upper air observations & the tephigram}} — because pybtex lowercases
an unbraced one and would publish a title its source does not carry. Record the
edition or year and the date the source was consulted, in note: a citation is
provenance, and provenance without a date ages into a claim about a document that
may since have changed. :all: on the directive means an entry added and never
cited still renders, so the file cannot quietly accumulate sources nothing uses.
Reviewing Claims#
Four of this project’s gates execute something and ask whether an answer came
back: tests/test_docs_snippets.py runs the python, check_docs_figures.py
compares the images, check_citations.py resolves the section anchors, and
check_documentation_links.py resolves the URLs. None of the four asks
whether the answer is the intended one, and a sentence that executes nothing
is read by nobody but a reviewer. The four questions below are that review, and
each is written to have a name for an answer, because “did you verify this?”
does not (#193).
Which member did you check? A sentence covering a set is checked against
every member of it, not against the first. Each, both, all and every
announce such a sentence; the commonest form announces nothing and simply names
two things. The review of #191 found four in one branch — a page proposed
as “Decode TEMP and BUFR with ecCodes”, where bufr_dump will not read a TTAA
bulletin; a specification saying the recipe points at ecCodes, which is true of
one of its two formats; “six entries, each with an onward pointer”, where two of
the six carry none; and “the page registers in docs/src/howtos/index.rst
and in tests/test_docs_snippets.py::DOCUMENTED”, where four membership lists
take the entry and the sentence named two. The set is routinely larger than the
sentence admits, so count it before writing each. Spec §9 now reads “most of
them naming what to reach for instead”, which is what counting produced. Say in
the pull request which member you counted.
Does the title survive that? A page title is a claim, and the least-reviewed sentence on the page: the ecCodes title above named a format the page’s own body then said the tool cannot read. Read a title against Title Style once the page is finished, and against the question above whenever it names more than one thing. This is the review spec §8.6 requires of a title, and this section is the checklist it names.
What did you run? A claim about a tool this project will never install
cannot be pinned by a gate. ecCodes is the standing case: it is a non-goal, so
it is never in the test feature, on any Python, in any tier
(scope spec §3.2). What can be recorded is what was run — the tool, its version,
the date, and what the run did not establish. The plan of #191 did this
before the recipe was written, under the heading “What was verified”: ecCodes
2.48.0 through pixi exec --spec eccodes; the key names and the MISSING
sentinel read off genuine bufr_dump -p output; and no transcript, because
ecCodes ships no sample sounding message and encoding one failed on a
delayed-replication array-size mismatch. That record is why the page shows an
invocation and describes its output in prose rather than inventing a session —
which is what the same branch nearly shipped.
Plans freeze on merge and are not published (docs spec §3.4), so provenance left only in one is provenance a reader of the claim cannot reach. Where the claim is published, carry the tool, version and date into the living specification beside it.
Does it say how, and did you read that? A sentence about what a function does is checked by reading it once. A sentence about how it arrives at the answer is a claim about an implementation that can change underneath the page, and has to be re-read whenever that implementation does. Both defects #206 records are of the second kind, and nothing here catches that shape: a false sentence about a function executes nothing, and the function it names resolves perfectly, so the four gates above have nothing to fail on.
Parcel Ascent and Normand’s Point said calc.normand_point “takes the dry adiabat through the parcel’s
temperature and the mixing-ratio line through its dewpoint, and returns where
they meet. Nothing is iterated and nothing is fitted”. It intersects nothing the
diagram draws; it delegates to MetPy, and this project’s own test is named
test_normand_point_is_the_metpy_lcl. That second sentence is worse than
unverified — it is a claim about a moving target, false against an older MetPy
and true today by accident. Why the Axes Are Rotated said tephpy computes
the isobars and pointed at the transform that runs the other way.
So prefer the contract. Say what a function takes, returns and raises, and leave how it gets there to the function. Where the method is the point — the rotated-axes page cannot explain why isobars are computed rather than ruled without describing the computation — describe it, and name in the pull request the function you read to check it. A page that describes method carries a maintenance obligation the contract does not, which is the reason to take one on deliberately rather than in passing.