目录

PyCharm debug报错no running event loop

背景

PyCharm调测streamlit程序时报错 在PyCharm中"Run/Debug Configurations” 中增加Python配置,选择module而非script,输入框中输入streamlit, Script Parameters中填入run xxx.py这样在run/debug时实际执行的命令是python -m streamlit run xxx.py 应该与预期一致

问题

实际执行过程中,在Run的时候没有问题,streamlit程序可以正常运行 但是Debug的时候报错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Traceback (most recent call last):
  File "xxxx\.venv\Lib\site-packages\streamlit\web\bootstrap.py", line 348, in run
    if asyncio.get_running_loop().is_running():
       ^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: no running event loop

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "xxx\Python311\Lib\asyncio\events.py", line 84, in _run
    self._context.run(self._callback, *self._args)
TypeError: 'Task' object is not callable

解决方案

PyCharm中Ctrl+Shift+A 输入Registry, 打开Registry...

找到python.debug.asyncio.repl,将后面的勾去掉,然后关闭重启PyCharm,问题解决

问题原因

Registry是Jetbrain系列的IDE的配置注册表,很多的配置写在这个注册表中 python.debug.asyncio.repl是控制Debug Console支持异步代码的控制开关,2023.3.3之后的版本默认开启

streamlit执行报错代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try:  
# Check if we're already in an event loop  
	if asyncio.get_running_loop().is_running():  
		# Use `asyncio.create_task` if we're in an async context  
		# TODO(lukasmasuch): Do we have to store a reference for the task here?        asyncio.create_task(main())  # noqa: RUF006  
	else:  
		# Otherwise, use `asyncio.run`  
		asyncio.run(main())  
except RuntimeError:  
	# get_running_loop throws RuntimeError if no running event loop  
	asyncio.run(main())

当打开这个开关并启动debug时,调试控制台会启动一个事件循环来来支持异步交互REPL(Read/Eval/Print/Loop)

1
2
3
4
5
6
7
8
9
graph TD
  A[用户启动调试] --> B[PyCharm 创建调试进程]
  B --> C[加载 pydevd 调试器]
  C --> D[检测异步环境]
  D --> E{是否异步代码?}
  E -- 是 --> F[创建控制事件循环]
  E -- 否 --> G[正常执行]
  F --> H[注入调试钩子]
  H --> I[运行用户代码]

在pydevd调试器启动时,会在自己的事件循环中运行,如果被调试的代码也启动了事件循环,比如上面的使用了asyncio.run(),就会导致Event loop is already running等错误,为了解决这种问题,pycharm使用了劫持事件循环进行patch,使事件循环支持重入

主要的patch代码在pydevd_nest_asyncio.py中,代码的主要逻辑是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if IS_ASYNCIO_DEBUGGER_ENV:
...
	def _apply(loop=None):  
	    """ Patch asyncio to make its event loop reentrant. """
	    ...

	def _patch_asyncio():  
	    """  
	    Patch asyncio module to use pure Python tasks and futures,
	    use module level _current_tasks, all_tasks and patch run method.    
	    """
	    ...

	def _patch_policy():  
	    """Patch the policy to always return a patched loop."""
	    ...

	def _patch_loop(loop):  
	    """Patch loop to make it reentrant."""
	    ...

	def _patch_task():  
	    """Patch the Task's step and enter/leave methods to make it reentrant."""
	    ...

...

这个patch主要解决的是可重入问题,patch的方式主要使用Monkey Patch的方式,比如对loop的patch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def _patch_loop(loop):  
    """Patch loop to make it reentrant."""

	def run_forever(self):
		...
	def run_until_complete(self, future):
		...
	def call_soon(self, callback, *args, context=None):
		...
	...
	if hasattr(loop, '_pydevd_nest_patched'):  
	    return  
	if not isinstance(loop, asyncio.BaseEventLoop):  
	    raise ValueError('Can\'t patch loop of type %s' % type(loop))  
	cls = loop.__class__  
	cls.run_forever = run_forever  
	cls.run_until_complete = run_until_complete  
	cls._run_once = _run_once  
	cls.call_soon = call_soon
	...

event loop默认的是WindowsProactorEventLoopPolicy, 如果我们把event loop策略设置成WindowsSelectorEventLoopPolicy,即使用非默认的event loop,就会出现这种问题,而在streamlit.web中确实显式设置了event loop类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
if env_util.IS_WINDOWS:
	try:
		from asyncio import (  # type: ignore[attr-defined]
			WindowsProactorEventLoopPolicy,
			WindowsSelectorEventLoopPolicy,
		)
	except ImportError:
		pass
		# Not affected
	else:
		if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy:
			# WindowsProactorEventLoopPolicy is not compatible with
			# Tornado 6 fallback to the pre-3.8 default of Selector
			asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())

那么到底为什么修改成非默认的event loop pydevd的patch会失效,这个没太搞清楚,应该是pydevd启动的ProactorEventLoop与应用启动的SelectorEventLoop在协同时存在一些冲突。

另外在pydevd_nest_asyncio.pyasyncio.run方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def run(main, debug=False):  
    loop = _PydevdAsyncioUtils.get_event_loop()  
    loop.set_debug(debug)  
    task = asyncio.ensure_future(main)  
    try:  
        return loop.run_until_complete(task)  
    finally:  
        if not task.done():  
            task.cancel()  
            with suppress(asyncio.CancelledError):  
                loop.run_until_complete(task)

在此处查看loop的取值,发现loop的_pydevd_nest_patched 这个patch标记字段有时候是不存在的,但是当你Evaluate loop._pydevd_nest_patched的取值时,发现相关的path标记又被加上去了,猜测可能是底层执行的C代码与Python代码在Loop的传递取值上也存在一些刷新上的GAP