Skip to content

Commit 17e80a6

Browse files
committed
🚀 raise RuntimeError when eval async code with non asyncio event loop
1 parent 2346d0b commit 17e80a6

File tree

6 files changed

+105
-16
lines changed

6 files changed

+105
-16
lines changed

async-pydevd/async_pydevd/asyncio_patch.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,17 @@
1010
pass
1111

1212

13+
def _is_async_debug_available(loop=None) -> bool:
14+
if loop is None:
15+
loop = asyncio.get_event_loop()
16+
17+
return loop.__class__.__module__.lstrip("_").startswith("asyncio")
18+
19+
1320
def _patch_asyncio_set_get_new():
21+
if not _is_async_debug_available():
22+
return
23+
1424
if sys.platform.lower().startswith("win"):
1525
try:
1626
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
@@ -20,7 +30,7 @@ def _patch_asyncio_set_get_new():
2030
apply()
2131

2232
def _patch_loop_if_not_patched(loop: AbstractEventLoop):
23-
if not hasattr(loop, "_nest_patched"):
33+
if not hasattr(loop, "_nest_patched") and _is_async_debug_available(loop):
2434
_patch_loop(loop)
2535

2636
def _patch_asyncio_api(func: Callable) -> Callable:

async-pydevd/async_pydevd/pydevd_patch.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
import asyncio
12
from typing import Any
23

4+
try:
5+
_ = _is_async_debug_available # noqa # only for testing purposes
6+
except NameError:
7+
8+
def _is_async_debug_available(_=None) -> bool:
9+
return True
10+
311

412
def is_async_code(code: str) -> bool:
513
# TODO: use node visitor to check if code contains async/await
@@ -22,6 +30,14 @@ def make_code_async(code: str) -> str:
2230

2331
def evaluate_expression(thread_id: object, frame_id: object, expression: str, doExec: bool) -> Any:
2432
if is_async_code(expression):
33+
if not _is_async_debug_available():
34+
cls = asyncio.get_event_loop().__class__
35+
36+
raise RuntimeError(
37+
f"Can not evaluate async code with event loop {cls.__module__}.{cls.__qualname__}. "
38+
"Only native asyncio event loop can be used for async code evaluating."
39+
)
40+
2541
doExec = False
2642

2743
try:
@@ -58,11 +74,10 @@ def line_breakpoint_init(self: LineBreakpoint, *args, **kwargs):
5874
# Update old breakpoints
5975
import gc
6076

61-
for obj in gc.get_objects():
77+
for obj in gc.get_objects(): # pragma: no cover
6278
if isinstance(obj, LineBreakpoint):
6379
normalize_line_breakpoint(obj)
6480

65-
6681
# 3. Add ability to use async code in console
6782
from _pydevd_bundle import pydevd_console_integration
6883

async-pydevd/tests/test_asyncio_patch.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,40 @@ class CustomEventLoop(BaseEventLoop):
1313
pass
1414

1515

16+
class AsyncioEventLoop(BaseEventLoop):
17+
__module__ = "asyncio"
18+
19+
1620
def _test_asyncio_patch():
1721
from async_pydevd import asyncio_patch # noqa # isort:skip
1822
from asyncio import get_event_loop, new_event_loop, set_event_loop # isort:skip
1923

2024
assert _is_patched(get_event_loop())
2125
assert _is_patched(new_event_loop())
2226

23-
loop = CustomEventLoop()
27+
loop = AsyncioEventLoop()
2428
set_event_loop(loop)
2529
assert _is_patched(loop)
2630

2731

32+
def _test_asyncio_patch_non_default_loop():
33+
from asyncio import get_event_loop, set_event_loop # isort:skip
34+
35+
set_event_loop(CustomEventLoop())
36+
37+
from async_pydevd import asyncio_patch # noqa # isort:skip
38+
39+
assert not _is_patched(get_event_loop())
40+
41+
2842
def test_asyncio_patch(run_in_process):
2943
run_in_process(_test_asyncio_patch)
3044

3145

46+
def test_asyncio_patch_non_default_loop(run_in_process):
47+
run_in_process(_test_asyncio_patch_non_default_loop)
48+
49+
3250
def _test_windows_asyncio_policy():
3351
from async_pydevd import asyncio_patch # noqa # isort:skip
3452
from asyncio.windows_events import WindowsSelectorEventLoopPolicy # isort:skip

async-pydevd/tests/test_pydevd_patch.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import sys
22
from unittest.mock import MagicMock
33

