← Back

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.pyethica/ 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 mixinINV_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.Assign targets that are a single ast.Name; LOGIN_WINDOW, LOGIN_MAX = 300, 10 is a Tuple target, so it raised KeyError (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 <<JSON heredoc 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.