Del*_*gan 8 python import mocking python-3.x python-importlib
假设我有一个这样的模块文件:
# my_module.py
print("hello")
Run Code Online (Sandbox Code Playgroud)
然后我有一个简单的脚本:
# my_script.py
import my_module
Run Code Online (Sandbox Code Playgroud)
这将打印"hello".
假设我想"覆盖"该print()函数,以便它返回"world".我怎么能以编程方式执行此操作(无需手动修改my_module.py)?
我想的是我需要以某种方式修改my_module导入之前或导入时的源代码.显然,我导入后无法执行此操作,因此使用解决方案unittest.mock是不可能的.
我还以为我可以读取文件my_module.py,执行修改,然后加载它.但这很难看,因为如果模块位于其他地方,它将无法工作.
我认为,好的解决方案就是利用importlib.
我阅读了文档,发现了一种非常相互交叉的方法:get_source(fullname).我以为我可以覆盖它:
def get_source(fullname):
source = super().get_source(fullname)
source = source.replace("hello", "world")
return source
Run Code Online (Sandbox Code Playgroud)
不幸的是,我对所有这些抽象类有点迷失,我不知道如何正确执行此操作.
我徒劳地试过:
spec = importlib.util.find_spec("my_module")
spec.loader.get_source = mocked_get_source
module = importlib.util.module_from_spec(spec)
Run Code Online (Sandbox Code Playgroud)
欢迎任何帮助.
Mar*_*gur 13
这是基于这个精彩演讲的内容的解决方案。它允许在导入指定模块之前对源进行任意修改。只要幻灯片没有遗漏任何重要内容,它就应该是合理正确的。这仅适用于 Python 3.5+。
import importlib
import sys
def modify_and_import(module_name, package, modification_func):
spec = importlib.util.find_spec(module_name, package)
source = spec.loader.get_source(module_name)
new_source = modification_func(source)
module = importlib.util.module_from_spec(spec)
codeobj = compile(new_source, module.__spec__.origin, 'exec')
exec(codeobj, module.__dict__)
sys.modules[module_name] = module
return module
Run Code Online (Sandbox Code Playgroud)
所以,使用这个你可以做
my_module = modify_and_import("my_module", None, lambda src: src.replace("hello", "world"))
Run Code Online (Sandbox Code Playgroud)
这不能回答动态修改导入模块的源代码的一般问题,但是可以“覆盖”或“猴子补丁”来使用该print()函数(因为它是 Python 3.x 中的内置函数)。 X)。就是这样:
#!/usr/bin/env python3
# my_script.py
import builtins
_print = builtins.print
def my_print(*args, **kwargs):
_print('In my_print: ', end='')
return _print(*args, **kwargs)
builtins.print = my_print
import my_module # -> In my_print: hello
Run Code Online (Sandbox Code Playgroud)
我首先需要更好地了解import操作。幸运的是,文档importlib中对此进行了很好的解释,并且浏览源代码也有所帮助。
这个import过程实际上分为两部分。首先,查找程序负责解析模块名称(包括点分隔的包)并实例化适当的加载程序。事实上,例如,内置模块不会作为本地模块导入。然后,根据查找器返回的内容调用加载器。该加载器从文件或缓存中获取源代码,如果先前未加载该模块则执行代码。
这很简单。这解释了为什么我实际上不需要使用来自 的抽象类importutil.abc:我不想提供自己的导入过程。相反,我可以创建一个从其中一个类继承importuil.machinery并覆盖的get_source()子类SourceFileLoader。然而,这不是正确的方法,因为加载器是由查找器实例化的,所以我不知道使用哪个类。我无法指定应该使用我的子类。
因此,最好的解决方案是让 finder 完成其工作,然后替换get_source()任何已实例化的 Loader 的方法。
不幸的是,通过查看代码源,我发现基本的加载器没有使用get_source()(仅由模块使用inspect)。所以我的整个想法无法实现。
最后我猜get_source()应该是手动调用,然后修改返回的源码,最后执行代码。这是 Martin Valgur 在他的回答中详细说明的。
如果需要与 Python 2 兼容,我认为除了读取源文件之外没有其他方法:
import imp
import sys
import types
module_name = "my_module"
file, pathname, description = imp.find_module(module_name)
with open(pathname) as f:
source = f.read()
source = source.replace('hello', 'world')
module = types.ModuleType(module_name)
exec(source, module.__dict__)
sys.modules[module_name] = module
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
3758 次 |
| 最近记录: |