Metadata-Version: 2.4
Name: ty-async
Version: 0.4.0
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python
Classifier: Topic :: Software Development :: Quality Assurance
Summary: Extremely fast ASYNC200 linter (flake8-async's user-configured blocking-call check), built on ruff's Python parser
Keywords: lint,async,flake8-async,ASYNC200
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# ty-async

Extremely fast linter implementing flake8-async's **ASYNC200** check
(user-configured blocking calls in `async` functions), built on
[ruff](https://github.com/astral-sh/ruff)'s Python parser. Drop-in
replacement for the flake8 flag, ~2 orders of magnitude faster.

```bash
pip install ty-async

ty-async src-dir another-dir \
  --exclude=src-dir/tests \
  --async200-blocking-calls='stripe.*->alt(),*.load_data->(),fcntl.flock->()'
```

## Stdlib preset

There is no official CPython list of "don't call this in the event loop",
but flake8-async's ASYNC21x–25x rules are the de-facto one. `--async200-stdlib`
appends a curated preset mirroring them plus high-signal extras: `time.sleep`,
`input`, pathlib `read_text`/`write_bytes`/…,
heavy `shutil`/`os.walk` filesystem work, `subprocess.*` and
`os.system`/`os.spawn*`/`os.wait*`, `urllib.request.urlopen`, `socket`
DNS lookups, `smtplib`/`ftplib`/`http.client`, `sqlite3.connect`, and
archive/compression helpers. Deliberately omitted: generic method names
(`.read`, `.recv`, `.acquire`, `.wait`, `.get`) and cheap single-syscall
ops (`os.stat`, `os.listdir`, `os.mkdir`, all of `os.path`, `open` and
`tempfile.*` — opening is cheap, it's reading/writing that blocks) —
too many false positives / too little blocking to matter. User patterns win on overlap, awaited calls stay exempt (so
`aiofiles`/`anyio` wrappers never fire), and it composes with
`--async200-transitive`.

## Transitive mode

`--async200-transitive` (with the same patterns flag) additionally builds a
name-based call graph and reports call sites in async functions that reach a
blocking call *through* sync helpers, with the chain as evidence:

```
app.py:161:18: ASYNC200T blocking sync call markdown.markdown reachable from async function via markdown_to_slack_blocks (client.py:176:12)
```

Calls are resolved by name only (same class → same file → globally unique;
ambiguous names are skipped, not guessed). Callables merely passed to
`asyncio.to_thread` / `run_in_executor` are not calls, so executor wrappers
are naturally exempt. Dynamic dispatch and framework callbacks are out of
scope. Suppress a site with `# noqa` / `# noqa: ASYNC200` on its line.

## Shield scope (ASYNC201)

`--async201` flags `asyncio.shield(...)` calls whose argument uses a name
bound by an enclosing `async with`:

```python
async with ctx.db() as db:
    await asyncio.shield(update_status(db, ...))  # ASYNC201
```

`shield` only protects the inner coroutine — on cancellation the `await`
raises immediately and the `async with` exits, closing `db` while the
shielded task is still using it (use-after-close, connection returned to
the pool mid-transaction). Fix: move the `async with` inside the shielded
coroutine so the resource's lifetime is shielded too. Matches bare `shield`
and any `*.shield` attribute call; resolved by name, so aliased resources
(`conn = db`) are missed. Suppress with `# noqa: ASYNC201`.

## Type escape hatches

`--type-hatches=no-any,no-getattr,no-object` enables three extra rules
(usable alone or together with the ASYNC200 flag, single pass either way):

- `no-getattr` — bare `getattr()` / `hasattr()` calls
- `no-any` — `Any` (or `typing.Any`, `t.Any`, …) inside an annotation
- `no-object` — `object` inside an annotation

Suppress a site with a trailing `# lint-ignore: no-any,no-getattr` comment
anywhere in the enclosing statement / function-signature span.

## Semantics

Semantics match flake8-async exactly: calls inside `async def` bodies
(nested `def`/`lambda` reset the context), directly-awaited calls are
skipped, patterns are fnmatch-ed against `ast.unparse(func)` (so `*`
crosses dots), `# noqa` / `# noqa: ASYNC200` are honored, and output
format and exit codes match flake8. Only ASYNC200 is implemented;
`--ignore` and `--asyncio` are accepted as no-ops.

