Skip to content

feat(story-2.2): phase-shortcut decorators @before_request, @after_response, @on_error#9

Merged
lesnik512 merged 10 commits into
mainfrom
story/2-2-phase-shortcut-decorators
May 31, 2026
Merged

feat(story-2.2): phase-shortcut decorators @before_request, @after_response, @on_error#9
lesnik512 merged 10 commits into
mainfrom
story/2-2-phase-shortcut-decorators

Conversation

@lesnik512
Copy link
Copy Markdown
Member

Summary

  • Adds three phase-shortcut decorators in src/httpware/middleware/__init__.py for writing lifecycle hooks without authoring a full Middleware class:
    • @before_request wraps async f(Request) -> Request and forwards the transformed request to the chain.
    • @after_response wraps async f(Request, Response) -> Response and applies f to the response.
    • @on_error wraps async f(Request, Exception) -> Response | None, catches Exception only (CancelledError propagates), returns the handler's Response or re-raises if the handler returns None.
  • Each decorator returns a private class instance with f captured via closure and a __repr__ of the form <before_request(f.__qualname__)> for clean chain inspection.
  • All three exported at both httpware.middleware.* and httpware.*.
  • 10 new tests in tests/test_middleware.py (24 total): request and response transformations, on_error swallow/re-raise paths, CancelledError non-capture, exception identity, Protocol satisfaction, mixed-chain composition, repr() content, package-root re-export.

Bundled-in doc fix: docs/engineering.md §8 line 145 had stale @on_request/@on_response names from the distillation — corrected to the canonical @before_request/@after_response.

Out of scope (subsequent stories): Request.with_* helper expansion (2-3), auth coercion (2-4), AsyncClient wiring (2-5).

Spec + plan: docs/superpowers/specs/2026-05-31-phase-shortcut-decorators-design.md, docs/superpowers/plans/2026-05-31-phase-shortcut-decorators-plan.md.

Test plan

  • just test — 184 passed, 1 deselected, 100% line coverage on the new factories.
  • just lint-ci clean (`eof-fixer`, `ruff format --check`, `ruff check --no-fix`, `ty check`).
  • tests/test_no_httpx2_leakage.py still passes.
  • `from httpware import before_request, after_response, on_error` and the subpackage path both resolve.
  • CI green on all matrix entries (3.11/3.12/3.13/3.14 + lint).

🤖 Generated with Claude Code

lesnik512 and others added 9 commits May 31, 2026 17:28
Three decorators (@before_request, @after_response, @on_error) wrap
async user functions into Middleware-conforming instances so consumers
can write lifecycle hooks without authoring a full Middleware class.

Decisions: archive-spec naming (before/after for phases around success;
on_error for the event); colocated with the Protocol in
middleware/__init__.py; per-decorator private class with f.__qualname__
in __repr__; @on_error catches Exception only (CancelledError
propagates), returns Response to recover or None to re-raise.

Bundled in: fix docs/engineering.md §8 line 142 stale decorator names
(@on_request/@on_response → @before_request/@after_response).

Strict epic boundary — AsyncClient wiring (2-5), Request helpers (2-3),
auth coercion (2-4) land separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps an async f(Request) -> Request into a Middleware that applies f
then forwards the (possibly transformed) request down the chain via
await next(...). Returns a private _BeforeRequestMiddleware instance
with a __repr__ that surfaces the original function name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps an async f(Request, Response) -> Response into a Middleware that
awaits next(...) then applies f to the result. Returns a private
_AfterResponseMiddleware instance with the standard __repr__.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps an async f(Request, Exception) -> Response | None into a
Middleware. Catches Exception (not BaseException, so CancelledError
propagates). If the handler returns a Response, that becomes the
caller's response; if it returns None, the original exception is
re-raised with traceback intact via bare `raise`.

Four tests pin the contract: recovery via Response, re-raise via None,
CancelledError flows past untouched, handler receives the original
exception instance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three tests verify the three decorators interoperate:
- isinstance() recognizes each as Middleware
- a mixed chain of @before_request + class middleware + @after_response
  applies each phase in the correct position
- repr() surfaces the phase name and the original user function's
  qualname for debug-friendly chain inspection

No production code changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…NGELOG

Adds before_request, after_response, on_error to httpware/__init__.py
imports and __all__ so consumers can `from httpware import …` in
addition to the subpackage path.

Fixes docs/engineering.md §8 line 145 to reflect the canonical
@before_request / @after_response / @on_error names (it had stale
@on_request / @on_response from the distillation).

CHANGELOG records the Story 2.2 surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… protocol

ty rejected `def stream(...) -> None:` on _FailingTransport because the
Transport protocol requires the method to return
AbstractAsyncContextManager[StreamResponse]. Aligns with the
_OkTransport.stream stub shape used since Story 2-1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the 95% → 100% coverage gap on src/httpware/middleware/__init__.py
flagged by the final code review. The three __repr__ bodies are
structurally identical, but the spec called for 100% line coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 self-assigned this May 31, 2026
Drops two `# noqa: PLC0415` suppressions on in-function imports of
`httpware` in the reexport tests. PLC0415 (import-outside-top-level)
was flagged as a code smell; hoisting the import keeps both reexport
assertions working and removes the suppression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 merged commit 1cb87fd into main May 31, 2026
5 checks passed
@lesnik512 lesnik512 deleted the story/2-2-phase-shortcut-decorators branch May 31, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant