diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..ade2d57 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,198 @@ +# Contributing + +Thanks for looking. gp-libs is pre-1.0 (`0.0.x`); a minor version bump may +still carry a breaking change. Bug reports with a reproduction, and notes on +where the documentation misled you, are the most useful contributions right +now. + +How this project writes prose — README, `CHANGES`, release notes, commit +messages, docstrings, source comments, and log messages — is set out +separately in [WRITING.md](WRITING.md). Read that before changing any of it. +The constraints every change is held to, and the map of what is where, are in +[AGENTS.md](../AGENTS.md). + +## Getting set up + +Install [git], [uv], and [just]. + +```console +$ git clone https://github.com/git-pull/gp-libs.git +``` + +```console +$ cd gp-libs +``` + +```console +$ uv sync --all-extras --dev +``` + +[git]: https://git-scm.com/ +[uv]: https://github.com/astral-sh/uv +[just]: https://just.systems/ + +## The gates + +Format: + +```console +$ uv run ruff format . +``` + +Lint: + +```console +$ uv run ruff check . --fix --show-fixes +``` + +Type-check — strict mypy across `src/` and `tests/`: + +```console +$ uv run mypy . +``` + +Test: + +```console +$ uv run pytest +``` + +**Imports.** Namespace stdlib imports — `import enum`, not +`from enum import Enum` — so a call site reads `enum.Enum`. Third-party +packages may use `from X import Y`. For typing, `import typing as t` and +access via the namespace: `t.NamedTuple`, `t.TYPE_CHECKING`. Every file +starts with `from __future__ import annotations`; ruff's isort +`required-imports` (`pyproject.toml`) enforces that one, the rest is +convention ruff does not check. + +Documentation is a gate, not a courtesy. Examples in `src/*.py` docstrings +and under `docs/` are executed by `uv run pytest`, because `testpaths` in +`pyproject.toml` lists `tests`, `docs`, and `src`; `README.md` is not in +`testpaths`, so its three `>>> ` examples do not currently run under CI. +There is no separate doctest step for the paths that are collected — a +green `pytest` is the proof. Which blocks qualify, and the one mistake that +silently removes a test, are in +[WRITING.md](WRITING.md#documented-examples-that-run). + +Before claiming a test or a gate works, show it failing. A gate that has +never been red is an assumption. + +CI (`.github/workflows/tests.yml`) runs the equivalent of the four commands +above (`ruff check .`, `ruff format . --check`, `mypy .`, +`py.test --cov=./ --cov-report=xml`) across a matrix of Python 3.10-3.14, +docutils 0.20 and 0.22.4, and pytest 8 and 9 — gp-libs supports a wider +version span than most consumers of it, because everything downstream +depends on this collector staying compatible. + +## Tests + +Write tests as standalone functions, not classes — no `class TestFoo:` +groupings. Use descriptive function names and file organization instead. + +Prefer fixtures from `tests/conftest.py` over `monkeypatch` and +`unittest.mock` when one exists; document in the test docstring why a +standard fixture was not used for an exceptional case. Use `tmp_path` +(`pathlib.Path`) over `tempfile`, and `monkeypatch` over `unittest.mock` +when you do need one. + +`tests/conftest.py` also provides the Sphinx `app_params`/`make_app_params` +fixtures (via `pytest_plugins = ["sphinx.testing.fixtures", "pytester"]`) +for tests that build a throwaway Sphinx app — used by +`tests/test_linkify_issues.py` and the plugin-suppression tests. +`tests/regressions/` holds one file per historical bug, named for its +issue. + +Run continuously with [pytest-watcher]: + +```console +$ just start +``` + +Or with [entr(1)] when you want a shell-only watcher: + +```console +$ just watch-test +``` + +[pytest-watcher]: https://github.com/olzhasar/pytest-watcher +[entr(1)]: http://eradman.com/entrproject/ + +## Documentation + +Build the docs: + +```console +$ just build-docs +``` + +Start the default preview server, which watches for file changes: + +```console +$ just start-docs +``` + +From inside `docs/`, the local `docs/justfile` has finer-grained recipes: +`just html` builds once, `just serve` serves the built output, `just watch` +rebuilds on change, `just dev` watches and serves together, and +`just design` disables incremental builds while you edit static assets. + +`docs/conf.py` also enables `sphinx.ext.doctest` with a +`doctest_global_setup` that imports `is_allowed_version` and +`pytest_ignore_collect`, so the `{doctest}` blocks under `docs/` are valid +input to Sphinx's own doctest builder as well as to pytest. Run it with +`just -f docs/justfile doctest`. Neither `tests.yml` nor `docs.yml` invokes +that recipe — it is a manual, local-only check, not a CI gate. `just +build-docs` is what CI runs, and it is also the only thing that catches a +broken `{ref}`/`{doc}` cross-reference; the doctests do not. + +## Releasing + +gp-libs is pre-1.0: minor version bumps may include breaking changes. [uv] +handles virtualenv creation, package requirements, versioning, building, +and publishing — there is no `setup.py` or requirements file. + +1. Update `CHANGES` with release notes. +2. Bump the version in `src/gp_libs.py` and `pyproject.toml`. +3. Create the release commit: + + ```console + $ git commit -m 'Tag v0.1.1' + ``` + +4. Push the branch for review: + + ```console + $ git push + ``` + +5. After review, the release owner creates and pushes the tag. Never + create tags. Never push tags. The owner handles tagging and tag pushes, + because a tag triggers the publish workflow. See + [Release commits](WRITING.md#release-commits). + +## Pull requests + +One subject per pull request. Unrelated cleanup found along the way +belongs in its own commit, and usually in its own pull request. + +Discuss a substantial change via an issue before making it. + +Commit format is in [WRITING.md](WRITING.md#commits). + +## Decorum + +- Participants will be tolerant of opposing views. +- Participants must ensure that their language and actions are free of + personal attacks and disparaging personal remarks. +- When interpreting the words and actions of others, participants should + always assume good intentions. +- Behaviour which can be reasonably considered harassment will not be + tolerated. + +Based on [Ruby's Community Conduct Guideline](https://www.ruby-lang.org/en/conduct/). + +## Security + +Please do not open a public issue for a vulnerability. Report it privately +through GitHub: +. diff --git a/.github/WRITING.md b/.github/WRITING.md new file mode 100644 index 0000000..0a79282 --- /dev/null +++ b/.github/WRITING.md @@ -0,0 +1,874 @@ +# Writing + +How this project writes prose, for humans and agents alike. It governs +`README.md`, `CHANGES`, release notes, commit messages, docstrings, source +comments, log messages, and the Markdown and reStructuredText under `docs/` +— every surface a reader reaches. + +For environment setup, the gates, and pull request workflow, see +[CONTRIBUTING.md](CONTRIBUTING.md). + +## Voice + +Three surfaces, one voice. A docstring says what a caller may rely on; a +`CHANGES` entry says what changed; prose says what happens. All three are +present tense, lead with the thing being described, and stop. Why it was +built that way belongs in the commit message, which is timestamped and +attached to the diff. + +The most useful editing operation is deleting the introductory sentence. + +Lead with verbs and name concrete things. Put identifiers in backticks. +Prefer short declarative sentences, one operational fact each. Do not +explain Python to Python developers; do explain this project's semantics. + +Type annotations describe shape. Documentation describes meaning. A sentence +that restates a signature has said nothing. + +Use MUST, SHOULD, and MAY only where the normative sense is meant. Say what +actually happens rather than that something is "supported". + +| Instead of | Prefer | +| --------------------------------- | --------------------------------- | +| "We added…" | "`DocutilsDocTestFinder.find` now accepts…" | +| "New and improved" | "`linkify_issues` now…" | +| "powerful", "seamless" | state the capability | +| "easily", "simply", "just" | omit | +| "simple", "obvious", "intuitive" | omit | +| "robust" | name the failure that is handled | +| "comprehensive" | name what is covered | +| "production-ready" | state the guarantee | +| "optimized", "blazingly fast" | give the magnitude | +| "various fixes" | name the components | +| "under the hood" | omit unless observable | +| "please note that", "note that" | state the fact | +| "leverage", "utilize" | "use" | +| "delve into" | "read", or omit | +| "best practices" | name the practice | +| "in order to" | "to" | + +## Who you are writing for + +The default reader is wiring gp-libs into their own project: pointing pytest +at `docs/` with `pytest_doctest_docutils`, running +`python -m doctest_docutils README.md` by hand, or adding `linkify_issues` +to a Sphinx `conf.py`. They are fluent in pytest and Sphinx as users — +`conftest.py`, fixtures, `extensions`, `testpaths` — and write +reStructuredText or Markdown daily, but you cannot assume they know +gp-libs' internals: docutils node traversal, directive registration, or how +the finder decides a block is a doctest. + +A second, smaller reader works *on* gp-libs or against its lower layers: +the doctest finder, the docutils compatibility shims, myst-parser directive +registration, or contributing. Serve them too, but mark their material +opt-in ("for the rarer cases", "advanced") so the default reader knows they +can stop. Never make the common case pay a comprehension tax for the +advanced one. + +Rules that follow: + +- **Second person, present tense, active.** "You point pytest at `docs/`", + not "Files are collected". Address the reader who is doing the thing. +- **Concept before configuration.** Open by saying what the tool *is* and + what it does for the reader. The `conf.py` key, the pytest flag — those + are the last details they need, not the first. A page that opens with + "set these keys" has buried the idea under its mechanics. +- **Say when they can stop.** Lead with the default and the reassurance: + install the plugin and `pytest docs/` just works; `issue_url_tpl` is the + one setting `linkify_issues` needs. Let a skimmer leave after one + paragraph. +- **Grant permission, do not demand attention.** "Reach for this when…" + tells readers they are in the right place without implying they must read + on. +- **Progressive disclosure.** Order by how many readers need it: the + default → the one option a few will tune (a custom `issue_re`, + `--doctest-docutils-modules`) → running `doctest_docutils` directly → the + docutils machinery underneath. Each step is for a smaller audience than + the last. +- **Lean on the pipeline.** The reader thinks in a chain: a `.rst` or `.md` + file is parsed (docutils, with myst-parser for Markdown), its examples + are collected, then run. Reinforce that chain when explaining why + Markdown needs myst-parser or why a fixture needs a visible + `conftest.py`. +- **Name the trade-off.** If a choice costs something — the plugin disables + pytest's standard doctest plugin, Markdown support goes through + myst-parser, fixtures reach only files a `conftest.py` can see — say so, + and say what it buys. State it; do not sell it. +- **Frame by concept, not by mechanism.** Do not headline a feature by its + `conf.py` key or pytest flag in prose; that names the implementation + surface, the reader's last concern. Name the concept. The mechanics + vocabulary — the flag spelling, the default regex — belongs in a + reference block or the API section, and only there. + +`docs/modules/linkify_issues/index.md` is the worked example: a +concept-first intro that says what the extension does (plain-text `#123` +becomes a link) before any `conf.py` key, a two-step default configuration +most readers can stop after, `issue_re` marked as optional tuning for the +smaller audience, an honest close that more complex needs mean forking, +and the API reference last. + +## README + +A README is the shortest path from "what is this?" to competent use, not +the project's autobiography. + +The first sentence is a contract. It says what abstraction the reader has +been handed, concretely enough to tell this package apart from the +neighbouring one. + +Get to a runnable command or snippet before anything the reader can skip. +A logo, a mission statement, a comparison matrix and three paragraphs of +history in front of the install line all cost the same thing. + +State the minimum Python version and meaningful platform constraints in +prose, not only in badges. `requires-python` in `pyproject.toml` is the +authority; the README must agree with it. + +Examples are executable, not illustrative fiction. See +[Documented examples that run](#documented-examples-that-run) for which +blocks are executed and how to write one that qualifies. + +Document the semantic model, not the flag list. `--help` already +enumerates flags, and gp-libs has none — it ships two pytest plugin +components, a Sphinx extension, and a CLI entry point on +`doctest_docutils` itself. What prose can say that a flag list cannot is +precedence, which files each collector reaches, and what a passing or +failing run means. + +State defaults explicitly — defaults are API. State negative guarantees +where they exist: "no `doctest_namespace` fixtures are registered for +`docs/`", "the pytest plugin blocks pytest's own doctest collection". They +establish boundaries faster than any amount of description. + +Headings stay conventional and stable, because people deep-link them. +Badges are few and load-bearing. + +## Documented examples that run + +Examples in this fleet are tests, and gp-libs is what makes that true: it +ships the collector every other repo in this fleet relies on. This section +documents every format `doctest_docutils` and `pytest_doctest_docutils` +actually support, verified against `src/doctest_docutils.py` and +`src/pytest_doctest_docutils.py` — not the aspirational set. + +**A fence tag is cosmetic; only a `>>> ` prompt executes.** A block written +as + + ```python + finder = DocutilsDocTestFinder() + ``` + +is prose that looks like a test. Nothing collects it, nothing runs it, and +it can be wrong for years. The same block written with prompts is a test: + + ```python + >>> finder = DocutilsDocTestFinder() + ``` + +This is the single most expensive mistake available when editing +documentation, because removing the prompts leaves a green test suite and +a silently deleted test. When editing a file that contains examples, count +the prompts before and after. + +**The fence tag is `python`.** Not `pycon`, not bare. This is uniform +across the fleet and tooling depends on it — even though, for gp-libs +specifically, the underlying finder is looser than the convention: it +matches any fenced block whose content matches doctest's own prompt regex, +regardless of the language tag on the fence. Do not rely on that; a `text` +or `pycon` fence with a working `>>> ` example is an accident other tools +in the fleet will not replicate. Use `python`. + +**Two independent collection paths.** `.py` files are handed straight to +pytest's own `DoctestModule` — the same class pytest's built-in `doctest` +plugin uses, called directly rather than through that plugin's hooks. This +repo's plugin registers under the `pytest11` entry-point key `sphinx` (see +`[project.entry-points.pytest11]` in `pyproject.toml`), and on +`pytest_configure` it blocks pytest's own `doctest` plugin +(`config.pluginmanager.set_blocked("doctest")`) so a `.py` docstring is +never collected twice. `.rst` and `.md` files go through +`DocutilsDocTestFinder` instead: it parses the file with docutils (`.rst`) +or myst-parser (`.md`), then walks the resulting tree for anything +doctest-shaped. A file's presence in `testpaths` (`pyproject.toml`) makes +it eligible for either path; only prompts make a block executable. + +**Where examples run in this repo.** `testpaths` lists `tests`, `docs`, +and `src`, so `.py` docstrings under `src/`, `.md` files under `docs/`, +and `.py` files under `tests/` are all collected. `README.md` is *not* +listed in `testpaths`, so pytest never reaches any of its three `>>> ` +prompts today — do not describe the README as tested. Keep the prompts as +prompts anyway: dropping a `>>> ` silently deletes what would otherwise be +a test, and the count (`rg -c '^\s*>>> ' README.md`) is the guard against +that. Note that adding `README.md` to `testpaths` would not make all three +runnable as-is — two of them are `>>> ` lines nested inside a fence that +is itself illustrating `.. doctest::`/`{doctest}` syntax as text, one +fence-level too deep for the finder's regex fallback to see past. Only the +bare `doctest_block` example would collect. Making the other two +executable, not just illustrative, would need its own follow-up. + +**Formats the finder collects**, for `.rst` and `.md`: + +- **A bare `>>> ` prompt.** Docutils' own `doctest_block` node type — a + plain paragraph starting with `>>> ` needs no directive in + reStructuredText. In Markdown this needs a fence (` ```python `); myst + gives you the same node. +- **The `.. doctest::` directive** (reStructuredText) or the + ` ```{doctest} ``` ` fence (MyST Markdown). Both register through the + same `DoctestDirective` class. Options, all read from `self.options` on + the directive: + - `:options:` — inline doctest flags, e.g. `+ELLIPSIS -NORMALIZE_WHITESPACE`. + - `:pyversion:` — a PEP 440 specifier (checked with `is_allowed_version`); + the example is skipped when the running interpreter does not satisfy + it. + - `:skipif:` — an expression the runner evaluates to decide whether to + skip. + - `:trim-doctest-flags:` / `:no-trim-doctest-flags:` — whether + `# doctest: +FLAG` comments are stripped from the rendered code before + display. + - `` in the block content is doctest's own convention for an + intentional blank line in expected output; it works here exactly as in + stdlib doctest, and the directive additionally pretty-prints it back to + a real blank line in the built HTML. +- **`.. testsetup::` / `.. testcleanup::`** (or the ` ```{testsetup} ``` ` + / ` ```{testcleanup} ``` ` fences). These render as hidden comments + instead of a visible code block, which is the entire effect they have: + each one is still an independent doctest that needs its own `>>> ` + prompts to execute, and — unlike Sphinx's `sphinx.ext.doctest` — it does + **not** share globals with the doctest blocks around it. Each collected + block gets its own copy of the file's globals + (`doctest.DocTest.__init__` copies `globs`), so a variable a + `testsetup` block assigns is gone by the next block. Nothing in this + repo's own documentation currently uses `testsetup`/`testcleanup`; the + only exercise they get is in `tests/`. Treat them as "hide this from the + rendered page" rather than "shared fixture for the examples that + follow" — write the setup you need directly into each example instead. + +**Flags.** `ELLIPSIS` and `NORMALIZE_WHITESPACE` are enabled globally via +`doctest_optionflags` in `pyproject.toml`, so `...` elides variable output +and whitespace differences do not fail a comparison. The stdlib flag set +(`ELLIPSIS`, `NORMALIZE_WHITESPACE`, `IGNORE_EXCEPTION_DETAIL`, +`DONT_ACCEPT_BLANKLINE`, `SKIP`, and the rest of +`doctest.OPTIONFLAGS_BY_NAME`) all work as usual. `pytest_doctest_docutils` +additionally registers, via `doctest.register_optionflag`: + +- `ALLOW_UNICODE` / `ALLOW_BYTES` — ignore a `u''`/`b''` string prefix + mismatch. These are not gp-libs inventions: they are the same flags + pytest's own (now-blocked) `doctest` plugin defines, re-registered here + so `.rst`/`.md` collection — which builds its own runner and reuses + pytest's checker (`_pytest.doctest._get_checker()`) — gets identical + behaviour to a `.py` doctest. +- `NUMBER` — also borrowed from pytest's checker: ignores floating-point + precision beyond what the literal in the expected output states. +- `HIDE` — the one flag that is actually gp-libs' own. It is a **no-op for + execution**: the output checker never consults it, so `+HIDE` never + changes whether an example passes. Registering it exists for one reason + — so `# doctest: +HIDE` *parses* instead of raising + `ValueError: invalid option`. It is a signal for documentation tooling + that wants to know "this example should run as a test but not appear in + rendered output"; gp-libs itself does not act on that signal anywhere. + `HIDE` is registered eagerly in `pytest_configure`, before any + docstring is parsed, because the `.py` collection path never calls the + function (`_get_flag_lookup`) that registers `ALLOW_UNICODE`, + `ALLOW_BYTES`, and `NUMBER` — without the eager registration, a `.py` + docstring using `+HIDE` would fail to parse. + +**`# doctest: +SKIP` is not permitted** in this fleet's own documentation. +It is a workaround that tests nothing. `doctest_docutils` supports it +mechanically — `tests/test_doctest_options.py` proves the mechanism works, +because gp-libs is the tool and has to prove its own flags function — but +that is a test of the tool, not licence to write `+SKIP` into a page. Use +the fixtures, `pyversion`, or `skipif` instead. + +**Do not downgrade a doctest to a non-executed block to make it pass.** A +`.. code-block::` or an unprompted fence does not run. If an example +cannot pass, fix the example or fix the code. + +**No `doctest_namespace` fixtures anywhere in this repo.** A `conftest.py` +only reaches files inside its own subtree, and the only `conftest.py` in +this repo lives at `tests/conftest.py` — invisible to `src/` and `docs/`. +Add objects to `doctest_namespace` from a fixture when you need shared +helpers for a group of `.rst`/`.md` examples: + +```python +import pytest + + +@pytest.fixture +def add_helpers(doctest_namespace): + def add(left, right): + return left + right + + doctest_namespace["add"] = add +``` + +— but until such a fixture exists and is visible to the file you are +editing, keep documentation examples self-contained: import what you use +inside the block. + +**Docstring examples** use the NumPy `Examples` section: + + Examples + -------- + >>> is_allowed_version('3.3', '<=3.5') + True + +**gp-libs dogfoods two more mechanisms most repos in this fleet do not.** +`docs/conf.py` adds `sphinx.ext.doctest` to `extra_extensions` and sets +`doctest_global_setup` (importing `is_allowed_version` and +`pytest_ignore_collect`), so the `{doctest}` blocks under `docs/` are also +valid input to Sphinx's own doctest builder, not only to pytest. Running +that builder is `just -f docs/justfile doctest` — a real recipe, but one +`tests.yml` and `docs.yml` never invoke. Treat it as a manual, +local-only check: useful when you are debugging a `docs/conf.py` change, +not a gate anything is blocked on. `uv run pytest` is what actually proves +a documentation example runs in CI. + +## The changelog + +`CHANGES` is the changelog. Not `CHANGELOG.md`. It is rendered as +[the project's changelog page](https://gp-libs.git-pull.com/history.html) +via `docs/history.md`. + +A ledger, not a narrative. It is scanned, and the question a reader is +asking is whether an entry affects them. Modeled on Django's release-notes +shape — deliverables get titles and prose, not bullets. + +**Release entry boilerplate.** Every release header is +`## gp-libs X.Y.Z (YYYY-MM-DD)`. The file opens with a +`## gp-libs X.Y.Z (unreleased)` placeholder block fenced by +`` and +`` HTML comments — new release entries land +immediately below the END marker, never above it. + +**Open with a multi-sentence lead paragraph.** Plain prose, no italic. +Open with the version as sentence subject ("gp-libs X.Y.Z ships …") so the +lead is self-contained when excerpted. Two to four sentences telling the +reader what shipped and who cares — user-visible takeaways, not internal +mechanism. Cross-reference detail docs with `{ref}` to keep the lead +compact. + +**Unreleased entries carry no lead paragraph and no version summary.** +Speaking for a release — what the version "is", "ships", or "focuses on" +— is presumptuous before its scope is final. Only the person cutting the +release writes that, and only when the user explicitly asks to release. +Never write or edit a lead paragraph from a feature branch, and never ask +or imply that a release should happen. + +**Each deliverable is a section, not a bullet.** Inside `### What's new`, +every distinct deliverable gets a `#### Deliverable title (#NN)` heading +naming it in user vocabulary, followed by one to three prose paragraphs +explaining what shipped. Do not wrap a paragraph in `- ` — bullets are for +enumerable lists, not paragraph containers. Cross-link detail docs +(`See {ref}\`foo\` for details.`) so prose stays focused. + +**The deliverable test.** Before writing an entry, ask: "What's the +deliverable, in user vocabulary?" If you cannot answer in one sentence, +the entry isn't ready. Mechanism — helper internals, byte counters, +schema-validation locations — belongs in pull request descriptions and +code comments, not the changelog. + +**Fixed subheadings**, in this order when present: `### Breaking changes`, +`### Dependencies`, `### What's new`, `### Fixes`, `### Documentation`, +`### Development`. Dev tooling (helper scripts, internal automation) lives +under `### Development`. For breaking changes, show the migration path +with concrete inline code (a `# Before` / `# After` fenced block). +Dependency floor bumps use the form +``Minimum `pkg>=X.Y.Z` (was `>=X.Y.W`)``. + +**PR refs `(#NN)`** sit in each deliverable's `####` heading. + +**When bullets are appropriate.** Catch-all sections (`### Fixes`, +occasionally `### Documentation`) with three or more genuinely small items +use bullets — one line each, never paragraphs. If a bullet swells past two +lines, promote it to a `#### Title (#NN)` heading with a prose body. + +**Anti-patterns.** Fragile metrics that go stale silently — token +ceilings, third-party version pins, percent benchmarks, exact byte counts. +Describe the capability, not the math. Private symbols (leading-underscore +identifiers) and algorithm names exposed for the first time. Walls of text +dressed up as bullets. Breaking changes buried mid-entry instead of given +their own subheading at the top. + +**Always link autodoc'd APIs.** Any class, method, function, exception, or +attribute that has its own rendered page must be cited with its role +(`{class}`, `{meth}`, `{func}`, `{exc}`, `{attr}`) — never plain backticks. +Doc pages without an explicit ref label use `{doc}`. Plain backticks are +correct for code syntax, environment variables, parameter names, and file +paths that are not doc pages — anything without an autodoc destination. + +**Summarization style.** When asked "what changed in the latest version?", +lead with the entry's lead paragraph (paraphrased if needed), followed by +each `####` deliverable heading under `### What's new` with a one-sentence +summary. Cite `(#NN)` only if asked for source links. Do not invent +versions, dates, or numbers not present in `CHANGES`. Do not quote line +numbers or file offsets — those shift as the file evolves. + +## Release notes + +`CHANGES` is the permanent ledger; a release page is editorial. Lead with +one paragraph naming the headline change, then three to five highlights, +then link the full changelog. + +Numbers over adjectives. A list of merged commit subjects is a merge log +wearing a release-note hat. Put the hand-written highlights above it. + +Versions are PEP 440 identifiers. Semantic-versioning meaning applies to +the documented public API — including `doctest_docutils`'s CLI arguments, +the `pytest11` entry point, `linkify_issues`' `conf.py` keys, and the +registered doctest flags, not only imported Python symbols. gp-libs is +pre-1.0: a minor version bump may still include a breaking change. + +## Docstrings + +New public functions and methods carry a doctest that exercises them — +doctests are both documentation and a test, and gp-libs is the tool that +runs them. This guides new work; not every existing function conforms +today, and fixing that opportunistically is welcome but is not a +prerequisite for an unrelated change. + +The prime directive: never restate the type. The annotation is the source +of truth; the docstring carries what the annotation cannot. + +This is documentation debt wearing a docstring: + + def get_test_name(node: Node) -> str: + """Get the test's name. + + Parameters + ---------- + node : Node + The node. + + Returns + ------- + str + The name. + """ + +Document instead the dimensions the type system cannot encode: + +- **Mutation.** What it changes in place. +- **Ownership.** What the caller must close, release, or keep alive. +- **Ordering.** Whether results come back in a guaranteed order. +- **Timing.** What has finished by the time the call returns. +- **Failure.** Which exceptions are raised and what triggers each. +- **Idempotence.** Whether calling twice does anything the second time. +- **Concurrency.** Whether calls are coalesced, queued, or independent. +- **Units and ranges.** What a number means and what values are accepted. +- **Boundary behaviour.** What zero, empty, and the maximum do. +- **Platform.** Behaviour that differs by docutils or myst-parser version. +- **Security boundary.** What is executed, and what is only read. + +Follow [NumPy docstring style](https://numpydoc.readthedocs.io/en/latest/format.html) +for every public function, method, and class — enforced by ruff's +`pydocstyle` rules (`convention = "numpy"` in `pyproject.toml`), not +relitigated in review. The first sentence stands alone; tooling truncates +there. PEP 257 applies: triple double quotes, an imperative one-line +summary ending in a period, a blank line before any extended description. +Do not repeat an introspectable signature. + +**Classes with fields** — `NamedTuple`, dataclasses — document every field +in an `Attributes` section: + +```python +class ConsoleExample(t.NamedTuple): + """Console example collected from a Markdown page. + + Attributes + ---------- + path : pathlib.Path + Markdown file the example was collected from. + """ +``` + +Autodoc renders every field whether or not you describe it, so an +undocumented `NamedTuple` field ships to the API docs as "Alias for field +number 0" and a dataclass field ships bare. Document all of them — a class +with three fields and two documented still ships a stub for the third. + +## Logging + +These rules guide future logging changes; existing code may not yet +conform. + +**Logger setup.** Use `logging.getLogger(__name__)` in every module. Add a +`NullHandler` in library `__init__.py` files. Never configure handlers, +levels, or formatters in library code — that is the application's job. + +**Structured context via `extra`.** Pass structured data on every log call +where useful for filtering, searching, or test assertions. + +Core keys (stable, scalar, safe at any log level): + +| Key | Type | Context | +|-----|------|---------| +| `doctest_source_file` | `str` | doctest source path (`.rst`, `.md`, `.py`) | +| `doctest_block_type` | `str` | block type (`doctest_block`, code fence) | +| `sphinx_extension` | `str` | Sphinx extension name | + +Treat established keys as compatibility-sensitive — downstream users may +build dashboards and alerts on them. Change deliberately. Keys are +`snake_case`, not dotted, with project-specific prefixes (`doctest_`, +`sphinx_`). Prefer stable scalars; avoid ad-hoc objects. + +**Lazy formatting.** `logger.debug("msg %s", val)`, not f-strings. Two +reasons: deferred string interpolation is skipped entirely when the level +is filtered, and aggregators group by message template — `"Running %s"` is +one signature grouped ×10,000, while f-strings make every line unique. +When computing `val` itself is expensive, guard with +`if logger.isEnabledFor(logging.DEBUG)`. + +**`stacklevel` for wrappers.** Increment for each wrapper layer so +`%(filename)s:%(lineno)d` and OTel `code.filepath` point to the real +caller. Verify whenever call depth changes. + +**Log levels.** + +| Level | Use for | Examples | +|-------|---------|----------| +| `DEBUG` | Internal mechanics | Doctest parsing, node traversal steps | +| `INFO` | Lifecycle, user-visible operations | Extension loaded, document processed | +| `WARNING` | Recoverable issues, deprecation | Deprecated directive, missing optional dependency | +| `ERROR` | Failures that stop an operation | Parse error, invalid configuration | + +**Message style.** Lowercase, past tense for events: "extension loaded", +"parse error". No trailing punctuation. Keep messages short; put details +in `extra`, not the message string. + +**Exception logging.** Use `logger.exception()` only inside `except` +blocks when not re-raising. Use `logger.error(..., exc_info=True)` when +the traceback is needed outside an `except` block. Avoid +`logger.exception()` followed by `raise` — it duplicates the traceback. +Either add context via `extra` that would otherwise be lost, or let the +exception propagate. + +**Testing logs.** Assert on `caplog.records` attributes, not string +matching on `caplog.text`. Scope capture with +`caplog.at_level(logging.DEBUG, logger="doctest_docutils")`. Filter +records rather than index by position. Assert on schema +(`record.sphinx_extension == "doctest_docutils"`), not substring matching. +`caplog.record_tuples` cannot access extra fields — always use +`caplog.records`. + +**Avoid:** f-strings or `.format()` in log calls; unguarded logging in hot +loops; catch-log-reraise without adding new context; `print()` for +diagnostics; logging secret environment variable values (log key names +only); non-scalar ad-hoc objects in `extra`; custom `extra` fields +referenced in format strings without safe defaults (a missing key raises +`KeyError`). + +## Source comments + +A comment ships only if it passes all three gates. Fail any: delete or +rewrite. Borderline: delete — borderline means the information is +reconstructible, which is what makes deletion cheap. + +**Loss.** Three years from now, would losing this cost a maintainer real +time rediscovering intent, an invariant, a constraint, or a failure mode +the code and tests do not already make obvious? + +**Elite.** Would SQLite, Redis, the Go standard library, or CPython write +this comment, at this length? Those projects state the constraint and +stop. They do not argue with an imagined objector. + +**Upkeep.** Will it stay true without maintenance? A comment that +hand-syncs a value the code owns — a count, an offset, a line reference, a +duplicated constant — is false the first time that value moves. + +### Ceiling + +One or two lines. A comment reaching four is either carrying several +facts, in which case split it, or arguing, in which case cut it to the +fact. + +Rationale, alternatives weighed, and the story of how the code got here +belong in the commit message: timestamped, attached to the exact diff, and +free to maintain. + +### Keep + +- Why over how: upstream quirks, protocol and compatibility constraints, + performance tradeoffs still part of the contract. +- Invariants, preconditions, ordering, lifetime, and concurrency + requirements that types and tests cannot express. +- Code that looks wrong but is not, so a later cleanup does not + reintroduce the bug. +- A high-level sketch of an algorithm whose local operations do not + reveal the whole. + +### Delete + +- Narration of the next lines; code translated into English. +- Restated names, types, defaults, or control flow. +- Values duplicated from the code and hand-synced. +- Justification, hedging, or apology for a choice. +- Speculation about future requirements. +- History version control already holds, including commented-out code. +- Ticket and issue numbers. They say nothing to a reader without tracker + access, and they rot when the tracker moves. Unfinished work goes in the + tracker, not the source. +- Transient observations — "currently", "for now", "the latest release" — + that go stale with no nearby edit. + +### The upkeep gate in practice + +It reaches values that track our own code. It does not reach frozen +external facts. + +Bad (Delete): + +```python +# There are 321 tests to complete for servers. +``` + +Good (Keep): + +```python +# CPython < 3.11 has no ExceptionGroup, so this branch stays. +``` + +### Documentation exception + +Doctests, minimal usage examples, and `Parameters`/`Returns`/`Attributes` +entries on public API are exempt from the loss gate — they serve the +caller, not the maintainer. They are exempt from nothing else. Ceiling: a +good man page entry. Autodoc ships every `NamedTuple` or dataclass field +whether or not you describe it, and a doctest that runs is also a test. + +## Terminology and capitalization + +Pick the domain noun and keep it. This project's own vocabulary is +`doctest_docutils` (the module), `pytest_doctest_docutils` (the plugin), +and `linkify_issues` (the Sphinx extension) — do not call the finder a +"scanner" in one paragraph and a "collector" in the next, and do not +alternate "issue link" with "reference link" once `linkify_issues` has +established the term. + +Stable vocabulary is what makes search, deep links, and an agent's +retrieval work at all. + +Python and PyPI keep their own capitalisation. Distribution names are +written as they are published. + +Do not write counts into prose — how many symbols exist, how many tests +there are. They go stale silently and no reader needs them. Counts that +pin a fixture or guard an invariant are different, and belong in code. + +## Cross-references + +Point the advanced reader at the deep-dive rather than inlining it, and +put the link where their interest peaks — on the phrase that made them +curious ("how the finder decides", "the docutils machinery") — not as a +standalone footnote the eye skips. Use `{class}`, `{meth}`, `{func}`, +`{mod}`, `{exc}`, `{attr}` for API objects; `{ref}` or `{doc}` for +documentation pages and section anchors; a Markdown link or reference link +for external projects. A `{ref}` must match its target's anchor exactly — +anchors mix underscore and hyphen forms across pages (`doctest_docutils`, +`linkify-issues`). + +Link the first prose mention of any symbol that has a useful destination +on that page. After the first linked mention, later mentions can stay +plain unless the distance or context makes another link useful. Do not +rely on a later reference section to satisfy the first-mention rule. If +the first occurrence would be a heading, grid-card teaser, or introductory +sentence, link that occurrence or retitle the heading so the first prose +mention can carry the link. Leave command examples, code blocks, and +literal configuration values as code; link the surrounding prose instead. + +`just build-docs` catches a broken cross-reference; the doctests do not — +build the docs before you commit a page with a `{ref}` or `{doc}` role on +it. + +## Markdown + +Prose wraps at 80 columns. Table rows, badge lines, and long links are +exempt, because breaking them harms rendering. A pull request or issue +body does not wrap at all: GitHub renders a single newline as a space in a +file and as a line break in a comment, so a wrapped comment body arrives +as ragged stubs. + +GitHub alert blocks — `> [!NOTE]`, `> [!WARNING]` — render as literal text +outside GitHub, so reserve them for at most one load-bearing warning per +document. Write the sentence so it carries the fact on its own, and a +renderer that drops the marker loses nothing. + +Do not use a local absolute path or an email address in anything +published. + +## Code blocks + +Code blocks are paste-and-run units: pasting one block runs exactly one +intended action. Executed examples are exempt — the test suite runs them, +nobody pastes them. + +- **One command per block.** Multiple steps may share a block only when + explicitly chained with `&&`, `;`, or `\` continuations — the chain is + then one logical command. +- **Explanations go in prose above the block**, never as `#` comments + inside it. +- **Command menus are per-command blocks with prose lead-ins**, not + tables. +- **Shell commands use the `console` tag with a `$ ` prefix.** This + separates interactive commands from scripts and enables prompt-aware + copy. +- **Split long commands with `\`** — one flag or flag+value pair per + indented continuation line, positional arguments last. + +Good — show the last ten commits as a graph: + +```console +$ git log \ + --max-count=10 \ + --graph \ + --oneline +``` + +Bad: + +```console +# Show the last ten commits as a graph +$ git log --max-count=10 --graph --oneline +``` + +## Commits + +``` +Scope(type[detail]): concise description + +why: Explanation of necessity or impact. + +what: +- Specific technical changes made +- Focused on a single topic +``` + +Keep the subject to 50 characters or fewer, excluding any trailing +`(#NN)` pull request reference, and wrap body lines at 72. Separate the +`why:` and `what:` blocks with a blank line. + +Routine maintenance commits drop the colon and take a capitalised +description, which is what distinguishes them at a glance in +`git log --oneline`: + +``` +py(deps[dev]) Bump dev packages +ai(rules[AGENTS]) Judge comments by three gates +``` + +Everything that changes behaviour keeps the colon. + +Common types: **feat**, **fix**, **refactor**, **docs**, **chore**, +**test**, **style**, **ci**, **py(deps)**, **py(deps[dev])**, +**ai(rules[AGENTS])**, **ai(claude[rules])**, **ai(claude[command])**. + +Example: + +``` +doctest_docutils(feat[parse]): Add support for myst-parser code blocks + +why: Enable doctest execution in Markdown documentation files + +what: +- Add detection for ```{doctest} fence syntax +- Register myst directives automatically +- Add tests for Markdown doctest parsing +``` + +For a multi-line message, use a heredoc so the formatting survives: + +```console +$ git commit -m "$(cat <<'EOF' +Scope(feat[detail]): Concise description + +why: Explanation of the change. + +what: +- First change +- Second change +EOF +)" +``` + +### Release commits + +Never create tags. Never push tags. The owner handles tagging and tag +pushes, because a tag triggers the publish workflow. + +A release commit subject is plain and short: `Tag v`. The +detailed why and what go in the body. Do not use the +`Scope(type[detail]):` format for a release — it buries the lede. + +## Slop prevention + +Treat AI slop as review-hostile noise, not as proof that text or code is +wrong. The goal is to maximise information density. + +- **AI signatures.** No "Generated by", no conversational filler, no + unexplained emoji, no tool metadata. +- **Brittle references.** No hard-coded line numbers, fragile file + counts, dated "as of" claims, bare SHAs, or local absolute paths — + unless they are strict evidentiary artefacts such as a benchmark log. +- **Diff narration.** Do not restate what moved, was renamed, or was + removed in anything the reader holds alongside the diff: code, + docstrings, README, CHANGES, or a pull request description. The diff + and commit message already carry it. +- **Branch-internal narrative.** Do not mention intermediate states, + abandoned approaches, or "no longer" behaviour unless users of a + published release actually experienced the old state (the + published-release test below). +- **Low-value scaffolding.** No ownerless TODOs, unused future-proofing, + debug artefacts, or defensive wrappers around failure modes nothing can + reach. +- **Prose inflation.** The diction table under [Voice](#voice) governs; + replace an inflated word with a concrete description of behaviour, + constraints, or trade-offs. +- **Coded labels.** Write rules and findings as plain imperatives. No + `[R1]`, `Option B`, or any index a reader has to decode in shipped + text. Internal agent bookkeeping may use ids; shipped text may not. + +Preserve the "why". Never delete a comment documenting an invariant, a +protocol constraint, a platform quirk, or an upstream workaround — those +are the facts [Source comments](#source-comments) keeps, and every other +comment is judged by it. Preserve exact counts, dates, and SHAs when they +serve as evidence in benchmark results, release notes, or lockfiles. + +### Durable source links + +Link to a pinned revision, never to trunk. A pinned permalink is not a +brittle reference; an unlinked SHA dropped into prose is. `blob/master/…` +links rot silently — the file moves, lines shift, and the anchor lands on +unrelated code while still resolving. + +- Prefer a release tag (`blob/v0.0.19/…`). Most durable, and it tells the + reader which released version the claim held for. +- Otherwise use a 7-char commit ref (`blob/9a29b1a/…`) reachable from + trunk. Use when there is no tag or the claim is about unreleased code. + Never a PR-head SHA — it can be rebased or garbage-collected. +- Reserve `blob/master/…` for living documents meant to always show the + latest state, such as this file and `CONTRIBUTING.md`. +- Line anchors (`#L120-L145`) are only safe on a pinned ref. + +### The published-release test + +Long-running branches accumulate tactical decisions — renames, refactors, +attempts-then-reverts. When deciding what counts as branch-internal, use +trunk or the parent branch as the baseline — not intermediate states +inside the current branch. Ask: did users of the most recently published +release ever experience this old name, old behaviour, or bug? If no, it +is branch-internal narrative — move it to the commit message and describe +only the final state in the artefact. + +Keep in shipped artefacts: deprecations and migration guides for symbols +that actually shipped; `### Fixes` entries for bugs that affected users of +a published release; comments explaining why the current code looks this +way that make sense to a reader who never saw the previous version. + +### Cleanup in hindsight + +When applying these rules retroactively from inside a feature branch, +first establish scope by diffing against the parent branch or trunk to +identify which commits this branch actually introduced. For in-branch +commits, prefer `fixup!` commits with `git rebase --autosquash` to address +each causal commit at its source, or a single cleanup commit at branch +tip. Default to leaving trunk or parent-branch commits alone; act on them +only on explicit instruction, and fold any resulting cleanup into a single +commit at branch tip rather than rewriting shared history. diff --git a/.github/contributing.md b/.github/contributing.md deleted file mode 100644 index c0eddac..0000000 --- a/.github/contributing.md +++ /dev/null @@ -1,27 +0,0 @@ -# Contributing - -When contributing to this repository, please first discuss the change you wish to make via issue, -email, or any other method with the maintainers of this repository before making a change. - -See [developing](../docs/developing.md) for environment setup and [AGENTS.md](../AGENTS.md) for -detailed coding standards. - -## Pull Request Process - -1. **Format and lint**: `uv run ruff format .` then `uv run ruff check . --fix --show-fixes` -2. **Type check**: `uv run mypy` -3. **Test**: `uv run pytest` — all tests must pass before submitting -4. **Document**: Update docs if your change affects the public interface -5. You may merge the Pull Request once you have the sign-off of one other developer. If you - do not have permission to do that, you may request a reviewer to merge it for you. - -## Decorum - -- Participants will be tolerant of opposing views. -- Participants must ensure that their language and actions are free of personal - attacks and disparaging personal remarks. -- When interpreting the words and actions of others, participants should always - assume good intentions. -- Behaviour which can be reasonably considered harassment will not be tolerated. - -Based on [Ruby's Community Conduct Guideline](https://www.ruby-lang.org/en/conduct/) diff --git a/AGENTS.md b/AGENTS.md index 69083c9..0b2184e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,751 +1,65 @@ # AGENTS.md -This file provides guidance to AI agents (including Claude Code, Cursor, and other LLM-powered tools) when working with code in this repository. - -## CRITICAL REQUIREMENTS - -### Test Success -- ALL tests MUST pass for code to be considered complete and working -- Never describe code as "working as expected" if there are ANY failing tests -- Even if specific feature tests pass, failing tests elsewhere indicate broken functionality -- Changes that break existing tests must be fixed before considering implementation complete -- A successful implementation must pass linting, type checking, AND all existing tests - -## Project Overview - -gp-libs is a Python library providing internal utilities and extensions for git-pull projects. It focuses on extending Sphinx documentation and pytest functionality with support for docutils-compatible markup formats. - -Key features: -- **doctest_docutils**: Reimplementation of Python's doctest with support for reStructuredText and Markdown -- **pytest_doctest_docutils**: pytest plugin for running doctests in documentation files -- **linkify_issues**: Sphinx extension that converts issue references (e.g., `#123`) to hyperlinks -- Supports testing doctest examples in `.rst` and `.md` files -- Powers documentation testing across the git-pull ecosystem - -## Development Environment - -This project uses: -- Python 3.10+ -- [just](https://github.com/casey/just) for command running (see also https://just.systems/) -- [uv](https://github.com/astral-sh/uv) for dependency management -- [ruff](https://github.com/astral-sh/ruff) for linting and formatting -- [mypy](https://github.com/python/mypy) for type checking -- [pytest](https://docs.pytest.org/) for testing - - [pytest-watcher](https://github.com/olzhasar/pytest-watcher) for continuous testing - -## Common Commands - -### Setting Up Environment - -```bash -# Install dependencies -uv pip install --editable . -uv pip sync - -# Install with development dependencies -uv pip install --editable . -G dev -``` - -### Running Tests - -```bash -# Run all tests -just test -# or directly with pytest -uv run pytest - -# Run a single test file -uv run pytest tests/test_doctest_docutils.py - -# Run a specific test -uv run pytest tests/test_doctest_docutils.py::test_function_name - -# Run tests with test watcher -just start -# or -uv run ptw . - -# Run tests with doctests -uv run ptw . --now --doctest-modules -``` - -### Linting and Type Checking - -```bash -# Run ruff for linting -just ruff -# or directly -uv run ruff check . - -# Format code with ruff -just ruff-format -# or directly -uv run ruff format . - -# Run ruff linting with auto-fixes -uv run ruff check . --fix --show-fixes - -# Run mypy for type checking -just mypy -# or directly -uv run mypy src tests - -# Watch mode for linting (using entr) -just watch-ruff -just watch-mypy -``` - -### Development Workflow - -Follow this workflow for code changes (see `.cursor/rules/dev-loop.mdc`): - -1. **Format First**: `uv run ruff format .` -2. **Run Tests**: `uv run pytest` -3. **Run Linting**: `uv run ruff check . --fix --show-fixes` -4. **Check Types**: `uv run mypy` -5. **Verify Tests Again**: `uv run pytest` - -### Documentation - -```bash -# Build documentation -just build-docs - -# Start documentation server with auto-reload -just start-docs - -# Update documentation CSS/JS -just design-docs -``` - -## Code Architecture - -gp-libs provides utilities for documentation testing and Sphinx extensions: - -``` -src/ -├── doctest_docutils.py # Core doctest reimplementation -├── pytest_doctest_docutils.py # pytest plugin -├── linkify_issues.py # Sphinx extension -├── docutils_compat.py # Compatibility layer -└── gp_libs.py # Package metadata -``` - -### Core Modules - -1. **doctest_docutils** (`src/doctest_docutils.py`) - - Reimplementation of Python's standard library `doctest` module - - Supports docutils-compatible markup (reStructuredText and Markdown) - - Handles `doctest_block`, `.. doctest::` directive, and ` ```{doctest} ` code blocks - - PEP-440 version specifier support for conditional tests - - Can be run directly: `python -m doctest_docutils README.md -v` - -2. **pytest_doctest_docutils** (`src/pytest_doctest_docutils.py`) - - pytest plugin integrating doctest_docutils with pytest - - Collects and runs doctests from `.rst` and `.md` files - - Full pytest fixture and conftest.py support - - Registered as `pytest11` entry point - -3. **linkify_issues** (`src/linkify_issues.py`) - - Sphinx extension for automatic issue linking - - Converts `#123` references to clickable hyperlinks - - Configured via `issue_url_tpl` in Sphinx conf.py - -4. **docutils_compat** (`src/docutils_compat.py`) - - Compatibility layer for cross-version docutils support - - Provides `findall()` abstraction for different docutils versions - -5. **gp_libs** (`src/gp_libs.py`) - - Package metadata (version, title, author, URLs) - -## Testing Strategy - -gp-libs uses pytest for testing with custom fixtures. The test suite includes: - -- Unit tests for doctest parsing and execution -- Integration tests for pytest plugin functionality -- Sphinx app factory for testing extensions - -### Test Structure - -``` -tests/ -├── test_doctest_docutils.py # Tests for doctest module -├── test_pytest_doctest_docutils.py # Tests for pytest plugin -├── test_linkify_issues.py # Tests for linkify extension -├── conftest.py # Fixtures and sphinx app factory -└── regressions/ # Regression tests -``` - -### Testing Guidelines - -1. **Use functional tests only**: Write tests as standalone functions, not classes. Avoid `class TestFoo:` groupings - use descriptive function names and file organization instead. - -2. **Use existing fixtures over mocks** (see `.cursor/rules/dev-loop.mdc`) - - Use fixtures from conftest.py instead of `monkeypatch` and `MagicMock` when available - - Document in test docstrings why standard fixtures weren't used for exceptional cases - -3. **Preferred pytest patterns** - - Use `tmp_path` (pathlib.Path) fixture over Python's `tempfile` - - Use `monkeypatch` fixture over `unittest.mock` - -4. **Running tests continuously** - - Use pytest-watcher during development: `uv run ptw .` - - For doctests: `uv run ptw . --now --doctest-modules` - -## Coding Standards - -For detailed coding standards, refer to `.cursor/rules/dev-loop.mdc`. Key highlights: - -### Imports - -- **Use namespace imports for stdlib**: `import enum` instead of `from enum import Enum`; third-party packages may use `from X import Y` -- **For typing**, use `import typing as t` and access via namespace: `t.NamedTuple`, etc. -- **Use `from __future__ import annotations`** at the top of all Python files - -### Docstrings - -Follow NumPy docstring style for all functions and methods (see `.cursor/rules/dev-loop.mdc`): - -```python -"""Short description of the function or class. - -Detailed description using reStructuredText format. - -Parameters ----------- -param1 : type - Description of param1 -param2 : type - Description of param2 - -Returns -------- -type - Description of return value -""" -``` - -**Classes with fields** — `NamedTuple`, dataclasses — document every field in -an `Attributes` section: - -```python -class ConsoleExample(t.NamedTuple): - """Console example collected from a Markdown page. - - Attributes - ---------- - path : pathlib.Path - Markdown file the example was collected from. - """ -``` - -Autodoc renders every field whether or not you describe it, so an -undocumented `NamedTuple` field ships to the API docs as "Alias for field -number 0" and a dataclass field ships bare. Document all of them — a class -with three fields and two documented still ships a stub for the third. - -### Doctests - -**All functions and methods MUST have working doctests.** Doctests serve as both documentation and tests. - -**CRITICAL RULES:** -- Doctests MUST actually execute - never comment out function calls or use placeholder output -- Doctests MUST NOT be converted to `.. code-block::` as a workaround (code-blocks don't run) -- If you cannot create a working doctest, **STOP and ask for help** - -**Available tools for doctests:** -- `doctest_namespace` fixtures: `tmp_path` (add more via `conftest.py`) -- Ellipsis for variable output: `# doctest: +ELLIPSIS` -- PEP-440 version specifiers via `is_allowed_version()` for version-conditional tests - -**`# doctest: +SKIP` is NOT permitted** - it's just another workaround that doesn't test anything. Use the fixtures and ellipsis patterns properly. - -**Simple doctest example:** -```python ->>> is_allowed_version('3.3', '<=3.5') -True ->>> is_allowed_version('3.3', '>3.2, <4.0') -True -``` - -**When output varies, use ellipsis:** -```python ->>> parse_document(content) # doctest: +ELLIPSIS - -``` - -**Additional guidelines:** -1. Use narrative descriptions for test sections rather than inline comments -2. Move complex examples to dedicated test files at `tests/examples//test_.py` -3. Keep doctests simple and focused on demonstrating usage -4. Add blank lines between test sections for improved readability - -### Logging Standards - -These rules guide future logging changes; existing code may not yet conform. - -#### Logger setup - -- Use `logging.getLogger(__name__)` in every module -- Add `NullHandler` in library `__init__.py` files -- Never configure handlers, levels, or formatters in library code — that's the application's job - -#### Structured context via `extra` - -Pass structured data on every log call where useful for filtering, searching, or test assertions. - -**Core keys** (stable, scalar, safe at any log level): - -| Key | Type | Context | -|-----|------|---------| -| `doctest_source_file` | `str` | doctest source path (.rst, .md, .py) | -| `doctest_block_type` | `str` | block type (doctest_block, code fence) | -| `sphinx_extension` | `str` | Sphinx extension name | - -Treat established keys as compatibility-sensitive — downstream users may build dashboards and alerts on them. Change deliberately. - -#### Key naming rules - -- `snake_case`, not dotted; project-specific prefixes (`doctest_`, `sphinx_`) -- Prefer stable scalars; avoid ad-hoc objects - -#### Lazy formatting - -`logger.debug("msg %s", val)` not f-strings. Two rationales: -- Deferred string interpolation: skipped entirely when level is filtered -- Aggregator message template grouping: `"Running %s"` is one signature grouped ×10,000; f-strings make each line unique - -When computing `val` itself is expensive, guard with `if logger.isEnabledFor(logging.DEBUG)`. - -#### stacklevel for wrappers - -Increment for each wrapper layer so `%(filename)s:%(lineno)d` and OTel `code.filepath` point to the real caller. Verify whenever call depth changes. - -#### Log levels - -| Level | Use for | Examples | -|-------|---------|----------| -| `DEBUG` | Internal mechanics | Doctest parsing, node traversal steps | -| `INFO` | Lifecycle, user-visible operations | Extension loaded, document processed | -| `WARNING` | Recoverable issues, deprecation | Deprecated directive, missing optional dependency | -| `ERROR` | Failures that stop an operation | Parse error, invalid configuration | - -#### Message style - -- Lowercase, past tense for events: `"extension loaded"`, `"parse error"` -- No trailing punctuation -- Keep messages short; put details in `extra`, not the message string - -#### Exception logging - -- Use `logger.exception()` only inside `except` blocks when you are **not** re-raising -- Use `logger.error(..., exc_info=True)` when you need the traceback outside an `except` block -- Avoid `logger.exception()` followed by `raise` — this duplicates the traceback. Either add context via `extra` that would otherwise be lost, or let the exception propagate - -#### Testing logs - -Assert on `caplog.records` attributes, not string matching on `caplog.text`: -- Scope capture: `caplog.at_level(logging.DEBUG, logger="doctest_docutils")` -- Filter records rather than index by position: `[r for r in caplog.records if hasattr(r, "doctest_source_file")]` -- Assert on schema: `record.sphinx_extension == "doctest_docutils"` not `"doctest_docutils" in caplog.text` -- `caplog.record_tuples` cannot access extra fields — always use `caplog.records` - -#### Avoid - -- f-strings/`.format()` in log calls -- Unguarded logging in hot loops (guard with `isEnabledFor()`) -- Catch-log-reraise without adding new context -- `print()` for diagnostics -- Logging secret env var values (log key names only) -- Non-scalar ad-hoc objects in `extra` -- Requiring custom `extra` fields in format strings without safe defaults (missing keys raise `KeyError`) - -### Git Commit Standards - -See `.cursor/rules/git-commits.mdc` for detailed commit message standards. - -Format commit messages as: -``` -Scope(type[detail]): concise description - -why: Explanation of necessity or impact. - -what: -- Specific technical changes made -- Focused on a single topic -``` - -Keep the subject ≤50 chars (excluding any trailing `(#NN)` PR ref); wrap -body lines at ≤72 chars. Separate the `why:` and `what:` blocks with a -blank line. - -Common commit types: -- **feat**: New features or enhancements -- **fix**: Bug fixes -- **refactor**: Code restructuring without functional change -- **docs**: Documentation updates -- **chore**: Maintenance (dependencies, tooling, config) -- **test**: Test-related updates -- **style**: Code style and formatting -- **py(deps)**: Dependencies -- **py(deps[dev])**: Dev Dependencies -- **ai(rules[AGENTS])**: AI rule updates -- **ai(claude[rules])**: Claude Code rules (CLAUDE.md) -- **ai(claude[command])**: Claude Code command changes - -Example: -``` -doctest_docutils(feat[parse]): Add support for myst-parser code blocks - -why: Enable doctest execution in Markdown documentation files - -what: -- Add detection for ```{doctest} fence syntax -- Register myst directives automatically -- Add tests for Markdown doctest parsing -``` -#### Release commits - -Never create tags. Never push tags. The user handles tagging and tag -pushes (tags trigger the CI publish workflow). - -Release commit subjects are plain and short: `Tag v`. Put -the detailed why/what in the commit body. Don't use the -`Scope(type[detail]):` format for releases — don't bury the lede. - -For multi-line commits, use heredoc to preserve formatting: -```bash -git commit -m "$(cat <<'EOF' -feat(Component[method]) add feature description - -why: Explanation of the change. - -what: -- First change -- Second change -EOF -)" -``` - -## Documentation Standards - -### Code Blocks - -Code blocks are paste-and-run units: pasting one block runs exactly one -intended action. Doctests and other executed examples are exempt — the test -suite runs them, nobody pastes them. - -- **One command per block.** Multiple steps may share a block only when - explicitly chained with `&&`, `;`, or `\` continuations — the chain is - then one logical command. -- **Explanations go in prose above the block**, never as `#` comments inside it. -- **Command menus are per-command blocks with prose lead-ins**, not tables. -- **Shell commands use the `console` tag with a `$ ` prefix.** This separates - interactive commands from scripts and enables prompt-aware copy. -- **Split long commands with `\`** — one flag or flag+value pair per indented - continuation line, positional arguments last. - -Good: - -Show the last ten commits as a graph: - -```console -$ git log \ - --max-count=10 \ - --graph \ - --oneline -``` - -Bad: - -```console -# Show the last ten commits as a graph -$ git log --max-count=10 --graph --oneline -``` - -### Changelog Conventions - -These rules apply when authoring entries in `CHANGES`, which is rendered as the Sphinx changelog page. Modeled on Django's release-notes shape — deliverables get titles and prose, not bullets. Older entries used a flat `### Section` + bullet shape; new entries follow the Django shape below. - -**Release entry boilerplate.** Every release header is `## gp-libs X.Y.Z (YYYY-MM-DD)`. The file opens with a `## gp-libs X.Y.Z (unreleased)` placeholder block fenced by `` and `` HTML comments — new release entries land immediately below the END marker, never above it. - -**Open with a multi-sentence lead paragraph.** Plain prose, no italic. Open with the version as sentence subject (*"gp-libs X.Y.Z ships …"*) so the lead is self-contained when excerpted. Two to four sentences telling the reader what shipped and who cares — user-visible takeaways, not internal mechanism. Cross-reference detail docs with `{ref}` to keep the lead compact. - -**Lead paragraphs are release-time material — off-limits to branches and PRs.** The unreleased entry carries no lead paragraph and no version summary: sections only (`### Breaking changes`, `### What's new` deliverables, `### Fixes`, …). Speaking for the release — what the version "is", "ships", or "focuses on" — is presumptuous before its scope is final; only the person cutting the release writes that, and only when the user explicitly asks to release. Never write or edit a lead from a feature branch, and never ask or imply that a release should happen. - -**Each deliverable is a section, not a bullet.** Inside `### What's new`, every distinct deliverable gets a `#### Deliverable title (#NN)` heading naming it in user vocabulary, followed by 1-3 prose paragraphs explaining what shipped. Don't wrap a paragraph in `- ` — bullets are for enumerable lists, not paragraph containers. Cross-link detail docs (`See {ref}\`foo\` for details.`) so prose stays focused. - -**The deliverable test.** Before writing an entry, ask: "What's the deliverable, in user vocabulary?" If you can't answer in one sentence, the entry isn't ready. Mechanism (helper internals, byte counters, schema-validation locations) belongs in PR descriptions and code comments, not the changelog. - -**Fixed subheadings**, in this order when present: `### Breaking changes`, `### Dependencies`, `### What's new`, `### Fixes`, `### Documentation`, `### Development`. Dev tooling (helper scripts, internal automation) lives under `### Development`. For breaking changes, show the migration path with concrete inline code (e.g. a `# Before` / `# After` fenced code block). Dependency floor bumps use the form ``Minimum `pkg>=X.Y.Z` (was `>=X.Y.W`)``. - -**PR refs `(#NN)`** sit in each deliverable's `####` heading. - -**When bullets are appropriate.** Catch-all sections (`### Fixes`, occasionally `### Documentation`) with 3+ genuinely small items use bullets — one line each, never paragraphs. If a bullet swells past two lines, promote it to a `#### Title (#NN)` heading with prose body. - -**Anti-patterns.** - -- Fragile metrics: token ceilings, third-party version pins, percent benchmarks, exact byte counts. Describe the *capability*, not the math. -- Internal jargon: private symbols (leading-underscore identifiers), algorithm names exposed for the first time, backend scaffolding. -- Walls of text dressed up as bullets. -- Buried breaking changes — they get their own subheading at the top of the entry. - -**Always link autodoc'd APIs.** Any class, method, function, exception, or attribute that has its own rendered page must be cited via the appropriate role (`{class}`, `{meth}`, `{func}`, `{exc}`, `{attr}`) — never with plain backticks. Doc pages without explicit ref labels use `{doc}`. Plain backticks are correct for code syntax, env vars, parameter names, and file paths that aren't doc pages — anything without an autodoc destination. - -**MyST roles.** Class references use `{class}`, methods use `{meth}`, functions use `{func}`, exceptions use `{exc}`, attributes use `{attr}`, internal anchors use `{ref}`, doc-path links use `{doc}`. - -**Summarization style.** When a user asks "what changed in the latest version?" or similar, lead with the entry's lead paragraph (paraphrased if needed), followed by each `####` deliverable heading under `### What's new` with a one-sentence summary. Cite `(#NN)` only if the user asks for source links. Don't invent versions, dates, or numbers not present in `CHANGES`. Don't quote line numbers or file offsets — those shift as the file evolves. - -## Debugging Tips - -See `.cursor/rules/avoid-debug-loops.mdc` for detailed debugging guidance. - -When stuck in debugging loops: - -1. **Pause and acknowledge the loop** -2. **Minimize to MVP**: Remove all debugging cruft and experimental code -3. **Document the issue** comprehensively for a fresh approach -4. **Format for portability** (using quadruple backticks) - -## Sphinx/Docutils-Specific Considerations - -### Directive Registration - -- Use `_ensure_directives_registered()` to auto-register required directives -- Supports myst-parser directives (`{doctest}`, `{tab}`) -- Handles both reStructuredText and Markdown syntax - -### Document Parsing - -- Uses docutils for parsing `.rst` files -- Uses myst-parser for parsing `.md` files -- Both formats support doctest blocks - -### linkify_issues Configuration - -In your Sphinx `conf.py`: -```python -extensions = ["linkify_issues"] -issue_url_tpl = "https://github.com/git-pull/gp-libs/issues/{issue_id}" -``` - -## References - -- Documentation: https://gp-libs.git-pull.com/ -- GitHub: https://github.com/git-pull/gp-libs -- PyPI: https://pypi.org/project/gp-libs/ - -## Comments earn their maintenance cost - -A comment ships only if it passes all three gates. Fail any: delete or rewrite. -Borderline: delete — borderline means the information is reconstructible, which -is what makes deletion cheap. - -**Loss.** Three years from now, would losing this cost a maintainer real time -rediscovering intent, an invariant, a constraint, or a failure mode the code and -tests do not already make obvious? - -**Elite.** Would SQLite, Redis, the Go standard library, or CPython write this -comment, at this length? Those projects state the constraint and stop. They do -not argue with an imagined objector. - -**Upkeep.** Will it stay true without maintenance? A comment that hand-syncs a -value the code owns — a count, an offset, a line reference, a duplicated -constant — is false the first time that value moves. - -### Ceiling - -One or two lines. A comment reaching four is either carrying several facts, in -which case split it, or arguing, in which case cut it to the fact. - -Rationale, alternatives weighed, and the story of how the code got here belong -in the commit message: timestamped, attached to the exact diff, and free to -maintain. - -A comment often holds both a constraint and the deliberation that found it. Keep -the constraint, cut the deliberation. "Runs at most once per second" survives; -"this is the right trade for now" does not. - -### Keep - -- Why over how: upstream quirks, protocol and compatibility constraints, - performance tradeoffs still part of the contract. -- Invariants, preconditions, ordering, lifetime, and concurrency requirements - that types and tests cannot express. -- Code that looks wrong but is not, so a later cleanup does not reintroduce the - bug. -- A high-level sketch of an algorithm whose local operations do not reveal the - whole. - -### Delete - -- Narration of the next lines; code translated into English. -- Restated names, types, defaults, or control flow. -- Values duplicated from the code and hand-synced. -- Justification, hedging, or apology for a choice. -- Speculation about future requirements. -- History version control already holds, including commented-out code. -- Ticket and issue numbers. They say nothing to a reader without tracker access, - and they rot when the tracker moves. Unfinished work goes in the tracker, not - the source. -- Transient observations — "currently", "for now", "the latest release" — - that go stale with no nearby edit. - -### The upkeep gate in practice - -It reaches values that track our own code. It does not reach frozen external -facts. - -Bad (Delete): - -```python -# There are 321 tests to complete for servers. -``` - -Good (Keep): - -```python -# CPython < 3.11 has no ExceptionGroup, so this branch stays. -``` - -### Documentation exception - -Doctests, minimal usage examples, and param, return, and raises lines on public -API are exempt from the loss gate — they serve the caller, not the maintainer. -They are exempt from nothing else. Ceiling: a good man page entry. - -NumPy-style `Parameters`, `Returns`, and `Attributes` sections and executable -doctests fall under this exception — autodoc ships every field whether or not -you describe it, and a doctest that runs is also a test. - -## AI Slop Prevention - -Treat AI slop as **review-hostile noise**, not as proof that text or -code is wrong. The goal is to maximize information density by removing -artifacts that make the repository harder to trust or navigate. - -### The Anti-Slop Rubric - -Before committing, audit all AI-assisted changes for these noise -patterns: - -- **AI Signatures:** Remove "Generated by", footers, conversational - filler ("Certainly!", "Here is..."), unexplained emojis (🤖, ✨), and - AI-tool metadata. -- **Brittle References:** Avoid hard-coded line numbers, fragile - file/test counts, dated "as of" claims, bare SHAs, and local - absolute paths unless they are strict evidentiary artifacts (e.g., - benchmark logs). -- **Diff Narration:** Do not restate what moved, was renamed, or was - removed in artifacts the downstream reader holds: code, docstrings, - README, CHANGES, PR descriptions, or release notes. The diff and - commit message already carry this history. -- **Branch-Internal Narrative:** Do not mention intermediate branch - states, abandoned approaches, or "no longer" behavior unless users - of a published release actually experienced the old state (**The - Published-Release Test**). -- **Low-Value Scaffolding:** Remove ownerless TODOs (`TODO: revisit`), - unused future-proofing, debug artifacts, and defensive wrappers that - do not protect a currently reachable failure mode. -- **Prose Inflation:** Replace generic AI "tells" like *comprehensive, - robust, seamless, production-ready, leverage, delve, tapestry,* and - *best practices* with concrete descriptions of behavior, - constraints, or trade-offs. -- **Coded Labels:** Write rules, options, and findings as plain - imperatives. Don't tag them with codes like `[R1]`, `A1`, or - `Option B` in artifacts a human reads — the reader shouldn't have to - decode an index. Internal agent bookkeeping may use ids; shipped text - may not. - -### Durable Source Links - -Link to a pinned revision, never to trunk. A pinned permalink is not a -brittle reference; an unlinked SHA dropped into prose is. `blob/master/…` -links rot silently — the file moves, lines shift, and the anchor lands -on unrelated code while still resolving. - -- Prefer a release tag (`blob/v1.4.0/…`). Most durable, and it tells - the reader which released version the claim held for. -- Otherwise use a 7-char commit ref (`blob/9a29b1a/…`) reachable from - trunk. Use when there is no tag or the claim is about unreleased - code. Never a PR-head SHA — it can be rebased or garbage-collected. -- Reserve `blob/master/…` for living documents meant to always show the - latest state, such as a contributing guide. -- Line anchors (`#L120-L145`) are only safe on a pinned ref. - -### Preservation & Context - -Subjective cleanup must never remove load-bearing rationale. Adjudicate -comments with the comment policy above; borderline cases are deleted, not -kept. - -- **Preserve the "Why":** You MUST NOT delete comments that document - invariants, protocol constraints, platform quirks, security - boundaries, and upstream workarounds. -- **Evidence is Immune:** Preserve exact counts, dates, and SHAs when - they serve as evidence in benchmark results, release notes, stack - traces, or lockfiles. -- **Behavior Over Inventory:** A useful description explains what - changed for the *system or user*; it does not provide an inventory - of files or functions the diff already shows. - -### The Published-Release Test - -Long-running branches accumulate tactical decisions — renames, -refactors, attempts-then-reverts. When deciding what counts as -branch-internal, use trunk or the parent branch as the baseline — not -intermediate states inside the current branch. Ask: - -> Did users of the most recently published release ever experience -> this old name, old behavior, or bug? - -If the answer is **no**, it is branch-internal narrative. Move it to -the commit message and describe only the final state in the artifact. - -**Keep in shipped artifacts:** -- Deprecations and migration guides for symbols that actually shipped. -- `### Fixes` entries for bugs that affected users of a published - release. -- Comments explaining *why the current code looks this way* - (invariants, platform quirks) that make sense to a reader who never - saw the previous version. - -### Cleanup in Hindsight - -When applying these rules retroactively from inside a feature branch, -first establish scope by diffing against the parent branch (or trunk) -to identify which commits this branch actually introduced. Then: - -- **In-branch commits:** Prompt the user with two options: `fixup!` - commits with `git rebase --autosquash` to address each causal commit - at its source, or a single cleanup commit at branch tip. -- **Trunk/Parent commits:** Default to leaving them alone. Act only on - explicit user instruction. If the user opts in, fold the cleanup - into a single commit at branch tip; do not rewrite shared history. -- **Scope guard:** If cleaning prior slop would touch a colleague's - work or expand the branch beyond its stated goal, stay in lane: - protect the current goal and leave prior slop alone. - -### Change Discipline - -- Make the smallest coherent change that solves the verified problem; - keep unrelated cleanup out of it. -- Reuse an existing file, component, helper, API, or test before adding - a new one. Modify in place when the change fits the file's - responsibility. -- Keep new APIs private until a caller outside the module needs them. +gp-libs is the doctest and documentation-testing tooling for the git-pull +fleet: it makes `>>> ` examples in `.py`, `.rst`, and `.md` files runnable +as pytest tests, and links issue references in Sphinx docs. Other repos in +the fleet depend on its collector; treat its public behaviour as shared +infrastructure, not this repo's private concern. + +Follow the conventions already in the tree, and keep a change scoped to +what was asked for. + +## What is here + +| Path | What it is | +| ---- | ---------- | +| `src/doctest_docutils.py` | doctest reimplementation that parses reStructuredText and Markdown | +| `src/pytest_doctest_docutils.py` | pytest plugin (`pytest11` entry point, key `sphinx`) that collects those doctests | +| `src/linkify_issues.py` | Sphinx extension: `#123` becomes an issue link | +| `src/docutils_compat.py` | cross-version docutils compatibility shim (`findall`) | +| `src/gp_libs.py` | package metadata (version, title, URLs) | +| `tests/` | unit and regression tests; `tests/conftest.py` provides Sphinx app fixtures | +| `docs/` | Sphinx/MyST documentation; dogfoods the doctest collector it documents | +| `CHANGES` | changelog, rendered at `docs/history.md` | + +## Which policy applies + +- Documentation, user-facing text, `CHANGES`, release notes, commit + messages, docstrings, and source comments: + [.github/WRITING.md](.github/WRITING.md) +- Environment, the gates, tests, documentation builds, releases, and pull + requests: [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) + +Each of those is the single home for its subject. Where a rule seems to be +stated twice, the file listed above is the one that governs. + +## Change discipline + +- Make the smallest coherent change that solves the verified problem; keep + unrelated cleanup out of it. +- Reuse an existing file, helper, API, or test before adding a new one. - Add a file only for a durable boundary — a distinct responsibility, - independent reuse, or splitting an oversized high-touch module — not - for a single-use helper or a one-line re-export. - -### Keep Instructions Lean + independent reuse, or splitting an oversized module — not for a + single-use helper or a one-line re-export. +- Add a test for every user-visible behaviour change, and a `CHANGES` + entry for every change to the public API, CLI, configuration, or + output. +- A passing gate is evidence only once it has been shown capable of + failing. Pair a new test with a deliberate break that proves it bites. + +`.py` files delegate straight to pytest's own `DoctestModule`; `.rst` and +`.md` files go through this package's own `DocutilsDocTestFinder`. Of the +non-stdlib doctest flags the pytest plugin registers, only `HIDE` is a +gp-libs invention — `ALLOW_UNICODE`, `ALLOW_BYTES`, and `NUMBER` are +borrowed from pytest's own (blocked) doctest plugin to keep behaviour +consistent. See +[Documented examples that run](.github/WRITING.md#documented-examples-that-run) +before changing collection, directive, or flag behaviour — it is the +fullest account of the mechanism in the fleet, and other repos rely on it +staying accurate. -Treat this file like code and prune it. +## References -- Delete a line whose removal would not cause a mistake. -- Move multi-step procedures into skills, path-specific rules into - nested AGENTS.md files, and hard limits into hooks or CI. -- Keep only non-obvious, broadly applicable defaults here. Anything a - reader can infer from the code, a manifest, or a linter does not - belong. +- Documentation: +- GitHub: +- PyPI: diff --git a/README.md b/README.md index 850dfc9..0c3411d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # gp-libs · [![Python Package](https://img.shields.io/pypi/v/gp-libs.svg)](https://pypi.org/project/gp-libs/) [![License](https://img.shields.io/github/license/git-pull/gp-libs.svg)](https://github.com/git-pull/gp-libs/blob/master/LICENSE) [![Code Coverage](https://codecov.io/gh/git-pull/gp-libs/branch/master/graph/badge.svg)](https://codecov.io/gh/git-pull/gp-libs) -Incubating / [dogfooding] some sphinx extensions and pytest plugins on -git-pull projects, e.g. [cihai], [vcs-python], or [tmux-python]. +Sphinx extensions and pytest plugins shared across git-pull's projects, +developed by [dogfooding] them on [cihai], [vcs-python], and +[tmux-python]. [dogfooding]: https://en.wikipedia.org/wiki/Eating_your_own_dog_food [cihai]: https://github.com/cihai @@ -84,7 +85,7 @@ $ python -m doctest_docutils README.md -v ### pytest plugin -_This plugin disables [pytest's standard `doctest` plugin]._ +_This plugin blocks [pytest's standard `doctest` plugin]._ This plugin integrates `doctest_docutils` with pytest so documentation examples run with the surrounding `conftest.py` setup. @@ -104,9 +105,10 @@ See more: ### Plain-text issue linker (`linkify-issues`) -We need to parse plain text, e.g. #99999, to point to the project tracker at -https://github.com/git-pull/gp-libs/issues/99999. This way the markdown looks -good anywhere you render it, including GitHub and GitLab. +`linkify_issues` turns a plain-text issue reference, e.g. #99999, into a link +to the project tracker at https://github.com/git-pull/gp-libs/issues/99999. +The source text stays plain, so it still reads correctly wherever it is +rendered unprocessed, including GitHub and GitLab. #### Configuration @@ -141,7 +143,7 @@ $ pip install --user gp-libs ### Developmental releases -You can test the unpublished version of g before its released. +You can test the unpublished version of gp-libs before it's released. - [pip](https://pip.pypa.io/en/stable/): @@ -152,17 +154,16 @@ You can test the unpublished version of g before its released. # Minimum requirements To lift the development burden of supporting legacy APIs, as this package is -lightly used, minimum constraints have been pinned: +lightly used, a minimum constraint is pinned in `pyproject.toml`: -- docutils: 0.20.1+ -- myst-parser: 2.0.0+ +- docutils: 0.20+ -If you have even passing interested in supporting legacy versions, file an -issue on the tracker. +`myst-parser` has no minimum version pinned. If you have a passing interest in +supporting legacy versions, file an issue on the tracker. # More information -- Python support: >= 3.10, pypy +- Python support: >= 3.10 - Source: - Docs: - Changelog: diff --git a/docs/AGENTS.md b/docs/AGENTS.md deleted file mode 100644 index 456e4b7..0000000 --- a/docs/AGENTS.md +++ /dev/null @@ -1,147 +0,0 @@ -# Documentation voice - -This file covers the *voice* of prose under `docs/` — how to frame a -page so a reader meets the idea before its configuration. It -complements the repository-root `AGENTS.md`, which already governs -code blocks, doctest rules, changelog conventions, and MyST roles. -When the two overlap, the root file wins; this one only answers the -question it leaves open: how should the prose sound? - -## Who you are writing for - -The default reader is wiring gp-libs into their own project: pointing -pytest at `docs/` with `pytest_doctest_docutils`, running -`python -m doctest_docutils README.md` by hand, or adding -`linkify_issues` to a Sphinx `conf.py`. They are fluent in pytest and -Sphinx as users — `conftest.py`, fixtures, `extensions`, `testpaths` — -and write reStructuredText or Markdown daily, but you cannot assume -they know gp-libs' internals: docutils node traversal, directive -registration, or how the finder decides a block is a doctest. - -A second, smaller reader works *on* gp-libs or against its lower -layers: the doctest finder, the docutils compatibility shims, -myst-parser directive registration, or contributing. Serve them too, -but mark their material opt-in ("for the rarer cases", "advanced") so -the default reader knows they can stop. Never make the common case pay -a comprehension tax for the advanced one. - -## Voice - -- **Second person, present tense, active.** "You point pytest at - `docs/`", not "Files are collected". Address the reader who is doing - the thing. -- **Concept before configuration.** Open by saying what the tool *is* - and what it does for the reader. The `conf.py` key, the pytest - flag — those are the last details they need, not the first. A page - that opens with "set these keys" has buried the idea under its - mechanics. -- **Say when they can stop.** Lead with the default and the - reassurance: install the plugin and `pytest docs/` just works; - `issue_url_tpl` is the one setting `linkify_issues` needs. Let a - skimmer leave after one paragraph. -- **Progressive disclosure.** Order by how many readers need it: the - default → the one option a few will tune (a custom `issue_re`, - `--doctest-docutils-modules`) → running `doctest_docutils` directly - → the docutils machinery underneath. Each step is for a smaller - audience than the last. -- **Lean on the pipeline.** The reader thinks in a chain: a `.rst` or - `.md` file is parsed (docutils, with myst-parser for Markdown), its - examples are collected, then run. It is the mental model the whole - toolkit hangs on; reinforce that chain when you explain why Markdown - needs myst-parser or why a fixture needs a visible `conftest.py`. -- **Name the trade-off.** If a choice costs something — the plugin - disables pytest's standard doctest plugin, Markdown support goes - through myst-parser, fixtures reach only files a `conftest.py` can - see — say so, and say what it buys. State it; don't sell it. -- **Frame by concept, not by mechanism.** Don't headline a feature by - its `conf.py` key or pytest flag in prose; that names the - implementation surface, the reader's last concern. Name the concept. - The mechanics vocabulary — the flag spelling, the default regex — - belongs in a reference block or the API section, and only there. - -## Examples that run - -Prose examples under `docs/` are doctests, and they actually execute — -`testpaths` in `pyproject.toml` includes `docs`, and pytest collects -them with this repo's own `pytest_doctest_docutils` plugin. The docs -dogfood the tool they describe; a broken example is a failing test. - -- Fence a `>>>` session as a ```` ```python ```` block or a - ```` ```{doctest} ```` directive — the finder collects both, plus - bare doctest blocks in reST. Use ```` ```console ```` for shell - commands at a `$` prompt. -- `ELLIPSIS` and `NORMALIZE_WHITESPACE` are on globally via - `doctest_optionflags`, so variable output can elide with `...` - without a per-example flag. -- No `doctest_namespace` fixtures are registered for `docs/` — no - `conftest.py` is visible to it — so keep examples self-contained: - import what you use inside the block. -- Console fences are checked by - `tests/test_docs_console_examples.py`. Keep one `$` prompt command per - block; safe `python -m doctest_docutils ...` examples with existing - local targets run in a temp-home sandbox, while install, watch, - server, git, and full-suite commands are policy-validated only. - -## What stays precise - -Warm the framing, never the facts. Resolution-order lists, default -regex patterns like `issue_re`, exact flag spellings, error strings, -and class or function cross-references carry meaning in their exact -form — leave them alone. The friendly voice belongs in the sentences -*around* a precise block, introducing it, not inside it paraphrasing -it into vagueness. - -## Cross-references - -Point the advanced reader at the deep-dive rather than inlining it, and -put the link where their interest peaks — on the phrase that made them -curious ("how the finder decides", "the docutils machinery") — not as -a standalone footnote the eye skips. Use the MyST roles listed in the -root `AGENTS.md` (`{class}`, `{meth}`, `{func}`, `{exc}`, `{attr}`, -`{ref}`, `{doc}`). A `{ref}` must match its target's anchor exactly — -anchors mix underscore and hyphen forms across pages -(`doctest_docutils`, `linkify-issues`). `just build-docs` catches a -broken cross-reference; the doctests do not — so build the docs before -you commit. - -Link the first prose mention of any symbol that has a useful -destination on that page. This includes Python objects, gp-libs APIs, -pytest and Sphinx concepts with intersphinx destinations, topic pages, -and external tools or projects. Use the most specific target -available: `{class}`, `{meth}`, `{func}`, `{mod}`, `{exc}`, or -`{attr}` for API objects; `{ref}` or `{doc}` for documentation pages -and section anchors; and a Markdown link or reference link for -external projects. After the first linked mention on a page, later -mentions can stay plain unless the distance or context makes another -link useful. - -Do not rely on a later reference section to satisfy the first-mention -rule. If the first occurrence would be a heading, grid-card teaser, or -introductory sentence, link that occurrence or retitle the heading so -the first prose mention can carry the link. Leave command examples, -code blocks, and literal configuration values as code; link the -surrounding prose instead. - -## A page that does this - -`docs/modules/linkify_issues/index.md` is the worked example: a concept-first -intro that says what the extension does (plain-text `#123` becomes a -link) before any `conf.py` key, a two-step default configuration most -readers can stop after, `issue_re` marked as optional tuning for the -smaller audience, an honest close that more complex needs mean -forking, and the API reference last. Read it before reshaping another -page. - -## Before you commit - -- Does the page open with what the feature *is*, or with how to - configure it? -- Can a reader who needs only the default stop after the first - paragraph? -- Is anything framed as "the key/the flag" that should be named by - concept instead? -- Are the advanced and internals-level parts clearly marked opt-in? -- Do the doctests still run — `uv run pytest docs/` — and did you - leave every code block, pattern, and cross-reference exact? -- Did `just build-docs` stay clean — no new warning, no broken - cross-reference? diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md deleted file mode 120000 index 47dc3e3..0000000 --- a/docs/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/docs/project/code-style.md b/docs/project/code-style.md index 18dd730..122a536 100644 --- a/docs/project/code-style.md +++ b/docs/project/code-style.md @@ -1,26 +1,7 @@ # Code Style -## Formatting - -gp-libs uses [ruff](https://github.com/astral-sh/ruff) for both linting and formatting. - -```console -$ uv run ruff format . -``` - -```console -$ uv run ruff check . --fix --show-fixes -``` - -## Type Checking - -Strict [mypy](https://mypy-lang.org/) is enforced across `src/` and `tests/`. - -```console -$ uv run mypy . -``` - -## Docstrings - -Follow [NumPy docstring style](https://numpydoc.readthedocs.io/en/latest/format.html) -for all public functions, methods, and classes. +Code style split across two documents. Formatting, linting, and +type-checking commands are in the +[gates section of `.github/CONTRIBUTING.md`](https://github.com/git-pull/gp-libs/blob/master/.github/CONTRIBUTING.md#the-gates). +Docstring conventions are in the +[docstrings section of `.github/WRITING.md`](https://github.com/git-pull/gp-libs/blob/master/.github/WRITING.md#docstrings). diff --git a/docs/project/contributing.md b/docs/project/contributing.md index 09ca4e9..74df72e 100644 --- a/docs/project/contributing.md +++ b/docs/project/contributing.md @@ -1,97 +1,7 @@ # Contributing -Install [git], [uv], and [just]. - -Clone: - -```console -$ git clone https://github.com/git-pull/gp-libs.git -``` - -```console -$ cd gp-libs -``` - -Install packages: - -```console -$ uv sync --all-extras --dev -``` - -## Tests - -```console -$ just test -``` - -### Automatically run tests on file save - -Use [pytest-watcher]: - -```console -$ just start -``` - -Use [entr(1)] when you want a shell-only watcher: - -```console -$ just watch-test -``` - -[pytest-watcher]: https://github.com/olzhasar/pytest-watcher - -## Documentation - -Build the docs: - -```console -$ just build-docs -``` - -Start the default preview server: - -```console -$ just start-docs -``` - -[sphinx-autobuild] builds the docs, watches for file changes, and launches the -preview server. - -From inside `docs/`, run the local docs justfile directly: - -```console -$ just start -``` - -[sphinx-autobuild]: https://github.com/executablebooks/sphinx-autobuild - -### Manual documentation - -Build from inside `docs/`: - -```console -$ just html -``` - -Serve the built HTML: - -```console -$ just serve -``` - -Watch and rebuild on file changes: - -```console -$ just watch -``` - -Watch and serve in one terminal: - -```console -$ just dev -``` - -[git]: https://git-scm.com/ -[uv]: https://github.com/astral-sh/uv -[just]: https://just.systems/ -[entr(1)]: http://eradman.com/entrproject/ +Contributing guidelines — environment setup, the gates, tests, documentation +builds, releases, and pull requests — moved to +[`.github/CONTRIBUTING.md`](https://github.com/git-pull/gp-libs/blob/master/.github/CONTRIBUTING.md). +Prose conventions moved to +[`.github/WRITING.md`](https://github.com/git-pull/gp-libs/blob/master/.github/WRITING.md). diff --git a/tests/test_docs_console_examples.py b/tests/test_docs_console_examples.py index 273104b..040adbe 100644 --- a/tests/test_docs_console_examples.py +++ b/tests/test_docs_console_examples.py @@ -76,8 +76,6 @@ def iter_documentation_markdown_files( relative = path.relative_to(repo_root) if "_build" in relative.parts: continue - if path.name in {"AGENTS.md", "CLAUDE.md"}: - continue yield path