使用Python导入钩子模拟子包

时间:2015-04-28 12:16:32

标签: python python-import

我要破解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)

所以问题现在很明确:如何实现导入钩子,以便所有这三个测试用例都可以通过?

2 个答案:

答案 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

我认为这样的方法比使用导入钩子的方法容易得多。