Repository navigation
docs: panel round 2 on #777 — green suite, honest Postgres fences, one redirect and one region parser, the shows: guard, the 0.26 notes - #782
Conversation
…se chain migration_errors.py now builds the chain the guide's table describes: 0002_author_slug_nullable (one expand step) and 0003_author_slug_required (backfill, add-constraint, contract), so one migration has one shape on the page and no number names two migrations. The DDL lock-retry output uses the same nullable migration. Each error region prints the class it caught (type(error).__name__), never a typed class path. The step-class test keeps its runtime half (expand-only raises DatabaseAheadError naming the contract step) and checks only that the page's refusal is the regenerated prints: block, which test_docs_output pins. Panel round 2 on #777: F26, F32, F36. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…ld-order writes The section no longer says 0.26 asks nothing: it adds the routeless RuntimeError's new ending (v0.14, the upgrade guide) for code matching the old text, and the field-order column lists of every write for tests and logs that compare captured SQL. The no-route pin is renamed to say it pins the current wording, and a test holds the section to both notes. Panel round 2 on #777: F31. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…es that prove what they say
tests/_docs_shared.py holds snippet_regions (name -> start, end, body;
a duplicated, unopened or unclosed region is refused naming its line) and
point_connect_at, the one redirect, which keeps the PostgreSQL query options
a block's URL carries and creates a ferro_search_path schema first. The
capture, the include reader, the README check and the migrations examples'
helper read regions through it; the capture and the prelude runner install
the redirect. tests/test_docs_shared.py pins both.
The label-addition block runs on Postgres against a plan type holding only
'free' (prelude label_addition, models from docs/examples/enum_labels.py and
its generated twin, now embedded in the page's tabs) and prints the type's
labels under a prints: above block. A prints: above block under a
backend=postgres fence is compared in the Postgres job. The JSON-claims block
and the two Session docstring fences run on Postgres (preludes jwt_claims,
invoices); the sslmode=require block is compile-only ("needs a TLS-enabled
server"). The conventions state one rule for a Postgres block; the
rowsecurity and testing docstring fences give true reasons.
Panel round 2 on #777: F27, F28, F29, F33, F34, F35.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…utput: and refusal: are compared on every page
FERRO_OUTPUT now matches every block the pages show as Ferro's own words:
each ferro migrate output and refusal (the `<database> · <target>` line,
`ferro migrate[ verb]: `, the config refusals), a line naming any exception
class Ferro defines (found by walking its subclasses), a traceback frame, and
a `path:N: ` prefix before a warning. A test runs it over every output:,
refusal: and prints: block whose source prints what Ferro raised or warned,
and VARIES is keyed by page and line, with a test that each entry still names
a shows: block.
Regions that caught an error and printed it bare now print
f"{type(error).__name__}: {error}", as the conventions say; seven blocks
regenerated.
output: and refusal: blocks are read from every page through the one fence
reader (no regex reader for refusals), so a typed block under either note on
any page must name a registered scenario; a fabricated-block test pins it.
Panel round 2 on #777: F30, F37 (and F38's refusal reader).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
0x054
left a comment
There was a problem hiding this comment.
Spec: FAIL
Spec = the Round 2 panel findings F26–F41 (#777 (comment)).
- Important · docs/pages/upgrading.md:79 at c70297e · Misunderstood (F31) · The new bullet says every write,
update()included, "lists its columns in the model's field order".update()does not:update_bind_payload(src/ferro/_bind_payload.py:323-344) keeps the order of the call's keyword arguments. Probe on SQLite, model fieldsalpha, beta: bytes, gamma:Thing.where(...).update(gamma=2, alpha="b", beta=b"y")sendsUPDATE "thing" SET "gamma" = ?, "alpha" = ?, "beta" = ? WHERE .... A reader who rewrites the expected SQL of anupdate()to field order, as the bullet says, gets a failing test. F31 asked the 0.26 section to name the behaviour changes truthfully. · Fix: say whatupdate()does now (the columns in the order the call passes them, with abytesor secret column no longer moved last), or takeupdate()off the list.test_the_upgrade_guide_names_the_reworded_no_route_messageonly checks for the words "field order", so it cannot catch this. - Minor · docs/pages/guide/schema/deploying.md:16,46-49,222-230 vs :71,124,131,142,213-215 at c70297e · Missing (F32, partly) · Each migration now has a single shape, but the number
0002names two migrations on the page: the four-step0002_author_slugand the one-step0002_author_slug_nullable. F32 objected to one number naming two migrations (0003then). The PR body says "no number names two migrations", which is not true for0002. · Fix: give the one-migration scenario and the two-release chain numbers that differ, or say in one line that they are alternative histories of the same project. - Minor · docs/examples/migration_errors.py:67,80,100,108 at c70297e · F36 pin · The hand-typed class paths are gone. But no test would fail if a region printed a typed name again:
test_docs_outputcompares the page with whatever the region prints.
Cannot verify: none.
F38 (partly), F39, F40 and F41 (partly) were optional; they are recorded in the table below, not counted against the verdict.
| Finding | Status | Where |
|---|---|---|
| F26 | ADDRESSED | tests/test_step_class.py:460 (runtime half kept; the page half points at the regenerated prints: block) |
| F27 | ADDRESSED | docs/examples/_preludes/label_addition.py:192-213, docs/pages/guide/schema/auto-migrate.md:116-131. Probe: live labels ['free'] before the block; the only DDL the block sends is ALTER TYPE "plan" ADD VALUE IF NOT EXISTS 'pro'; then ['free', 'pro']. Collected as test_a_postgres_text_block_is_what_its_example_prints_there[postgres-guide/schema/auto-migrate.md:127] |
| F28 | ADDRESSED | tests/_docs_shared.py:123 |
| F29 | ADDRESSED | tests/_docs_shared.py:45 |
| F30 | ADDRESSED | tests/test_docs_output.py:103,128,188,198. Matches ferro migrate: …, No database route…, Cannot add NOT NULL column…, ModelDoesNotExist: … (with or without the module path), path:N: UserWarning:, the · target line |
| F31 | PARTLY | docs/pages/upgrading.md:74-79, tests/test_routing_errors.py:34,44. The update() claim is wrong (above) |
| F32 | PARTLY | deploying.md: 0002 names two migrations (above) |
| F33 | ADDRESSED | tests/_docs_shared.py:100,156. connections.md:57 is compile-only, reason="needs a TLS-enabled server"; no tenant_a/test_42 schema is left after the run |
| F34 | ADDRESSED | docs/solutions/patterns/docs-page-conventions.md:197-218; src/ferro/session.py:295,422 (collected as test_docstring_example_on_postgres[postgres-src/ferro/session.py:295-300] and […:422-425]); row-level-security.md:313 |
| F35 | ADDRESSED | src/ferro/rowsecurity.py:5, src/ferro/migrations/testing.py:56,63 |
| F36 | ADDRESSED | docs/examples/migration_errors.py:67,80,100,108 (no regression pin, above) |
| F37 | ADDRESSED | tests/test_docs_migrations.py:230,251,277 |
| F38 | PARTLY | _quoted_refusals (tests/test_docs_migrations.py:268) now uses the shared fence reader; _PYTEST_OUTPUT (tests/test_docs_testing_fixtures.py:34) is left |
| F39 | LEFT | tests/_docs_sql.py:56 |
| F40 | LEFT | tests/test_docs_lint.py:20 |
| F41 | PARTLY | check_printed(database_url=) (tests/_docs_sql.py:551); _on_postgres (tests/test_docs_output.py:58) still re-matches PRINTS |
Quality: PASS
- Minor · docs/examples/enum_labels.py:6, docs/examples/enum_labels_annotated.py:6 at c70297e · Both docstrings send the reader to
docs/examples/_preludes/enum_labels.py, which does not exist. The prelude islabel_addition.py. - Minor · depth (Q4, Follow-up) · tests/test_docs_shared.py:91-106,139-147 at c70297e ·
test_the_capture_and_the_prelude_runner_install_the_one_redirectchecks the source text for"ferro.connect ="and"point_connect_at(".test_the_four_region_readers_are_onechecks that old attribute names are absent. Both test past the interface: renaming a function breaks them while behaviour is unchanged.test_a_redirect_keeps_the_postgres_options_the_block_namesand the region tests already pin the behaviour. - Minor · docs/pages/guide/schema/overview.md:47 at c70297e · The regenerated block names
RunRefused. That class is not exported fromferro.migrationsand appears in no docs page (it is defined in src/ferro/migrations/report.py:46), while the region catchesMigrationRefused. A reader cannot look the name up.
Checked: (g) both sys.path.insert(0, <repo root>) calls resolve from __file__, so they work from any working directory. The only regular packages at the repo root are tests and benchmarks, and nothing in the import chain shadows an installed package. (f) The seven blocks whose caught errors now print their class (connections, mutations, auto-migrate, renames-and-retirements, multiple-databases ×2, overview) are regenerated, and their lead-ins still read correctly. The conventions text (docs-page-conventions.md:99-101) states the type(exc).__name__ form.
Verify: PR head c70297e passed (strict build: no issues; targeted docs tests 1153 passed, 126 skipped on SQLite and 1297 passed with --db-backends=sqlite,postgres; full suite pytest tests --db-backends=sqlite,postgres -n 8: 5739 passed, 69 skipped, 2 xfailed; _docs_checks exit 0; ruff check and ruff format --check clean)
Trial merge: not run (base unchanged, feat/docs-rewrite at 8ae9aee)
…ng names one migration per number update() keeps the order the call passes its columns, as it did in 0.25; the note now says so, and lists only save(), save(only=), save(exclude=) and bulk_create() for field order. A test pins that wording, and a new column-order test pins update() keeping a bytes column where the call put it. The Deploying guide's CI check, deploy-step run and status report now come from the two-release chain the rest of the page follows (0002_author_slug_nullable, 0003_author_slug_required), so no number names two migrations; a test pins it. Review of #782: F31 (update order), F32 (one number per migration). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
0x054
left a comment
There was a problem hiding this comment.
Scoped re-review, c70297e..74ee91e.
Spec: PASS
- Important · docs/pages/upgrading.md:79 (the
update()field-order claim): ADDRESSED at 74ee91e. The bullet now namessave(),save(only=...),save(exclude=...)andbulk_create()for field order. It saysupdate()is unchanged and lists the columns in the order the call passes them. I checked that againstgit show v0.25.0:src/ferro/_bind_payload.py:update_bind_payloadalready kept the call's order there, so "unchanged" is accurate. Pinned bytest_the_upgrade_guide_states_each_write_door_s_column_order(tests/test_save_column_order.py:215), and the behaviour bytest_query_update_keeps_a_raw_column_where_the_call_puts_it(tests/test_save_column_order.py:193). - Minor · docs/pages/guide/schema/deploying.md (F32,
0002naming two migrations): ADDRESSED at 74ee91e. The page names exactly0001_create_author,0002_author_slug_nullableand0003_author_slug_required. Theci-check,server-upandstatus-stepsoutputs are regenerated from the two-release chain (_chain_writtenand_chain_late_row_failed, tests/test_docs_migrations.py:806-822). Pinned bytest_the_deploying_guide_names_one_migration_per_number(tests/test_docs_migrations.py:1499).
Cannot verify: none
Quality: PASS
No new Critical or Important findings in the fix diff.
Verify: PR head 74ee91e passed. Strict build: no issues. Docs set + test_step_class.py test_routing_errors.py test_save_column_order.py: 1163 passed, 126 skipped on SQLite; 1315 passed with --db-backends=sqlite,postgres. Full suite pytest tests --db-backends=sqlite,postgres -n 8: 5743 passed, 69 skipped, 2 xfailed. _docs_checks exit 0. ruff check and ruff format --check clean.
Trial merge: not run (base unchanged, feat/docs-rewrite at 8ae9aee)
Panel round 2 on #777, every finding F26–F37 in one slice. The full suite is green on both dialects again; four Postgres fences now prove what they claim; there is one
connect()redirect and one snippet-region reader; theshows:guard recognises every block of Ferro output; and the 0.26 upgrade notes name the two behaviour changes.Head:
c70297e. Four commits:f994165(F26, F32, F36),e112bfb(F31),3fae90c(F27, F28, F29, F33, F34, F35),c70297e(F30, F37, part of F38).Findings
F26: stale numbering pin in
test_step_class.pyThe test is renamed
test_expand_only_refuses_a_contract_step_as_the_deploying_guide_shows. Its runtime half is unchanged: it raises the realDatabaseAheadError, checkscontract_steps == ["0002_drop_bio:01"], and checks the message names the step. Its page half no longer compares a hand-typed number. It now checks two things: the guide shows theexpand-onlyrecipe, and the page's refusal is the regenerated<!-- prints: migration_errors.py:ahead-by-contract -->block, whichtest_docs_output.pypins.pytest tests/test_step_class.py -k deployinggaveFAILED …test_the_deploying_guide_quotes_the_refusal_expand_only_raises[sqlite].F27: the label-addition block never added a label
docs/examples/enum_labels.py, with its generated_annotatedtwin, and the page's two tabs embed them. This replaces the two compile-only tabs.label_addition, builds the earlier release's schema on the target database before the block runs:CREATE TYPE "plan" AS ENUM ('free')plusauthorin the shape the create pass leaves.{test=run prelude=label_addition backend=postgres}) connects withmigrate_updates=True, then prints the type's labels under<!-- prints: above -->:['free', 'pro'].ferro.sqllog at DEBUG. The only DDL it sent wasALTER TYPE "plan" ADD VALUE IF NOT EXISTS 'pro'.prints: aboveblock under abackend=postgresfence is compared in the Postgres job (check_printed(..., database_url=)). Pinned bytest_a_block_under_a_postgres_fence_is_compared_only_on_postgresandtest_a_block_under_a_postgres_fence_holds_what_it_prints_there[same|edited]. RED: an import error untilprinting_fenceexisted.F28: two
connect()redirectsThere is now one,
point_connect_at(url, *, before=None)intests/_docs_shared.py, re-exported bytests/_docs_harness.py. The capture (withbefore=capture.snapshot) and the prelude runner both install it. Pinned bytest_the_capture_and_the_prelude_runner_install_the_one_redirect.F29: four snippet-region parsers
There is now one,
snippet_regions(text, *, where=). It returns name →SnippetRegion(start, end, body)and refuses a region that starts twice, ends without starting, or never ends. Four callers read regions through it:_docs_sql(viaregion_lines), the harness's include reader, the README check, and_migrations_project._region._migrations_projectcan import it without the harness's griffe and pytest-examples dependencies.tests/test_docs_shared.py: the parse, the Markdown form, three malformed cases, every region file reading cleanly, andtest_the_four_region_readers_are_one.F30: the
shows:guard missed most Ferro outputFERRO_OUTPUTnow also matches:<database> · <target>line andferro migrate[ verb]:Name:formpath:N:prefix before a warningNo database routeandCannot add NOT NULL columntest_the_recogniser_matches_every_block_of_ferro_outputruns it over everyoutput:andrefusal:block, and over everyprints:block whose source prints what Ferro raised or warned (f"{type(e).__name__}: {e}"orwarning.message).VARIESis keyed bypage:line.test_each_varies_entry_names_a_shows_blockcatches an entry that has gone stale.F31: the 0.26 notes said 0.26 "asks nothing"
That claim is gone. Two bullets are added:
No database routestill holds.test_no_route_message_is_the_shipped_textis renamed…_is_the_current_wording, with a comment that says what it pins.test_the_upgrade_guide_names_the_reworded_no_route_messagepins the section. RED: 1 failed.F32: one migration, two shapes; one number, two migrations
migration_errors.pynow builds the two-release chain from the guide's table:0002_author_slug_nullable: one expand step0003_author_slug_required: backfill, add-constraint, contractThe table, the prose and the three regenerated blocks name exactly these. The DDL lock-retry output uses the same nullable migration. The four-step
0002_author_slugstays the one-migration shape used elsewhere on the page, and no number names two migrations. The prose now says what a deployment does about release 1 meeting the contract step.F33: the Postgres redirect dropped URL options
I chose to carry options.
redirected_urlputs a PostgreSQL block's own query options onto the test URL. When a block names aferro_search_path, that schema is created first and dropped at exit. Soconnections.md:298andbackends.md:88run withtenant_aandtest_42. Thesslmode=requireblock is back to{test=compile reason="needs a TLS-enabled server"}. Pinned bytest_a_redirect_keeps_the_postgres_options_the_block_names(five cases).F34: two opposite rules for a Postgres-only block
session.py:295,:422: nowbackend=postgreswith a newinvoicesprelude;test_docstring_example_on_postgresruns both.auto-migrate.md:104,:125: now embedded models (F27).row-level-security.md:313: runs on Postgres with a newjwt_claimsprelude that creates the guide'sauth_uid(), and prints the claimedsubunderprints: above.F35: untrue compile reasons
rowsecurity.py:5: "a model fragment; its imports are left out".testing.py:56,:63: "a valueround_tripreturns;RoundTripResultis not imported here".F36: hand-typed class path
migration_errors.pyprintsf"{type(error).__name__}: {error}". The Deploying blocks now readPendingMigrationsError: …andDatabaseAheadError: …. The tracked-database block on Overview readsRunRefused: ….F37:
output:/refusal:notes unchecked off the listed pages_marked(kind)reads those notes from every page, throughpage_fences._registry_gapscompares them withOUTPUTSandREFUSALS, so a block under either note on any page must name a registered scenario. Pinned bytest_a_marked_block_on_any_page_is_compared[output|refusal], which adds a fabricated block onhowto/testing.md. No separate RED run: the test exercises the new helper.F38–F41
test_docs_migrations._quoted_refusals(regex) now uses the one fence reader._PYTEST_OUTPUTandtest_docs_migrations.py:1396are left.check_printedtakesdatabase_url, and the Postgres selection is_on_postgres. A full selector is left.Interfaces changed
tests/_docs_shared.py:snippet_regions,SnippetRegion,point_connect_at(url, *, before=None),redirected_url, all re-exported bytests/_docs_harness.py._docs_sql.snippet_rangesandSNIPPET,_docs_harness._section(now_included),test_docs_examples._MODELS_SECTION, and the prelude runner's ownpoint_connect_at._docs_sql: addedregion_lines,printing_fenceandon_postgres;check_printedtakesdatabase_url=.label_addition,invoices,jwt_claims. New example:enum_labels.pyand its twin.Verify
All on
c70297e:uv run --no-sync zensical build --clean --strict: no issues found.pytest tests/test_doc*.py tests/test_docstring_examples.py tests/test_step_class.py tests/test_routing_errors.py -n 8: 1153 passed, 126 skipped.--db-backends=sqlite,postgres: 1297 passed.pytest tests --db-backends=sqlite,postgres -n 8: 5739 passed, 69 skipped, 2 xfailed.python -m tests._docs_checks docs/pages docs/examples src/ferro: exit 0.ruff check src/ferro tests docs/examplesandruff format --check .: clean.Verify block corrected: no
Exit steps
Part of #698 (panel round 2 on PR #777)
Fix report: review round 1 (commit
74ee91e)F31:
update()column orderThe bullet named
update()among the writes that list columns in field order. That was wrong:update()keeps the order the call passes its keyword arguments, as it did in 0.25.save(),save(only=...),save(exclude=...)andbulk_create()for field order.update()is unchanged: it lists the columns in the order the call passes them, abytesor secret column included.test_the_upgrade_guide_states_each_write_door_s_column_orderpins that wording. It replaces the old check for the bare words "field order". RED: 1 failed.test_query_update_keeps_a_raw_column_where_the_call_puts_itpins the behaviour:update(e=, blob=b"y", a=)sendsSET "e", "blob", "a".F32: one number per migration
Three outputs on the Deploying guide now come from the same two-release chain as the rest of the page:
ci-check,server-upandstatus-steps. The new scenarios are_chain_writtenand_chain_late_row_failedintests/test_docs_migrations.py. The status prose names0003_author_slug_required.A grep of every
NNNN_name on the page now gives0001_create_author,0002_author_slug_nullableand0003_author_slug_required, one name per number.test_the_deploying_guide_names_one_migration_per_numberpins it. RED: the page atc70297ehad0002_author_slugand0002_author_slug_nullable.Verify on
74ee91etest_step_class,test_routing_errorsandtest_save_column_order: SQLite 1163 passed (126 skipped); with--db-backends=sqlite,postgres1315 passed._docs_checks: exit 0.ruff checkandruff format --check .: clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP