背景
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.py
中 asyncio.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