From 20c9f946150f485b3c841f8ea2bae60d7926b716 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Sun, 9 Nov 2025 17:25:14 +0100
Subject: [PATCH 1/4] drop python3.9, add+fix testing for 3.14
---
.github/workflows/ci.yml | 25 +++---
.pre-commit-config.yaml | 6 +-
flake8_async/__init__.py | 4 +-
flake8_async/visitors/visitor91x.py | 17 ++--
flake8_async/visitors/visitors.py | 3 +-
pyproject.toml | 4 +-
tests/autofix_files/async910.py | 35 ++++----
tests/autofix_files/async910.py.diff | 8 +-
tests/autofix_files/async91x_autofix.py | 84 ++++++++++++++++++
tests/autofix_files/async91x_autofix.py.diff | 34 +++++++-
tests/autofix_files/async91x_py310.py | 90 --------------------
tests/autofix_files/async91x_py310.py.diff | 31 -------
tests/eval_files/async103.py | 36 ++++++++
tests/eval_files/async103_104_py310.py | 58 -------------
tests/eval_files/async104.py | 17 +++-
tests/eval_files/async910.py | 34 ++++----
tests/eval_files/async91x_autofix.py | 80 +++++++++++++++++
tests/eval_files/async91x_py310.py | 86 -------------------
tests/test_flake8_async.py | 2 +-
tox.ini | 2 +-
20 files changed, 316 insertions(+), 340 deletions(-)
delete mode 100644 tests/autofix_files/async91x_py310.py
delete mode 100644 tests/autofix_files/async91x_py310.py.diff
delete mode 100644 tests/eval_files/async103_104_py310.py
delete mode 100644 tests/eval_files/async91x_py310.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a3d9f114..e6ef0a63 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,9 +13,9 @@ jobs:
pyright:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
cache: pip
- name: Install typing dependencies
@@ -30,14 +30,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
+ python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
fail-fast: false
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
- name: Install dependencies
run: python -m pip install --upgrade pip setuptools tox
- name: Run tests with flake8
@@ -50,11 +51,11 @@ jobs:
strategy:
fail-fast: false
steps:
- - uses: actions/checkout@v4
- - name: Set up Python 3.13
- uses: actions/setup-python@v5
+ - uses: actions/checkout@v5
+ - name: Set up Python 3.14
+ uses: actions/setup-python@v6
with:
- python-version: 3.13
+ python-version: 3.14
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools tox
@@ -67,9 +68,9 @@ jobs:
needs: [pyright, test]
if: github.repository == 'python-trio/flake8-async' && github.ref == 'refs/heads/main'
steps:
- - uses: actions/checkout@v4
- - name: Set up Python 3
- uses: actions/setup-python@v5
+ - uses: actions/checkout@v5
+ - name: Set up Python
+ uses: actions/setup-python@v6
- name: Install tools
run: python -m pip install --upgrade build pip setuptools wheel twine gitpython
- name: Upload new release
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7d9a8b69..5858f2b7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -29,7 +29,7 @@ repos:
rev: v3.21.0
hooks:
- id: pyupgrade
- args: [--py39-plus]
+ args: [--py310-plus]
exclude: tests/eval_files/async103.py
- repo: https://github.com/pycqa/isort
@@ -41,8 +41,8 @@ repos:
rev: v1.18.2
hooks:
- id: mypy
- # uses py311 syntax, mypy configured for py39
- exclude: tests/(eval|autofix)_files/.*_py(310|311).py
+ # uses py311 syntax, mypy configured for py310
+ exclude: tests/(eval|autofix)_files/.*_py311.py
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.407
diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py
index 89eb93dd..5a2dbb85 100644
--- a/flake8_async/__init__.py
+++ b/flake8_async/__init__.py
@@ -151,7 +151,9 @@ def from_source(
) -> Plugin:
plugin = Plugin.__new__(cls)
super(Plugin, plugin).__init__()
- plugin._tree = ast.parse(source)
+ plugin._tree = ast.parse(
+ source, filename=str(filename) if filename is not None else ""
+ )
plugin.filename = str(filename) if filename else None
plugin.module = cst_parse_module_native(source)
return plugin
diff --git a/flake8_async/visitors/visitor91x.py b/flake8_async/visitors/visitor91x.py
index 433c3757..474bfe0e 100644
--- a/flake8_async/visitors/visitor91x.py
+++ b/flake8_async/visitors/visitor91x.py
@@ -166,17 +166,12 @@ class LoopState:
default_factory=set[Statement]
)
uncheckpointed_before_break: set[Statement] = field(default_factory=set[Statement])
- # pyright emits reportUnknownVariableType, requiring the generic to default_factory
- # to be specified.
- # But for these we require a union, and `|` doesn't work on py39, and uses of
- # `Union` gets autofixed by ruff.
- # So.... let's just ignore the error for now
- artificial_errors: set[ # pyright: ignore[reportUnknownVariableType]
- cst.Return | cst.Yield
- ] = field(default_factory=set)
- nodes_needing_checkpoints: list[ # pyright: ignore[reportUnknownVariableType]
- cst.Return | cst.Yield | ArtificialStatement
- ] = field(default_factory=list)
+ artificial_errors: set[cst.Return | cst.Yield] = field(
+ default_factory=set[cst.Return | cst.Yield]
+ )
+ nodes_needing_checkpoints: list[cst.Return | cst.Yield | ArtificialStatement] = (
+ field(default_factory=list[cst.Return | cst.Yield | ArtificialStatement])
+ )
def copy(self):
return LoopState(
diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py
index e4fa1c83..4c83607b 100644
--- a/flake8_async/visitors/visitors.py
+++ b/flake8_async/visitors/visitors.py
@@ -128,8 +128,7 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
nursery_type = "task group"
start_methods = ("create_task",)
else:
- # incorrectly marked as not covered on py39
- continue # pragma: no cover # https://github.com/nedbat/coveragepy/issues/198
+ continue
body_call = node.body[0].value
if isinstance(body_call, ast.Await):
diff --git a/pyproject.toml b/pyproject.toml
index ae0b6180..8fa5e895 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,7 +54,7 @@ skip_glob = "tests/*_files/*"
[tool.mypy]
check_untyped_defs = true
disable_error_code = ["no-untyped-def", "misc", "no-untyped-call", "no-any-return"]
-python_version = "3.9"
+python_version = "3.10"
strict = true
warn_unreachable = true
warn_unused_ignores = false
@@ -84,7 +84,7 @@ extend-exclude = [
"tests/autofix_files/*"
]
line-length = 90
-target-version = "py39"
+target-version = "py310"
[tool.ruff.lint]
ignore = [
diff --git a/tests/autofix_files/async910.py b/tests/autofix_files/async910.py
index 4d97ec32..0d67a694 100644
--- a/tests/autofix_files/async910.py
+++ b/tests/autofix_files/async910.py
@@ -430,23 +430,24 @@ async def try_bare_except_reraises():
...
-async def return_in_finally_bare_except_checkpoint():
- try:
- await trio.sleep(0)
- except:
- await trio.sleep(0)
- finally:
- return
-
-
-async def return_in_finally_bare_except_empty():
- try:
- await trio.sleep(0)
- except:
- ...
- finally:
- await trio.lowlevel.checkpoint()
- return # error: 8, 'return', Statement('function definition', lineno-6)
+# return in finally is a SyntaxError on py314. We currently don't have
+# test infra to set a max python version for an eval file.
+# async def return_in_finally_bare_except_checkpoint():
+# try:
+# await trio.sleep(0)
+# except:
+# await trio.sleep(0)
+# finally:
+# return
+#
+#
+# async def return_in_finally_bare_except_empty():
+# try:
+# await trio.sleep(0)
+# except:
+# ...
+# finally:
+# return # error: 8, 'return', Statement('function definition', lineno-6)
# early return
diff --git a/tests/autofix_files/async910.py.diff b/tests/autofix_files/async910.py.diff
index 3fb6131c..c765401b 100644
--- a/tests/autofix_files/async910.py.diff
+++ b/tests/autofix_files/async910.py.diff
@@ -154,13 +154,7 @@
# safe
-@@ x,16 x,19 @@
- except:
- ...
- finally:
-+ await trio.lowlevel.checkpoint()
- return # error: 8, 'return', Statement('function definition', lineno-6)
-
+@@ x,11 x,13 @@
# early return
async def foo_return_1():
diff --git a/tests/autofix_files/async91x_autofix.py b/tests/autofix_files/async91x_autofix.py
index 35fe6ff2..746166aa 100644
--- a/tests/autofix_files/async91x_autofix.py
+++ b/tests/autofix_files/async91x_autofix.py
@@ -144,3 +144,87 @@ async def no_checkpoint(): # ASYNC910: 0, "exit", Statement("function definitio
except TypeError:
...
await trio.lowlevel.checkpoint()
+
+
+# structural pattern matching
+async def match_subject() -> None:
+ match await foo():
+ case False:
+ pass
+
+
+async def match_not_all_cases() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
+ None
+):
+ match foo():
+ case 1:
+ ...
+ case _:
+ await foo()
+ await trio.lowlevel.checkpoint()
+
+
+async def match_no_fallback() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
+ None
+):
+ match bar():
+ case 1:
+ await foo()
+ case 2:
+ await foo()
+ case _ if True:
+ await foo()
+ await trio.lowlevel.checkpoint()
+
+
+async def match_fallback_is_guarded() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
+ None
+):
+ match bar():
+ case 1:
+ await foo()
+ case 2:
+ await foo()
+ case _ if foo():
+ await foo()
+ await trio.lowlevel.checkpoint()
+
+
+async def match_all_cases() -> None:
+ match bar():
+ case 1:
+ await foo()
+ case 2:
+ await foo()
+ case _:
+ await foo()
+
+
+async def match_fallback_await_in_guard() -> None:
+ # The case guard is only executed if the pattern matches, so we can mostly treat
+ # it as part of the body, except for a special case for fallback+checkpointing guard.
+ match foo():
+ case 1 if await foo():
+ ...
+ case _ if await foo():
+ ...
+
+
+async def match_checkpoint_guard() -> None:
+ # The above pattern is quite cursed, but this seems fairly reasonable to do.
+ match foo():
+ case 1 if await foo():
+ ...
+ case _:
+ await foo()
+
+
+async def match_not_checkpoint_in_all_guards() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
+ None
+):
+ match foo():
+ case 1:
+ ...
+ case _ if await foo():
+ ...
+ await trio.lowlevel.checkpoint()
diff --git a/tests/autofix_files/async91x_autofix.py.diff b/tests/autofix_files/async91x_autofix.py.diff
index e6e6625d..30825a7f 100644
--- a/tests/autofix_files/async91x_autofix.py.diff
+++ b/tests/autofix_files/async91x_autofix.py.diff
@@ -78,8 +78,40 @@
yield # ASYNC911: 8, "yield", Statement("function definition", lineno-2) # ASYNC911: 8, "yield", Statement("yield", lineno)
async def bar():
-@@ x,3 x,4 @@
+@@ x,6 x,7 @@
await foo("1") # type: ignore[call-arg]
except TypeError:
...
+ await trio.lowlevel.checkpoint()
+
+
+ # structural pattern matching
+@@ x,6 x,7 @@
+ ...
+ case _:
+ await foo()
++ await trio.lowlevel.checkpoint()
+
+
+ async def match_no_fallback() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
+@@ x,6 x,7 @@
+ await foo()
+ case _ if True:
+ await foo()
++ await trio.lowlevel.checkpoint()
+
+
+ async def match_fallback_is_guarded() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
+@@ x,6 x,7 @@
+ await foo()
+ case _ if foo():
+ await foo()
++ await trio.lowlevel.checkpoint()
+
+
+ async def match_all_cases() -> None:
+@@ x,3 x,4 @@
+ ...
+ case _ if await foo():
+ ...
++ await trio.lowlevel.checkpoint()
diff --git a/tests/autofix_files/async91x_py310.py b/tests/autofix_files/async91x_py310.py
deleted file mode 100644
index 2f691d02..00000000
--- a/tests/autofix_files/async91x_py310.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# ARG --enable=ASYNC910,ASYNC911,ASYNC913
-# AUTOFIX
-# ASYNCIO_NO_AUTOFIX
-import trio
-
-
-async def foo(): ...
-
-
-async def match_subject() -> None:
- match await foo():
- case False:
- pass
-
-
-async def match_not_all_cases() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
- None
-):
- match foo():
- case 1:
- ...
- case _:
- await foo()
- await trio.lowlevel.checkpoint()
-
-
-async def match_no_fallback() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
- None
-):
- match foo():
- case 1:
- await foo()
- case 2:
- await foo()
- case _ if True:
- await foo()
- await trio.lowlevel.checkpoint()
-
-
-async def match_fallback_is_guarded() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
- None
-):
- match foo():
- case 1:
- await foo()
- case 2:
- await foo()
- case _ if foo():
- await foo()
- await trio.lowlevel.checkpoint()
-
-
-async def match_all_cases() -> None:
- match foo():
- case 1:
- await foo()
- case 2:
- await foo()
- case _:
- await foo()
-
-
-async def match_fallback_await_in_guard() -> None:
- # The case guard is only executed if the pattern matches, so we can mostly treat
- # it as part of the body, except for a special case for fallback+checkpointing guard.
- match foo():
- case 1 if await foo():
- ...
- case _ if await foo():
- ...
-
-
-async def match_checkpoint_guard() -> None:
- # The above pattern is quite cursed, but this seems fairly reasonable to do.
- match foo():
- case 1 if await foo():
- ...
- case _:
- await foo()
-
-
-async def match_not_checkpoint_in_all_guards() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
- None
-):
- match foo():
- case 1:
- ...
- case _ if await foo():
- ...
- await trio.lowlevel.checkpoint()
diff --git a/tests/autofix_files/async91x_py310.py.diff b/tests/autofix_files/async91x_py310.py.diff
deleted file mode 100644
index 47c84f3d..00000000
--- a/tests/autofix_files/async91x_py310.py.diff
+++ /dev/null
@@ -1,31 +0,0 @@
----
-+++
-@@ x,6 x,7 @@
- ...
- case _:
- await foo()
-+ await trio.lowlevel.checkpoint()
-
-
- async def match_no_fallback() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
-@@ x,6 x,7 @@
- await foo()
- case _ if True:
- await foo()
-+ await trio.lowlevel.checkpoint()
-
-
- async def match_fallback_is_guarded() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
-@@ x,6 x,7 @@
- await foo()
- case _ if foo():
- await foo()
-+ await trio.lowlevel.checkpoint()
-
-
- async def match_all_cases() -> None:
-@@ x,3 x,4 @@
- ...
- case _ if await foo():
- ...
-+ await trio.lowlevel.checkpoint()
diff --git a/tests/eval_files/async103.py b/tests/eval_files/async103.py
index 82bf8e1b..a8bef0cc 100644
--- a/tests/eval_files/async103.py
+++ b/tests/eval_files/async103.py
@@ -340,3 +340,39 @@ def foo() -> Any: ...
...
except BaseException: # ASYNC103_trio: 11, "BaseException"
...
+
+# structural pattern matching
+try:
+ ...
+except BaseException as e: # ASYNC103_trio: 7, "BaseException"
+ match foo():
+ case True:
+ raise e
+ case False:
+ ...
+ case _:
+ raise e
+
+try:
+ ...
+except BaseException: # ASYNC103_trio: 7, "BaseException"
+ match foo():
+ case True:
+ raise
+
+try:
+ ...
+except BaseException: # safe
+ match foo():
+ case True:
+ raise
+ case False:
+ raise
+ case _:
+ raise
+try:
+ ...
+except BaseException: # ASYNC103_trio: 7, "BaseException"
+ match foo():
+ case _ if foo():
+ raise
diff --git a/tests/eval_files/async103_104_py310.py b/tests/eval_files/async103_104_py310.py
deleted file mode 100644
index 965fe32e..00000000
--- a/tests/eval_files/async103_104_py310.py
+++ /dev/null
@@ -1,58 +0,0 @@
-"""Test for ASYNC103/ASYNC104 with structural pattern matching
-
-ASYNC103: no-reraise-cancelled
-ASYNC104: cancelled-not-raised
-"""
-
-# ARG --enable=ASYNC103,ASYNC104
-
-
-def foo() -> Any: ...
-
-
-try:
- ...
-except BaseException as e: # ASYNC103_trio: 7, "BaseException"
- match foo():
- case True:
- raise e
- case False:
- ...
- case _:
- raise e
-
-try:
- ...
-except BaseException: # ASYNC103_trio: 7, "BaseException"
- match foo():
- case True:
- raise
-
-try:
- ...
-except BaseException: # safe
- match foo():
- case True:
- raise
- case False:
- raise
- case _:
- raise
-try:
- ...
-except BaseException: # ASYNC103_trio: 7, "BaseException"
- match foo():
- case _ if foo():
- raise
-try:
- ...
-except BaseException: # ASYNC103_trio: 7, "BaseException"
- match foo():
- case 1:
- return # ASYNC104: 12
- case 2:
- raise
- case 3:
- return # ASYNC104: 12
- case blah:
- raise
diff --git a/tests/eval_files/async104.py b/tests/eval_files/async104.py
index 8b845c2f..9d63f7ae 100644
--- a/tests/eval_files/async104.py
+++ b/tests/eval_files/async104.py
@@ -104,7 +104,9 @@ def foo2():
else:
return # type: ignore[unreachable] # error: 12
finally:
- return # error: 12
+ # a return here would also be an error - but it's a syntax error
+ # from py314+ and we don't have the test infra to handle that properly.
+ pass
# don't avoid re-raise with continue/break
@@ -183,3 +185,16 @@ def foo_cancelled_not_handled():
return # ASYNC104: 8
except:
return # would otherwise error
+
+try:
+ ...
+except BaseException: # ASYNC103_trio: 7, "BaseException"
+ match foo():
+ case 1:
+ return # ASYNC104: 12
+ case 2:
+ raise
+ case 3:
+ return # ASYNC104: 12
+ case blah:
+ raise
diff --git a/tests/eval_files/async910.py b/tests/eval_files/async910.py
index f8d7680e..d3701558 100644
--- a/tests/eval_files/async910.py
+++ b/tests/eval_files/async910.py
@@ -409,22 +409,24 @@ async def try_bare_except_reraises():
...
-async def return_in_finally_bare_except_checkpoint():
- try:
- await trio.sleep(0)
- except:
- await trio.sleep(0)
- finally:
- return
-
-
-async def return_in_finally_bare_except_empty():
- try:
- await trio.sleep(0)
- except:
- ...
- finally:
- return # error: 8, 'return', Statement('function definition', lineno-6)
+# return in finally is a SyntaxError on py314. We currently don't have
+# test infra to set a max python version for an eval file.
+# async def return_in_finally_bare_except_checkpoint():
+# try:
+# await trio.sleep(0)
+# except:
+# await trio.sleep(0)
+# finally:
+# return
+#
+#
+# async def return_in_finally_bare_except_empty():
+# try:
+# await trio.sleep(0)
+# except:
+# ...
+# finally:
+# return # error: 8, 'return', Statement('function definition', lineno-6)
# early return
diff --git a/tests/eval_files/async91x_autofix.py b/tests/eval_files/async91x_autofix.py
index 7ce0a359..f5e22d46 100644
--- a/tests/eval_files/async91x_autofix.py
+++ b/tests/eval_files/async91x_autofix.py
@@ -128,3 +128,83 @@ async def no_checkpoint(): # ASYNC910: 0, "exit", Statement("function definitio
await foo("1") # type: ignore[call-arg]
except TypeError:
...
+
+
+# structural pattern matching
+async def match_subject() -> None:
+ match await foo():
+ case False:
+ pass
+
+
+async def match_not_all_cases() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
+ None
+):
+ match foo():
+ case 1:
+ ...
+ case _:
+ await foo()
+
+
+async def match_no_fallback() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
+ None
+):
+ match bar():
+ case 1:
+ await foo()
+ case 2:
+ await foo()
+ case _ if True:
+ await foo()
+
+
+async def match_fallback_is_guarded() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
+ None
+):
+ match bar():
+ case 1:
+ await foo()
+ case 2:
+ await foo()
+ case _ if foo():
+ await foo()
+
+
+async def match_all_cases() -> None:
+ match bar():
+ case 1:
+ await foo()
+ case 2:
+ await foo()
+ case _:
+ await foo()
+
+
+async def match_fallback_await_in_guard() -> None:
+ # The case guard is only executed if the pattern matches, so we can mostly treat
+ # it as part of the body, except for a special case for fallback+checkpointing guard.
+ match foo():
+ case 1 if await foo():
+ ...
+ case _ if await foo():
+ ...
+
+
+async def match_checkpoint_guard() -> None:
+ # The above pattern is quite cursed, but this seems fairly reasonable to do.
+ match foo():
+ case 1 if await foo():
+ ...
+ case _:
+ await foo()
+
+
+async def match_not_checkpoint_in_all_guards() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
+ None
+):
+ match foo():
+ case 1:
+ ...
+ case _ if await foo():
+ ...
diff --git a/tests/eval_files/async91x_py310.py b/tests/eval_files/async91x_py310.py
deleted file mode 100644
index 9367fac1..00000000
--- a/tests/eval_files/async91x_py310.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# ARG --enable=ASYNC910,ASYNC911,ASYNC913
-# AUTOFIX
-# ASYNCIO_NO_AUTOFIX
-import trio
-
-
-async def foo(): ...
-
-
-async def match_subject() -> None:
- match await foo():
- case False:
- pass
-
-
-async def match_not_all_cases() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
- None
-):
- match foo():
- case 1:
- ...
- case _:
- await foo()
-
-
-async def match_no_fallback() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
- None
-):
- match foo():
- case 1:
- await foo()
- case 2:
- await foo()
- case _ if True:
- await foo()
-
-
-async def match_fallback_is_guarded() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
- None
-):
- match foo():
- case 1:
- await foo()
- case 2:
- await foo()
- case _ if foo():
- await foo()
-
-
-async def match_all_cases() -> None:
- match foo():
- case 1:
- await foo()
- case 2:
- await foo()
- case _:
- await foo()
-
-
-async def match_fallback_await_in_guard() -> None:
- # The case guard is only executed if the pattern matches, so we can mostly treat
- # it as part of the body, except for a special case for fallback+checkpointing guard.
- match foo():
- case 1 if await foo():
- ...
- case _ if await foo():
- ...
-
-
-async def match_checkpoint_guard() -> None:
- # The above pattern is quite cursed, but this seems fairly reasonable to do.
- match foo():
- case 1 if await foo():
- ...
- case _:
- await foo()
-
-
-async def match_not_checkpoint_in_all_guards() -> ( # ASYNC910: 0, "exit", Statement("function definition", lineno)
- None
-):
- match foo():
- case 1:
- ...
- case _ if await foo():
- ...
diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py
index 771f59d3..339fbdba 100644
--- a/tests/test_flake8_async.py
+++ b/tests/test_flake8_async.py
@@ -324,7 +324,7 @@ def test_eval(
):
expected = []
- plugin = Plugin.from_source(content)
+ plugin = Plugin.from_source(content, filename=path)
errors = assert_expected_errors(
plugin,
*expected,
diff --git a/tox.ini b/tox.ini
index dce35389..553d11d5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,7 +2,7 @@
[tox]
# default environments to run without `-e`
# trailing comma gives the empty environ - i.e. no flake8 default python
-envlist = py{39,310,311,312,313}-{flake8},
+envlist = py{310,311,312,313,314}-{flake8},
# create a default testenv, whose behaviour will depend on the name it's called with.
# for CI you can call with `-e flake8_6,flake8_7` and let the CI handle python version
From 36aacf167072c99b8b24fa19b561393c5812bd98 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Sun, 9 Nov 2025 16:30:08 +0000
Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
flake8_async/visitors/helpers.py | 8 +++-----
tests/eval_files/async232.py | 6 +++---
tests/eval_files/async232_asyncio.py | 6 +++---
3 files changed, 9 insertions(+), 11 deletions(-)
diff --git a/flake8_async/visitors/helpers.py b/flake8_async/visitors/helpers.py
index ea2da3db..5764e16e 100644
--- a/flake8_async/visitors/helpers.py
+++ b/flake8_async/visitors/helpers.py
@@ -9,7 +9,7 @@
from collections.abc import Sized
from dataclasses import dataclass
from fnmatch import fnmatch
-from typing import TYPE_CHECKING, Generic, TypeVar, Union
+from typing import TYPE_CHECKING, Generic, TypeVar
import libcst as cst
import libcst.matchers as m
@@ -35,11 +35,9 @@
T = TypeVar("T", bound=Flake8AsyncVisitor)
T_CST = TypeVar("T_CST", bound=Flake8AsyncVisitor_cst)
- T_EITHER = TypeVar(
- "T_EITHER", bound=Union[Flake8AsyncVisitor, Flake8AsyncVisitor_cst]
- )
+ T_EITHER = TypeVar("T_EITHER", bound=Flake8AsyncVisitor | Flake8AsyncVisitor_cst)
-T_Call = TypeVar("T_Call", bound=Union[cst.Call, ast.Call])
+T_Call = TypeVar("T_Call", bound=cst.Call | ast.Call)
def error_class(error_class: type[T]) -> type[T]:
diff --git a/tests/eval_files/async232.py b/tests/eval_files/async232.py
index 5fd52f44..117737e3 100644
--- a/tests/eval_files/async232.py
+++ b/tests/eval_files/async232.py
@@ -81,7 +81,7 @@ async def file_text_5(f: TextIOWrapper | None = None):
f.read() # ASYNC232: 8, 'read', 'f', "trio"
-async def file_text_6(f: Optional[TextIOWrapper] = None):
+async def file_text_6(f: TextIOWrapper | None = None):
f.read() # ASYNC232: 4, 'read', 'f', "trio"
if f:
f.read() # ASYNC232: 8, 'read', 'f', "trio"
@@ -227,12 +227,12 @@ async def attribute_access_on_object():
# The type checker is very naive, and will not do any parsing of logic pertaining
# to the type
-async def type_restricting_1(f: Optional[TextIOWrapper] = None):
+async def type_restricting_1(f: TextIOWrapper | None = None):
if f is None:
f.read() # ASYNC232: 8, 'read', 'f', "trio"
-async def type_restricting_2(f: Optional[TextIOWrapper] = None):
+async def type_restricting_2(f: TextIOWrapper | None = None):
if isinstance(f, TextIOWrapper):
return
f.read() # ASYNC232: 4, 'read', 'f', "trio"
diff --git a/tests/eval_files/async232_asyncio.py b/tests/eval_files/async232_asyncio.py
index ae070b7d..2fb6b5c7 100644
--- a/tests/eval_files/async232_asyncio.py
+++ b/tests/eval_files/async232_asyncio.py
@@ -82,7 +82,7 @@ async def file_text_5(f: TextIOWrapper | None = None):
f.read() # ASYNC232_asyncio: 8, 'read', 'f'
-async def file_text_6(f: Optional[TextIOWrapper] = None):
+async def file_text_6(f: TextIOWrapper | None = None):
f.read() # ASYNC232_asyncio: 4, 'read', 'f'
if f:
f.read() # ASYNC232_asyncio: 8, 'read', 'f'
@@ -228,12 +228,12 @@ async def attribute_access_on_object():
# The type checker is very naive, and will not do any parsing of logic pertaining
# to the type
-async def type_restricting_1(f: Optional[TextIOWrapper] = None):
+async def type_restricting_1(f: TextIOWrapper | None = None):
if f is None:
f.read() # ASYNC232_asyncio: 8, 'read', 'f'
-async def type_restricting_2(f: Optional[TextIOWrapper] = None):
+async def type_restricting_2(f: TextIOWrapper | None = None):
if isinstance(f, TextIOWrapper):
return
f.read() # ASYNC232_asyncio: 4, 'read', 'f'
From 7ae7f7b1687e25e543cde96d8cffc552b47ea574 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Sun, 9 Nov 2025 17:33:27 +0100
Subject: [PATCH 3/4] fix Union -> |
---
flake8_async/visitors/flake8asyncvisitor.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/flake8_async/visitors/flake8asyncvisitor.py b/flake8_async/visitors/flake8asyncvisitor.py
index 46b236c6..8e4297a9 100644
--- a/flake8_async/visitors/flake8asyncvisitor.py
+++ b/flake8_async/visitors/flake8asyncvisitor.py
@@ -4,7 +4,7 @@
import ast
from abc import ABC
-from typing import TYPE_CHECKING, Any, Union
+from typing import TYPE_CHECKING, Any
import libcst as cst
from libcst.metadata import PositionProvider
@@ -16,7 +16,7 @@
from ..runner import SharedState
- HasLineCol = Union[ast.expr, ast.stmt, ast.arg, ast.excepthandler, Statement]
+ HasLineCol = ast.expr | ast.stmt | ast.arg | ast.excepthandler | Statement
class Flake8AsyncVisitor(ast.NodeVisitor, ABC):
From db6894071299502464a9b9b5517c6260569967a4 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Sun, 9 Nov 2025 17:59:21 +0100
Subject: [PATCH 4/4] optional is intended for testing
---
.pre-commit-config.yaml | 3 ++-
tests/eval_files/async232.py | 2 +-
tests/eval_files/async232_asyncio.py | 2 +-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5858f2b7..10b7af63 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -30,7 +30,8 @@ repos:
hooks:
- id: pyupgrade
args: [--py310-plus]
- exclude: tests/eval_files/async103.py
+ # async232 explicitly tests Optional[]
+ exclude: tests/eval_files/async(103|232|232_asyncio).py
- repo: https://github.com/pycqa/isort
rev: 7.0.0
diff --git a/tests/eval_files/async232.py b/tests/eval_files/async232.py
index 117737e3..b1b7f1f8 100644
--- a/tests/eval_files/async232.py
+++ b/tests/eval_files/async232.py
@@ -81,7 +81,7 @@ async def file_text_5(f: TextIOWrapper | None = None):
f.read() # ASYNC232: 8, 'read', 'f', "trio"
-async def file_text_6(f: TextIOWrapper | None = None):
+async def file_text_6(f: Optional[TextIOWrapper] = None):
f.read() # ASYNC232: 4, 'read', 'f', "trio"
if f:
f.read() # ASYNC232: 8, 'read', 'f', "trio"
diff --git a/tests/eval_files/async232_asyncio.py b/tests/eval_files/async232_asyncio.py
index 2fb6b5c7..5f40dd31 100644
--- a/tests/eval_files/async232_asyncio.py
+++ b/tests/eval_files/async232_asyncio.py
@@ -82,7 +82,7 @@ async def file_text_5(f: TextIOWrapper | None = None):
f.read() # ASYNC232_asyncio: 8, 'read', 'f'
-async def file_text_6(f: TextIOWrapper | None = None):
+async def file_text_6(f: Optional[TextIOWrapper] = None):
f.read() # ASYNC232_asyncio: 4, 'read', 'f'
if f:
f.read() # ASYNC232_asyncio: 8, 'read', 'f'