Skip to content

Docstrings: connect, fields, checks, row security, raw SQL, sessions, settings and exceptions #720

Closed

Description

@0x054

Parent

#698

What to build

version, reset_engine and set_default_connection render with no description; Field documents neither db_type nor db_check and carries two broken Pydantic links; raw.execute documents neither sql nor *args nor the placeholder syntax; FerroNullable and nullable are described as Alembic-only (they set NOT NULL in every emitter); connect links a page that does not exist and sends renames to Alembic; set_default_connection talks about operations removed in 0.13; about 45 dataclass and Pydantic fields have no docstring.

Every public docstring in these modules follows I-14. The connect (95 lines), PoolConfig (~90) and Session.set_config (68) essays move into the Connections guide and the RLS concepts page and are linked; set_config's statements match #session-settings.

Acceptance criteria

  • Every public function, method, class, property and dataclass/Pydantic field in the owned modules has a docstring in Google style as griffe parses it (no griffe warning at default level with docstring_parser="google").
  • Every summary is one line, in the infinitive ("Fetch one record by primary key."), ending with a period; class summaries are noun phrases ending with a period (docs conventions from AGENTS.md, written by the AGENTS.md ticket (AGENTS.md: rewrite I-8 for the lambda-only query API, add a docstring invariant, record the docs conventions #703)).
  • Every parameter appears in Args: with its default stated inline; ruff check --select D417 reports nothing for the owned files.
  • No Sphinx roles (:class:, :meth:, :func:, :attr:, :data:) and no RST :: blocks remain; cross-references are Markdown autorefs and resolve in the strict build.
  • Returns:, Raises:, Attributes: and Args: sections render as tables with one row per item (continuation lines indented, no blank line under a section header).
  • No ADR numbers, issue numbers, codenames, Rust internals or test paths in docstrings; explanations longer than a paragraph move to a guide or concepts page and are linked.
  • Examples sections use the lambda style named after the model, run against the shared reference prelude inside a session, and are marked to run ({test=run prelude=reference}).

Files owned

  • Modify: src/ferro/__init__.py, src/ferro/_core.pyi, src/ferro/fields.py, src/ferro/columns.py, src/ferro/checks.py, src/ferro/composite_indexes.py, src/ferro/composite_uniques.py, src/ferro/rowsecurity.py, src/ferro/raw.py, src/ferro/session.py, src/ferro/registry.py, src/ferro/settings.py, src/ferro/state.py, src/ferro/exceptions.py, docs/pages/guide/connections.md, docs/pages/concepts/row-level-security.md

Interfaces

Test scenarios

  • Happy path: tests/test_docstring_examples.py runs every marked example in the owned modules and they pass.
  • Happy path: the strict build succeeds and the rendered API pages for the owned objects contain no literal :class:-style roles.
  • Edge case: ruff check --select D417 over the owned modules exits 0.
  • Error path: removing one parameter from an owned docstring's Args: makes ruff check --select D417 fail.

Verify

uv run --no-sync zensical build --clean --strict
uv run --no-sync pytest tests/test_docstring_examples.py -q --no-cov -p no:cacheprovider
uv run --no-sync python -m tests._docs_checks src/ferro/__init__.py src/ferro/_core.pyi src/ferro/fields.py src/ferro/columns.py src/ferro/checks.py src/ferro/composite_indexes.py src/ferro/composite_uniques.py src/ferro/rowsecurity.py src/ferro/raw.py src/ferro/session.py src/ferro/registry.py src/ferro/settings.py src/ferro/state.py src/ferro/exceptions.py
uv run --no-sync ruff check --select D417 src/ferro/__init__.py src/ferro/fields.py src/ferro/columns.py src/ferro/checks.py src/ferro/composite_indexes.py src/ferro/composite_uniques.py src/ferro/rowsecurity.py src/ferro/raw.py src/ferro/session.py src/ferro/registry.py src/ferro/settings.py src/ferro/state.py src/ferro/exceptions.py
uv run --no-sync pytest tests/test_docs_site.py -q --no-cov -p no:cacheprovider

Blocked by

Audit findings

Rows from the docs audit this ticket covers (file, line, rule, fix)

src/ferro/__init__.py

Sev Line Rule Finding Suggested fix
H 310 connect stale fact / rule 22 The closing line points to "the Alembic bridge — see docs/guide/migrations.md". That path does not exist (the page is docs/pages/guide/migrations.md, URL /guide/migrations/), and ferro now has its own migrations door (ferro migrate new) for renames, which renamed_from drives. "For changes the pass refuses (renames, primary-key changes), write a migration with ferro migrate new; see Migrations."
M 310 connect rule 1 / 21 95 lines. migrate_updates alone is a 30-line nested bullet essay with bold labels, and four paragraphs of locking and timeout behaviour sit between Args: and Raises:. There are no Examples on the main entry point of the library. url "(e.g., "sqlite:example.db?mode=rwc")" is not in code font. "mostly cosmetic" (rule 16). Keep Args: to one or two lines each, add Examples: (await ferro.connect("sqlite:app.db?mode=rwc", auto_migrate=True)), and move the per-backend capability matrix and lock rules to the schema-management guide with a link.
M 204 PoolConfig rule 1 / griffe The class docstring is a ~90-line explanation essay ("What it costs, and there are four costs worth knowing…"), but the three fields are never listed with defaults (max_connections=5, min_connections=0, settings_delivery="transaction"). The fields have no docstrings. SQL samples are indented blocks after an RST ::, not fenced. Add Attributes: with defaults and constraints (ge=1). Move the pinned-connection essay to the sessions/RLS concept page and link to it.
M 304 PoolConfig.validate public surface A validator named validate is public, undocumented and shadows pydantic's deprecated BaseModel.validate classmethod. It renders as an API member. Rename it _check_bounds (a private validator).
M 505 create_tables rule 21 / I-11 The summary wraps onto two lines. Paragraph 2 is internals ("Compiles and pushes the current registry SchemaIR to the Rust runtime before delegating to the Rust create entrypoint"). No example. "Create the tables that are missing for the registered models." Keep the "model defined after connect() is still created" fact in plain words. Add an example that prints report.statements.
M 531 migrate rule 21 / 9 Same internals paragraph. Prose sits between Args: and Returns:. No example. As above. Add report = await ferro.migrate(destructive=True) with the printed statements.
M 140, 103 clear_registry, ensure_resolved_modelset rule 1 / I-11 Both are pure maintainer notes (join-table registry, "Clean path (O(1))", REGISTRY.reset_for_test(), #153, #245) with no statement of when a user calls them. Lead with the user case ("Call between tests that redefine models."), then one short reason. Move the internals to comments. Consider whether ensure_resolved_modelset belongs in __all__.

src/ferro/_core.pyi re-exports

Sev Line Rule Finding Suggested fix
M 760–764 griffe / rule 21 The stub has no docstrings. The site takes the text from the compiled module by inspection, so it is the Rust doc comment: third-person verbs ("Returns…", "Shuts down…", "Selects…") break the infinitive pattern. A rustdoc # Errors heading renders as an "Errors" heading inside site/api/connection/ (seen for reset_engine), with "Returns a PyErr", which names no Python exception. Add Google docstrings to the .pyi stubs: "Return the Ferro core version.", "Shut down the engine and close every session.", with Raises: naming the Python exception.
M 761 set_default_connection stale fact "Selects the default connection used by legacy unqualified operations." Unqualified operations were removed in v0.13; the default connection is now what engines.session() opens with no name. "Set the connection engines.session() opens when called without a name."
L 760 reset_engine rule 16 "This is useful for testing environments" is a hedge. "Call it between tests to start from a clean engine."

Summary

"Missing" leaves out __init__ and pydantic model_config, and counts an attribute as documented when its class's Attributes:/Args: section covers it. The verb form column counts functions and methods only: I = infinitive ("Apply…"), N = noun phrase ("The database called…"), 3P = third-person ("Returns…").

Module Public objects Missing docstring griffe warnings / mis-parses Verb form (functions) Grade
ferro/__init__.py (connect, PoolConfig, create_tables, migrate, registry helpers) 10 4 (PoolConfig.max_connections, .min_connections, .settings_delivery, .validate) 0 warnings; 2 silent mis-parses (create_tables, migrate Returns) 5 I C
_core.pyi (version, reset_engine, set_default_connection) 3 3 (rendered empty on the API page) 0 n/a D
exceptions.py 16 2 (ModelDoesNotExist.model, .pk) 6 base-class notices (static-load noise, not docstring) n/a (classes) B
fields.py (Field, BackRef, ManyToMany) 3 0 (but 4 params undocumented) 1 unresolved autoref (pydantic.fields.FieldInfo, build07.log:6) + 2 broken relative links 3 I C
checks.py 3 2 (Check.suffix, .predicate) 0 n/a B
rowsecurity.py 12 2 (RowSecurity.policies, .force) 0 n/a B
raw.py 7 3 (Transaction.execute/fetch_all/fetch_one) 0 3 I C
session.py 10 5 (Session.connection_name, .session_id, .settings, .effective_settings, Session.query) 2 debug "possible admonition skipped" (session.py:282, :381); 1 Returns type left in the description (:395) 3 I C
settings.py 24 10 (DatabaseSettings' 8 fields, FerroSettings.python_path, .databases) 0 1 I / 5 N C
pass_report.py 17 10 (PassReport ×2, Report ×4, Subject ×4 fields) 0 n/a B
migrations core (runner, _baseline, _drift, generate, errors, report, steps, context) 63 13 (all exception attrs + DriftReport.lines/operations/refusal + BaselineReport.recorded/data_steps_listed/drift) 0 warnings; 28 Sphinx roles rendered literally on api/migrations 13 I / 9 N C
migrations.testing 12 0 0 5 I / 2 N B
migrations.alembic 3 0 0 1 I / 2 N B
cli (main, render_refusal, Global, 10 verbs + help text) 15 0 0 12 I C (one H defect in --help)
Total 198 54 0 real warnings; 3 silent mis-parses; 1 unresolved autoref 46 I / 18 N / 0 3P

Verb form (rule 21): the project uses infinitives. 46 function summaries open with one ("Apply", "Run", "Declare") and none use the third person. The 18 that break the pattern are value-returning methods written as noun phrases: FerroSettings.database "The database called name…", DriftReport.render "The text ferro migrate drift prints…", status "Where the database stands…", harness "A Harness over…", ferro_options "The options env.py passes…", and the five HistoricalModels methods. Recommendation: keep the infinitive ("Return the database called name…") and convert those 18.
Exception classes use three forms: a state sentence ("A NOT NULL constraint rejected the statement.", ×9), "Raised when…" (ModelDoesNotExist), and a label prefix ("require_applied(): the database is…", ×5 in migrations.errors/report).

griffe, mechanically: at default level griffe reports no Google-style warnings for anything in scope. That result is misleading. Two problems are silent:

  1. Unindented continuation lines in Returns: become extra return items. returns_multiple_items defaults to true, so each continuation line is parsed as a new item. ferro.migrate's Returns renders as a five-row table, every row typed PassReport, each holding a sentence fragment (confirmed in site/api/connection/index.html). create_tables renders as two rows.
  2. Sphinx/RST cross-reference roles do not parse. The Google parser leaves :func:, :class:, :meth:, :data: alone, and the Markdown renderer prints them literally. In-scope modules hold 193 role occurrences, 36 of them in the internal cli/output.py. Rendered pages show :class:<code>…</code> 28× on api/migrations, 6× on api/transactions, 5× on api/fields, 2× each on api/connection and api/exceptions. RST :: literal-block markers also render as a literal "::" (8 places on the API pages).

src/ferro/__init__.py (connect, PoolConfig, create_tables, migrate, clear_registry, ensure_resolved_modelset)

  • Kind (rule 1): reference with long explanation mixed in (PoolConfig is a 90-line essay)
  • Running example: Invoice with myapp.tenant_id in PoolConfig; none elsewhere
  • Verdict: connect's Args: is the best parameter table in scope. Two Returns sections render broken, and three public entries document implementation instead of behaviour. C
Sev Line Rule Finding Suggested fix
H 523, 549 Google/griffe The continuation lines of create_tables/migrate Returns: are not indented, so griffe splits them into 2 and 5 return items. The page shows a table with five PassReport rows of fragments ("order (…), and every warning", "partway raises its usual error…"). Indent every continuation line by four spaces under the first.
M 204–296 1, 21 PoolConfig has no Attributes:, so max_connections (default 5, ≥1), min_connections (0) and settings_delivery render with no description. The class docstring then runs ~90 lines on pinning trade-offs ("What it costs, and there are four costs worth knowing…"). Add Attributes: with defaults inline, and move the pinning trade-offs to the RLS/connections guide with a link.
M 104–115, 140–160 1, I-11 ensure_resolved_modelset and clear_registry describe internals ("Clean path (O(1)): one in-process generation comparison", "Delegates to the Rust core…", "join envelope would let a future assemble step resurrect…", REGISTRY.reset_for_test()). Both render on api/connection. Open with what the caller gets and when to call it. Move the mechanics to comments or docs/solutions/.
M 507–518 I-11 create_tables: "Compiles and pushes the current registry SchemaIR to the Rust runtime before delegating to the Rust create entrypoint". Same pattern in migrate. "Creates tables for models defined after connect() too."
L 507, 533 21 "Manually create…", "Manually run…": the adverb comes before the verb, and create_tables' summary runs across two lines. "Create the missing tables for every registered model."
L 395–417 22 connect mixes Raises description cases ("A connection…" / "an auto-migrate flag…"). It ends with prose after Raises: pointing at the repo path docs/guide/migrations.md, which is wrong (docs/pages/guide/…) and not a link. Link the guide page. Make Raises descriptions sentence-case.
L 204ff, 362 22 RST :: literal markers and indented (unfenced) code blocks get no syntax highlighting, and the :: renders as text. Fence with python` / sql`.

src/ferro/_core.pyi — version, reset_engine, set_default_connection

  • Kind: reference
  • Verdict: all three sit on api/connection as bare signatures. The .pyi stub has no docstrings, and griffe reads the stub, not the compiled module. The runtime __doc__ exists but is rustdoc-formatted (# Errors headings, "Returns a PyErr"). It would render badly even if inspected. D
Sev Line Rule Finding Suggested fix
H 760–764 21 Three public, documented-on-page functions render with no description. Add Python-facing docstrings to the stub (or Python wrappers): "Return the Ferro core version string.", "Shut down every engine and session…", with a Raises: in Python terms.
M runtime __doc__ 22 Rust # Errors / "Returns a PyErr" are rustdoc conventions. Python users see OperationalError/ValueError. Name the Python exception classes.

src/ferro/exceptions.py

  • Kind: reference
  • Running example: none
  • Verdict: short, accurate, consistent state sentences and a proper Attributes: on FerroError. One outlier. B
Sev Line Rule Finding Suggested fix
M 122–126 21, 22 ModelDoesNotExist alone opens "Raised when…". Its summary spans three lines and its two :meth: roles render literally. model/pk are undocumented. "No row matched the primary key." Then a paragraph naming Model.get and save, and Attributes: model, pk.
L 1–23 22 The module docstring's tree uses :class: roles. It is not rendered on the page (api/exceptions.md has its own tree), so this is harmless today. Convert to Markdown links if it is ever rendered.

src/ferro/fields.py — Field, BackRef, ManyToMany

  • Kind: reference
  • Running example: User in Field's Examples
  • Verdict: the parameter list was copied from Pydantic, along with Pydantic's relative links. Ferro's own db_type and db_check (I-1 item 3 / 8) are the parameters left undocumented. C
Sev Line Rule Finding Suggested fix
H 473, 486 22 See [Strict Mode](../concepts/strict_mode.md) and [Union Mode](../concepts/unions.md#union-modes) are Pydantic's doc paths. Neither page exists in docs/pages/concepts/, so both links are dead on api/fields. Point to https://docs.pydantic.dev/latest/concepts/strict_mode/ and …/unions/#union-modes.
H 495 22 [FieldInfo][pydantic.fields.FieldInfo] is an unresolved autoref (build07.log:6) because only the Python inventory is configured. Add https://docs.pydantic.dev/latest/objects.inv to inventories in zensical.toml.
H 362ff 21 db_type, db_check and reverse_index are in the signature but not in Args:. db_type decides column storage and db_check emits ck_<table>_<col>. Document each with its default and the token vocabulary (link DbTypeToken).
M 362 21 The summary "Build field metadata with Pydantic and Ferro options" has no period. Add the period.
L 491–493 20 !!! warning Deprecated sits inside an Args: item; it renders, but inside a table cell. Keep, or move to a section-level admonition.
L 492 22 Pydantic typo carried over: "Maximum number of allow digits". "allowed digits".
L 621ff 21 ManyToMany leaves **kwargs undocumented ("forwarded to Field"). BackRef has no example. One line each.

src/ferro/checks.py — Check

  • Kind: reference
  • Verdict: a good entry: what it is, the naming rule with its reason, and a doctest with a lambda predicate (I-8 ✓). B
Sev Line Rule Finding Suggested fix
L 73–87 21 suffix and predicate have no Attributes:/Args: entries, so the page shows them bare. Add Args: (suffix: bare identifier [a-z][a-z0-9_]*; predicate: lambda over the model).

src/ferro/rowsecurity.py — RowPolicy, RowSecurity

  • Kind: reference
  • Verdict: clear, with a reason beside each rule (NULLIF, force=True) and good doctests. B
Sev Line Rule Finding Suggested fix
M 251–262 21 RowSecurity has no Args:. *policies and force (default True) are explained in prose only. Add Args:.
L 76, 84 22 :: RST literal markers before the two forms. Use fenced python blocks.

src/ferro/raw.py — execute, fetch_all, fetch_one, Transaction

  • Kind: reference
  • Verdict: execute's Args: documents only autocommit. The core parameters (sql, *args, using, session) and the placeholder syntax are missing everywhere, and Transaction's three public methods have no docstrings. C
Sev Line Rule Finding Suggested fix
H 76–135 21 sql, *args, using and session are undocumented in all three functions. Nothing says how *args bind ($1 vs ? per dialect). ValueError (empty SQL) and TypeError (unbindable value) are raised but not listed. Add Args: + Raises: and one example per function showing the placeholder and the returned dict.
M 166–178 21 Transaction.execute/fetch_all/fetch_one render with no description on api/transactions. One line each: "Run … on this transaction's connection."
M 84 I-11 "Honors the active transaction() block via the _CURRENT_TRANSACTION ContextVar" names a private symbol. "Runs inside the enclosing transaction() block, if any."
M 106 17 "Plainly: an autocommit statement is not tenant-scoped." Drop "Plainly:". Make it a !!! warning (data exposure).
L 147–160 22 Transaction uses :func:/:class: roles (6 render literally on api/transactions) and names the internal RouteHandle. "structurally impossible to violate" is internal reasoning. Markdown links, and drop the internals sentence.
L 139 16 "Callers should LIMIT 1 if…": a hedge without a reason. "Add LIMIT 1: the rest of the result is fetched and discarded." (Verify that claim against the code first.)

src/ferro/session.py — Session, set_config, close, current_session, engines

  • Kind: reference mixed with explanation (set_config is 68 lines)
  • Running example: Invoice + myapp.tenant_id, used consistently (I-8 ✓)
  • Verdict: accurate and example-led, but too long for reference. It has unfenced code that griffe half-reads as admonitions, and one sentence sends readers to the source. C
Sev Line Rule Finding Suggested fix
M 282, 381 Google/griffe griffe logs "Possible admonition skipped" for both: indented example blocks after a prose line ending in : read as admonition candidates. Same pattern at 80, 274, 304. Use fenced python blocks.
M 329–332 1, 18 set_config Raises: RuntimeError holds a 9-line paragraph ending "(see the Ferro source for the exact sequencing)". A reference entry must not send the reader to the source. Keep one line per raise. Move the failure sequencing to the RLS guide.
M 286–302 1 "When the new value takes effect, precisely." is a bold pseudo-heading over explanation prose in a method docstring. Move it to guide/row-level-security.md and link it from here.
M 98–101, 367 21 The dataclass fields connection_name, session_id, settings, effective_settings and the public query() method have no docs. Add Attributes: and a one-line query docstring (or make query private).
L 88–90 22 Points to EngineManager.session, which is not on any API page (engines is rendered as an attribute only). Render ferro.session.EngineManager.session on api/connection, or inline the contract.
L 395–396 Google Returns: Session | None: The ambient session… — griffe keeps "Session | None:" inside the description, so the cell reads "Session | None: The ambient session". Returns: The ambient session, or None outside every session.
L 378 17 "it can just ask for it". "it calls current_session()".
L all 22 Single backticks here, double backticks in every other module. Both render as code, but spelling is mixed (honourable/behaviours vs Honors). Pick one; a lint rule can enforce it.

src/ferro/settings.py — FerroSettings, DatabaseSettings, SettingsError

  • Kind: reference mixed with implementation notes
  • Verdict: the methods are terse and refusal-aware. The two settings classes, though, leave every config key undocumented on the page and point at a module docstring the page does not render. C
Sev Line Rule Finding Suggested fix
H 341 1, 24 FerroSettings: "finds the file (see the module docstring for the lookup)". The module docstring is not rendered on api/connection, so the lookup order is unreachable from the reference. Inline the lookup order (pyproject [tool.ferro] / ferro.toml, walk-up) or link the guide section.
M 226–233 21 DatabaseSettings fields name, models, dialects, url_env (default "DATABASE_URL"), directory, tracking_schema (Postgres only), ddl_lock_timeout ("5s") and lock_timeout ("30s") render bare. These are the user's config keys. Add Attributes: with defaults and constraints inline.
M 345–350, 376 I-11 FerroSettings describes plumbing ("__init__ hands the located path to settings_customise_sources through the init source… The validators below…"). settings_customise_sources, a pydantic hook, renders as a public method. Cut the plumbing paragraph. Hide the hook (filters or members).
M 474, 494, 284, 300 21 database, database_for, url_for and import_models all raise SettingsError but have no Raises:. name, model and override have no Args:. Add the sections.
M 474, 494, 272, 284, 376 21 Five methods open with noun phrases ("The database called…", "Seconds a run…waits"). Infinitive: "Return the database called name…".
L 265–268 22 :data:MAX_LOCK_TIMEOUT_S`` renders literally, and the summary runs across three lines. One-line summary. Say ""0" means no limit".
L 365 21 FerroSettings(config=...)'s config parameter is mentioned in prose only. Args: config.

Activity

  1. added
    documentationImprovements or additions to documentation
    ready-for-agentUnblocked and fully specified; an agent may claim it
    size:LCross-cutting or multi-worker; local waves only
    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-720 (reviewed by reviewer-720)
    Branch: sdd/720-docstrings-rest
    PR: #747
    Verify: passed ea7c2a9

    Review: Spec PASS, Quality PASS, with no Critical or Important findings. The Verify block is green on ea7c2a9. The base 94b04ae has not moved, so the trial merge is the head itself, and the full suite passed there (3498 passed, 342 skipped, 2 xfailed). Removing the PoolConfig.validate shadow restores Pydantic's own method and keeps the bounds check. The three engine wrappers match _core in signature, return value and exceptions. Two Minor findings: module docstrings in checks.py, rowsecurity.py and registry.py still cite ADR numbers, I-1 or CONTEXT.md (no page renders them), and the Rust doc comments in connection.rs still carry the old wording. Ready to merge into feat/docs-rewrite.

  5. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Merged in #747 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:LCross-cutting or multi-worker; local waves only

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions