Skip to content

Commit 8b1a1cf

Browse files
jakkdlZac-HD
andauthored
moar asyncio support, test infra improvements (#213)
* Add asyncio-specific error messages for 2xx. Add check in tests to detect if [library]_NO_ERROR should be used, add it to several checks. Specify why NO_[library] and [library]_NO_ERROR is specified in a lot of eval files. * update error codes in readme * * added 113 support for asyncio * split off anyio_create_task_group testing into async113_anyio.py * improved comments --------- Co-authored-by: Zac Hatfield-Dodds <[email protected]>
1 parent fe751bc commit 8b1a1cf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+576
-77
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Changelog
22
*[CalVer, YY.month.patch](https://calver.org/)*
33

4-
## Future
4+
## 24.3.1
55
- Removed TRIO117, MultiError removed in trio 0.24.0
66
- Renamed the library from flake8-trio to flake8-async, to indicate the checker supports more than just `trio`.
77
- Renamed all error codes from TRIOxxx to ASYNCxxx
88
- Renamed the binary from flake8-trio to flake8-async
99
- Lots of internal renaming.
10-
- Added asyncio support for ASYNC106
10+
- Added asyncio support for several error codes
1111
- added `--library`
1212

1313
## 23.5.1

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ Note: 22X, 23X and 24X has not had asyncio-specific suggestions written.
4848
- **ASYNC210**: Sync HTTP call in async function, use `httpx.AsyncClient`. This and the other ASYNC21x checks look for usage of `urllib3` and `httpx.Client`, and recommend using `httpx.AsyncClient` as that's the largest http client supporting anyio/trio.
4949
- **ASYNC211**: Likely sync HTTP call in async function, use `httpx.AsyncClient`. Looks for `urllib3` method calls on pool objects, but only matching on the method signature and not the object.
5050
- **ASYNC212**: Blocking sync HTTP call on httpx object, use httpx.AsyncClient.
51-
- **ASYNC220**: Sync process call in async function, use `await nursery.start([trio/anyio].run_process, ...)`.
52-
- **ASYNC221**: Sync process call in async function, use `await [trio/anyio].run_process(...)`.
53-
- **ASYNC222**: Sync `os.*` call in async function, wrap in `await [trio/anyio].to_thread.run_sync()`.
54-
- **ASYNC230**: Sync IO call in async function, use `[trio/anyio].open_file(...)`.
55-
- **ASYNC231**: Sync IO call in async function, use `[trio/anyio].wrap_file(...)`.
51+
- **ASYNC220**: Sync process call in async function, use `await nursery.start([trio/anyio].run_process, ...)`. `asyncio` users can use [`asyncio.create_subprocess_[exec/shell]`](https://docs.python.org/3/library/asyncio-subprocess.html).
52+
- **ASYNC221**: Sync process call in async function, use `await [trio/anyio].run_process(...)`. `asyncio` users can use [`asyncio.create_subprocess_[exec/shell]`](https://docs.python.org/3/library/asyncio-subprocess.html).
53+
- **ASYNC222**: Sync `os.*` call in async function, wrap in `await [trio/anyio].to_thread.run_sync()`. `asyncio` users can use [`asyncio.loop.run_in_executor`](https://docs.python.org/3/library/asyncio-subprocess.html).
54+
- **ASYNC230**: Sync IO call in async function, use `[trio/anyio].open_file(...)`. `asyncio` users need to use a library such as [aiofiles](https://pypi.org/project/aiofiles/), or switch to [anyio](https://github.com/agronholm/anyio).
55+
- **ASYNC231**: Sync IO call in async function, use `[trio/anyio].wrap_file(...)`. `asyncio` users need to use a library such as [aiofiles](https://pypi.org/project/aiofiles/), or switch to [anyio](https://github.com/agronholm/anyio).
5656
- **ASYNC232**: Blocking sync call on file object, wrap the file object in `[trio/anyio].wrap_file()` to get an async file object.
57-
- **ASYNC240**: Avoid using `os.path` in async functions, prefer using `[trio/anyio].Path` objects.
57+
- **ASYNC240**: Avoid using `os.path` in async functions, prefer using `[trio/anyio].Path` objects. `asyncio` users should consider [aiopath](https://pypi.org/project/aiopath) or [anyio](https://github.com/agronholm/anyio).
5858

5959
### Warnings disabled by default
6060
- **ASYNC900**: Async generator without `@asynccontextmanager` not allowed. You might want to enable this on a codebase since async generators are inherently unsafe and cleanup logic might not be performed. See https://github.com/python-trio/flake8-async/issues/211 and https://discuss.python.org/t/using-exceptiongroup-at-anthropic-experience-report/20888/6 for discussion.

flake8_async/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838

3939
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
40-
__version__ = "23.5.1"
40+
__version__ = "24.3.1"
4141

4242

4343
# taken from https://github.com/Zac-HD/shed

flake8_async/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def __iter__(self):
8383

8484
def cmp(self):
8585
# column may be ignored/modified when autofixing, so sort on that last
86-
return self.line, self.code, self.args, self.col
86+
return self.line, self.code, self.args, self.message, self.col
8787

8888
# for sorting in tests
8989
def __lt__(self, other: Error) -> bool:

flake8_async/visitors/visitor2xx.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,26 @@ def visit_blocking_call(self, node: ast.Call):
169169
# Process invocations 202
170170
@error_class
171171
class Visitor22X(Visitor200):
172+
# not the prettiest, as it requires duplicating eval files, and will give
173+
# nonsensical error messages if using asyncio+trio/anyio
172174
error_codes: Mapping[str, str] = {
173175
"ASYNC220": (
174176
"Sync call {} in async function, use "
175177
"`await nursery.start({}.run_process, ...)`."
176178
),
179+
"ASYNC220_asyncio": (
180+
"Sync call {} in async function, use "
181+
"`asyncio.create_subprocess_[exec/shell]."
182+
),
177183
"ASYNC221": "Sync call {} in async function, use `await {}.run_process(...)`.",
184+
"ASYNC221_asyncio": (
185+
"Sync call {} in async function, use "
186+
"`asyncio.create_subprocess_[exec/shell]."
187+
),
178188
"ASYNC222": "Sync call {} in async function, wrap in `{}.to_thread.run_sync()`.",
189+
"ASYNC222_asyncio": (
190+
"Sync call {} in async function, use `asyncio.loop.run_in_executor`."
191+
),
179192
}
180193

181194
def visit_blocking_call(self, node: ast.Call):
@@ -222,7 +235,14 @@ def is_p_wait(arg: ast.expr) -> bool:
222235
error_code = "ASYNC220"
223236
break
224237

225-
if error_code is not None:
238+
if error_code is None:
239+
return
240+
if self.library == ("asyncio",):
241+
self.error(node, func_name, error_code=error_code + "_asyncio")
242+
else:
243+
# the asyncio + anyio/trio case is probably not worth special casing,
244+
# so we simply suggest e.g. `[trio/asyncio/anyio].run_process()` despite
245+
# asyncio.run_process not existing
226246
self.error(node, func_name, self.library_str, error_code=error_code)
227247

228248

@@ -231,6 +251,14 @@ class Visitor23X(Visitor200):
231251
error_codes: Mapping[str, str] = {
232252
"ASYNC230": "Sync call {0} in async function, use `{1}.open_file(...)`.",
233253
"ASYNC231": "Sync call {0} in async function, use `{1}.wrap_file({0})`.",
254+
"ASYNC230_asyncio": (
255+
"Sync call {0} in async function, "
256+
" use a library such as aiofiles or anyio."
257+
),
258+
"ASYNC231_asyncio": (
259+
"Sync call {0} in async function, "
260+
" use a library such as aiofiles or anyio."
261+
),
234262
}
235263

236264
def visit_Call(self, node: ast.Call):
@@ -249,16 +277,23 @@ def visit_blocking_call(self, node: ast.Call):
249277
error_code = "ASYNC231"
250278
else:
251279
return
252-
self.error(node, func_name, self.library_str, error_code=error_code)
280+
if self.library == ("asyncio",):
281+
self.error(node, func_name, error_code=error_code + "_asyncio")
282+
else:
283+
self.error(node, func_name, self.library_str, error_code=error_code)
253284

254285

255286
@error_class
256287
class Visitor232(Visitor200):
257288
error_codes: Mapping[str, str] = {
258289
"ASYNC232": (
259-
"Blocking sync call {1} on file object {0}, wrap the file object"
290+
"Blocking sync call {1} on file object {0}, wrap the file object "
260291
"in `{2}.wrap_file()` to get an async file object."
261-
)
292+
),
293+
"ASYNC232_asyncio": (
294+
"Blocking sync call {1} on file object {0}, wrap the file object "
295+
" to get an async file object."
296+
),
262297
}
263298

264299
def __init__(self, *args: Any, **kwargs: Any):
@@ -279,13 +314,30 @@ def visit_blocking_call(self, node: ast.Call):
279314
and (anno := self.variables.get(node.func.value.id))
280315
and (anno in io_file_types or f"io.{anno}" in io_file_types)
281316
):
282-
self.error(node, node.func.attr, node.func.value.id, self.library_str)
317+
if self.library == ("asyncio",):
318+
self.error(
319+
node,
320+
node.func.attr,
321+
node.func.value.id,
322+
error_code="ASYNC232_asyncio",
323+
)
324+
else:
325+
self.error(
326+
node,
327+
node.func.attr,
328+
node.func.value.id,
329+
self.library_str,
330+
error_code="ASYNC232",
331+
)
283332

284333

285334
@error_class
286335
class Visitor24X(Visitor200):
287336
error_codes: Mapping[str, str] = {
288337
"ASYNC240": "Avoid using os.path, prefer using {1}.Path objects.",
338+
"ASYNC240_asyncio": (
339+
"Avoid using os.path, use a library such as aiopath or anyio."
340+
),
289341
}
290342

291343
def __init__(self, *args: Any, **kwargs: Any):
@@ -324,10 +376,11 @@ def visit_ImportFrom(self, node: ast.ImportFrom):
324376
def visit_Call(self, node: ast.Call):
325377
if not self.async_function:
326378
return
379+
error_code = "ASYNC240_asyncio" if self.library == ("asyncio",) else "ASYNC240"
327380
func_name = ast.unparse(node.func)
328381
if func_name in self.imports_from_ospath:
329-
self.error(node, func_name, self.library_str)
382+
self.error(node, func_name, self.library_str, error_code=error_code)
330383
elif (m := re.fullmatch(r"os\.path\.(?P<func>.*)", func_name)) and m.group(
331384
"func"
332385
) in self.os_funcs:
333-
self.error(node, m.group("func"), self.library_str)
386+
self.error(node, m.group("func"), self.library_str, error_code=error_code)

flake8_async/visitors/visitors.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
9393
var_name = item.optional_vars.id
9494

9595
# check for trio.open_nursery
96-
nursery = get_matching_call(item.context_expr, "open_nursery")
96+
nursery = get_matching_call(
97+
item.context_expr, "open_nursery", base=("trio",)
98+
)
9799

98100
# `isinstance(..., ast.Call)` is done in get_matching_call
99101
body_call = cast("ast.Call", node.body[0].value)
@@ -169,6 +171,7 @@ def is_nursery_call(node: ast.expr):
169171
in (
170172
"trio.Nursery",
171173
"anyio.TaskGroup",
174+
"asyncio.TaskGroup",
172175
)
173176
)
174177

@@ -209,7 +212,7 @@ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
209212
self.error(node, node.name)
210213

211214

212-
# Suggests replacing all `trio.sleep(0)` with the more suggestive
215+
# Suggests replacing all `[trio|anyio].sleep(0)` with the more suggestive
213216
# `trio.lowlevel.checkpoint()`
214217
@error_class
215218
class Visitor115(Flake8AsyncVisitor):

tests/autofix_files/async100.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# type: ignore
22
# AUTOFIX
3-
# NOASYNCIO
3+
# ASYNCIO_NO_ERROR # timeout primitives are named differently in asyncio
44

55
import trio
66

tests/autofix_files/async100_simple_autofix.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# NOASYNCIO
1+
# ASYNCIO_NO_ERROR - no asyncio.move_on_after
22
# AUTOFIX
33
import trio
44

tests/autofix_files/noqa.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# AUTOFIX
2-
# NOANYIO # TODO
3-
# NOASYNCIO
2+
# NOASYNCIO - does not trigger on ASYNC100
43
# ARG --enable=ASYNC100,ASYNC911
54
from typing import Any
65

tests/autofix_files/noqa_testing.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# AUTOFIX
2-
# NOANYIO # TODO
32
# ARG --enable=ASYNC911
43
import trio
54

0 commit comments

Comments
 (0)