如何在 pytest 中使用 monkeypatch 或 mock 删除库?

How to remove a library with monkeypatch or mock in pytest?

如果我的库有一个 contrib extra,其中有依赖项(比如 requests),我希望用户必须安装才能访问 CLI API,但是我在 CI 中的测试期间安装了 contrib extra 如何使用 pytest's MonkeyPatch 在测试期间删除依赖项以确保我的检测正确?

例如,如果 contrib extra 将额外安装 requests,所以我希望用户必须执行

$ python -m pip install mylib[contrib]

然后能够在命令行上有一个 CLI API 看起来像

$ mylib contrib myfunction

其中 myfunction 使用 requests 依赖项

# mylib/src/mylib/cli/contrib.py
import click
try:
    import requests
except ModuleNotFoundError:
    pass # should probably warn though, but this is just an example

# ...

@click.group(name="contrib")
def cli():
    """
    Contrib experimental operations.
    """

@cli.command()
@click.argument("example", default="-")
def myfunction(example):
   requests.get(example)
   # ...

如何在我的 pytest 测试中模拟或 monkeypatch out requests 以确保用户会正确地收到警告与 ModuleNotFoundError 如果他们只是

$ python -m pip install mylib
$ mylib contrib myfunction

?在阅读了有关 pytest 标签的其他一些问题后,我认为我仍然不明白该怎么做,所以我在这里问。

我最终所做的是有效的,我已经确认这是一个合理的方法 thanks to Anthony Sottile,是通过将其设置为来模拟额外的依赖项(此处 requests)不存在Nonesys.modules 中,然后重新加载需要使用 requests 的模块。 我测试有一个实际的投诉,即 requests 不存在要使用 caplog.

导入

这是我目前正在使用的测试(名称已更改以匹配我在上述问题中的玩具示例问题)

import mylib
import sys
import logging
import pytest
from unittest import mock
from importlib import reload
from importlib import import_module

# ...

def test_missing_contrib_extra(caplog):
    with mock.patch.dict(sys.modules):
        sys.modules["requests"] = None
        if "mylib.contrib.utils" in sys.modules:
            reload(sys.modules["mylib.contrib.utils"])
        else:
            import_module("mylib.cli")

    with caplog.at_level(logging.ERROR):
        # The 2nd and 3rd lines check for an error message that mylib throws
        for line in [
            "import of requests halted; None in sys.modules",
            "Installation of the contrib extra is required to use mylib.contrib.utils.download",
            "Please install with: python -m pip install mylib[contrib]",
        ]:
            assert line in caplog.text
        caplog.clear()

我应该注意到,这实际上是在 中提倡的,@hoefling 链接到上面(在我解决了这个问题之后但在我开始发布这个之前发布)。

如果人们有兴趣在实际的图书馆中看到这个,c.f。以下两个 PR:

注意事项: Anthony Sottile 警告说

reload() can be kinda iffy -- I'd be careful with it (things which have old references to the old module will live on, sometimes it can introduce new copies of singletons (doubletons? tripletons?)) -- I've tracked down many-a-test-pollution problems to reload()

所以如果我实施更安全的替代方案,我会修改这个答案。