背景

在 Python 项目开发中,随着代码包数量和复杂度的增加,为了更好地管理多个代码包的命名空间及其依赖,推荐使用 PEP 420 提供的命名空间包功能。通过这种方式,可以构建属于同一发行商(vendor)下的多个独立代码包,且这些包可以分别位于不同的代码仓库中。

在此基础上,某些代码包可能需要进一步支持可选功能模块(例如 optional1optional2),用户可以根据需要选择安装这些功能模块。本文将分阶段说明如何实现:

  1. 按 PEP 420 构建统一的目录风格以管理同一 vendor 下的多个代码包。
  2. 为某个代码包添加支持可选功能的扩展设计及测试策略。

项目目录结构

按 PEP 420 构建命名空间包

以下是适用于多个代码包的推荐目录结构,每个代码包独立版本管理,且可以分别推送到 PyPI:

vendor-package1-repo/
├── src/
│   └── vendor/
│       └── package1/
│           ├── __init__.py  # 定义主包的接口
│           ├── core.py      # 主包核心功能
├── tests/
│   └── test_core.py         # 测试主包功能
├── setup.py                  # 配置分发信息
├── README.md                 # 项目说明文档
└── pyproject.toml            # 构建工具配置

vendor-package2-repo/
├── src/
│   └── vendor/
│       └── package2/
│           ├── __init__.py  # 定义 package2 的接口
│           ├── feature.py   # package2 核心功能
├── tests/
│   └── test_feature.py      # 测试 package2 功能
├── setup.py                  # 配置分发信息
├── README.md                 # 项目说明文档
└── pyproject.toml            # 构建工具配置

增加支持可选功能的目录结构

对于某些代码包(例如 vendor.package1),可能需要进一步支持可选功能模块。此时可在原目录结构中增加子模块:

vendor-package1-repo/
├── src/
│   └── vendor/
│       └── package1/
│           ├── __init__.py  # 定义主包的接口
│           ├── core.py      # 主包核心功能
│           ├── optional1/
│           │   ├── __init__.py  # optional1 的入口
│           │   └── feature1.py
│           └── optional2/
│               ├── __init__.py  # optional2 的入口
│               └── feature2.py
├── tests/
│   ├── test_core.py          # 测试主包功能
│   ├── optional1/
│   │   └── test_feature1.py  # 测试 optional1
│   └── optional2/
│       └── test_feature2.py  # 测试 optional2
├── setup.py                  # 配置分发信息
├── README.md                 # 项目说明文档
└── pyproject.toml            # 构建工具配置

setup.py 配置

使用 setuptools 配置主包及其可选功能。通过 extras_require 定义可选功能模块及其依赖。

示例配置

from setuptools import setup, find_namespace_packages

setup(
    name="vendor-package1",  # 分发时的包名,符合 PyPI 命名规则
    version="0.1.0",
    description="Vendor Package1 with optional features",
    author="Your Name",
    author_email="your.email@example.com",
    url="https://github.com/yourname/vendor-package1",
    packages=find_namespace_packages(where="src"),  # 自动发现命名空间包
    package_dir={"": "src"},  # 指定 src 目录为包的根目录
    python_requires=">=3.6",
    install_requires=[
        # 主包的必需依赖项
        "some-core-library>=1.0",
    ],
    extras_require={
        "optional1": [
            "optional-library1>=2.0",  # optional1 所需的依赖
        ],
        "optional2": [
            "optional-library2>=3.0",  # optional2 所需的依赖
        ],
        "all": [
            "optional-library1>=2.0",
            "optional-library2>=3.0",
        ],
    },
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
    ],
)

配置说明

  • extras_require 定义了可选功能及其依赖:

    • optional1optional2 分别对应各功能模块的依赖。
    • all 用于安装所有可选功能。
  • find_namespace_packages 自动发现命名空间包。
  • package_dir 指定源码根目录为 src,避免包根目录污染。

主包入口设计

src/vendor/package1/__init__.py 中,通过 Facade 模式提供统一的入口,同时支持按需导入可选模块。

示例代码

