背景
在 Python 项目开发中,随着代码包数量和复杂度的增加,为了更好地管理多个代码包的命名空间及其依赖,推荐使用 PEP 420 提供的命名空间包功能。通过这种方式,可以构建属于同一发行商(vendor)下的多个独立代码包,且这些包可以分别位于不同的代码仓库中。
在此基础上,某些代码包可能需要进一步支持可选功能模块(例如 optional1
和 optional2
),用户可以根据需要选择安装这些功能模块。本文将分阶段说明如何实现:
- 按 PEP 420 构建统一的目录风格以管理同一 vendor 下的多个代码包。
- 为某个代码包添加支持可选功能的扩展设计及测试策略。
项目目录结构
按 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
与 pyproject.toml
配置
Python 官方(PyPA)推荐使用 pyproject.toml
进行打包,而不再依赖 setup.py
,原因如下:
setup.py
直接执行代码,可能导致不安全的行为(如setup.py install
可能会执行恶意代码)。setup.py
方式已被PEP 517
和PEP 518
取代,现代pip
版本不会直接调用setup.py
。pyproject.toml
提供标准化的构建系统,使得不同构建工具(如setuptools
、poetry
、hatch
)可以互换。
setup.py
方式(旧方法,兼容性考虑)
from setuptools import setup, find_namespace_packages
setup(
name="vendor-package1",
version="0.1.0",
packages=find_namespace_packages(where="src"),
package_dir={"": "src"},
install_requires=["requests", "numpy"],
extras_require={
"optional1": ["matplotlib"],
"optional2": ["pandas"]
},
)
配置说明
extras_require
定义了可选功能及其依赖:optional1
和optional2
分别对应各功能模块的依赖。all
用于安装所有可选功能。
find_namespace_packages
自动发现命名空间包。package_dir
指定源码根目录为src
,避免包根目录污染。
pyproject.toml
方式(推荐方法)
在 vendor-package1-repo/pyproject.toml
中:
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "vendor-package1"
version = "0.1.0"
dependencies = [
"requests",
"numpy"
]
[project.optional-dependencies]
optional1 = ["matplotlib"]
optional2 = ["pandas"]
[tool.setuptools]
packages = ["vendor.package1"]
主包入口设计
在 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()
如果安装了 optional1
或 optional2
,可以按需导入:
from vendor.package1.optional1 import OptionalFeature1
from vendor.package1.optional2 import OptionalFeature2
feature1 = OptionalFeature1()
feature2 = OptionalFeature2()
测试策略
由于每个代码包及其可选功能均位于独立代码仓库中,并独立版本管理,因此测试需要考虑以下需求:
- 统一全量测试:测试主包功能以及所有可选功能。
- 部分功能测试:测试主包功能以及某个具体的可选功能。
- 版本兼容性测试:验证主包与不同版本的依赖包是否兼容。
示例测试代码
全量测试
在全量测试中,需要确保安装了所有可选功能的依赖:
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
文件中指定要包含的文件,确保 src/
目录正确被打包,同时排除无关的缓存文件:
global-exclude *.pyc
global-exclude __pycache__
recursive-exclude * *.pyc
recursive-include src *
此外,为了避免非必要的文件影响最终的包体积,我们不包含 docs/
和 tests/
目录,因为:
docs/
目录通常包含文档和示例,适用于开发阶段,而不是最终的分发包。tests/
目录仅用于测试,不影响最终用户的使用,因此无需包含。
使用 setup.py
进行打包(旧方式,兼容性考虑)
python setup.py sdist bdist_wheel
其中:
sdist
(Source Distribution):生成.tar.gz
源码包,适用于需要构建的环境。bdist_wheel
(Binary Distribution):生成.whl
轮子包,适用于直接安装的环境,提高安装速度。
但官方已不推荐使用此方式,建议改为 pyproject.toml
方式。
使用 pyproject.toml
和 python -m build
进行打包(推荐)
安装 build
工具(如果尚未安装):
pip install build
然后使用以下命令进行打包:
python -m build
这将生成 dist/
目录,包含 .tar.gz
和 .whl
供发布。
dist 目录中生成的分发包
生成的包会出现在 dist/
目录中,例如:
dist/
yourpackage-0.1.0.tar.gz
yourpackage-0.1.0-py3-none-any.whl
.tar.gz
和 .whl
的内容和作用
.tar.gz
(源代码包)- 包含所有源代码,适用于需要从源码构建的情况。
- 安装时,
pip
会自动运行setup.py install
进行编译和安装。
.whl
(轮子包)- 预编译的二进制格式,可以直接安装。
- 安装速度快,适用于发布到 PyPI 供其他用户下载。
egg-info
和 dist-info
目录的作用
egg-info/
目录- 在打包过程中,
setuptools
可能会在源代码目录下生成yourpackage.egg-info/
目录。 - 该目录包含包的元数据,如
PKG-INFO
、SOURCES.txt
和dependency_links.txt
,用于pip
和setuptools
解析包的信息。 - 这个目录通常不会包含在最终的
.whl
和.tar.gz
里,但可能会残留在源代码目录下。
- 在打包过程中,
dist-info/
目录.whl
包通常包含yourpackage-X.X.X.dist-info/
目录。- 该目录包含
.whl
的元数据,如METADATA
、WHEEL
、RECORD
,用于存储包的详细信息、依赖关系和安装记录。 - 这是
wheel
格式包特有的,确保安装时pip
能正确解析包的信息。
上传到 PyPI
- 注册 PyPI 账户
如果尚未注册 PyPI 账户,请访问 https://pypi.org/ 进行注册。 创建
.pypirc
(可选)
方便上传时自动填写用户名和密码:[pypi] username = your-username password = your-password
上传包到 PyPI
twine upload dist/*
成功上传后,用户可以通过
pip install yourpackage
直接安装你的包。
本地测试安装
在上传前,你可以本地测试安装你的包:
pip install dist/yourpackage-0.1.0-py3-none-any.whl
这样可以验证 .whl
是否正确可用。
如果有任何问题,可以检查 twine check dist/*
确保包的格式正确。
用户安装与使用
安装主包
用户只需安装主包时:
pip install vendor-package1
安装特定可选功能
用户需要安装 optional1
或 optional2
时:
pip install vendor-package1[optional1]
pip install vendor-package1[optional2]
安装所有可选功能
一次性安装所有可选功能:
pip install vendor-package1[all]
注意事项
- 清晰的文档说明:在
README.md
中说明可选功能的用途及安装方法,帮助用户快速上手。 - 依赖管理:确保
extras_require
中的依赖版本与功能需求匹配。 - 版本兼容性:明确标注主包和可选模块之间的兼容版本关系,避免因版本不匹配导致问题。
- 向后兼容性:在
__init__.py
中使用try-except
捕获未安装的可选模块,避免因缺少依赖导致错误。
通过上述设计,vendor.package1
及其可选功能模块可以实现灵活的安装与使用,同时保证多个代码包间的独立性和兼容性。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。