1. FlyPython首页
  2. Python高级话题

在 Python 3.10 中查找和报告 asyncio 错误

我今天在 Python 3.10 中发现了一个错误!关于我如何找到它以及我在弄清楚发生了什么后处理它的过程的一些说明。

针对 Python 3.10 测试数据集

今天早上我终于开始尝试将 Datasette 升级到刚刚发布的 Python 3.10。我首先添加"3.10"到 GitHub Actions 测试 Datasette 的 Python 版本矩阵:

strategy:
  matrix:
    python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]

该行以前看起来像这样:

python-version: [3.6, 3.7, 3.8, 3.9]

我根据Jeff Triplett 的提示切换到此处使用带引号的字符串,他指出这3.10实际上是 YAML 语法中的一个浮点数,将被视为引用 Python 3.1!

拉取请求中的这一行更改就是针对新版本的 Python 运行测试所需要的全部内容……但它们以惊人的方式失败了。

令人讨厌的是,根错误并没有直接显示在测试中,因为它被触发,然后被 Datasette 自己的错误处理捕获并变成了 500 HTTP 错误。所以,我需要在本地运行 Python 3.10 和调试器。

在本地运行 Python 3.10

我偶尔会使用pyenv在我的机器上管理多个 Python 版本。我通过 Homebrew 安装了这个。

我必须先运行brew upgrade pyenv,因为我安装的版本不知道 Python 3.10。然后我运行了这个:

pyenv install 3.10.0

这让我在我的 Mac 上安装了 Python 3.10 ~/.pyenv/versions/3.10.0/bin/python.

最后,我使用这个版本的 Python 创建了一个新的虚拟环境。

~/.pyenv/versions/3.10.0/bin/python \
  -m venv /tmp/py310
# Now activate that virtual environment:
source /tmp/py310/bin/activate

我把它放在我的/tmp目录中,这样我以后就不必记得清理它了。

完成此操作后,我将我的 Datasette 测试依赖项安装到新环境中并用于pytest运行我的测试:

% cd ~/Dropbox/Development/datasette
% pip install -e '.[test]'

