Skip to content

Docs harness: run inline blocks with a prelude, run docstring examples, and check lambda names and error checks #704

Closed

Description

@0x054

Parent

#698

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 after docs/examples/_preludes/<name>.py, which declares models, connects to an in-memory SQLite database and opens a session; top-level await works. The existing {test=run} and {lint=check} keep working.
  • Docstring examples. tests/test_docstring_examples.py collects fenced Python examples from the docstrings of every module under src/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 (Author and Post, 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, before and any other Query/Model method taking a callable) must take a parameter of two or more characters; a try whose except asserts must have an else that raises. They read Python files, Python fences in Markdown, and docstrings. Run as python -m tests._docs_checks PATH... (files or directories); exit 1 lists path:line: message. Not wired into the test suite as a gate yet.

Acceptance criteria

  • A Markdown fence {test=run prelude=reference} containing print(await Author.where(lambda author: author.id == 1).first()) runs and its failure fails the test.
  • tests/test_docstring_examples.py collects every fenced example in src/ferro docstrings and runs the marked ones.
  • python -m tests._docs_checks docs/examples lists today's one-letter lambdas (about 66 in four scripts) and the 12 unfailing error checks, and exits 1.
  • Existing tests/test_docs_examples.py results 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

Test scenarios

  • Happy path: a fence with {test=run prelude=reference} that queries Author passes.
  • Error path: the same fence raising RuntimeError fails its test with the block's path and line.
  • Edge case: lambda author: author.id passes the lambda check; lambda a: a.id fails it with the line number; a lambda passed to sorted() is ignored.
  • Edge case: try: ... except X: assert ... without else is reported; with else: raise AssertionError it is not.
  • Integration: a docstring example in a scratch module marked {test=run prelude=reference} is collected and run by test_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 8

Blocked 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.md does not exist, and docs/examples/relationships.py (m2m-usage) and existence_tests.py (m2m) create M2M join tables through auto_migrate=True and 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, but main() is a no-op ("import smoke check only"). The db and db_transaction fixtures 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*.py exercises 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

  1. I-8 one-letter lambda parameters in four embedded examples: 66 lambdas (aggregations 24, partial_selects 16, populated_relations 15, existence_tests 11), plus 6 in their twins' bodies and 57 lines in inline page blocks. predicates.py, traversal.py, relationships.py and quickstart.py show the correct form. Fix the four files and add a lint test (an AST check of docs/examples/*.py and inline blocks: a lambda passed to where/select/order_by/include/exists takes a parameter of at least two characters, ideally snake_case(Model)).
  2. Error checks that cannot fail: 12 try/except blocks assert only inside except, with no else (aggregations ×5, populated_relations ×3, partial_selects ×1, mutations ×3). The "loud limits" the pages advertise are untested. table_checks.py:38 shows the fix.
  3. 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.
  4. 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.py and migrations_backfill.py show the real-looking alternative.
  5. 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.
  6. 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-fields 24, migrate-from-sqlalchemy 18, upgrade-guide 17, row-level-security 13, connections 11). tests/test_documentation_features.py tests hand copies, not the docs, and 4 of its tests are skipped with a stale reason.
  7. 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.
  8. Two Annotated idioms across twins: Field(default=None, …) inside Annotated in 11 pairs, against FerroField(…)] = None in 7 pairs. Plus 18 # noqa: E711/E712 comments rendered on pages, avoidable with a ruff per-file ignore for docs/examples/*.

Activity

  1. added
    documentationImprovements or additions to documentation
    ready-for-agentUnblocked and fully specified; an agent may claim it
    size:MNeeds a plan; one or two local workers
    on Oct 19, 2026
  2. added theissue type on Oct 19, 2026
  3. self-assigned this
    on Oct 19, 2026
  4. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Status: verify-passed
    Worker: worker-704 (reviewed by reviewer-704)
    Branch: sdd/704-docs-harness
    PR: #728
    Verify: passed 4c85007; trial merge into feat/docs-rewrite at 20d9f97 passed

    Review: Spec PASS, Quality PASS, two Minor findings.
    Minor (spec): 10 error checks reported, not 12. mutations.py:56 and :113 catch without asserting and still need an else: raise from the page ticket that owns mutations.py.
    Minor (quality): a docstring fence marked {test=run} with no prelude fails with a bare KeyError: 'prelude'.

  5. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Merged in #728 into feat/docs-rewrite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationready-for-agentUnblocked and fully specified; an agent may claim itsize:MNeeds a plan; one or two local workers

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions