将函数参数注释为特定模块

Chr*_*ial 5 python pytest python-typing

我有一个导入特定模块的 pytest 夹具。这是必需的,因为导入模块非常昂贵,因此我们不想在导入时(即在 pytest 测试收集期间)执行此操作。这会产生如下代码:

@pytest.fixture
def my_module_fix():
    import my_module
    yield my_module

def test_something(my_module_fix):
    assert my_module_fix.my_func() = 5
Run Code Online (Sandbox Code Playgroud)

我正在使用 PyCharm,并希望在我的测试中进行类型检查和自动完成。为了实现这一点,我必须以某种方式将my_module_fix参数注释为具有模块的类型my_module

我不知道如何实现这一目标。我发现我可以注释my_module_fix为 type types.ModuleType,但这还不够:它不是任何模块,它始终是my_module

jed*_*rds 4

如果我听到你的问题,你有两个(或三个)单独的目标

\n
    \n
  1. 延期进口slowmodule
  2. \n
  3. 自动完成功能可以像标准导入一样继续工作
  4. \n
  5. (可能?)输入(例如 mypy?)以继续工作
  6. \n
\n

我可以想到至少五种不同的方法,但我只会简单地提到最后一种,因为它太疯狂了。

\n
\n

将模块导入到您的测试中

\n

这是(到目前为止)最常见和恕我直言首选的解决方案。

\n

例如代替

\n
import slowmodule\n\ndef test_foo():\n    slowmodule.foo()\n\ndef test_bar():\n    slowmodule.bar()\n
Run Code Online (Sandbox Code Playgroud)\n

你会写:

\n
def test_foo():\n    import slowmodule\n    slowmodule.foo()\n\ndef test_bar():\n    import slowmodule\n    slowmodule.bar()\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  • [延迟导入]这里,模块将按需/延迟导入。因此,如果您将 pytest 设置为快速失败,并且另一个测试在 pytest 到达您的 ( test_footest_bar ) 测试之前另一个测试失败,则该模块将永远不会被导入,并且您将永远不会产生运行时成本。

    \n

    由于Python的模块缓存,后续的导入语句实际上不会重新导入模块,而只是获取对已导入模块的引用。

    \n
  • \n
  • [自动完成/打字]当然,在这种情况下,自动完成将继续按您的预期工作。这是一个完美的导入模式。

    \n
  • \n
\n

虽然它确实需要添加潜在的许多额外的导入语句(每个测试函数中一个),但它会立即清楚发生了什么(无论是否清楚为什么会发生)。

\n
\n

[3.7+] 用模块代理你的模块__getattr__

\n

slowmodule_proxy.py如果您创建一个包含以下内容的模块(例如):

\n
def __getattr__(name):\n    import slowmodule\n    return getattr(slowmodule, name)\n
Run Code Online (Sandbox Code Playgroud)\n

在你的测试中,例如

\n
import slowmodule\n\ndef test_foo():\n    slowmodule.foo()\n\ndef test_bar():\n    slowmodule.bar()\n
Run Code Online (Sandbox Code Playgroud)\n

代替:

\n
import slowmodule\n
Run Code Online (Sandbox Code Playgroud)\n

你写:

\n
import slowmodule_proxy as slowmodule\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  • [延迟导入]感谢PEP-562,您可以从 中“请求”任何名称slowmodule_proxy,它将从 中获取并返回相应的名称slowmodule。正如上面一样,包含函数import 内部slowmodule将导致仅在调用并执行函数时而不是在模块加载时导入。当然,模块缓存在这里仍然适用,因此每个解释器会话只会遭受一次导入惩罚。

    \n
  • \n
  • [自动完成]但是,虽然延迟导入可以工作(并且您的测试运行没有问题),但这种方法(如上所述)将“破坏”自动完成:

    \n
  • \n
\n

在此输入图像描述

\n

现在我们进入了 PyCharm 的领域。一些 IDE 将对模块执行“实时”分析,并实际加载模块并检查其成员。(PyDev 有这个选项)。如果 PyCharm 这样做,实现module.__dir__(相同的 PEP)或__all__允许您的代理模块伪装成实际的slowmodule和自动完成将工作。\xe2\x80\xa0 但是,PyCharm 不这样做。

