Skip to content

feat: AsyncClient.aclose() + modern-di setup-friction recipe#29

Merged
lesnik512 merged 5 commits into
mainfrom
feat/modern-di-recipe-and-aclose
Jun 7, 2026
Merged

feat: AsyncClient.aclose() + modern-di setup-friction recipe#29
lesnik512 merged 5 commits into
mainfrom
feat/modern-di-recipe-and-aclose

Conversation

@lesnik512

@lesnik512 lesnik512 commented Jun 6, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds httpware.AsyncClient.aclose() — a standalone async teardown method mirroring the body of __aexit__. Idempotent, owns-client-only. Enables non-context-manager scoping (DI containers, background workers).
  • Ships a setup-friction recipe at docs/recipes/modern-di.md walking through wiring AsyncClient into a modern-di container's lifecycle: minimal Factory + finalizer → multi-backend DuplicateProviderTypeError → per-backend wrapper-subclass fix → middleware in kwargs=.
  • Adds a Recipes nav section to mkdocs.yml and one back-link from docs/index.md.

Why bundle the method with the docs

The recipe's finalizer reads as finalizer=AsyncClient.aclose — a clean one-liner. Without the method, the only path was await c.__aexit__(None, None, None), which signals a library gap and reads as a workaround. Shipping both keeps the public surface coherent with the project's own naming convention (CLAUDE.md names aclose() as the sole a-prefixed method exception, but it didn't exist).

What's deliberately NOT in this PR

  • No dedicated aclose test for the _owns_client=False branch. The spec excluded it on the basis that test_aexit_does_not_close_borrowed_httpx2_client covers the same guard expression. The final reviewer flagged this as a minor coverage gap; line coverage is 100% (the enforced gate), but branch coverage on the new method is < 100%. Happy to add test_aclose_does_not_close_borrowed_httpx2_client in a follow-up if desired.
  • No async def workaround snippet alongside the "lambda doesn't work" warning in the recipe. The direct-method form is strictly better; a closure rewrite would be padding.
  • No back-links from docs/resilience.md, docs/middleware.md, docs/errors.md, docs/testing.md — those are topical reference pages and shouldn't fan into a recipe.
  • No back-link from modern-di/docs/integrations/ — that's a separate PR in the sibling repo if desired.

Commits

SHA Subject
a2c1fbc docs(spec): brainstorm modern-di setup-friction recipe + AsyncClient.aclose
05b12a2 docs(plan): implementation plan for modern-di recipe + AsyncClient.aclose
a1b9faa feat(client): add AsyncClient.aclose() standalone teardown
0142110 docs(recipes): add modern-di setup-friction recipe
e91892f docs(recipes): correct DuplicateProviderTypeError message text

Test plan

  • just test — 253 passed (251 baseline + 2 new), 100% coverage enforced
  • just lint — ruff format + ruff check + ty check all clean
  • uv run --with mkdocs --with mkdocs-material mkdocs build --strict — clean, generates recipes/modern-di/index.html
  • End-to-end verification against the published modern-di: finalizer round-trip, multi-backend distinct providers + both finalizers fire, documented DuplicateProviderTypeError raises with the exact message text the recipe shows
  • Reviewer: eyeball the rendered recipe page locally with uv run --with mkdocs --with mkdocs-material mkdocs serve and open http://127.0.0.1:8000/recipes/modern-di/

🤖 Generated with Claude Code

lesnik512 and others added 5 commits June 6, 2026 22:43
…aclose

Setup-friction recipe for wiring `httpware.AsyncClient` into a `modern-di`
container. Documents two non-obvious moments real users will hit:

- Lifecycle finalizer: bridging AsyncClient's context-manager teardown
  with modern-di's Factory(cache_settings=CacheSettings(finalizer=...))
  via the unbound `AsyncClient.aclose` (modern-di auto-awaits async
  coroutine functions; a `lambda c: c.aclose()` would NOT work because
  iscoroutinefunction returns False on lambdas).

- Multi-backend collision: two Factory(creator=AsyncClient,...) providers
  raise DuplicateProviderTypeError at container construction (verified
  against modern_di/registries/providers_registry.py:42-44). Idiomatic
  fix is a per-backend wrapper subclass so each Factory has a distinct
  bound_type.

While drafting, found that `httpware.AsyncClient` exposes __aenter__ /
__aexit__ but no standalone aclose() — even though the CLAUDE.md naming
convention names it as if it exists. Spec scopes adding aclose() into
the same PR so the recipe finalizer can be the clean `AsyncClient.aclose`
instead of a workaround calling __aexit__ directly.

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

Three tasks: (1) TDD aclose() with two new tests, (2) write the recipe
page + mkdocs nav + index back-link, (3) verification (round-trip
finalizer call, multi-backend distinct providers, exact-name-of-error
gate for the documented DuplicateProviderTypeError).

Plan corrects a spec discrepancy: lifecycle tests go in
tests/test_client_lifecycle.py, not the non-existent tests/test_client.py.
Test names tightened to match the existing test_aexit_* convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the body of __aexit__: closes the underlying httpx2 client iff
we own it and it isn't already closed. Idempotent.

Use case: DI containers, background workers, anything not request-shaped
that can't lean on `async with`. Aligns the library with its own CLAUDE.md
naming convention which already names aclose() as the sole a-prefixed
method exception.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Linear-narrative walk-through of wiring AsyncClient into a modern-di
container: minimal Factory + finalizer → multi-backend type collision
(DuplicateProviderTypeError) → per-backend wrapper-subclass fix →
middleware in kwargs.

Adds a new top-level "Recipes" nav section (single item for now) and
one back-link from the index's "Where to go next".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end verification against the published modern-di surfaced that
the actual exception message reads "Provider is duplicated by type
<class 'httpware.client.AsyncClient'>. To resolve this issue: ..." —
not the shorter "AsyncClient is already registered" the recipe showed.
Module path and class name were already correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 self-assigned this Jun 7, 2026
@lesnik512 lesnik512 merged commit aa0d9f6 into main Jun 7, 2026
5 checks passed
@lesnik512 lesnik512 deleted the feat/modern-di-recipe-and-aclose branch June 7, 2026 05:56
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