Bug-fix report — server.py break-out (2026-06-15)
One real defect (plus two tooling gotchas) found by the Docker smoke during the
server.py → ethica/ package break-out. Patch-note summary:
../Patch/patch-notes-2026-06-15-server-breakout-24d2407.md.
The break-out was behaviour-preserving and every step was smoke-gated, which is exactly why
this was caught before it could ship.
BUG-1 · Class-level Handler attributes lost when the class was recomposed
Symptom: at the final step (dispatch + compose), the smoke dropped from 157/0 to 150/7.
The 7 failing routes returned empty/500 bodies (one — the unknown-slug fall-through — returned
no response at all): /admin/audit, the company Billing tab, the customer /account page
(+ its invoices), and the live CMS page / unknown-slug fall-through. The kept web container’s
log showed AttributeError: 'Handler' object has no attribute 'INV_STATUS' (and AUDIT_CATS,
RESERVED_TOP).
Cause: the handler methods were moved out of the monolithic Handler class into route-group
mixin classes by an extractor that moved only defs (method nodes). Three class-level
attributes defined in the class body — INV_STATUS (invoice status→colour, used by
_inv_badge), AUDIT_CATS (audit-page filter categories), and RESERVED_TOP (reserved
top-level URL slugs, used by the live-content fall-through) — are Name = … assignments, not
methods, so they were left behind on the old class. At step 20 the old Handler class was
replaced by the composed class Handler(…mixins…): pass in app.py, and those three
attributes vanished. Every method that read self.INV_STATUS / self.AUDIT_CATS /
self.RESERVED_TOP then raised AttributeError at request time → 500/empty.
Fix: restored each attribute onto its owning mixin — INV_STATUS on BillingHandlers,
AUDIT_CATS on SettingsHandlers, RESERVED_TOP on PublicHandlers (so self.X resolves via
the same MRO as the methods that use them). Re-smoke: 157/0. No behaviour change — the
attributes are identical to the originals, just relocated to live with the methods that read them.
Why the earlier steps were green: through step 19 the attributes were still on the (not-yet-
replaced) concrete Handler, so self.X resolved fine; only step 20’s class replacement
exposed the gap — which the per-step smoke immediately flagged.
Prevention: when extracting methods out of a class, also relocate the class body’s non-method
assignments (or assert the source class has no ast.Assign left before discarding it).
Tooling notes (extraction scripts, not the app)
- N-1 · tuple-unpacking assignment skipped. The AST mover keyed on
ast.Assigntargets that are a singleast.Name;LOGIN_WINDOW, LOGIN_MAX = 300, 10is aTupletarget, so it raisedKeyError(before any file write — no damage). Captured that block by line-search instead. - N-2 · unquoted heredoc ate a backticked word. A module-doc spec written with an unquoted
<<JSONheredoc ran`pages`as a command substitution, dropping the word from a docstring (cosmetic; fixed inline). Switched the remaining specs to Python-built JSON.
Verification
Docker smoke 157/0 on the merged main (24d2407); py_compile clean across server.py +
ethica/**; docs_reader (27) + newsletter (7) unit tests pass.