Repository navigation
Docs harness: run inline blocks with a prelude, run docstring examples, and check lambda names and error checks #704
Description
Parent
What to build
184 inline Python blocks are only compiled, docstring examples never run, 12 error checks pass when the error stops happening, and 160 lambdas use one-letter parameters. This ticket adds the capabilities; page and docstring tickets use them; the enforce-guards ticket (#724) makes them mandatory.
- Inline blocks with a prelude. A fence marked
{test=run prelude=<name>}runs in a subprocess afterdocs/examples/_preludes/<name>.py, which declares models, connects to an in-memory SQLite database and opens a session; top-levelawaitworks. The existing{test=run}and{lint=check}keep working. - Docstring examples.
tests/test_docstring_examples.pycollects fenced Python examples from the docstrings of every module undersrc/ferro(via griffe) and runs those marked{test=run prelude=<name>}the same way. Unmarked examples are compiled only. - A shared reference prelude
docs/examples/_preludes/reference.py: one small running example (AuthorandPost, about four real-looking rows) for docstrings and the API reference. - Checks in
tests/_docs_checks.py: a lambda passed to a query method (where,select,order_by,include,exists,count,aggregate,group_by,after,beforeand any otherQuery/Modelmethod taking a callable) must take a parameter of two or more characters; atrywhoseexceptasserts must have anelsethat raises. They read Python files, Python fences in Markdown, and docstrings. Run aspython -m tests._docs_checks PATH...(files or directories); exit 1 listspath:line: message. Not wired into the test suite as a gate yet.
Acceptance criteria
- A Markdown fence
{test=run prelude=reference}containingprint(await Author.where(lambda author: author.id == 1).first())runs and its failure fails the test. -
tests/test_docstring_examples.pycollects every fenced example insrc/ferrodocstrings and runs the marked ones. -
python -m tests._docs_checks docs/exampleslists today's one-letter lambdas (about 66 in four scripts) and the 12 unfailing error checks, and exits 1. - Existing
tests/test_docs_examples.pyresults are unchanged (426 pass, the same skips).
Files owned
- Create:
tests/_docs_checks.py,tests/test_docs_checks.py,tests/test_docstring_examples.py,docs/examples/_preludes/reference.py,docs/examples/_preludes/__init__.py - Modify:
tests/test_docs_examples.py
Interfaces
- Consumes: nothing from other tickets
- Produces: Fence marker
{test=run prelude=<name>}for Markdown fences and docstring examples; preludes live indocs/examples/_preludes/<name>.py. Used by every page and docstring ticket. - Produces: Prelude
reference(Author,Post; connected, in a session). Used by the three docstring tickets (Docstrings: Model, Query and relations #719, Docstrings: connect, fields, checks, row security, raw SQL, sessions, settings and exceptions #720, Docstrings: the migrations API, PassReport and the CLI commands #721). - Produces:
tests/_docs_checks.py:Violation(path: Path, line: int, message: str),lambda_param_violations(path: Path) -> list[Violation],unfailing_error_checks(path: Path) -> list[Violation], CLIpython -m tests._docs_checks PATH...exiting 1 on any violation. Used by every page and docstring ticket's Verify, and made a test gate by the enforce-guards ticket (Enforce the docs guards: rendered-site checks, inline blocks run by default, lambda and error-check lint in the suite #724).
Test scenarios
- Happy path: a fence with
{test=run prelude=reference}that queriesAuthorpasses. - Error path: the same fence raising
RuntimeErrorfails its test with the block's path and line. - Edge case:
lambda author: author.idpasses the lambda check;lambda a: a.idfails it with the line number; a lambda passed tosorted()is ignored. - Edge case:
try: ... except X: assert ...withoutelseis reported; withelse: raise AssertionErrorit is not. - Integration: a docstring example in a scratch module marked
{test=run prelude=reference}is collected and run bytest_docstring_examples.py.
Verify
uv run --no-sync pytest tests/test_docs_checks.py tests/test_docstring_examples.py -q --no-cov -p no:cacheprovider
uv run --no-sync pytest tests/test_docs_examples.py -q --no-cov -p no:cacheprovider -n 8Blocked by
None (can start immediately)
Audit findings
Rows from the docs audit this ticket covers (file, line, rule, fix)
Test run
uv run --no-sync pytest tests/test_docs_examples.py tests/test_docs_migrations.py tests/test_documentation_features.py -q -p no:cacheprovider --no-cov
426 passed, 5 skipped, 0 failed (95.8 s). Skips:
tests/test_docs_migrations.py:739:FERRO_POSTGRES_URL is not set: this output needs Postgres(one Postgres-only output block).tests/test_documentation_features.py:687/709/733/756: four many-to-many tests, all@pytest.mark.skip(reason="Many-to-many join tables not automatically created - see coming-soon.md"). The reason is stale:docs/pages/coming-soon.mddoes not exist, anddocs/examples/relationships.py(m2m-usage) andexistence_tests.py(m2m) create M2M join tables throughauto_migrate=Trueand pass. These four tests should be unskipped or deleted.
Which tests execute which examples
| Test | What it executes |
|---|---|
tests/test_docs_examples.py::test_docs_example_scripts |
Every docs/examples/*.py not starting with _ (45 files), each as a subprocess on SQLite, asserting exit code 0. This is the only test that runs the non-migration examples. |
tests/test_docs_migrations.py::test_both_declaration_styles_generate_the_same_migrations |
The 7 migration pairs (migrations_{quickstart,backfill,rename,retire,retire_label,squash,testing} and their _annotated twins) a second time, and asserts both styles write byte-identical migrations/ trees. This is the only I-7 equivalence check in the repo. |
tests/test_docs_examples.py::test_docs_examples |
Inline page blocks (not examples): compile only (see the inline-blocks section). |
tests/test_documentation_features.py |
No docs file at all. It is 50 hand-copied re-implementations of guide features (section comments cite docs/guide/... and docs/getting-started/tutorial.md, paths that no longer exist). It cannot detect a docs example going stale. |
Executed by no test in a meaningful way:
testing_conftest.py: it runs as a script, butmain()is a no-op ("import smoke check only"). Thedbanddb_transactionfixtures the Testing how-to tells readers to copy are never run under pytest._migrations_project.py: a helper, imported by the migration examples (excluded by the_prefix, by design).- Postgres paths: every example runs on SQLite only.
row_level_security*.pyexercises the SQLite "PostgreSQL-only" warning, not the policies the page shows (see that section). - No twin equivalence check exists for the 11 non-migration pairs (quickstart, aggregations, existence_tests, partial_selects, populated_relations, predicates, relationships, row_level_security, soft_deletes, table_checks, timestamps, traversal). Each twin only has to run. Its
main()body is a smaller smoke test, not the parent's assertions. I diffed every embedded twin section by hand. All are equivalent apart from the declaration style (findings below).
Inline (non-snippet) code blocks in docs/pages: the rule 9 gap
pytest-examples finds 364 Python fences in docs/pages: 180 are snippet-backed (--8<--, excluded from the inline test) and 184 are inline. The inline test compiles each of the 184 with ast.PyCF_ALLOW_TOP_LEVEL_AWAIT and nothing more. The opt-ins {test=run} and {lint=check} exist in tests/test_docs_examples.py, but 0 of 184 blocks use either one. So 184 inline Python examples are syntax-checked only: undefined names, wrong APIs, stale signatures and I-8 violations all pass. Non-Python fences (text, sql, bash, toml, yaml) are untested except the <!-- output: id --> blocks test_docs_migrations.py compares on five schema pages.
| Page | Snippet-backed | Inline (compile only) |
|---|---|---|
guide/models-and-fields.md |
5 | 24 |
howto/migrate-from-sqlalchemy.md |
0 | 18 |
howto/upgrade-guide.md |
0 | 17 |
guide/row-level-security.md |
4 | 13 |
guide/connections.md |
2 | 11 |
guide/schema/auto-migrate.md |
0 | 10 |
concepts/identity-map.md |
0 | 8 |
guide/queries.md |
79 | 7 |
guide/mutations.md |
13 | 7 |
concepts/performance.md |
0 | 6 |
guide/schema/data-steps.md |
2 | 5 |
concepts/query-typing.md, concepts/type-safety.md, guide/relationships.md, guide/transactions.md, guide/schema/deploying.md, howto/testing.md |
0–15 | 4 each |
api/exceptions.md, concepts/architecture.md, guide/raw-sql.md, guide/schema/alembic.md, guide/schema/testing.md |
0–3 | 3 each |
api/migrations.md, concepts/backends.md, faq.md, howto/multiple-databases.md, howto/pagination.md, howto/soft-deletes.md, howto/timestamps.md |
0–3 | 2 each |
api/connection.md, getting-started/installation.md, guide/aggregations.md, guide/schema/migrations.md, reference/configuration.md |
0–14 | 1 each |
getting-started/quickstart.md, guide/schema/overview.md, howto/adopting-migrations.md, reference/cli.md |
all | 0 |
Evidence that compile-only lets defects through: 57 lines in page inline blocks use one-letter lambda parameters (I-8), e.g. howto/pagination.md lines 59–60 and its Annotated twin (lambda t: t.id > cursor for Article), guide/queries.md ×10, howto/upgrade-guide.md ×8, api/queries.md ×5. Also, the inline Assignment/Annotated tab pairs on pages such as guide/mutations.md:39–67 and howto/pagination.md:40–95 satisfy I-7's "compile-checked" clause, but no test checks that the two tabs declare the same model.
Cross-cutting patterns
- I-8 one-letter lambda parameters in four embedded examples: 66 lambdas (
aggregations24,partial_selects16,populated_relations15,existence_tests11), plus 6 in their twins' bodies and 57 lines in inline page blocks.predicates.py,traversal.py,relationships.pyandquickstart.pyshow the correct form. Fix the four files and add a lint test (an AST check ofdocs/examples/*.pyand inline blocks: a lambda passed towhere/select/order_by/include/existstakes a parameter of at least two characters, ideallysnake_case(Model)). - Error checks that cannot fail: 12
try/exceptblocks assert only insideexcept, with noelse(aggregations ×5, populated_relations ×3, partial_selects ×1, mutations ×3). The "loud limits" the pages advertise are untested.table_checks.py:38shows the fix. - Results hidden below the section marker in about 50 sections across 9 files. The reader sees a query, never its answer (rule 8). Only three snippets print anything, and no page shows printed output from an example.
- Placeholder data (rule 10) in 5 files:
"a1"/"b1"/"o1"/"ledger-a"/"Pair left/right", with tied totals that make a top-N example nondeterministic.relationships.py,predicates.pyandmigrations_backfill.pyshow the real-looking alternative. - No equivalent SQL beside any example (rule 13). Comments describe the SQL ("renders NOT EXISTS", "COUNT(column)") in 6 files, and the query pages show 5 SQL blocks for 101 Python blocks.
- 184 inline page blocks are compile-only (rule 9). The
{test=run}/{lint=check}opt-ins exist and are used 0 times. Five pages hold 83 of them (models-and-fields24,migrate-from-sqlalchemy18,upgrade-guide17,row-level-security13,connections11).tests/test_documentation_features.pytests hand copies, not the docs, and 4 of its tests are skipped with a stale reason. - Running-example models the page never shows (rule 3) in 4 files:
Customer(mutations),Account(transactions),Event(raw_sql),Metric(multiple_databases). The models sit outside any section, so the I-7 twin is also skipped. - Two Annotated idioms across twins:
Field(default=None, …)insideAnnotatedin 11 pairs, againstFerroField(…)] = Nonein 7 pairs. Plus 18# noqa: E711/E712comments rendered on pages, avoidable with a ruff per-file ignore fordocs/examples/*.
Activity
chore(docs): AGENTS.md I-8 for the lambda-only API, I-14 docstrings, …
0x054 commentedon Oct 19, 2026
Status: verify-passed
Worker: worker-704 (reviewed by reviewer-704)
Branch:
sdd/704-docs-harnessPR: #728
Verify: passed 4c85007; trial merge into
feat/docs-rewriteat 20d9f97 passedReview: Spec PASS, Quality PASS, two Minor findings.
Minor (spec): 10 error checks reported, not 12.
mutations.py:56and:113catch without asserting and still need anelse: raisefrom the page ticket that ownsmutations.py.Minor (quality): a docstring fence marked
{test=run}with no prelude fails with a bareKeyError: 'prelude'.feat(docs): docs examples harness — prelude fences, docstring example…
0x054 commentedon Oct 19, 2026
Merged in #728 into
feat/docs-rewrite