Python: 拦截类加载操作
总结:当我导入某个Python模块时,我想拦截这个动作,而不是加载所需的类,而是加载我选择的另一个类。
原因:我正在处理一些旧代码。在开始进行一些增强或重构之前,我需要写一些单元测试代码。然而,这段代码导入了一个特定的模块,这在单元测试环境中会失败。(因为它依赖于数据库服务器)
伪代码:
from LegacyDataLoader import load_me_data
...
def do_something():
data = load_me_data()
所以,理想情况下,当Python在单元测试中执行上面的导入语句时,会加载一个替代类,比如说MockDataLoader。
我仍在使用2.4.3版本。我想我可以操作一个导入钩子。
编辑
非常感谢到目前为止的回答,它们都很有帮助。
有一种特别的建议是关于操作PYTHONPATH的,但在我的情况下并不奏效。所以我在这里详细说明一下我的具体情况。
原始代码库是这样组织的:
./dir1/myapp/database/LegacyDataLoader.py
./dir1/myapp/database/Other.py
./dir1/myapp/database/__init__.py
./dir1/myapp/__init__.py
我的目标是增强Other模块中的Other类。但由于这是旧代码,我在没有先为它加上测试套件的情况下,不太敢直接动手。
现在我引入这段单元测试代码:
./unit_test/test.py
内容很简单:
from myapp.database.Other import Other
def test1():
o = Other()
o.do_something()
if __name__ == "__main__":
test1()
当CI服务器运行上述测试时,测试失败了。这是因为Other类使用了LegacyDataLoader,而LegacyDataLoader无法从CI服务器建立与数据库服务器的连接。
现在我们按照建议添加一个假类:
./unit_test_fake/myapp/database/LegacyDataLoader.py
./unit_test_fake/myapp/database/__init__.py
./unit_test_fake/myapp/__init__.py
修改PYTHONPATH为:
export PYTHONPATH=unit_test_fake:dir1:unit_test
现在测试因为另一个原因失败了:
File "unit_test/test.py", line 1, in <module>
from myapp.database.Other import Other
ImportError: No module named Other
这与Python在模块中解析类和属性的方式有关。
4 个回答
有更简单的方法来解决这个问题,但我假设你不能修改包含 from LegacyDataLoader import load_me_data
的文件。
最简单的做法可能是创建一个新的文件夹,叫做 testing_shims,然后在里面创建一个 LegacyDataLoader.py 文件。在这个文件里,你可以定义任何你想要的假版本的 load_me_data。运行单元测试的时候,把 testing_shims 这个文件夹放到你的 PYTHONPATH 环境变量的最前面。或者,你也可以修改你的测试运行器,让 testing_shims 成为 sys.path
中的第一个值。
这样一来,当你导入 LegacyDataLoader 时,系统会找到你的文件,而不是去加载真实的代码。
导入语句会从系统模块中查找匹配的名称,如果找到了,就会把相关内容拿过来。所以最简单的方法就是确保在其他任何东西尝试导入真正的模块之前,先把你自己的模块放进系统模块中,并用目标名称命名。
# in test code
import sys
import MockDataLoader
sys.modules['LegacyDataLoader'] = MockDataLoader
import module_under_test
虽然有几种不同的变体,但这个基本的方法应该能很好地完成你在问题中描述的内容。一个稍微简单一点的方法是,使用一个模拟函数来替换你提到的那个函数:
# in test code
import module_under_test
def mock_load_me_data():
# do mock stuff here
module_under_test.load_me_data = mock_load_me_data
这样做就是直接在模块里替换掉相应的名称,所以当你运行测试代码时,假设是你问题中的 do_something()
,它就会调用你自己写的模拟函数。
你可以通过定义自己的 __import__
函数来拦截 import
和 from ... import
语句,然后把它赋值给 __builtin__.__import__
(记得保存之前的值,因为你的新函数可能需要调用它;而且你需要 import __builtin__
来获取内置对象模块)。
举个例子(这是针对 Py2.4 的,因为你问的是这个),在 aim.py 文件中保存以下内容:
import __builtin__
realimp = __builtin__.__import__
def my_import(name, globals={}, locals={}, fromlist=[]):
print 'importing', name, fromlist
return realimp(name, globals, locals, fromlist)
__builtin__.__import__ = my_import
from os import path
现在:
$ python2.4 aim.py
importing os ('path',)
这样你就可以拦截任何你想要的特定导入请求,并在返回之前根据需要修改导入的模块。这就是你所寻找的那种“钩子”,对吧?