打包命名空间包

命名空间包允许您将单个中的子包和模块分散到多个独立的分发包中(在本文档中为避免歧义,统称为分发)。例如,如果您有以下包结构:

mynamespace/
    __init__.py
    subpackage_a/
        __init__.py
        ...
    subpackage_b/
        __init__.py
        ...
    module_b.py
pyproject.toml

并且您在代码中像这样使用此包:

from mynamespace import subpackage_a
from mynamespace import subpackage_b

那么您可以将这些子包拆分为两个独立的分发:

mynamespace-subpackage-a/
    pyproject.toml
    src/
        mynamespace/
            subpackage_a/
                __init__.py

mynamespace-subpackage-b/
    pyproject.toml
    src/
        mynamespace/
            subpackage_b/
                __init__.py
            module_b.py

每个子包现在都可以单独安装、使用和版本化。

命名空间包对于大量松散相关的包(例如来自单个公司的大量客户端库)可能很有用。但是,命名空间包有几个注意事项,并非在所有情况下都适用。一个简单的替代方法是在所有分发上使用前缀,例如import mynamespace_subpackage_a(您甚至可以使用import mynamespace_subpackage_a as subpackage_a来保持导入对象简短)。

创建命名空间包

目前有两种不同的创建命名空间包的方法,其中后一种不推荐使用:

  1. 使用原生命名空间包。这种类型的命名空间包在PEP 420中定义,并可在 Python 3.3 及更高版本中使用。如果您的命名空间中的包只需要支持 Python 3 并通过pip安装,则推荐使用此方法。

  2. 使用传统命名空间包。这包括pkgutil 风格的命名空间包pkg_resources 风格的命名空间包

原生命名空间包

Python 3.3 添加了来自PEP 420隐式命名空间包。创建原生命名空间包所需的一切只是从命名空间包目录中省略__init__.py。一个示例文件结构(遵循src-layout):

mynamespace-subpackage-a/
    pyproject.toml # AND/OR setup.py, setup.cfg
    src/
        mynamespace/ # namespace package
            # No __init__.py here.
            subpackage_a/
                # Regular import packages have an __init__.py.
                __init__.py
                module.py

非常重要的是,每个使用命名空间包的分发都必须省略__init__.py或使用 pkgutil 风格的__init__.py。如果任何分发不这样做,它将导致命名空间逻辑失败,并且其他子包将无法导入。

src-layout目录结构允许大多数构建后端自动发现包。有关更多信息,请参阅src 布局与平面布局。但是,如果您想自己管理包的排除或包含,可以在顶级pyproject.toml中进行配置:

[build-system]
...

[tool.setuptools.packages.find]
where = ["src/"]
include = ["mynamespace.subpackage_a"]

[project]
name = "mynamespace-subpackage-a"
...

使用setup.cfg也可以实现同样的目的:

[options]
package_dir =
    =src
packages = find_namespace:

[options.packages.find]
where = src

setup.py

from setuptools import setup, find_namespace_packages

setup(
    name='mynamespace-subpackage-a',
    ...
    packages=find_namespace_packages(where='src/', include=['mynamespace.subpackage_a']),
    package_dir={'': 'src'},
)

Setuptools默认会搜索目录结构以查找隐式命名空间包。

原生命名空间包示例项目中可以找到两个原生命名空间包的完整工作示例。

注意

由于原生和 pkgutil 风格的命名空间包在很大程度上兼容,因此您可以在仅支持 Python 3 的分发中使用原生命名空间包,在需要支持 Python 2 和 3 的分发中使用 pkgutil 风格的命名空间包。

传统命名空间包

PEP 420之前用于创建命名空间包的这两种方法现在被认为是过时的,不应使用,除非您需要与已使用此方法的包兼容。此外,pkg_resources已被弃用。

要迁移现有包,所有共享命名空间的包必须同时迁移。

警告

虽然原生命名空间包和 pkgutil 风格的命名空间包在很大程度上兼容,但 pkg_resources 风格的命名空间包与其他方法不兼容。不建议在提供包到同一命名空间的不同分发中使用不同的方法。

pkgutil 风格的命名空间包

Python 2.3 引入了pkgutil模块和pkgutil.extend_path()函数。这可用于声明需要与 Python 2.3+ 和 Python 3 兼容的命名空间包。这是实现最高兼容性级别的推荐方法。

要创建 pkgutil 风格的命名空间包,您需要为命名空间包提供一个__init__.py文件:

mynamespace-subpackage-a/
    src/
        pyproject.toml # AND/OR setup.cfg, setup.py
        mynamespace/
            __init__.py  # Namespace package __init__.py
            subpackage_a/
                __init__.py  # Regular package __init__.py
                module.py

命名空间包的__init__.py文件需要包含以下内容:

__path__ = __import__('pkgutil').extend_path(__path__, __name__)

使用命名空间包的每个分发都必须包含这样的__init__.py。如果任何分发不这样做,它将导致命名空间逻辑失败,并且其他子包将无法导入。 __init__.py中的任何额外代码都将无法访问。

pkgutil 命名空间示例项目中可以找到两个 pkgutil 风格的命名空间包的完整工作示例。

pkg_resources 风格的命名空间包

Setuptools提供了pkg_resources.declare_namespace函数和setup()namespace_packages参数。它们可以一起用于声明命名空间包。虽然这种方法不再推荐使用,但它在大多数现有命名空间包中广泛存在。如果您正在现有命名空间包中创建一个使用此方法的新分发,那么建议继续使用此方法,因为不同的方法不兼容,并且不建议尝试迁移现有包。

要创建 pkg_resources 风格的命名空间包,您需要为命名空间包提供一个__init__.py文件:

mynamespace-subpackage-a/
    src/
        pyproject.toml # AND/OR setup.cfg, setup.py
        mynamespace/
            __init__.py  # Namespace package __init__.py
            subpackage_a/
                __init__.py  # Regular package __init__.py
                module.py

命名空间包的__init__.py文件需要包含以下内容:

__import__('pkg_resources').declare_namespace(__name__)

使用命名空间包的每个分发都必须包含这样的__init__.py。如果任何分发不这样做,它将导致命名空间逻辑失败,并且其他子包将无法导入。 __init__.py中的任何额外代码都将无法访问。

注意

一些旧的建议建议在命名空间包的__init__.py中包含以下内容:

try:
    __import__('pkg_resources').declare_namespace(__name__)
except ImportError:
    __path__ = __import__('pkgutil').extend_path(__path__, __name__)

这样做的想法是,在 setuptools 不可用的极少数情况下,包会回退到 pkgutil 风格的包。这是不可取的,因为 pkgutil 和 pkg_resources 风格的命名空间包不兼容。如果 setuptools 的存在是一个问题,那么包应该通过install_requires明确依赖 setuptools。

最后,每个分发都必须在setup.py中为setup()提供namespace_packages参数。例如:

from setuptools import find_packages, setup

setup(
    name='mynamespace-subpackage-a',
    ...
    packages=find_packages()
    namespace_packages=['mynamespace']
)

pkg_resources 命名空间示例项目中可以找到两个 pkg_resources 风格的命名空间包的完整工作示例。