Python:更改导入文件类型的优先级(.py 在 .so 之前)
Python: Changing precedence of import file types (.py before .so)
如果我在包含 A.py
和 A.so
的目录中执行 import A
,则会导入 .so
文件。我有兴趣更改导入文件类型的顺序,以便 .py
优先于 .so
,尽管只是暂时的,即在代码行 i
和 j
之间。当然这可以通过 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,后者又存储了 loaders 的 list
,按顺序由 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.py
、B2.py
、C2.py
、B2.so
和 C2.so
,它们以与原始测试文件相同的方式相互导入。
只需在进行更改之前进行完整备份 copy.deepcopy(sys.path_importer_cache)
,并在完成后将此备份粘贴到 sys
,就可以摆脱涉及 edits
的复杂簿记。它在上面的有限测试中确实有效,但由于导入机制的各个部分可能包含对不同嵌套对象的引用,我认为仅使用 mutation 更安全。
如果我在包含 A.py
和 A.so
的目录中执行 import A
,则会导入 .so
文件。我有兴趣更改导入文件类型的顺序,以便 .py
优先于 .so
,尽管只是暂时的,即在代码行 i
和 j
之间。当然这可以通过 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,后者又存储了 loaders 的 list
,按顺序由 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.py
、B2.py
、C2.py
、B2.so
和 C2.so
,它们以与原始测试文件相同的方式相互导入。
只需在进行更改之前进行完整备份 copy.deepcopy(sys.path_importer_cache)
,并在完成后将此备份粘贴到 sys
,就可以摆脱涉及 edits
的复杂簿记。它在上面的有限测试中确实有效,但由于导入机制的各个部分可能包含对不同嵌套对象的引用,我认为仅使用 mutation 更安全。