Initial extracted documentation set
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Async Boundaries
|
||||
|
||||
Keep sync and async APIs as separate, explicit surfaces when their semantics differ.
|
||||
|
||||
## Why
|
||||
|
||||
Async stops being an implementation detail as soon as it changes:
|
||||
|
||||
- how resources are acquired and released
|
||||
- whether methods must be awaited
|
||||
- which transport or session types are valid
|
||||
- how cancellation behaves
|
||||
- whether the caller needs an event loop
|
||||
|
||||
Trying to hide that behind one magical API usually makes things worse. The common failure mode is a fake sync wrapper over async internals: it breaks inside existing event loops, hides resource lifetime, and muddies cancellation.
|
||||
|
||||
Mature libraries usually accept the split and make it visible.
|
||||
|
||||
## The pattern
|
||||
|
||||
1. Expose separate sync and async entrypoints when semantics differ.
|
||||
2. Keep their shapes parallel where that helps learnability.
|
||||
3. Keep resource and transport types distinct.
|
||||
4. Make lifecycle visible with normal sync and async context management.
|
||||
5. Do not smuggle event-loop control into a sync-looking API.
|
||||
|
||||
## When to use
|
||||
|
||||
Use this when:
|
||||
|
||||
- your library owns network, database, filesystem, or other long-lived resources
|
||||
- sync and async variants need different transport or session implementations
|
||||
- callers need predictable lifetime control
|
||||
- the library must work in many runtime environments
|
||||
|
||||
## When not to use
|
||||
|
||||
Do not split APIs just for symmetry.
|
||||
|
||||
Skip the split when:
|
||||
|
||||
- async adds no meaningful semantic difference
|
||||
- the operation is trivial and one-shot
|
||||
- a separate async API would mostly duplicate noise
|
||||
|
||||
But if the alternative requires hidden `asyncio.run(...)`, loop-detection tricks, or silent runtime switching, the split is probably the cleaner design.
|
||||
|
||||
## Preferred shape
|
||||
|
||||
```python
|
||||
class Client:
|
||||
def get(self, url: str) -> Response:
|
||||
...
|
||||
|
||||
class AsyncClient:
|
||||
async def get(self, url: str) -> Response:
|
||||
...
|
||||
```
|
||||
|
||||
Make the surfaces parallel enough to learn once, but distinct enough that their lifecycle stays honest.
|
||||
|
||||
## Why this works
|
||||
|
||||
- callers immediately know whether `await` is involved
|
||||
- transport types stay correct
|
||||
- connection pooling and cleanup remain visible
|
||||
- cancellation behavior is not hidden behind sync-looking calls
|
||||
|
||||
## Counterexamples
|
||||
|
||||
### Fake sync wrapper over async internals
|
||||
|
||||
```python
|
||||
def get(url: str) -> Response:
|
||||
return asyncio.run(_async_get(url))
|
||||
```
|
||||
|
||||
This breaks in environments that already have an event loop and hides lifecycle costs.
|
||||
|
||||
### One class with mode flags
|
||||
|
||||
```python
|
||||
client = Client(async_mode=True)
|
||||
```
|
||||
|
||||
Now methods, cleanup, and caller expectations depend on ambient configuration instead of the type.
|
||||
|
||||
### Shared transport types that are not really shared
|
||||
|
||||
If one path needs `BaseTransport` and the other needs `AsyncBaseTransport`, pretending they are interchangeable is lying to the caller.
|
||||
|
||||
## Source signals
|
||||
|
||||
### HTTPX
|
||||
|
||||
- `httpx/_client.py:594-661` defines `Client` as the sync entrypoint and documents that it can be shared between threads.
|
||||
- `httpx/_client.py:639-660` types the sync constructor in sync-native terms, including `BaseTransport`.
|
||||
- `httpx/_client.py:1275-1304` uses normal sync context management for lifecycle.
|
||||
- `httpx/_client.py:1307-1375` defines `AsyncClient` separately and documents task-sharing semantics.
|
||||
- `httpx/_client.py:1316-1318` shows async usage with `async with` and `await`.
|
||||
- `httpx/_client.py:1353-1374` keeps the constructor parallel but switches to async-native types like `AsyncBaseTransport`.
|
||||
- `httpx/_client.py:1445-1452` initializes `AsyncHTTPTransport` on the async path rather than pretending the sync transport is reusable.
|
||||
|
||||
### SQLAlchemy
|
||||
|
||||
- `examples/asyncio/async_orm.py:15-18` imports async-specific engine and session primitives.
|
||||
- `examples/asyncio/async_orm.py:61-67` creates an `async_sessionmaker(...)` and enters explicit async session and transaction scopes.
|
||||
- `examples/asyncio/async_orm.py:78-104` uses async-native query and commit methods.
|
||||
- `examples/inheritance/joined.py:16` imports the sync `Session` separately.
|
||||
- `examples/inheritance/joined.py:93-120` shows the corresponding sync session lifetime and explicit commit boundary.
|
||||
|
||||
## Bottom line
|
||||
|
||||
If sync and async usage have different semantics, give them different types.
|
||||
|
||||
Parallel APIs are good.
|
||||
Pretending the difference is not there is not.
|
||||
Reference in New Issue
Block a user