# src/vendor/package1/__init__.py

# 导入主包核心功能
from .core import CoreClass

__all__ = ["CoreClass"]

# 尝试导入 optional1 和 optional2,如果未安装,则忽略
try:
    from .optional1 import OptionalFeature1
    __all__.append("OptionalFeature1")
except ImportError:
    pass

try:
    from .optional2 import OptionalFeature2
    __all__.append("OptionalFeature2")
except ImportError:
    pass

使用示例

用户安装主包后,可以直接使用核心功能:

from vendor.package1 import CoreClass

obj = CoreClass()
obj.some_method()

如果安装了 optional1optional2,可以按需导入:

from vendor.package1.optional1 import OptionalFeature1
from vendor.package1.optional2 import OptionalFeature2

feature1 = OptionalFeature1()
feature2 = OptionalFeature2()

测试策略

由于每个代码包及其可选功能均位于独立代码仓库中,并独立版本管理,因此测试需要考虑以下需求:

  1. 统一全量测试:测试主包功能以及所有可选功能。
  2. 部分功能测试:测试主包功能以及某个具体的可选功能。
  3. 版本兼容性测试:验证主包与不同版本的依赖包是否兼容。

示例测试代码

全量测试

在全量测试中,需要确保安装了所有可选功能的依赖:

pip install vendor-package1[all]

示例代码:

# tests/test_all.py
import unittest
from vendor.package1 import CoreClass
from vendor.package1.optional1 import OptionalFeature1
from vendor.package1.optional2 import OptionalFeature2

class TestAllFeatures(unittest.TestCase):
    def test_core(self):
        obj = CoreClass()
        self.assertEqual(obj.some_method(), "expected result")

    def test_optional1(self):
        feature1 = OptionalFeature1()
        self.assertTrue(feature1.some_method())

    def test_optional2(self):
        feature2 = OptionalFeature2()
        self.assertTrue(feature2.some_method())

部分功能测试

只安装需要测试的功能依赖,例如:

pip install vendor-package1[optional1]

示例代码:

# tests/optional1/test_feature1.py
import unittest
from vendor.package1.optional1 import OptionalFeature1

class TestOptionalFeature1(unittest.TestCase):
    def test_feature1(self):
        feature1 = OptionalFeature1()
        self.assertTrue(feature1.some_method())

版本兼容性测试

在测试环境中,明确安装需要测试的依赖版本,例如:

pip install vendor-package2==1.0.0

测试代码应验证功能是否正常运行:

# tests/test_version_compatibility.py
import unittest
from vendor.package1 import CoreClass

class TestVersionCompatibility(unittest.TestCase):
    def test_with_specific_version(self):
        obj = CoreClass()
        self.assertTrue(obj.compatible_with_package2())

构建与分发

添加 MANIFEST.in

MANIFEST.in 文件中指定要包含的文件:

include README.md
include LICENSE
recursive-include src *

构建和上传到 PyPI

使用 build 工具构建并上传:

# 安装构建工具
pip install build twine

# 构建分发包
python -m build

# 上传到 PyPI
twine upload dist/*

用户安装与使用

安装主包

用户只需安装主包时:

pip install vendor-package1

安装特定可选功能

用户需要安装 optional1optional2 时:

pip install vendor-package1[optional1]
pip install vendor-package1[optional2]

安装所有可选功能

一次性安装所有可选功能:

pip install vendor-package1[all]

注意事项

  1. 清晰的文档说明:在 README.md 中说明可选功能的用途及安装方法,帮助用户快速上手。
  2. 依赖管理:确保 extras_require 中的依赖版本与功能需求匹配。
  3. 版本兼容性:明确标注主包和可选模块之间的兼容版本关系,避免因版本不匹配导致问题。
  4. 向后兼容性:在 __init__.py 中使用 try-except 捕获未安装的可选模块,避免因缺少依赖导致错误。

通过上述设计,vendor.package1 及其可选功能模块可以实现灵活的安装与使用,同时保证多个代码包间的独立性和兼容性。


vistart
0 声望0 粉丝

未破壳的雏。