Python:更改导入文件类型的优先级(.py 在 .so 之前)

Python: Changing precedence of import file types (.py before .so)

如果我在包含 A.pyA.so 的目录中执行 import A,则会导入 .so 文件。我有兴趣更改导入文件类型的顺序,以便 .py 优先于 .so,尽管只是暂时的,即在代码行 ij 之间。当然这可以通过 some importlib 魔法来实现?

目前我通过将 .py 复制到一个单独的目录来解决这个问题,将此目录添加到 sys.path 然后 进行导入,这太糟糕了。

为什么需要?

.so 文件是 .py 文件的 cython 编译版本。我正在 cython 上做一些自定义代码转换,为此我需要导入 .py 源,即使 "equivalent" .so 存在。

测试设置

下面是一个简单的测试设置。

# A.py
import B
# B.py
import C
print('hello from B')
# C.py
pass

运行 python A.py 成功打印出来自 B.py 的消息。现在添加 B.so(因为 .so 文件的内容无关紧要,B.so 确实是一个文本文件就可以了):

# B.so
this is a fake binary

现在 python A.py 失败了。虽然 importlib 是现代的做事方式,但到目前为止我只知道如何使用已弃用的 imp 模块直接导入特定文件。正在将 A.py 更新为

# A.py
import imp
B = imp.load_source('B', 'B.py')

让它再次工作。但是,引入 C.so 再次破坏它,因为 .py 而不是 .so 的查找未在导入机制中全局注册:

# C.so
this is a fake binary

请注意,在此示例中,我只允许编辑 A.py。我需要 Python 3.8 的解决方案,但我怀疑 3.x 的任何解决方案也适用于 3.8。

我现在有了一个可行的解决方案。它有点hacky,但我认为它很强大。

事实证明,sys.path_importer_cache 存储了各种 finders,后者又存储了 loaderslist,按顺序由 import 开采。这些加载器存储为 2 元组,第一个元素恰好是给定加载器处理的文件扩展名。

我简单地遍历了所有 list 的加载程序,并将扩展名为 .so 的加载程序推到 list 的后面,实现了尽可能低的优先级(我可以删除他们完全,但我无法导入 any .so 文件)。我跟踪对 sys.path_importer_cache 的更改,并在完成特殊导入后撤消它们。所有这些都巧妙地包含在上下文管理器中:

import collections, contextlib, sys

@contextlib.contextmanager
def disable_loader(ext):
    ext = '.' + ext.lstrip('.')
    # Push any loaders for the ext extension to the back
    edits = collections.defaultdict(list)
    path_importer_cache = list(sys.path_importer_cache.values())
    for i, finder in enumerate(path_importer_cache):
        loaders = getattr(finder, '_loaders', None)
        if loaders is None:
            continue
        for j, loader in enumerate(loaders):
            if j + len(edits[i]) == len(loaders):
                break
            if loader[0] != ext:
                continue
            # Loader for the ext extension found.
            # Push to the back.
            loaders.append(loaders.pop(j))
            edits[i].append(j)
    try:
        # Yield control back to the caller
        yield
    finally:
        # Undo changes to path importer cache
        for i, edit in edits.items():
            loaders = path_importer_cache[i]._loaders
            for j in reversed(edit):
                loaders.insert(j, loaders.pop())

# Demonstrate import failure
try:
    import A
except Exception as e:
    print(e)

# Demonstrate solution
with disable_loader('.so'):
    import A

# Demonstrate (wanted) failure outside with statement
import A2

请注意,要使 import A2 正确失败,您需要复制测试设置,以便您也有 A2.pyB2.pyC2.pyB2.soC2.so,它们以与原始测试文件相同的方式相互导入。

只需在进行更改之前进行完整备份 copy.deepcopy(sys.path_importer_cache),并在完成后将此备份粘贴到 sys,就可以摆脱涉及 edits 的复杂簿记。它在上面的有限测试中确实有效,但由于导入机制的各个部分可能包含对不同嵌套对象的引用,我认为仅使用 mutation 更安全。