CancelLens
CancelLens tests what happens when an asyncio operation is cancelled while suspended.
It observes actual suspension points, repeats the operation on fresh state, and sends
real Task.cancel() requests. Your oracle checks resource release, state consistency,
and whether another operation can use the same pool or resource.
It works with ordinary pytest tests, native Python coroutines and real I/O. Cancellation sweeps are an existing technique; a passing sweep is evidence for the checked schedules and your invariants, not a proof of general cancellation safety.
Installation
Install a published release:
python -m pip install cancellens
For a source checkout:
uv sync --locked
uv build
uv pip install dist/cancellens-0.1.0-py3-none-any.whl
The runtime uses only the standard library. Tests can use pytest without a plugin or pytest-asyncio.
Quick start
import asyncio
from cancellens import Case, Completion, require, sweep
def test_pool_survives_cancellation():
async def factory():
return asyncio.Semaphore(1)
async def operation(pool):
await pool.acquire()
try:
await asyncio.sleep(0)
finally:
pool.release()
async def oracle(pool, completion: Completion):
require(completion.state in ("returned", "cancelled"), "operation terminated")
require(not pool.locked(), "pool slot returned")
await pool.acquire() # Check reuse of the SAME resource.
pool.release()
async def teardown(pool):
if pool.locked():
pool.release()
asyncio.run(sweep(Case(factory, operation, oracle, teardown))).assert_passed()
Move pool.release() out of finally to the happy path and cancellation loses the
slot. The oracle reports FAIL before teardown repairs it. The
pool example includes
both versions.
Case lifecycle
Every attempt creates fresh state through four async callbacks:
factory()creates and returns the state. It owns cleanup of incomplete setup.operation(state)returns a native Python coroutine and runs in a real Task.oracle(state, completion)checks state before harness cleanup. It can explicitly await application-owned cleanup and exercise the same resource again.teardown(state)cleans up the attempt, including failed attempts. A successfully returned factory resource is retained for teardown even if caller cancellation interrupts the handoff.
Use require(condition, "invariant name") or raise InvariantViolation to report a
contract failure. Other oracle exceptions, including AssertionError, are errors in
evaluating the test. Use static, non-sensitive invariant text: it appears in reports.
Completion.state is returned, cancelled, raised, pending, or not_started.
value and exception are available in memory. Operation return values, exceptions,
and suppressed cancellation are judged by your oracle. Operation Task completion
preserves cancelled() and exception() for done callbacks.
Schedules, outcomes and coverage
A sweep runs two uncancelled baselines and compares their complete traces. It then
cancels at each baseline point. With max_depth=2, the default, it also tests a second
cancellation at later points observed on each valid single-cancel path. Those points
can include cleanup that never ran in the successful baseline.
Requests are sent only after the required trace prefix matches. The second request also requires confirmed delivery of the first. There are no random delays selecting cleanup targets, and no extra awaits are inserted into the operation.
- PASS: the schedules in the report's scope were evaluated and satisfied the oracle.
- FAIL: an attributable schedule violated an explicit invariant.
- UNRESOLVED: a target or delivery could not be confirmed, replay diverged, a budget cut coverage short, or no suspension points existed.
- ERROR: setup, instrumentation, oracle code or infrastructure failed.
Each attempt retains its schedule, trace, cancellation acceptance/delivery, operation completion, oracle and teardown outcomes. FAIL and ERROR are preserved independently of cleanup failures. An oracle violation on an invalid schedule remains visible but is not promoted to an established cancellation bug.
report.coverage exposes planned/valid single and double schedules, baseline points,
attempt count, completeness and reasons. Incomplete coverage cannot produce PASS.
report.assert_passed() gives a short pytest failure with schedule and coverage.
from cancellens import Limits
limits = Limits(
max_points=100,
max_attempts=100, # Includes both baselines.
max_depth=2, # Choose 1 for a single-cancel sweep.
phase_timeout=2.0,
total_timeout=30.0,
cleanup_timeout=0.1,
)
Reports and replay
import json
from cancellens import Schedule, replay
# Inside an async test, with a Case already defined:
report = await sweep(case)
saved = json.loads(report.to_json())
assert saved["schema_version"] == 1
schedule_data = next(a["schedule"] for a in saved["attempts"] if a["schedule"]["cancel_at"])
replayed = await replay(case, Schedule.from_dict(schedule_data))
replayed.assert_passed()
A schedule stores one-based targets and the full observed prefix through its last target. Replay checks visible frame locations, bytecode offsets, code fingerprints and yielded types. Different Python minor versions are refused; changed source or scheduling may make a schedule unresolved. A replay report covers the selected schedule, not a whole sweep. An empty schedule is an uncancelled replay control.
Reports omit locals, object reprs, absolute paths, SQL, response bodies, operation values and exception messages. Code metadata, exception types and explicit invariant text remain visible. See the design and report reference.
Supported environments
CPython 3.10–3.14, standard asyncio event loop, default Task factory. CI runs the suite on Linux and Windows across all five versions. macOS is also used for local validation. Python 3.15+, PyPy, uvloop, free-threaded builds, custom/eager factories and Trio/AnyIO cancellation semantics are outside v0.1's supported scope.
Observation follows nested Python awaits. Child Tasks and shielded work are boundaries: only the caller's wait is visible, and shielded cleanup may outlive the caller. The oracle must check or explicitly await it. On CPython 3.10, a cancelled child Task can lose the cancellation request marker; uncertain delivery is UNRESOLVED. Child suppression or replacement of the marker can also prevent attribution on later versions.
C awaitables and coroutine_wrapper can hide deeper source frames. No exact source
location is promised for every extension awaitable. Wrapper frames and Task.get_coro()
identity are not preserved. Lifecycle phases run in separate Tasks, so pass setup
configuration explicitly in case state rather than relying on changed ContextVars.
Prefix repeatability does not imply program-wide determinism or exhaustive interleavings.
Deadlines and cleanup
In-process deadlines are cooperative. They cannot stop a blocked event loop, arbitrary native code, a cancellation-resistant coroutine or a running thread. After a deadline, the oracle observes state before teardown and emergency cancellation. The exploration budget can be extended by the current oracle/teardown windows and final drain.
Caller cancellation propagates after attempted bounded teardown; another external cancellation can interrupt finalization. Cases own application Tasks, threads and external resources. CancelLens does not automatically roll back arbitrary side effects.
Use an external watchdog for potentially hanging tests:
uv run python validation/watchdog.py --timeout 90 -- uv run pytest -q
The validation watchdog terminates/kills the child process group on POSIX and the immediate child on Windows. It cannot undo external side effects.
External validation: aiosqlite
The aiosqlite case uses real SQLite connections, temporary files and upstream worker threads. Event-controlled connector hooks select cancelled opening without replacing the upstream lifecycle. Independent native queries and worker joins check the resource before final cleanup.
- Base
9b127cecc7de843bf4f343b6da63b641866533fb: FAIL because the native connection remains open. - Proposed fix
005b3aa77a67606d2264996aeff4f69448fb79fc: PASS for the same logical opening schedule.
Recorded evidence contains repeated fresh sweeps, opening replays and uncancelled controls. This verifies the selected proposed patch's opening contract, not all aiosqlite cancellation behavior. The optional HTTPX/httpcore double-cancel integration is not verified in v0.1.
Contributing and releases
See Contributing for locked development setup, regression checks and integration reproduction. Changes are listed in the changelog.
Related work
- SQLAlchemy cancellation testing provides greenlet-aware sweeps and connection/pool accounting for its async dialects.
- Pyleak detects task/thread leaks and loop blocking.
- Aiotools cancellation utilities provide application cancellation/waiting helpers with transparent caller cancellation.
- Simloom explores asyncio programs in a simulated scheduler/network environment.
CancelLens uses independent coroutine-protocol instrumentation, the standard loop, real I/O and user-defined resource oracles. No code from these projects is incorporated.
Metadata
Release files for cancellens 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| cancellens-0.1.0.tar.gz | 104.1 kB | With provenance | Details for cancellens-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cancellens-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for cancellens-0.1.0-py3-none-any.whl |
Total release size: 123.2 kB
Release files / cancellens-0.1.0.tar.gz
| Download URL | cancellens-0.1.0.tar.gz |
|---|---|
| Size | 104.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
51e972ddd6e4b188568603debca39f8618c96cacb5fbc7cab52d6e0c2521cbbf
|
|
BLAKE2b-256 checksum How to use checksums |
fca1f3fb80e0a5b823aeb78935162f25b9b4d4890cea02c141f3d983bff2a687
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / cancellens-0.1.0-py3-none-any.whl
| Download URL | cancellens-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e80118f7c3f232ebb3772731ed6a3eb0b91ee1d75fdc2fb31ad25e18182adabd
|
|
BLAKE2b-256 checksum How to use checksums |
439ed992c47596b8e7bf2a8cd1228af67ec150c3681f1629b59e451412115401
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logSecurity for cancellens 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for cancellens-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.