4-
from pytest import fixture, mark
4+
from pytest import fixture, mark, raises
55

6-
from .utils import ctxmanager # noqa
6+
from .utils import ctxmanager, regular # noqa
77

88

99
def _as_async(code: str):
@@ -132,3 +132,21 @@ def _with_locals():
132132

133133
assert "f" in g.gi_frame.f_locals
134134
assert g.gi_frame.f_locals["f"] == 10
135+
136+
137+
def test_async_evaluate_is_not_available_for_eventloop(mocker):
138+
mocker.patch("async_pydevd.pydevd_patch._is_async_debug_available", return_value=False)
139+
140+
from async_pydevd.pydevd_patch import evaluate_expression
141+
142+
with raises(
143+
RuntimeError,
144+
match=r"^Can not evaluate async code with event loop asyncio.unix_events._UnixSelectorEventLoop. "
145+
r"Only native asyncio event loop can be used for async code evaluating.$",
146+
):
147+
evaluate_expression(
148+
object(),
149+
object(),
150+
"await regular()",
151+
True,
152+
)

helpers/poetry.lock

+8-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/java/com/uriyyo/evaluate_async_code/AsyncPyDebugUtils.kt

+30-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ fun Sdk.whenSupport(block: () -> Unit) {
4949
block()
5050
}
5151

52-
fun pydevd_async_init() = """
52+
val pydevd_async_init: () -> String = {
53+
"""
5354
def _patch_pydevd():
5455
${PYDEVD_ASYNC_PLUGIN.prependIndent(" ").split("\n").dropLast(5).joinToString("\n")}
5556
@@ -58,6 +59,7 @@ import sys
5859
if not hasattr(sys, "__async_eval__"):
5960
_patch_pydevd()
6061
""".trimIndent()
62+
}.memoize()
6163

6264
val PYDEVD_ASYNC_PLUGIN = """
6365
import asyncio
@@ -278,7 +280,17 @@ except ImportError:
278280
pass
279281
280282
283+
def _is_async_debug_available(loop=None) -> bool:
284+
if loop is None:
285+
loop = asyncio.get_event_loop()
286+
287+
return loop.__class__.__module__.lstrip("_").startswith("asyncio")
288+
289+
281290
def _patch_asyncio_set_get_new():
291+
if not _is_async_debug_available():
292+
return
293+
282294
if sys.platform.lower().startswith("win"):
283295
try:
284296
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
@@ -288,7 +300,7 @@ def _patch_asyncio_set_get_new():
288300
apply()
289301
290302
def _patch_loop_if_not_patched(loop: AbstractEventLoop):
291-
if not hasattr(loop, "_nest_patched"):
303+
if not hasattr(loop, "_nest_patched") and _is_async_debug_available(loop):
292304
_patch_loop(loop)
293305
294306
def _patch_asyncio_api(func: Callable) -> Callable:
@@ -434,8 +446,16 @@ def async_eval(expr: str, _globals: Optional[dict] = None, _locals: Optional[dic
434446
435447
sys.__async_eval__ = async_eval
436448
449+
import asyncio
437450
from typing import Any
438451
452+
try:
453+
_ = _is_async_debug_available # noqa # only for testing purposes
454+
except NameError:
455+
456+
def _is_async_debug_available(_=None) -> bool:
457+
return True
458+
439459
440460
def is_async_code(code: str) -> bool:
441461
# TODO: use node visitor to check if code contains async/await
@@ -458,6 +478,14 @@ original_evaluate = pydevd_vars.evaluate_expression
458478
459479
def evaluate_expression(thread_id: object, frame_id: object, expression: str, doExec: bool) -> Any:
460480
if is_async_code(expression):
481+
if not _is_async_debug_available():
482+
cls = asyncio.get_event_loop().__class__
483+
484+
raise RuntimeError(
485+
f"Can not evaluate async code with event loop {cls.__module__}.{cls.__qualname__}. "
486+
"Only native asyncio event loop can be used for async code evaluating."
487+
)
488+
461489
doExec = False
462490
463491
try:
@@ -498,7 +526,6 @@ for obj in gc.get_objects():
498526
if isinstance(obj, LineBreakpoint):
499527
normalize_line_breakpoint(obj)
500528
501-
502529
# 3. Add ability to use async code in console
503530
from _pydevd_bundle import pydevd_console_integration
504531

0 commit comments

Comments
 (0)