运行pytest -x --pdb在第一个失败的测试时停止,并将我放入调试器中,在那里我终于可以访问完整的回溯,它看起来像这样:

  File "datasette/datasette/views/base.py", line 122, in dispatch_request
    await self.ds.refresh_schemas()
  File "datasette/datasette/app.py", line 344, in refresh_schemas
    await self._refresh_schemas()
  File "datasette/datasette/app.py", line 349, in _refresh_schemas
    await init_internal_db(internal_db)
  File "datasette/datasette/utils/internal_db.py", line 5, in init_internal_db
    await db.execute_write(
  File "datasette/datasette/database.py", line 102, in execute_write
    return await self.execute_write_fn(_inner, block=block)
  File "datasette/datasette/database.py", line 113, in execute_write_fn
    reply_queue = janus.Queue()
  File "janus/__init__.py", line 39, in __init__
    self._async_not_empty = asyncio.Condition(self._async_mutex)
  File "lib/python3.10/asyncio/locks.py", line 234, in __init__
    raise ValueError("loop argument must agree with lock")
ValueError: loop argument must agree with lock

因此,有些事情asyncio.Condition()正在由 调用janus.Queue()

Janus 的一些背景

这里引用Janus库将自己描述为“Python 的线程安全异步感知队列”。

我在 Datasette 中将它用于写入队列——一种通过首先在内存中排队来安全地写入 SQLite 数据库的机制。

Janus 提供了一种机制,允许异步事件任务通过以 Python 自己的标准库队列为模型的队列类向 Python 线程发送和接收消息,反之亦然。它真的很整洁。

在问题评论中跟踪调查

任何时候我在调查一个 bug 时,我都会确保有一个相关的 GitHub 问题或拉取请求,给我一个地方来建立我的调查的详细笔记。

为此,我使用了我的PR #1481现在我收到了一条错误消息——“循环参数必须与锁定一致”——我做了一些初步的谷歌搜索,令我惊讶的是它几乎没有提及——我的第一个线索是这可能与新的 Python 3.10 版本有关本身。

我根据回溯追踪到 Python 和 Janus 库中的相关源代码,并从问题评论中链接到它们。我也粘贴了相关代码的副本,因为 GitHub 只是神奇地嵌入了来自同一存储库的代码,而不是来自其他存储库的代码。您可以在此处查看这些评论

我总是喜欢在问题评论中包含最有可能导致错误的代码的副本,以免自己以后不得不再次挖掘它,并在错误修复后作为历史记录。

我还在 Janus 存储库中启动了一个问题,标题为 Python 3.10 错误:“ValueError: loop argument must agree with lock”我将此与我自己的问题联系起来,并在两个地方发布了相关研究。

Janus 中的两行代码导致了这些错误(注意我添加的注释):

自我_async_mutex  = 异步Lock ()
 # “循环参数必须与锁一致”这里引发异常:
self . _async_not_empty  = 异步条件自我_async_mutex

这就是我开始怀疑 Python 错误的地方。上面的代码可以简化为:

异步条件( asyncio . Lock () )

这是在 Python 中使用条件的一种记录方式:您可以使用可选锁实例化条件,这允许多个条件共享该锁。

所以我在 Python 3.10 解释器中尝试过……但它没有抛出错误:

% ~/.pyenv/versions/3.10.0/bin/python
Python 3.10.0 (default, Oct  7 2021, 13:45:58) [Clang 12.0.0 (clang-1200.0.32.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> print(asyncio.Condition(asyncio.Lock()))
<asyncio.locks.Condition object at 0x1062521d0 [unlocked]>

通过浏览源代码,我可以看到这里发生了一些涉及事件循环的事情——所以我凭直觉尝试在事件循环中运行该代码,如下所示:

>>>异步 DEF 例子():
...     打印(asyncio.Condition(asyncio.Lock()))
... 
>>> asyncio.run(example())
回溯(最近一次调用最后一次):
  ...
  示例中的文件“<stdin>”,第 2 行
  文件“/Users/simon/.pyenv/versions/3.10.0/lib/python3.10/asyncio/locks.py”,第 234 行,在 __init__ 中
    raise ValueError("循环参数必须与锁一致")
ValueError:循环参数必须与锁定一致

有一个例外!

所以看起来这可能是一个真正的 Python 3.10 错误。毕竟,我知道这段代码可以在 Python 的早期版本中运行(因为 Janus 和 Datasette 在那里运行良好)。事实上,我可以这样确认:

~ % python3.9
Python 3.9.7 (default, Sep  3 2021, 12:45:31) 
[Clang 12.0.0 (clang-1200.0.32.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> async def example():
...     print(asyncio.Condition(asyncio.Lock()))
... 
>>> asyncio.run(example())
<asyncio.locks.Condition object at 0x1049aab50 [unlocked]>

将错误报告给 Python 问题跟踪器

bugs.python.org上跟踪 Python 错误——幸好他们支持使用 GitHub(以及 Google 甚至 OpenID)登录,所以我使用我的 GitHub 帐户登录以报告错误。

我提交了问题 45416,标题为:

“循环参数必须与锁一致”实例化 asyncio.Condition

并在我演示该错误的最小 Python 脚本中包含了一个简短的描述。

然后我添加了一条评论,指向我一直在解决的相关 GitHub 问题之一,以及另一条评论链接到Python 自己的测试套件中最相关的测试,我使用 GitHub 搜索术语Condition.

每当我针对一个项目(我自己的或其他人的)提交问题时,我总是喜欢做一些额外的研究并链接到可能出错的代码和可能的测试。

作为维护者,这为我节省了大量时间——如果问题直接链接到代码和测试,我就不必花任何时间四处寻找它们。

我希望这也可以为我向其报告错误的人节省时间,从而增加他们深入研究并帮助我找到修复程序的机会!

然后我发了一条关于它的推文。我有一群具有相关经验的追随者,所以我认为是时候多看看这个问题了。

我包含了我的重现步骤脚本的屏幕截图,以防止任何可能偶然感兴趣的人不必点击问题本身。

Łukasz Langa 救援

Łukasz 是“CPython 常驻开发人员”,这意味着他获得了 Python 软件基金会的报酬,可以全职从事 Python 工作。

在我提交问题后不到一个小时,他发表了一条评论,确认了该错误并为其提供了两个短期解决方法!

我真的很喜欢他的第一个解决方法,它看起来像这样:

>>> l = asyncio.Lock()
>>> getattr (l, ' _get_loop ' , lambda : None )()
<_UnixSelectorEventLoop running=True closed=False debug=False>

它通过调用lock._get_loop()方法工作,但前提是它存在——所以即使这是一个未记录的内部方法,即使在针对可能删除该方法的未来 Python 版本运行时,它也应该是安全的。

你可以在这里看到这个方法——它安全地填充了self._loop属性,这有助于解决这个错误。

向 Janus 提交 PR

应用此变通方法的最佳位置是 Janus——所以我在那里提交了一个 PR,它添加了变通方法并更新了他们的 CI 配置以针对 Python 3.10 进行测试:Janus PR #359该 PR 的 GitHub Actions 测试现在通过 Python 3.10。

他们的 README链接到一个 gitter 聊天,所以我也在那里放了一个指向我的 PR 的链接。希望能早日合并!

我应该做的

我对这种情况感觉很好——该错误已被修复,感谢 Łukasz,我们有一个很好的解决方法,我很乐观 Datasette 的上游库将很快修复。

要是我早几周做这件事就好了!

Python的3.10发行计划首批出货的阿尔法一年前,在2020年10月,共计7个阿尔法,贝塔4,并在周一2021年10月4日最后3.10发布前2个候选发布版。

如果我花时间让 Datasette 的测试套件在 Python 3.10 上运行这些预发布版本中的任何一个,我本可以在它出现在最终版本中之前帮助发现这个错误!

所以吸取了教训:Python 有 alphas、betas 和 RCs 是有原因的。对于未来的 Python,我将注意在最终版本发布之前针对我的项目针对这些进行测试。

原创文章,作者:flypython,如若转载,请注明出处:http://flypython.com/advanced-python/561.html