如何检查 Python 包中的任何模块是否从另一个包导入?

How to check if any module in a Python package imports from another package?

我想确保一个包 ("pkg-foo") 中的所有模块都不会从另一个包 ("pkg-block") 导入。

更新:由于Python的动态性,我知道有很多黑魔法导入模块的方法。但是,我只对检查显式导入感兴趣(例如 import pkg.blockfrom pkg.block import ...)。

我想通过 pkg-foo 中的单元测试强制执行此操作,以确保它永远不会从 pkg-block.

导入

我怎样才能做到这一点?我使用 Python 3.8+ 并希望使用内置插件或 setuptools.

当前半生不熟的解决方案

# pkg_resources is from setuptools
from pkg_resources import Distribution, working_set

# Confirm pgk-block is not in pkg-foo's install_requires
foo_pkg: Distribution = working_set.by_key[f"foo-pkg"]
for req in foo_pkg.requires():
    assert "pkg-block" not in str(req)

然而,仅仅因为 pkg-block 没有在 setup.pyinstall_requires 中声明并不意味着它没有被导入包中。所以,这只是一个半生不熟的解决方案。

我的想法是我需要抓取 pkg-foo 中的所有模块并检查每个模块是否从 pgk-block 导入。

所以我的建议是从概念上把这个问题分成两部分。

第一个子问题:确定在pkg-foo中导入的所有模块。让我们使用 mod_foo 作为 pkg-foo

中的任意导入模块

第二个子问题:判断是否有mod_foo来自pkg-block。如果这些模块中有 none 个在 pkg-block 中,则通过单元测试,否则,单元测试失败。

要解决第一个子问题,您可以使用class modulefinder.ModuleFinder。如the example from the documentation所示,可以对pkg-foo中的每个模块做modulefinder.ModuleFinder.run_script(pathname)。然后你可以通过从字典 modulefinder.ModuleFinder.modules 中抓取键来获取模块名称。所有这些模块都将成为您的 mod-foo 个模块。

要解决第二个子问题,可以使用mod_foo.__spec__如前所述here, mod_foo.__spec__ will be an instance of 'importlib.machinery.ModuleSpec' which is defined here。如刚刚链接到的文档中所述,此对象将具有属性 name,即:

A string for the fully-qualified name of the module.

因此我们需要检查 pkg-block 是否在 mod_foo.__spec__.name 为每个 mod_foo 给出的完全限定名称中。

将所有这些放在一起,按照以下代码行应该可以满足您的需要:

import modulefinder

def verify_no_banned_package(pkg_foo_modules, pkg_ban):
    """
    Package Checker
    :param pkg_foo_modules: list of the pathnames of the modules in pkg-foo
    :param pkg_ban: banned package
    :return: True if banned package not present in imports, False otherwise
    """

    imported_modules = set()

    for mod in pkg_foo_modules:
        mod_finder = modulefinder.ModuleFinder()
        mod_finder.run_script(mod)
        mod_foo_import_mods = mod_finder.modules.keys()
        imported_modules.update(mod_foo_import_mods)

    for mod_foo in imported_modules:
        mod_foo_parent_full_name = mod_foo.__spec__.name
        if pkg_ban in mod_foo_parent_full_name.split(sep="."):
            return False
    return True

我认为在你的情况下最简单的方法是模拟一个不应导入的模块,即 pkg-block。为了模拟一个模块把它放在 sys.path 的开头,最简单的方法可能是在项目文件夹中创建一个假模块(或临时替换原来的模块),所以它最终会在你的 PYTHONPATH 马上。这会将所有导入“重定向”到您的模拟模块。在 mock 模块本身中放置如下内容:

print(f'Module {__name__} imported')


def __getattr__(name):
    print(f'Imported attribute {name} from module {__name__}')

上面的代码将打印一行,通知您模块或其属性已导入,这适用于直接模块导入和 from 导入,函数内部导入,importlib 导入(除了通过路径导入,但我觉得这太深奥了)。为了简化单元测试中导入的检测,您可能希望引发异常而不仅仅是打印。您也不必进行特定测试来验证导入是否未发生,而只需 运行 您的常规测试套件 pkg-foo 并查看是否有任何测试因特定于导入的异常而失败。

示例:

假设文件结构如下:

|---main.py
|
\---pkg
    |---block.py
    |---foo.py
    |---__init__.py

其中 pkg/block.py

print(f'Module {__name__} imported')


def __getattr__(name):
    print(f'Imported attribute {name} from module {__name__}')

pkg/foo.py

import importlib

import pkg.block
from . import block
from .block import abc

importlib.import_module('pkg.block')


def from_function():
    from pkg.block import from_function

main.py

import pkg.foo

if __name__ == '__main__':
    pkg.foo.from_function()

然后在执行 main.py 之后你会得到:

Module pkg.block imported
Imported attribute __path__ from module pkg.block
Imported attribute abc from module pkg.block
Imported attribute abc from module pkg.block
Imported attribute __path__ from module pkg.block
Imported attribute from_function from module pkg.block
Imported attribute from_function from module pkg.block