Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/data/messages/a/async-context-manager-with-regular-with/bad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from contextlib import asynccontextmanager


@asynccontextmanager
async def async_context():
yield


with async_context(): # [async-context-manager-with-regular-with]
print("This will cause an error at runtime")
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import asyncio
from contextlib import asynccontextmanager


@asynccontextmanager
async def async_context():
yield


async def main():
async with async_context():
print("This works correctly")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- `PEP 492 - Coroutines with async and await syntax <https://peps.python.org/pep-0492/>`_
- `contextlib.asynccontextmanager <https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager>`_
3 changes: 3 additions & 0 deletions doc/user_guide/checkers/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Async checker Messages
Used when an async context manager is used with an object that does not
implement the async context management protocol. This message can't be
emitted when using Python < 3.5.
:async-context-manager-with-regular-with (E1145): *Context manager '%s' is async and should be used with 'async with'.*
Used when an async context manager is used with a regular 'with' statement
instead of 'async with'.


Bad-Chained-Comparison checker
Expand Down
1 change: 1 addition & 0 deletions doc/user_guide/messages/messages_overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ All messages in the error category:
error/assigning-non-slot
error/assignment-from-no-return
error/assignment-from-none
error/async-context-manager-with-regular-with
error/await-outside-async
error/bad-configuration-section
error/bad-except-order
Expand Down
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/10999.new_check
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add new check ``async-context-manager-with-regular-with`` to detect async context managers used with regular ``with`` statements instead of ``async with``.

Refs #10999
21 changes: 20 additions & 1 deletion pylint/checkers/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,12 @@ def _similar_names(
"Used when an instance in a with statement doesn't implement "
"the context manager protocol(__enter__/__exit__).",
),
"E1145": (
"Context manager '%s' is async and should be used with 'async with'.",
"async-context-manager-with-regular-with",
"Used when an async context manager is used with a regular 'with' statement "
"instead of 'async with'.",
),
"E1130": (
"%s",
"invalid-unary-operand-type",
Expand Down Expand Up @@ -1872,7 +1878,9 @@ def _check_invalid_slice_index(self, node: nodes.Slice) -> None:
if invalid_slice_step:
self.add_message("invalid-slice-step", node=node.step, confidence=HIGH)

@only_required_for_messages("not-context-manager")
@only_required_for_messages(
"not-context-manager", "async-context-manager-with-regular-with"
)
def visit_with(self, node: nodes.With) -> None:
for ctx_mgr, _ in node.items:
context = astroid.context.InferenceContext()
Expand All @@ -1886,6 +1894,17 @@ def visit_with(self, node: nodes.With) -> None:
inferred.parent, self.linter.config.contextmanager_decorators
):
continue
# Check if it's an AsyncGenerator decorated with asynccontextmanager
if isinstance(inferred, astroid.bases.AsyncGenerator):
async_decorators = ["contextlib.asynccontextmanager"]
if decorated_with(inferred.parent, async_decorators):
self.add_message(
"async-context-manager-with-regular-with",
node=node,
args=(inferred.parent.name,),
confidence=INFERENCE,
)
continue
# If the parent of the generator is not the context manager itself,
# that means that it could have been returned from another
# function which was the real context manager.
Expand Down
13 changes: 13 additions & 0 deletions tests/functional/a/async_context_manager_with_regular_with.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# pylint: disable=missing-function-docstring
"""Test async context manager used with regular 'with'."""

from contextlib import asynccontextmanager


@asynccontextmanager
async def async_cm():
yield


with async_cm(): # [async-context-manager-with-regular-with]
pass
Original file line number Diff line number Diff line change
@@ -0,0 +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
Loading