diff --git a/doc/data/messages/a/async-context-manager-with-regular-with/bad.py b/doc/data/messages/a/async-context-manager-with-regular-with/bad.py new file mode 100644 index 0000000000..72b520128b --- /dev/null +++ b/doc/data/messages/a/async-context-manager-with-regular-with/bad.py @@ -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") diff --git a/doc/data/messages/a/async-context-manager-with-regular-with/good.py b/doc/data/messages/a/async-context-manager-with-regular-with/good.py new file mode 100644 index 0000000000..f9882a4d87 --- /dev/null +++ b/doc/data/messages/a/async-context-manager-with-regular-with/good.py @@ -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") diff --git a/doc/data/messages/a/async-context-manager-with-regular-with/related.rst b/doc/data/messages/a/async-context-manager-with-regular-with/related.rst new file mode 100644 index 0000000000..642c8592f7 --- /dev/null +++ b/doc/data/messages/a/async-context-manager-with-regular-with/related.rst @@ -0,0 +1,2 @@ +- `PEP 492 - Coroutines with async and await syntax `_ +- `contextlib.asynccontextmanager `_ diff --git a/doc/user_guide/checkers/features.rst b/doc/user_guide/checkers/features.rst index 5040bf68e5..66a20c5492 100644 --- a/doc/user_guide/checkers/features.rst +++ b/doc/user_guide/checkers/features.rst @@ -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 diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index bdfa2f3269..f71738c83c 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -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 diff --git a/doc/whatsnew/fragments/10999.new_check b/doc/whatsnew/fragments/10999.new_check new file mode 100644 index 0000000000..3df5eeb28e --- /dev/null +++ b/doc/whatsnew/fragments/10999.new_check @@ -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 diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index ed8d6d2bca..fcb16102f9 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -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", @@ -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() @@ -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. diff --git a/tests/functional/a/async_context_manager_with_regular_with.py b/tests/functional/a/async_context_manager_with_regular_with.py new file mode 100644 index 0000000000..812598cb27 --- /dev/null +++ b/tests/functional/a/async_context_manager_with_regular_with.py @@ -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 diff --git a/tests/functional/a/async_context_manager_with_regular_with.txt b/tests/functional/a/async_context_manager_with_regular_with.txt new file mode 100644 index 0000000000..0674a67998 --- /dev/null +++ b/tests/functional/a/async_context_manager_with_regular_with.txt @@ -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