我要破解Python导入系统。假设我们有以下目录结构:
.
├── main
│ ├── main.py
│ └── parent
│ └── __init__.py
└── pkg1
├── __init__.py
├── sub
│ ├── __init__.py
│ └── import_global.py
└── success.py
启动脚本为main.py
,因此应该有最顶层的模块parent
。现在,我想模拟一个子包,其全名为parent.intermediate.pkg1
,确实是指pkg1
目录。
实际上不存在intermediate
模块,但是,我确实需要模拟一个(在我的实际项目中,将动态生成此中间模块的名称)。所以我决定使用Python导入钩子。
首先,让我介绍一下pkg1
的内容。
的pkg1 /分/ import_global.py:
from operator import add
Value = add(1, 2)
的pkg1 / success.py:
Value = 'Success'
和(main.py的一部分),我做了一些测试用例:
class MainTestCase(unittest.TestCase):
def test_success(self):
from parent.intermediate.pkg1 import success
self.assertEqual(success.Value, "Success")
def test_import_global(self):
from parent.intermediate.pkg1.sub import import_global
self.assertEqual(import_global.Value, 3)
def test_not_found(self):
def F():
from parent.intermediate.pkg1 import not_found
self.assertRaises(ImportError, F)
unittest.main()
所有__init__.py
都是空的。现在它将实现导入钩子。我已经起草了两个版本,每个版本都有一些问题。
第一个版本:
class PkgLoader(object):
def install(self):
sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self]
def find_module(self, fullname, path=None):
if fullname.startswith('parent.'):
return self
def load_module(self, fullname):
if fullname in sys.modules:
return sys.modules[fullname]
parts = fullname.split('.')[1:]
path = os.path.join(os.path.dirname(__file__), '..')
# intermediate module
m = None
ns = 'parent.intermediate'
if ns in sys.modules:
m = sys.modules[ns]
elif parts[0] == 'intermediate':
m = imp.new_module(ns)
m.__name__ = ns
m.__path__ = [ns]
m.__package__ = '.'.join(ns.rsplit('.', 1)[:-1])
else:
raise ImportError("Module %s not found." % fullname)
# submodules
for p in parts[1:]:
ns = '%s.%s' % (ns, p)
fp, filename, options = imp.find_module(p, [path])
if ns in sys.modules:
m = sys.modules[ns]
else:
m = imp.load_module(ns, fp, filename, options)
sys.modules[ns] = m
path = filename
return m
loader = PkgLoader()
loader.install()
test_import_global
失败的地方:
E..
======================================================================
ERROR: test_import_global (__main__.MainTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main.py", line 54, in test_import_global
from parent.intermediate.pkg1.sub import import_global
File "main.py", line 39, in load_module
m = imp.load_module(ns, fp, filename, options)
File "../pkg1/sub/import_global.py", line 1, in <module>
from operator import add
File "main.py", line 35, in load_module
fp, filename, options = imp.find_module(p, [path])
ImportError: No module named operator
----------------------------------------------------------------------
Ran 3 tests in 0.005s
FAILED (errors=1)
现在,对于第二个版本,我修改了load_module
:
def load_module(self, fullname):
if fullname in sys.modules:
return sys.modules[fullname]
parts = fullname.split('.')[1:]
path = os.path.join(os.path.dirname(__file__), '..')
# intermediate module
m = None
ns = 'parent.intermediate'
if ns in sys.modules:
m = sys.modules[ns]
elif parts[0] == 'intermediate':
m = imp.new_module(ns)
m.__name__ = ns
m.__path__ = [ns]
m.__package__ = '.'.join(ns.rsplit('.', 1)[:-1])
else:
raise ImportError("Module %s not found." % fullname)
# submodules
for p in parts[1:]:
ns = '%s.%s' % (ns, p)
# ======> The modification starts here <======
try:
fp, filename, options = imp.find_module(p, [path])
except ImportError:
return None
# ======> The modification ends here <======
if ns in sys.modules:
m = sys.modules[ns]
else:
m = imp.load_module(ns, fp, filename, options)
sys.modules[ns] = m
path = filename
return m
test_not_found
失败的地方:
.F.
======================================================================
FAIL: test_not_found (__main__.MainTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main.py", line 65, in test_not_found
self.assertRaises(ImportError, F)
AssertionError: ImportError not raised
----------------------------------------------------------------------
Ran 3 tests in 0.004s
FAILED (failures=1)
所以问题现在很明确:如何实现导入钩子,以便所有这三个测试用例都可以通过?
答案 0 :(得分:1)
哦,我已经找到了解决方案,但我的真实项目可能需要更多的测试用例。基本意见是在http_proxy
阶段而不是imp.find_module
阶段执行find_module
,以便我们可以避免系统使用我们的自定义加载程序加载不存在模块。
以下是解决方案:
load_module
随意评论我的解决方案,如果您发现任何潜在的错误,请随时通知我。
答案 1 :(得分:1)
您可以在运行时创建模块,也可以使用sys.modules
字典。
所以,如果你有一个目录结构,如:
project-root/main.py
project-root/sub/
project-root/sub/__init__.py
你当然可以这样做:
import sub # Import child package
sf1 = sub.SubFoo(1) # Test that import worked
但是如果你想“假装”sub
实际上是另一个包中的子包,你可以这样做:
import sys, types
import sub # Import child package
sf1 = sub.SubFoo(1) # Test that import worked
fake = types.ModuleType('fake') # Create empty "fake" module
fake.sub = sub # Add "sub" module to the "fake" module
sys.modules['fake'] = fake # Add "fake" to sys.modules
sf2 = fake.sub.SubFoo(2) # Test that calling works through "fake" module
在我的测试代码中,我sub
的{{1}}仅包含:
__init__.py
如果你运行class SubFoo:
def __init__(self, x=None):
print("Created SubFoo(%s)" % x)
,你会得到:
main.py
我认为这样的方法比使用导入钩子的方法容易得多。