# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.6] - 2026-06-09

### Added

- Public docs on https://aiointercept.readthedocs.io

### Improved

- The per-`start()` DNS-cache purge now tracks live `TCPConnector` instances in a `weakref.WeakSet` instead of scanning the whole heap via `gc.get_objects()`, so its cost no longer grows with the process heap size. Especially impactful for large applications and test suites. Thanks to [@agroebe](https://github.com/agroebe).

## [0.1.5] - 2026-06-02

### Added

- Public `async start()` / `async stop()` lifecycle methods. `__aenter__` / `__aexit__` now delegate to them, so the mock can be driven from setup/teardown hooks (e.g. `unittest.IsolatedAsyncioTestCase`).
- Unmatched requests now log a diagnostic `WARNING` on `aiointercept.core` explaining why nothing matched: an exhausted finite `repeat`, an `ndiff` against the closest registered `METHOD URL`, or the registered patterns.

## [0.1.4] - 2026-05-23

### Added

- Auto-discovered pytest plugin with `aiointercept_server` (session-scoped) and `aiointercept_mock` (function-scoped, calls `clear()` between tests and re-pins `_caller_loop` so async callbacks dispatch on the test's loop). Requires `pytest-asyncio`.
- `benchmarks/bench_compare.py` — standalone script comparing `aiointercept` (dns on/off) against `aioresponses` across nine scenarios. Run via `uv sync --group benchmarks && uv run python benchmarks/bench_compare.py`.

### Fixed

- `clear()` followed by re-registering an HTTPS handler no longer breaks requests that reuse an existing keep-alive connection. `add()` now records the host in `_https_hosts` eagerly when the registered URL is `https://`, so the dispatcher keeps the correct scheme even when aiohttp skips the SSL hook on reused connections.

## [0.1.3] - 2026-05-17

### Fixed

- The intercept `TestServer` no longer runs on the caller's event loop. It now starts on a dedicated daemon thread with its own loop, so callers that block their own loop between `__aenter__` and `__aexit__` (e.g. Starlette/FastAPI `TestClient`, which holds the loop during a synchronous `client.get(...)` call) can no longer deadlock the mock server. Plain mocks (`m.get(url, status=...)`) under `TestClient` now work by construction.
- During the DNS-cache purge in `_clear_existing_connector_caches`, the `isinstance(obj, aiohttp.TCPConnector)` check is now performed inside `contextlib.suppress(Exception)`, so a stray object whose `__class__` lookup raises during `gc.get_objects()` iteration no longer aborts the purge for the remaining connectors.

### Changed

- Async user callbacks registered via `callback=` are now executed on the caller's event loop (the loop active when `__aenter__` was called), even though the server runs on its own loop. This preserves prior semantics: `asyncio.Event`, `asyncio.Queue`, `asyncio.Lock`, and other loop-bound primitives shared between the test and the callback continue to work. Sync callbacks are unaffected and continue to run on the server loop.
- Package metadata modernized: `license` is now an SPDX expression (`"MIT"`) and the deprecated `License :: OSI Approved :: MIT License` trove classifier was removed.

### Internal

- Expanded the `ruff` lint rule set (`UP`, `B`, `C4`, `SIM`, `N`, `RUF`, `PT`, `TCH`, `ASYNC`, `PERF`, `RET`, `ARG`) with `line-length = 120` and `target-version = "py310"`, and reformatted `aiointercept/core.py` accordingly: PEP 585 `type[X]` over `typing.Type[X]`, string-form `cast("X", ...)` annotations, `contextlib.suppress` instead of `try/except/pass`, and consolidated imports. No behaviour change.
- Bumped the `mypy` dev dependency to `>=2.0.0,<2.1.0`.

### Known limitations

- The narrow combination of an **async callback** *and* a caller loop that is fully blocked (e.g. an async `callback=` inside a Starlette `TestClient` request) still deadlocks: the callback is scheduled onto the caller's loop, which cannot run it while it is blocked. Plain mocks under `TestClient` are unaffected.

## [0.1.2] - 2026-05-12

### Added

- `AiointerceptRequest` is now exported from the top-level `aiointercept` package so users can type-annotate recorded requests without reaching into `aiointercept.core`.

### Fixed

- `clear()` now also resets the internal `_https_hosts` set, so a host previously seen with HTTPS traffic is no longer incorrectly treated as HTTPS after `clear()` is called.
- `exception=` (any truthy value) now correctly registers the target host in `_host_list` before returning, ensuring DNS is redirected to the mock server by design rather than by the fallback path.
- `passthrough_unmatched=True` now proxies unmatched paths for URL-registered hosts (not just pattern-registered ones). Previously, a registered host with an unknown path would close the connection even when `passthrough_unmatched=True`.
- `_host_list` is now a `set` instead of a `list`, preventing duplicate host entries when the same URL is registered multiple times.
- Passing `passthrough_unmatched=True` without `mock_external_urls=True` now raises `ValueError` at construction time instead of being silently ignored.

### Changed

- `mock_external_urls` now defaults to `False`, making it an optional parameter. Callers that omit it get the recommended no-DNS-patching mode.
- Renamed `AiointerceptRequest._captured_body` → `AiointerceptRequest.captured_body` (now public). 

### Internal

- Renamed `AiointercepRequest` → `AiointerceptRequest` (added missing `t`).
- Renamed `AiointerceptRequstKwargs` → `AiointerceptRequestKwargs` (fixed `Requst` → `Request`).
- Replaced the `Exception`-class-as-sentinel pattern with a named `_CloseConnection` sentinel for the "close transport" handler marker.
- Added comments on the 502 fallback responses in `_dispatch` clarifying they only surface if `transport.close()` does not take effect.

## [0.1.1] - 2026-05-04

Initial public release.

### Added

- Real `aiohttp.web` test server — requests travel through an actual HTTP stack instead of being short-circuited in memory.
- Two interception modes controlled by the `mock_external_urls` constructor argument:
  - `False` — server starts on localhost; point your client at `m.server_url` directly. No global state patched.
  - `True` — patches `ThreadedResolver`/`AsyncResolver` at the class level so any `aiohttp` request is redirected, regardless of hostname.
- HTTPS interception (`mock_external_urls=True`): patches `TCPConnector._get_ssl_context` to strip TLS for intercepted hosts and reconstructs the original `https://` URL server-side via an injected `X-Aiointercept-Orig-Scheme` header.
- `aioresponses`-compatible registration API: `m.get/post/put/patch/delete/head/options`, `m.add`, `CallbackResult`.
- Regex pattern matching via compiled `re.Pattern` URLs.
- Sync and async callback support; callbacks receive `url`, `headers`, `query`, and `json`.
- `repeat=True` (unlimited) and `repeat=N` (finite) response queuing; multiple `add()` calls for the same URL queue responses in order.
- `passthrough` — list of hosts to bypass the mock and hit the real network.
- `passthrough_unmatched=True` — forward unregistered requests to the real server instead of raising `ClientConnectionError`.
- `m.requests` — dict keyed by `(METHOD, yarl.URL)` recording every intercepted request, with parsed `headers`, `query`, and `json` in `request.kwargs`.
- `m.clear()` — reset registered handlers and recorded requests without tearing down the server.
- `m.server_url` — base URL of the local test server (available inside the `async with` block).
- Assertion helpers: `assert_called`, `assert_not_called`, `assert_called_once`, `assert_any_call`, `assert_called_with`, `assert_called_once_with`.
- SSL context caching to avoid redundant per-host lookups.
- Decorator usage with optional `param=` to name the injected mock argument.

### Known limitations

- **Bare IP addresses** (`http://1.2.3.4/path`) are not intercepted when `mock_external_urls=True` because DNS patching has no effect on numeric addresses.
- **`exception=`** only closes the connection, surfacing a `ClientConnectionError` on the client. Raising arbitrary exception types is not supported.
- **`timeout=` passthrough** is not supported.
- **`CallbackResult(response_class=)`** is silently ignored.
- request `**kwargs` contains only `headers`, `query`, and `json` — not the full `aiohttp` request kwargs set.
