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'