\n

尽管如此,您可以欺骗 PyCharm 为您提供自动完成建议:

\n
if False:\n    import slowmodule\nelse:\n    import slowmodule_proxy as slowmodule\n
Run Code Online (Sandbox Code Playgroud)\n

解释器只会执行else分支,导入代理并命名它slowmodule(这样你的测试代码可以继续引用slowmodule不变)。

\n

但 PyCharm 现在将为底层模块提供自动完成功能:

\n

在此输入图像描述

\n

\xe2\x80\xa0虽然实时分析非常有用,但它也存在静态语法分析所没有的(潜在的)安全问题。类型提示和存根文件的成熟使其不再是一个问题。

\n
\n

slowmodule显式代理

\n

如果您真的讨厌动态代理方法(或者您必须以这种方式欺骗 PyCharm),您可以显式代理该模块。

\n

slowmodule(如果API 稳定,您可能只想考虑这一点。)

\n

如果slowmodule有方法foobar您将创建一个代理模块,例如:

\n
def foo(*args, **kwargs):\n    import slowmodule\n    return slowmodule.foo(*args, **kwargs)\n\ndef bar(*args, **kwargs):\n    import slowmodule\n    return slowmodule.bar(*args, **kwargs)\n
Run Code Online (Sandbox Code Playgroud)\n

(使用argskwargs将参数传递给底层可调用函数。您可以向这些函数添加类型提示以镜像函数slowmodule。)

\n

在你的测试中,

\n
import slowmodule_proxy as slowmodule\n
Run Code Online (Sandbox Code Playgroud)\n

和之前一样。在方法内部导入可以为您提供所需的延迟导入,并且模块缓存负责多个导入调用。

\n

由于它是一个真实的模块,其内容可以进行静态分析,因此无需“欺骗”PyCharm。

\n

if False因此,此解决方案的好处是您的测试导入不会出现奇怪的外观。slowmodule然而,这样做的代价是必须与模块一起维护代理文件,这在API 不稳定的情况下可能会很痛苦。

\n
\n

[3.5+] 使用importlibLazyLoader 代替代理模块

\n

您可以遵循与文档中所示slowmodule_proxy类似的模式,而不是代理模块。importlib

\n
\n
>>> import importlib.util\n>>> import sys\n>>> def lazy_import(name):\n...     spec = importlib.util.find_spec(name)\n...     loader = importlib.util.LazyLoader(spec.loader)\n...     spec.loader = loader\n...     module = importlib.util.module_from_spec(spec)\n...     sys.modules[name] = module\n...     loader.exec_module(module)\n...     return module\n...\n>>> lazy_typing = lazy_import("typing")\n>>> #lazy_typing is a real module object,\n>>> #but it is not loaded in memory yet.\n
Run Code Online (Sandbox Code Playgroud)\n
\n

不过,你仍然需要愚弄 PyCharm,所以类似:

\n
if False:\n    import slowmodule\nelse:\n    slowmodule = lazy_import(\'slowmodule\')\n
Run Code Online (Sandbox Code Playgroud)\n

将是必要的。

\n

除了模块成员访问的单个附加间接级别(以及两个次要版本可用性差异)之外,我并不清楚从这种方法相对于以前的代理模块方法可以获得什么(如果有的话) , 然而。

\n
\n

使用importlibFinder/Loader 机制来挂钩导入(不要这样做)

\n

可以创建一个自定义模块查找器/加载器,它(仅)挂钩您的slowmodule导入,并加载例如您的代理模块。

\n

然后,您可以在测试中导入慢模式之前导入“importhook”模块,例如

\n
import myimporthooks\nimport slowmodule\n\ndef test_foo():\n    ...\n
Run Code Online (Sandbox Code Playgroud)\n

(在这里,myimporthooks将使用importlib\ 的查找器和加载器机制来执行与importhook包类似的操作,但拦截并重定向导入尝试,而不仅仅是充当导入回调。)

\n

但这太疯狂了。 不仅你想要的(看似)可以通过(无限)更常见和受支持的方法来实现,而且它非常脆弱,容易出错,而且,如果不深入 PyTest 的内部(这可能会干扰模块加载器本身),很难说它是否有效。

\n