【问题标题】:Conditionally mapped module import条件映射模块导入
【发布时间】:2018-07-01 20:20:36
【问题描述】:

我目前正在为 Python 2.7 开发一组抽象模块,我将把它作为一个 python 包发布:

 myabstractpkg
   - abstract
      - core
       - logging
       - ...
     - nodes
     - ...

这些模块将在一组完全不同的包中实现:

 myimppkg
   - implementation
     - core
       - logging
       - ...
     - nodes
     - ...

然而,在运行时,我希望能够在使用已实现模块的工具中始终像这样进行导入:

from myabstractpkg.api import nodes
from myabstractpkg.api.core.logging import Logger

这样,开发人员总是从“虚拟”api 模块导入,然后由该模块决定导入器的实际指向位置。

我知道我可以通过修改模块 dict 以某种方式破解它:

from myimppkg import implementation
sys.modules["myabstractpkg.api"] = implementation

或者对myabstractpackage.api__init__.py 中的所有内容进行巧妙的导入,但这对我来说感觉有点脆弱。

我想知道你们是否对最好的方法有一些意见。对于整个重新映射模块的事情,我可能会走上一条非常丑陋的轨道,所以如果你们有任何更智能、更 Python 的解决方案,对于我的 API 抽象、实现、使用方法,我很想听听。

【问题讨论】:

  • 嗨 Matthew,我已经在使用入口点来注册我的实现包。但是,我不知道如何处理这些导入的映射,因此开发人员总是会使用那个 api 包进行导入。在Application A 中,它们应该自动使用Implementation A,而在Application B 中,脚本应该始终使用Implementation B
  • cpython 的decimal module does this。原因不同(如果找不到 c 扩展名,它会依赖 python 实现),但try/except ImportError 方法可能值得考虑。
  • @Tim 您能否将您现有的入口点代码添加到问题中,以便我们了解已经完成的工作?
  • 你为什么要为 Python 2 编写一个新包?

标签: python python-2.7 python-import


【解决方案1】:

我相信最好使用entry_points capabilitiessetuptools 为您服务。因此,在您的具体实现之一的 setup.py 中,您可以像这样定义 entry_points

setup(
    name="concrete_extension"
    entry_points={
        "abstract_pkg_extensions": [
            "concrete = concrete_extension"
        ]
    }
}

然后你可以在你的抽象包中有一个扩展模块,它做这样的事情:

import pkg_resources
import os
import sys

from . import default

extensions = { "default": default }
extensions.update({e.name: e.load() for e in 
pkg_resources.iter_entry_points("my_pkg_extensions")})

current_implementation_name = None
current_implementation = None

def set_implementation(name):
    global current_implementation_name, current_implementation
    try:
        current_implementation = extensions[name]
        current_implementation_name = name

        # allow imports like from foo.current_implementation.bar import baz
        sys.modules["{}.current_implementation".format(__name__)] = current_implementation
        # here is where you would do any additional stuff
        # -- e.g. --
        # from . import logger
        # logger.Logger = current_implementation.Logger
    except KeyError:
        raise NotImplementedError("No implementation for: {}".format(name))

set_implementation(os.environ.get("CURRENT_IMPLEMENTATION_NAME", "default"))

然后您可以使用ext.current_implementation 访问当前实现,并在导入之前在程序环境中设置您的实现,或者您可以在导入任何使用的子模块之前在代码中显式调用set_implementation ext.current_implementation.

有关sys.modules 条目的更多信息及其工作原理,请参阅this question

【讨论】:

  • @Tim 我在这里做了很多假设,如有必要,我很乐意为您提供更清晰的信息,但这与我过去使用的模式相似。
  • 感谢您提供深入的示例!这与我那天晚上尝试的非常接近。这种方法效果很好。然而,虽然这有效import scdcc; scdcc.api.core,但当我尝试在另一个模块的命名空间中导入子模块时,我仍然会遇到 ImportError。 from scdcc.api import core # ImportError: No module named api。关于如何解决这个问题的任何想法?
  • 因为api 是一个变量,你不能像afaik 那样在导入时使用. 表示法遍历它,所以这是这种模式的一个限制......虽然同样,实际代码会有所帮助在这里确定是否可以做任何事情。
  • 我只是通过将入口点方法与此处解释的导入方法相结合使其工作:stackoverflow.com/a/24324577/9288624 我需要添加一个名为 api 的目录,其中包含一个空的 __init__.py,然后我设置相应 dict 条目的值:sys.modules["scdcc.api"] = implementation_module
  • @Tim 已编辑以将其合并。很高兴你成功了。
猜你喜欢
  • 2022-01-13
  • 2021-11-06
  • 2012-11-06
  • 2019-01-23
  • 2014-02-13
  • 2011-04-04
  • 2011-03-30
  • 1970-01-01
相关资源
最近更新 更多