Metadata-Version: 2.4
Name: async-doctor
Version: 0.1.0
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Rust
Classifier: Topic :: Software Development :: Quality Assurance
License-File: LICENSE
License-File: THIRD_PARTY_NOTICES.md
Summary: Your agent blocks the event loop. This catches it.
Keywords: asyncio,static-analysis,linter,event-loop,blocking
License-Expression: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/maolonglong/async-doctor/releases
Project-URL: Homepage, https://github.com/maolonglong/async-doctor
Project-URL: Issues, https://github.com/maolonglong/async-doctor/issues
Project-URL: Repository, https://github.com/maolonglong/async-doctor

# async-doctor

**Your agent blocks the event loop. This catches it.**

Zero-config static analysis for Python: finds sync/blocking IO reachable from
`async def` code, and reports evidence an agent can act on. Non-goals (no
runtime probing, no Trio-only / gevent-eventlet modelling) live in
[ARCHITECTURE.md](ARCHITECTURE.md).

## Install

```bash
# Once published to PyPI:
uvx async-doctor .
pipx run async-doctor .
pip install async-doctor && async-doctor .

# From this repo (wheel):
maturin build --release
uvx --from ./target/wheels/async_doctor-*.whl async-doctor --version

# From source (Rust toolchain required):
cargo install --git https://github.com/maolonglong/async-doctor async_doctor
```

Activate the project venv (or set `VIRTUAL_ENV`) before scanning so third-party
imports resolve. Without a project environment, results silently degrade —
`async-doctor` reports the interpreter/venv it used and prints a short note.

## Quick start

```bash
async-doctor .
async-doctor --score .
async-doctor --format json .
async-doctor --format markdown . >> "$GITHUB_STEP_SUMMARY"
async-doctor --format sarif . > async-doctor.sarif

async-doctor explain blocking-sleep
async-doctor why app.py:12 --path .
async-doctor install-skill --path .

# CI / agent loops (full analysis; filtered reporting):
async-doctor --changed .
async-doctor --changed=origin/main .
async-doctor --write-baseline baseline.json .
async-doctor --baseline baseline.json .
```

Every finding includes the full call chain (`file:line` per hop), a concrete fix
hint, a stable rule id, a `docsAnchor` into [docs/rules.md](docs/rules.md), and
a runnable `why` command. No config file — flags and inline comments only.

```python
time.sleep(1)  # async-doctor: ignore[blocking-sleep]: brief sync boundary in CLI wrapper
```

Unused ignores fail the run. Exit `0` = clean, `1` = findings (or unused
suppressions), `2` = tool error. Incomplete analysis sets `complete=false` and
does **not** use exit 2. Candidates and **advice** never affect the exit code.

By default, entries under `tests/` / `docs/` / `examples/` / `scripts/` /
hidden dirs are skipped (`--include-non-production` to include).
`reason=unresolved` candidates are hidden unless `--show-unresolved`.

## What it detects

- Sync/blocking IO reachable from `async def` (sleep, socket, file, subprocess,
  locks, smtplib/ftplib, zip/tar, glob/pathlib walks, `input`, futures waits)
  with prove-only conditions and entry labels
- Site-packages follow down to stdlib sinks; third-party hops marked in JSON
- Third-party rules: `requests`, sync `httpx`, redis, SQLAlchemy, psycopg,
  boto3 client/resource calls, sync OpenAI/Anthropic, and more — async twins
  never fire as findings
- Higher-order / offload: `asyncio.to_thread`, executors, inferred wrappers
- **Advice** (`offload-has-async-twin`, `dangling-task`): offload cuts that only reach sinks with
  a high-confidence same-package async twin installed in the scanned venv, and
  bare `asyncio.create_task` / `ensure_future` statements that discard the
  returned `Task`
  (non-blocking; SARIF `note`; text **Advice** section, capped unless `--expand`)
- **Errors:** `nested-event-loop` (`asyncio.run` / … from async without offload;
  loop-state guards like `if loop is None` / early-return `if loop is not None:
  return` are dropped), `offload-coroutine` (offload target is `async def`),
  `sync-with-async-cm` (sync `with` entering an async-only context manager)

## GitHub Actions (SARIF / code scanning)

Upload SARIF so findings appear in GitHub code scanning. `async-doctor` exits
`1` when it finds issues — append `|| true` (same pattern as GitHub’s ESLint
SARIF example) so the upload step still runs:

```yaml
name: async-doctor
on:
  push:
    branches: [main]
  pull_request:

jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      # required for all workflows
      security-events: write
      # required for private repositories
      actions: read
      contents: read
    steps:
      - uses: actions/checkout@v7
      - uses: astral-sh/setup-uv@v10.3.3
      - name: Install project deps
        run: uv sync --frozen
      - name: Run async-doctor
        run: |
          source .venv/bin/activate
          # Once published to PyPI: uvx async-doctor …
          async-doctor --format sarif . > async-doctor.sarif || true
      - uses: github/codeql-action/upload-sarif@v4
        with:
          sarif_file: async-doctor.sarif
```

## Large repositories / memory

On this Cloud VM (Linux x86_64, 4 cores, 15 GiB RAM), the `litellm/` package
subdirectory (**3009** `.py` files, with venv) cold-scans in about **15 s** at
**~2.1 GiB** peak RSS. Full `home-assistant/core` (~20k `.py` files) completes
around **~38 s** / **~5.8 GiB**. Prefer a package subdirectory
(`async-doctor path/to/pkg`) when memory is tight. Warm runs reuse the
whole-scan cache when inputs are unchanged. See
[docs/benchmark.md](docs/benchmark.md).

## Benchmark

Public cold/warm numbers on pinned asyncio corpora:
[docs/benchmark.md](docs/benchmark.md). Reproduce with `./bench/run.sh`.

## Agent integrations

```bash
# Built-in zero-Node installer (writes .agents/skills/async-doctor/SKILL.md + .claude/skills symlink):
async-doctor install-skill --path .

# Or via Node skills CLI once the repo is public:
npx skills add maolonglong/async-doctor
```

Works once the repo is public. For GitHub code scanning, see the SARIF workflow
above.

## License

MIT. Bundled typeshed stubs (via `ty_vendored`) are Apache-2.0; see
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).

