Skip to content

Commit 7d420b0

Browse files
committed
Merge remote-tracking branch 'origin/main' into async_fun_could_be_sync
2 parents 4594072 + 783827a commit 7d420b0

26 files changed

+664
-62
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
runs-on: ubuntu-latest
3131
strategy:
3232
matrix:
33-
python-version: ['3.9', '3.10', '3.11', '3.12', 3.13-dev]
33+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
3434
fail-fast: false
3535
steps:
3636
- uses: actions/checkout@v4
@@ -62,10 +62,21 @@ jobs:
6262
- name: Run tests
6363
run: python -m tox -e flake8_7 -- --onlyfuzz --no-cov -n auto
6464

65+
check_release:
66+
runs-on: ubuntu-latest
67+
strategy:
68+
fail-fast: false
69+
steps:
70+
- uses: actions/checkout@v4
71+
- name: Set up Python 3
72+
uses: actions/setup-python@v5
73+
- name: Test changelog & version
74+
run: python tests/check_changelog_and_version.py
75+
6576
release:
6677
runs-on: ubuntu-latest
67-
needs: [pyright, test]
68-
if: github.repository == 'python-trio/flake8-async' && github.ref == 'refs/heads/main'
78+
needs: [pyright, test, check_release]
79+
if: github.repository == 'python-trio/flake8-async' && github.ref == 'refs/heads/main'
6980
steps:
7081
- uses: actions/checkout@v4
7182
- name: Set up Python 3
@@ -77,5 +88,4 @@ jobs:
7788
TWINE_USERNAME: __token__
7889
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
7990
run: |
80-
python tests/test_changelog_and_version.py --ensure-tag
8191
python -m build && twine upload --skip-existing dist/*

.pre-commit-config.yaml

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ default_language_version:
33
python: python3.12
44
# pyright requires internet connection to run, which the pre-commit ci app doesn't have.
55
# it instead runs in a github action
6+
# check-release-changelog is run as a dedicated job
67
ci:
7-
skip: [pyright]
8+
skip: [pyright, check-release-changelog]
89

910
repos:
1011
- repo: https://github.com/astral-sh/ruff-pre-commit
11-
rev: v0.7.1
12+
rev: v0.9.2
1213
hooks:
1314
- id: ruff
1415
args: [--fix]
@@ -25,7 +26,7 @@ repos:
2526
- id: autoflake
2627

2728
- repo: https://github.com/asottile/pyupgrade
28-
rev: v3.19.0
29+
rev: v3.19.1
2930
hooks:
3031
- id: pyupgrade
3132
args: [--py39-plus]
@@ -37,14 +38,14 @@ repos:
3738
- id: isort
3839

3940
- repo: https://github.com/pre-commit/mirrors-mypy
40-
rev: v1.13.0
41+
rev: v1.14.1
4142
hooks:
4243
- id: mypy
4344
# uses py311 syntax, mypy configured for py39
4445
exclude: tests/eval_files/.*_py311.py
4546

4647
- repo: https://github.com/RobertCraigie/pyright-python
47-
rev: v1.1.386
48+
rev: v1.1.392.post0
4849
hooks:
4950
- id: pyright
5051
# ignore warnings about new version being available, no other warnings
@@ -100,3 +101,16 @@ repos:
100101
rev: v1.0.0
101102
hooks:
102103
- id: sphinx-lint
104+
105+
- repo: local
106+
hooks:
107+
- id: check-release-changelog
108+
name: check-release-changelog
109+
language: system
110+
entry: python3 tests/check_changelog_and_version.py --allow-future-in-changelog
111+
files: flake8_async/__init__.py|docs/changelog.rst
112+
113+
- repo: meta
114+
hooks:
115+
- id: check-hooks-apply
116+
- id: check-useless-excludes

docs/changelog.rst

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,38 @@ Changelog
44

55
`CalVer, YY.month.patch <https://calver.org/>`_
66

7-
24.10.3
7+
25.1.1
88
=======
99
- Add :ref:`ASYNC124 <async124>` async-function-could-be-sync
1010
- :ref:`ASYNC91x <ASYNC910>` now correctly handles ``await()`` in parameter lists.
1111
- Fixed a bug with :ref:`ASYNC91x <ASYNC910>` and nested empty functions.
1212

13+
24.11.4
14+
=======
15+
- :ref:`ASYNC100 <async100>` once again ignores :func:`trio.open_nursery` and :func:`anyio.create_task_group`, unless we find a call to ``.start_soon()``.
16+
17+
24.11.3
18+
=======
19+
- Revert :ref:`ASYNC100 <async100>` ignoring :func:`trio.open_nursery` and :func:`anyio.create_task_group` due to it not viewing ``.start_soon()`` as introducing a :ref:`cancel point <cancel_point>`.
20+
21+
24.11.2
22+
=======
23+
- Fix crash in ``Visitor91x`` on ``async with a().b():``.
24+
25+
24.11.1
26+
=======
27+
- :ref:`ASYNC100 <async100>` now ignores :func:`trio.open_nursery` and :func:`anyio.create_task_group`
28+
as cancellation sources, because they are :ref:`schedule points <schedule_point>` but not
29+
:ref:`cancellation points <cancel_point>`.
30+
- :ref:`ASYNC101 <async101>` and :ref:`ASYNC119 <async119>` are now silenced for decorators in :ref:`transform-async-generator-decorators`.
31+
1332
24.10.2
1433
=======
1534
- :ref:`ASYNC102 <async102>` now also warns about ``await()`` inside ``__aexit__``.
1635

1736
24.10.1
1837
=======
19-
- Add :ref:`ASYNC123 <async123>` bad-exception-group-flattening
38+
- Add :ref:`ASYNC123 <async123>` bad-exception-group-flattening.
2039

2140
24.9.5
2241
======
@@ -45,7 +64,7 @@ Changelog
4564

4665
24.8.1
4766
======
48-
- Add config option ``transform-async-generator-decorators``, to list decorators which
67+
- Add config option :ref:`transform-async-generator-decorators`, to list decorators which
4968
suppress :ref:`ASYNC900 <async900>`.
5069

5170
24.6.1

docs/glossary.rst

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ Exception classes:
8888

8989
Checkpoint
9090
----------
91-
Checkpoints are points where the async backend checks for cancellation and
92-
can switch which task is running, in an ``await``, ``async for``, or ``async with``
91+
Checkpoints are points where the async backend checks for :ref:`cancellation <cancel_point>` and
92+
:ref:`can switch which task is running <schedule_point>`, in an ``await``, ``async for``, or ``async with``
9393
expression. Regular checkpoints can be important for both performance and correctness.
9494

9595
Trio has extensive and detailed documentation on the concept of
@@ -99,11 +99,11 @@ functions defined by Trio will either checkpoint or raise an exception when
9999
iteration, and when exhausting the iterator, and ``async with`` will checkpoint
100100
on at least one of enter/exit.
101101

102+
The one exception is :func:`trio.open_nursery` and :func:`anyio.create_task_group`. They do not checkpoint on entry, and on exit they insert a :ref:`schedule point <schedule_point>`. However, if sub-tasks are cancelled they will be propagated on exit, so if you're starting tasks you can usually treat the exit as a :ref:`cancel point <cancel_point>`.
103+
102104
asyncio does not place any guarantees on if or when asyncio functions will
103105
checkpoint. This means that enabling and adhering to :ref:`ASYNC91x <ASYNC910>`
104-
will still not guarantee checkpoints.
105-
106-
For anyio it will depend on the current backend.
106+
will still not guarantee checkpoints on asyncio (even if used via anyio).
107107

108108
When using Trio (or an AnyIO library that people might use on Trio), it can be
109109
very helpful to ensure that your own code adheres to the same guarantees as
@@ -116,6 +116,33 @@ To insert a checkpoint with no other side effects, you can use
116116
:func:`trio.lowlevel.checkpoint`/:func:`anyio.lowlevel.checkpoint`/:func:`asyncio.sleep(0)
117117
<asyncio.sleep>`
118118

119+
.. _schedule_point:
120+
121+
Schedule Point
122+
--------------
123+
A schedule point is half of a full :ref:`checkpoint`, which allows the async backend to switch the running task, but doesn't check for cancellation (the other half is a :ref:`cancel_point`).
124+
While you are unlikely to need one, they are available as :func:`trio.lowlevel.cancel_shielded_checkpoint`/:func:`anyio.lowlevel.cancel_shielded_checkpoint`, and equivalent to
125+
126+
.. code-block:: python
127+
128+
from trio import CancelScope, lowlevel
129+
# or
130+
# from anyio import CancelScope, lowlevel
131+
132+
with CancelScope(shield=True):
133+
await lowlevel.checkpoint()
134+
135+
asyncio does not have any direct equivalents due to their cancellation model being different.
136+
137+
138+
.. _cancel_point:
139+
140+
Cancel Point
141+
------------
142+
A schedule point is half of a full :ref:`checkpoint`, which will raise :ref:`cancelled` if the enclosing cancel scope has been cancelled, but does not allow the scheduler to switch to a different task (the other half is a :ref:`schedule_point`).
143+
While you are unlikely to need one, they are available as :func:`trio.lowlevel.checkpoint_if_cancelled`/:func:`anyio.lowlevel.checkpoint_if_cancelled`.
144+
Users of asyncio might want to use :meth:`asyncio.Task.cancelled`.
145+
119146
.. _channel_stream_queue:
120147

121148
Channel / Stream / Queue

docs/rules.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ _`ASYNC100` : cancel-scope-no-checkpoint
1313
A :ref:`timeout_context` does not contain any :ref:`checkpoints <checkpoint>`.
1414
This makes it pointless, as the timeout can only be triggered by a checkpoint.
1515
This check also treats ``yield`` as a checkpoint, since checkpoints can happen in the caller we yield to.
16+
:func:`trio.open_nursery` and :func:`anyio.create_task_group` are excluded, as they are :ref:`schedule points <schedule_point>` but not :ref:`cancel points <cancel_point>` (unless they have tasks started in them).
1617
See :ref:`ASYNC912 <async912>` which will in addition guarantee checkpoints on every code path.
1718

18-
ASYNC101 : yield-in-cancel-scope
19+
_`ASYNC101` : yield-in-cancel-scope
1920
``yield`` inside a :ref:`taskgroup_nursery` or :ref:`timeout_context` is only safe when implementing a context manager - otherwise, it breaks exception handling.
2021
See `this thread <https://discuss.python.org/t/preventing-yield-inside-certain-context-managers/1091/23>`_ for discussion of a future PEP.
2122
This has substantial overlap with :ref:`ASYNC119 <ASYNC119>`, which will warn on almost all instances of ASYNC101, but ASYNC101 is about a conceptually different problem that will not get resolved by :pep:`533`.

docs/usage.rst

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ adding the following to your ``.pre-commit-config.yaml``:
3333
minimum_pre_commit_version: '2.9.0'
3434
repos:
3535
- repo: https://github.com/python-trio/flake8-async
36-
rev: 23.2.5
36+
rev: 25.1.1
3737
hooks:
3838
- id: flake8-async
3939
# args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC]
@@ -318,3 +318,21 @@ Specified patterns must not have parentheses, and will only match when the patte
318318
def my_blocking_call(): # it's also safe to use the name in other contexts
319319
...
320320
arbitrary_other_function(my_blocking_call=None)
321+
322+
.. _transform-async-generator-decorators:
323+
324+
``transform-async-generator-decorators``
325+
----------------------------------------
326+
Comma-separated list of decorators that make async generators safe, disabling
327+
:ref:`ASYNC900 <ASYNC900>`, :ref:`ASYNC101 <ASYNC101>`, and :ref:`ASYNC119 <ASYNC119>` warnings for functions decorated with any of them.
328+
``[pytest.]fixture`` and ``[contextlib.]asynccontextmanager`` are always considered safe.
329+
Decorators can be dotted or not, as well as support * as a wildcard.
330+
331+
Example
332+
^^^^^^^
333+
334+
.. code-block:: none
335+
336+
transform-async-generator-decorators =
337+
fastapi.Depends
338+
trio_util.trio_async_generator

flake8_async/__init__.py

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

3939

4040
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
41-
__version__ = "24.10.3"
41+
__version__ = "25.1.1"
4242

4343

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

flake8_async/visitors/helpers.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -337,15 +337,20 @@ def build_cst_matcher(attr: str) -> m.BaseExpression:
337337
return m.Attribute(value=build_cst_matcher(body), attr=m.Name(value=tail))
338338

339339

340-
def identifier_to_string(attr: cst.Name | cst.Attribute) -> str | None:
341-
if isinstance(attr, cst.Name):
342-
return attr.value
343-
if not isinstance(attr.value, (cst.Attribute, cst.Name)):
344-
return None
345-
lhs = identifier_to_string(attr.value)
346-
if lhs is None:
347-
return None
348-
return lhs + "." + attr.attr.value
340+
def identifier_to_string(node: cst.CSTNode) -> str | None:
341+
"""Convert a simple identifier to a string.
342+
343+
If the node is composed of anything but cst.Name + cst.Attribute it returns None.
344+
"""
345+
if isinstance(node, cst.Name):
346+
return node.value
347+
if (
348+
isinstance(node, cst.Attribute)
349+
and (lhs := identifier_to_string(node.value)) is not None
350+
):
351+
return lhs + "." + node.attr.value
352+
353+
return None
349354

350355

351356
def with_has_call(

flake8_async/visitors/visitor101.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ def visit_FunctionDef(self, node: cst.FunctionDef):
7070
self.save_state(node, "_yield_is_error", "_safe_decorator")
7171
self._yield_is_error = False
7272
self._safe_decorator = func_has_decorator(
73-
node, "contextmanager", "asynccontextmanager", "fixture"
73+
node,
74+
"contextmanager",
75+
"asynccontextmanager",
76+
"fixture",
77+
*self.options.transform_async_generator_decorators,
7478
)
7579

7680
# trigger on leaving yield so any comments are parsed for noqas

flake8_async/visitors/visitor103_104.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@
5151
"ASYNC103": _async103_common_msg,
5252
"ASYNC104": "Cancelled (and therefore BaseException) must be re-raised.",
5353
}
54-
for poss_library in _suggestion_dict:
54+
for poss_library, _lib_suggest in _suggestion_dict.items():
5555
_error_codes[f"ASYNC103_{'_'.join(poss_library)}"] = (
56-
_async103_common_msg + _suggestion.format(_suggestion_dict[poss_library])
56+
_async103_common_msg + _suggestion.format(_lib_suggest)
5757
)
5858

5959

0 commit comments

Comments
 (0)