Skip to content

Commit 01f022d

Browse files
feat: add check for async context managers used with regular with (#10575)
Signed-off-by: Emmanuel Ferdman <[email protected]>
1 parent a96dc99 commit 01f022d

File tree

9 files changed

+65
-1
lines changed

9 files changed

+65
-1
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from contextlib import asynccontextmanager
2+
3+
4+
@asynccontextmanager
5+
async def async_context():
6+
yield
7+
8+
9+
with async_context(): # [async-context-manager-with-regular-with]
10+
print("This will cause an error at runtime")
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import asyncio
2+
from contextlib import asynccontextmanager
3+
4+
5+
@asynccontextmanager
6+
async def async_context():
7+
yield
8+
9+
10+
async def main():
11+
async with async_context():
12+
print("This works correctly")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- `PEP 492 - Coroutines with async and await syntax <https://peps.python.org/pep-0492/>`_
2+
- `contextlib.asynccontextmanager <https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager>`_

doc/user_guide/checkers/features.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Async checker Messages
2929
Used when an async context manager is used with an object that does not
3030
implement the async context management protocol. This message can't be
3131
emitted when using Python < 3.5.
32+
:async-context-manager-with-regular-with (E1145): *Context manager '%s' is async and should be used with 'async with'.*
33+
Used when an async context manager is used with a regular 'with' statement
34+
instead of 'async with'.
3235

3336

3437
Bad-Chained-Comparison checker

doc/user_guide/messages/messages_overview.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ All messages in the error category:
5353
error/assigning-non-slot
5454
error/assignment-from-no-return
5555
error/assignment-from-none
56+
error/async-context-manager-with-regular-with
5657
error/await-outside-async
5758
error/bad-configuration-section
5859
error/bad-except-order
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add new check ``async-context-manager-with-regular-with`` to detect async context managers used with regular ``with`` statements instead of ``async with``.
2+
3+
Refs #10999

pylint/checkers/typecheck.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,12 @@ def _similar_names(
299299
"Used when an instance in a with statement doesn't implement "
300300
"the context manager protocol(__enter__/__exit__).",
301301
),
302+
"E1145": (
303+
"Context manager '%s' is async and should be used with 'async with'.",
304+
"async-context-manager-with-regular-with",
305+
"Used when an async context manager is used with a regular 'with' statement "
306+
"instead of 'async with'.",
307+
),
302308
"E1130": (
303309
"%s",
304310
"invalid-unary-operand-type",
@@ -1869,7 +1875,9 @@ def _check_invalid_slice_index(self, node: nodes.Slice) -> None:
18691875
if invalid_slice_step:
18701876
self.add_message("invalid-slice-step", node=node.step, confidence=HIGH)
18711877

1872-
@only_required_for_messages("not-context-manager")
1878+
@only_required_for_messages(
1879+
"not-context-manager", "async-context-manager-with-regular-with"
1880+
)
18731881
def visit_with(self, node: nodes.With) -> None:
18741882
for ctx_mgr, _ in node.items:
18751883
context = astroid.context.InferenceContext()
@@ -1883,6 +1891,17 @@ def visit_with(self, node: nodes.With) -> None:
18831891
inferred.parent, self.linter.config.contextmanager_decorators
18841892
):
18851893
continue
1894+
# Check if it's an AsyncGenerator decorated with asynccontextmanager
1895+
if isinstance(inferred, astroid.bases.AsyncGenerator):
1896+
async_decorators = ["contextlib.asynccontextmanager"]
1897+
if decorated_with(inferred.parent, async_decorators):
1898+
self.add_message(
1899+
"async-context-manager-with-regular-with",
1900+
node=node,
1901+
args=(inferred.parent.name,),
1902+
confidence=INFERENCE,
1903+
)
1904+
continue
18861905
# If the parent of the generator is not the context manager itself,
18871906
# that means that it could have been returned from another
18881907
# function which was the real context manager.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# pylint: disable=missing-function-docstring
2+
"""Test async context manager used with regular 'with'."""
3+
4+
from contextlib import asynccontextmanager
5+
6+
7+
@asynccontextmanager
8+
async def async_cm():
9+
yield
10+
11+
12+
with async_cm(): # [async-context-manager-with-regular-with]
13+
pass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
async-context-manager-with-regular-with:12:0:13:8::Context manager 'async_cm' is async and should be used with 'async with'.:INFERENCE

0 commit comments

Comments
 (0)