项目结构与pyproject.toml
现代Python项目规范与打包配置
一、概述
Python项目结构规范与打包配置是现代Python开发的基础技能。随着Python语言生态的演进,项目配置方式经历了从 setup.py 到 setup.cfg 再到 pyproject.toml 的演进过程。理解这些配置方式及其背后的PEP标准,对于构建可维护、可分发、符合社区最佳实践的Python项目至关重要。
本文将从Python打包标准的演进历史出发,系统讲解现代Python项目的目录布局方案、pyproject.toml 的完整字段含义与配置方法、setuptools集成方式,以及依赖分组管理、版本管理策略等核心内容。无论你是Python初学者希望规范自己的项目,还是有经验的开发者需要迁移到新的打包标准,本文都能提供全面的参考。
学习目标:
- 理解Python打包标准的演进历程(PEP 518、517、621、660)
- 掌握src布局与扁平布局的优劣与选择依据
- 精通 pyproject.toml 所有核心字段的配置
- 学会依赖分组管理、版本控制与项目元数据配置
- 能够在实际项目中运用现代Python项目结构规范
二、项目配置标准演进
Python项目打包配置标准的演进是一段从"约定俗成"到"标准化"的历程。理解这段历史有助于我们理解为什么 pyproject.toml 成为当前推荐的标准。
2.1 演进时间线
PEP 518 (2016)
引入 pyproject.toml 文件格式,用于指定构建系统依赖。这是Python项目第一次拥有标准化的构建元数据文件格式,使用TOML作为配置语言。从此,构建工具不再依赖 setup.py 的隐式安装。
PEP 517 (2017)
定义构建系统的标准接口(build-backend)。允许使用非setuptools的构建后端,如flit、poetry、hatchling等。构建过程通过钩子函数标准化,实现了构建工具与构建后端的解耦。
PEP 621 (2020)
将项目元数据(项目名称、版本、作者、依赖等)标准化地放入 pyproject.toml 的 [project] 表中。结束了元数据分散在 setup.py 或 setup.cfg 中的局面,使项目元数据与构建工具无关。
PEP 660 (2021)
定义可编辑安装(editable install)的标准协议,替代 pip install -e . 的旧有实现方式,使其适用于所有PEP 517兼容的构建后端。
2.2 各PEP的核心意义
PEP 518 - 构建系统依赖声明
在 pyproject.toml 的 [build-system] 表中声明构建项目所需的工具和版本。例如,声明需要 setuptools 和 wheel,pip会在构建项目时自动安装这些依赖。这解决了"先有鸡还是先有蛋"的问题——不再需要手动安装构建工具。
PEP 517 - 构建后端接口
定义了构建后端的标准API,包括 build_wheel、build_sdist、get_requires_for_build_wheel 等钩子。任何实现了这些钩子的工具都可以作为构建后端,如 setuptools.build_meta、hatchling、flit_core、pdm.backend、poetry.core.masonry.api 等。
PEP 621 - 项目元数据标准化
将 name、version、description、authors、license、dependencies、optional-dependencies、scripts、entry-points 等元数据统一放在 [project] 表中,使元数据声明与构建工具解耦,实现打包配置的标准化。
PEP 660 - 可编辑安装标准化
规范了 pip install -e . 的行为,使所有PEP 517兼容的构建后端都能正确支持可编辑安装。之前只有 setuptools 能可靠支持此功能。
三、项目布局方案
Python项目有两种主流的目录布局方案:扁平布局(Flat Layout) 和 src布局(src Layout)。选择合适的布局方案是项目组织的第一步。
3.1 扁平布局(Flat Layout)
将Python包目录直接放在项目根目录下,与 pyproject.toml、README.md 等文件平级。
my-project/
├── pyproject.toml # 项目配置
├── README.md # 项目说明
├── LICENSE # 许可证
├── my_package/ # 源码目录(与项目配置平级)
│ ├── __init__.py
│ ├── module_a.py
│ ├── module_b.py
│ └── sub_package/
│ ├── __init__.py
│ └── module_c.py
├── tests/ # 测试目录
│ ├── __init__.py
│ ├── test_module_a.py
│ └── test_module_b.py
└── docs/ # 文档目录
└── index.md
扁平布局的优势
- 简洁直观: 目录结构扁平,易于理解,特别适合小型项目
- 导入方便: 开发环境中可直接 import my_package,无需额外配置
- 历史兼容: 早期Python项目多采用此结构,社区适应度高
扁平布局的劣势
- 导入混淆风险: 运行测试时,当前目录可能被添加到 sys.path,导致导入的是本地源码而非安装的包,可能隐藏打包错误
- 命名冲突: 项目目录中的其他文件可能与包名冲突
- 测试隔离性差: 测试可能无意中测试了未安装的本地代码
3.2 src布局(src Layout)
将Python包目录放在项目根目录下的 src/ 子目录中,这是当前社区推荐的标准布局方案。
my-project/
├── pyproject.toml # 项目配置
├── README.md # 项目说明
├── LICENSE # 许可证
├── src/ # 源码根目录
│ └── my_package/ # 实际包目录(在src下)
│ ├── __init__.py
│ ├── module_a.py
│ ├── module_b.py
│ └── sub_package/
│ ├── __init__.py
│ └── module_c.py
├── tests/ # 测试目录
│ ├── __init__.py
│ ├── test_module_a.py
│ └── test_module_b.py
├── docs/
│ └── index.md
└── examples/
└── basic_usage.py
src布局的优势
- 强制测试隔离: 运行测试时必须先安装包(pip install -e .),确保测试的是打包后的版本而非本地源码,能提前发现打包配置错误
- 清晰的项目边界: 明确区分了源码(src/)与项目元文件(配置文件、文档等)
- 避免导入意外: 不会因为当前工作目录而意外导入未安装的本地模块
- 社区推荐: 被Python打包用户指南(Python Packaging User Guide)推荐为最佳实践
src布局的劣势
- 结构稍复杂: 多了一层 src/ 目录,对新手不够直观
- 需要额外配置: 需要确保打包配置正确指向 src/ 下的包
- 调试不便: 开发调试时需要使用可编辑安装
布局选择建议
- 库/框架项目: 强烈推荐 src 布局,确保分发包的正确性
- 应用项目: 可根据团队习惯选择,但src布局更利于长期维护
- 快速原型/教学示例: 扁平布局更简洁直接
- 企业级项目: 必须使用 src 布局,配合严格的测试和CI流程
四、pyproject.toml 完整字段解释
pyproject.toml 是Python项目的核心配置文件,采用TOML格式。一个完整的 pyproject.toml 包含多个顶级表(table),每个表负责不同方面的配置。
4.1 [build-system] - 构建系统配置
声明构建项目所需的工具和后端。这是 pyproject.toml 中最基础的配置,pip在构建项目时会读取此节。
[build-system]
requires = ["setuptools>=69.0.0", "wheel"]
build-backend = "setuptools.build_meta"
# 使用 Hatchling 作为构建后端
[build-system]
requires = ["hatchling>=1.18.0"]
build-backend = "hatchling.build"
# 使用 Flit 作为构建后端
[build-system]
requires = ["flit_core>=3.9.0"]
build-backend = "flit_core.buildapi"
# 使用 PDM 后端
[build-system]
requires = ["pdm-backend>=2.1.0"]
build-backend = "pdm.backend"
字段说明:
- requires:构建项目所需的最小依赖列表,pip会在构建前安装这些包。必须包含构建后端包本身
- build-backend:构建后端的Python导入路径。pip调用此对象的方法执行实际构建
- backend-path(可选):指定查找构建后端模块的额外路径,用于本地开发中的构建后端
4.2 [project] - 项目元数据(PEP 621)
这是PEP 621标准化的核心配置表,包含了项目的所有元数据,与构建后端无关。
完整示例
[project]
name = "my-awesome-package"
version = "2025.4.0"
description = "一个用于演示现代Python项目结构的示例包"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "yourname@example.com"},
{name = "Contributor Name", email = "contributor@example.com"},
]
maintainers = [
{name = "Maintainer Name", email = "maintainer@example.com"},
]
keywords = ["python", "project-structure", "pyproject-toml", "tutorial"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: Chinese (Simplified)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries",
]
dependencies = [
"requests>=2.31.0",
"click>=8.1.0",
"rich>=13.0.0",
"pydantic>=2.0.0",
]
[project.urls]
homepage = "https://github.com/yourname/my-awesome-package"
repository = "https://github.com/yourname/my-awesome-package"
documentation = "https://my-awesome-package.readthedocs.io"
changelog = "https://github.com/yourname/my-awesome-package/blob/main/CHANGELOG.md"
[project.scripts]
my-cli = "my_package.cli:main"
run-server = "my_package.server:run"
[project.gui-scripts]
my-gui = "my_package.gui:launch"
[project.entry-points."my_plugin"]
my-plugin = "my_package.plugins:register"
[project.entry-points."console_scripts"]
custom-cmd = "my_package.commands:execute"
4.3 核心字段详解
name(必需)
项目在PyPI上的注册名称。必须符合PEP 508规范:只能包含字母、数字、-、_ 和 .,且不能以 - 或 . 开头。PyPI会自动将 _ 转换为 -,所以 my_package 和 my-package 被认为是同一个包。
version(推荐动态管理)
项目的版本号。可以使用静态字符串(如 "2025.4.0"),也可以使用动态方式。建议遵循语义化版本(SemVer,如 1.2.3)或日历化版本(CalVer,如 2025.4.0)。
requires-python(推荐)
声明项目支持的Python版本范围。使用PEP 440版本比较语法。pip在安装时会检查当前Python版本是否满足此要求,若不满足则报错。
常见写法示例:
- ">=3.10" - 要求Python 3.10及以上
- ">=3.10, <3.14" - 要求3.10到3.13
- ">=3.9, !=3.10.*" - 要求3.9以上但排除3.10系列
dependencies(运行时依赖)
项目运行时所需的第三方包列表。每个依赖项是一个PEP 508格式的字符串,可包含版本约束。pip 在安装项目时会自动解析并安装这些依赖。版本约束的常用操作符:
- >=1.0 - 大于等于1.0
- ~=1.0 - 兼容版本(等价于 >=1.0, ==1.*)
- !=1.5 - 排除特定版本
- * - 任意版本(不推荐,应明确约束范围)
optional-dependencies(可选依赖组)
按用途分组定义的可选依赖,用户通过 pip install mypackage[extra_name] 安装。这是管理不同场景依赖的标准做法。
scripts(控制台入口点)
定义安装后可在命令行直接执行的脚本命令。格式为 命令名 = "模块:函数"。pip会在安装时生成对应的可执行文件(Windows下为 .exe)。
entry-points(插件入口点)
定义项目的插件系统接入点。其他包可以通过注册同名入口点来扩展功能。按分组(group)组织,例如 [project.entry-points."console_scripts"] 是 [project.scripts] 的底层等价形式,[project.entry-points."my_plugin"] 则是自定义插件分组。
4.4 可选依赖组配置
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-cov>=5.0.0",
"pytest-xdist>=3.5.0",
"pytest-mock>=3.12.0",
"ipython>=8.15.0",
"ipdb>=0.13.0",
]
lint = [
"ruff>=0.3.0",
"mypy>=1.8.0",
"pre-commit>=3.6.0",
]
doc = [
"mkdocs>=1.5.0",
"mkdocs-material>=9.5.0",
"mkdocstrings[python]>=0.24.0",
]
test = [
"pytest>=8.0.0",
"pytest-cov>=5.0.0",
"coverage>=7.4.0",
]
all = [
"my-awesome-package[dev]",
"my-awesome-package[lint]",
"my-awesome-package[doc]",
"my-awesome-package[test]",
]
依赖组之间可以相互引用,如上面的 all 组组合了所有其他组。用户可以通过以下方式安装特定组:
# 安装开发依赖
pip install my-awesome-package[dev]
# 同时安装多个组
pip install my-awesome-package[dev,test,doc]
# 安装所有依赖
pip install my-awesome-package[all]
4.5 [tool] 表 - 工具配置
不同工具的配置放在 [tool.工具名] 下,实现了配置的统一管理,无需为每个工具单独创建配置文件。
# setuptools 特定配置
[tool.setuptools]
package-dir = {"" = "src"}
packages = ["my_package", "my_package.sub_package"]
[tool.setuptools.package-data]
my_package = ["py.typed", "*.dat"]
# pytest 配置
[tool.pytest.ini_options]
minversion = "8.0"
addopts = "-ra -q --strict-markers"
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
markers = [
"slow: 需要较长时间运行的测试",
"network: 需要网络连接的测试",
"integration: 集成测试",
]
# ruff 配置
[tool.ruff]
target-version = "py310"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
[tool.ruff.format]
quote-style = "double"
# mypy 配置
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = false
[tool.mypy.overrides]
module = "tests/*"
disallow_untyped_defs = false
# coverage 配置
[tool.coverage.run]
source = ["my_package"]
branch = true
parallel = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.:",
"raise AssertionError",
"raise NotImplementedError",
]
五、setuptools 配置
尽管 pyproject.toml 已经能够承载大部分配置,但某些setuptools特有的配置(如命名空间包、C扩展、数据文件、自定义构建命令等)仍然需要 setup.cfg 或 setup.py 来处理。
5.1 setup.cfg 配置
setup.cfg 是INI格式的配置文件,在PEP 621之前是setuptools的主要配置方式。引入 pyproject.toml 后,元数据部分已迁移到 [project],但setuptools特有的选项仍需在 setup.cfg 或 [tool.setuptools] 中配置。
# setup.cfg - PEP 621 迁移前的传统配置方式
[metadata]
name = my-awesome-package
version = 2025.4.0
description = 一个用于演示的示例包
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/yourname/my-awesome-package
author = Your Name
author_email = yourname@example.com
license = MIT
license_files = LICENSE
classifiers =
Development Status :: 4 - Beta
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Programming Language :: Python :: 3
Programming Language :: Python :: 3.10
[options]
packages = find:
package_dir =
= src
python_requires = >=3.10
install_requires =
requests>=2.31.0
click>=8.1.0
rich>=13.0.0
[options.packages.find]
where = src
exclude =
tests*
docs*
[options.extras_require]
dev =
pytest>=8.0.0
pytest-cov>=5.0.0
lint =
ruff>=0.3.0
mypy>=1.8.0
[options.entry_points]
console_scripts =
my-cli = my_package.cli:main
run-server = my_package.server:run
setup.cfg 与 pyproject.toml 的关系
- PEP 621 后,元数据优先放在 pyproject.toml 的 [project] 中
- setuptools特有配置(如 packages、package-dir)放在 pyproject.toml 的 [tool.setuptools] 中
- 如果使用 pyproject.toml 的 [project],setup.cfg 中不应再包含 [metadata] 和 [options] 中的对应字段,否则会冲突
- 复杂的自定义构建逻辑(C扩展、自定义命令)仍需 setup.py
5.2 setup.py - 最小化方案
在现代Python项目中,setup.py 已不再是必需的配置文件。如果使用了 pyproject.toml 声明构建系统,setup.py 可以完全省略。只有在需要执行复杂构建逻辑(如Cython扩展、自定义构建命令、编译C/C++扩展)时才需要保留。
# setup.py - 最小化版本(仅在需要复杂构建逻辑时保留)
from setuptools import setup
# 如果 pyproject.toml 已包含所有元数据,此文件可完全删除
# 以下是一些需要 setup.py 的场景:
# 1. C 扩展
from setuptools import Extension
ext_modules = [
Extension(
"my_package._core",
sources=["src/my_package/_core.c"],
include_dirs=["include/"],
),
]
# 2. 自定义构建命令
from setuptools import Command
class CustomBuildCommand(Command):
description = "运行自定义构建步骤"
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
# 执行自定义构建逻辑
self.announce("执行自定义构建步骤...", level=3)
# 例如:生成代码、编译资源等
setup(
ext_modules=ext_modules,
cmdclass={
"custom_build": CustomBuildCommand,
},
)
"在现代Python项目中,setup.py 应被视为"最后的手段"。优先使用 pyproject.toml 声明所有元数据,仅当构建过程需要图灵完备的编程能力时,才引入 setup.py。"
六、三种配置方式对比
为了帮助理清三种配置方式的关系和适用场景,下表从多个维度进行了系统对比。
| 对比维度 |
pyproject.toml |
setup.cfg |
setup.py |
| 文件格式 |
TOML |
INI |
Python |
| 引入时间 |
PEP 518 (2016) |
setuptools 30.3.0 (2016) |
distutils (2000) |
| 声明式/命令式 |
完全声明式 |
完全声明式 |
命令式(可编程) |
| 构建系统声明 |
支持 |
不支持 |
不支持 |
| 项目元数据 |
支持(PEP 621标准) |
支持(非标准) |
支持 |
| 依赖声明 |
支持 |
支持 |
支持 |
| 可选依赖组 |
支持 |
支持 |
支持 |
| 入口点/脚本 |
支持 |
支持 |
支持 |
| C扩展构建 |
不支持 |
不支持 |
支持 |
| 自定义构建命令 |
不支持 |
不支持 |
支持 |
| 工具配置统一 |
支持([tool.*]) |
不支持 |
不支持 |
| 构建后端无关性 |
支持 |
仅限setuptools |
仅限setuptools |
| 可编辑安装 |
支持(PEP 660) |
支持 |
支持 |
| 推荐使用场景 |
所有新项目(首选) |
渐进迁移(过渡方案) |
仅在需要复杂构建逻辑时 |
6.1 推荐的最佳实践
现代Python项目配置建议
- 优先使用 pyproject.toml:将项目元数据、依赖和工具配置统一放在 pyproject.toml 中
- 利用 [tool.*] 统一管理工具配置:如 [tool.pytest]、[tool.ruff]、[tool.mypy],减少根目录下的配置文件数量
- 删除不必要的配置文件:如果 pyproject.toml 已覆盖所有配置,删除 setup.cfg、setup.py、pytest.ini、.mypy.ini、.ruff.toml 等冗余文件
- 仅在必要时保留 setup.py:当你需要C扩展、自定义构建命令、编译优化等复杂操作时才保留
- 使用 src 布局:配合 [tool.setuptools.package-dir] 配置,确保打包正确
6.2 完整的最小现代配置
# pyproject.toml - 现代Python项目的完整配置示例
[build-system]
requires = ["setuptools>=69.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-package"
version = "2025.4.0"
description = "一个现代Python项目配置示例"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [{name = "Your Name", email = "yourname@example.com"}]
keywords = ["python", "tutorial"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"requests>=2.31.0",
"rich>=13.0.0",
]
[project.optional-dependencies]
dev = ["pytest>=8.0.0", "ipython>=8.15.0"]
lint = ["ruff>=0.3.0", "mypy>=1.8.0"]
[project.urls]
homepage = "https://github.com/yourname/my-awesome-package"
[project.scripts]
my-cli = "my_package.cli:main"
[tool.setuptools]
package-dir = {"" = "src"}
packages = {find = {where = ["src"]}}
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
[tool.ruff]
target-version = "py310"
line-length = 100
[tool.mypy]
python_version = "3.10"
strict = true
[tool.coverage.run]
source = ["my_package"]
七、项目版本管理
版本管理是项目配置中的关键环节。Python社区主要采用两种版本方案:语义化版本(SemVer) 和 日历化版本(CalVer)。
7.1 语义化版本(SemVer)
格式:主版本号.次版本号.修订号(如 2.5.1)
- 主版本号(MAJOR): 不兼容的API修改,如 1.0.0 到 2.0.0
- 次版本号(MINOR): 向下兼容的功能新增,如 1.0.0 到 1.1.0
- 修订号(PATCH): 向下兼容的问题修复,如 1.0.0 到 1.0.1
7.2 日历化版本(CalVer)
格式:YYYY.MM.MICRO(如 2025.4.1)
- YYYY: 发布的年份
- MM: 发布的月份
- MICRO: 当月发布序号(从0开始)
采用日历化版本的项目包括:pytest(如 8.3.0)、requests、black 等。日历化版本的优势在于版本号直接反映发布时间,用户可以通过版本号了解包的时效性。
7.3 动态版本管理策略
在 pyproject.toml 中,版本可以动态设置,避免手动更新多个文件。
# pyproject.toml 中使用动态版本
[project]
# 不设置静态 version,而是声明动态字段
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}
# my_package/__init__.py - 在源码中维护版本
__version__ = "2025.4.0"
__version_info__ = (2025, 4, 0)
# 也可以从文件读取版本
[tool.setuptools.dynamic]
version = {file = "VERSION.txt"}
# VERSION.txt
2025.4.0
版本管理最佳实践
- 单一真实来源(Single Source of Truth): 版本号只在 __init__.py 或 VERSION.txt 中维护一次,通过动态版本机制同步到打包配置
- 使用Git标签关联版本: 每次发布时创建对应版本的Git标签(如 v2025.4.0)
- 遵循PEP 440版本格式: 预发布版本使用 a(alpha)、b(beta)、rc(release candidate)后缀,如 2025.5.0rc1
- 实现 __version__ 属性: 允许用户通过 import my_package; my_package.__version__ 获取版本
八、依赖分组管理
合理组织依赖分组是现代Python项目的重要实践。通过 optional-dependencies,可以为不同使用场景(开发、测试、文档、代码检查等)分别声明依赖,避免将所有依赖混在一起。
8.1 依赖分组策略
[project.optional-dependencies]
# 核心开发工具
dev = [
"ipython>=8.15.0",
"ipdb>=0.13.0",
"watchdog>=4.0.0",
"rich>=13.0.0",
]
# 测试相关
test = [
"pytest>=8.3.0",
"pytest-cov>=5.0.0",
"pytest-xdist>=3.5.0",
"pytest-mock>=3.12.0",
"pytest-asyncio>=0.23.0",
"factory-boy>=3.3.0",
"coverage>=7.4.0",
]
# 代码质量检查
lint = [
"ruff>=0.3.0",
"mypy>=1.8.0",
"pre-commit>=3.6.0",
"bandit>=1.7.0",
"safety>=3.0.0",
]
# 文档构建
doc = [
"mkdocs>=1.5.0",
"mkdocs-material>=9.5.0",
"mkdocstrings[python]>=0.24.0",
"mkdocs-glightbox>=0.3.0",
]
# 类型检查存根
types = [
"types-requests>=2.31.0",
"types-pyyaml>=6.0.0",
]
# CI/CD 依赖
ci = [
"tox>=4.15.0",
"nox>=2024.0.0",
"build>=1.2.0",
"twine>=5.0.0",
]
# 性能分析
perf = [
"pytest-benchmark>=4.0.0",
"py-spy>=0.3.0",
"cProfile",
]
# 安全审计
security = [
"bandit>=1.7.0",
"safety>=3.0.0",
"pip-audit>=2.7.0",
]
# 一站式安装
all = [
"my-awesome-package[dev]",
"my-awesome-package[test]",
"my-awesome-package[lint]",
"my-awesome-package[doc]",
"my-awesome-package[types]",
"my-awesome-package[ci]",
]
8.2 分组使用场景
# 开发环境安装(源码贡献者)
pip install -e ".[dev,test]"
# CI/CD 环境
pip install -e ".[test,lint,ci]"
# 文档构建环境
pip install -e ".[doc]"
# 生产环境(仅安装运行时依赖)
pip install my-awesome-package
# 完整安装(贡献者一站式安装)
pip install -e ".[all]"
8.3 constraint 文件与 lock 文件
对于生产环境,依赖分组声明只定义版本约束的上限和下限,实际安装的精确版本需要锁定:
# requirements-dev.txt - 锁定所有开发依赖的精确版本
# 使用 pip-compile 或 pip freeze 生成
-e .
pytest==8.3.2
pytest-cov==5.0.0
ruff==0.5.0
mypy==1.11.0
...
# 安装方式:
# pip install -r requirements-dev.txt
依赖管理原则
- 宽进严出: pyproject.toml 中声明宽松的版本范围(如 >=8.0.0),通过 lock 文件锁定精确版本用于部署
- 分组清晰: 每个场景一个组,避免创建过于笼统的组(如 all 仅用于方便贡献者)
- 最小依赖原则: 运行时依赖只包含真正需要的包,开发工具放在 dev 组中
- 定期更新: 使用 pip list --outdated 或 Dependabot 等工具定期检查依赖更新
九、综合项目示例
下面展示一个完整的现代Python项目结构,综合运用了本文讲解的所有概念。
9.1 完整项目目录结构
my-awesome-package/
├── pyproject.toml # 项目核心配置
├── README.md # 项目说明文档
├── LICENSE # MIT 许可证
├── VERSION.txt # 版本号文件
├── CHANGELOG.md # 版本更新日志
├── .gitignore # Git 忽略规则
├── .pre-commit-config.yaml # pre-commit 钩子配置
├── src/ # 源码目录
│ └── my_package/
│ ├── __init__.py # 包初始化,含 __version__
│ ├── __main__.py # python -m my_package 入口
│ ├── cli.py # 命令行接口
│ ├── config.py # 配置管理
│ ├── core.py # 核心逻辑
│ ├── models.py # 数据模型
│ ├── utils.py # 工具函数
│ └── sub_package/
│ ├── __init__.py
│ └── module_c.py
├── tests/ # 测试套件
│ ├── __init__.py
│ ├── conftest.py # pytest fixtures
│ ├── test_cli.py
│ ├── test_core.py
│ ├── test_models.py
│ └── test_utils.py
├── docs/ # 文档
│ ├── mkdocs.yml
│ └── docs/
│ ├── index.md
│ ├── installation.md
│ └── usage.md
└── scripts/ # 辅助脚本
├── release.sh # 发布脚本
└── generate_docs.sh # 文档生成脚本
9.2 pyproject.toml 完整配置
[build-system]
requires = ["setuptools>=69.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-package"
dynamic = ["version"]
description = "现代Python项目结构最佳实践示例"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "yourname@example.com"},
]
keywords = [
"python", "project-structure", "pyproject-toml",
"best-practices", "tutorial",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"click>=8.1.0",
"rich>=13.0.0",
"pydantic>=2.0.0",
"pyyaml>=6.0.0",
"requests>=2.31.0",
]
[project.optional-dependencies]
dev = ["pytest>=8.0.0", "ipython>=8.15.0", "ipdb>=0.13.0"]
test = ["pytest-cov>=5.0.0", "pytest-xdist>=3.5.0", "coverage>=7.4.0"]
lint = ["ruff>=0.3.0", "mypy>=1.8.0", "pre-commit>=3.6.0"]
doc = ["mkdocs>=1.5.0", "mkdocs-material>=9.5.0", "mkdocstrings[python]>=0.24.0"]
all = ["my-awesome-package[dev]", "my-awesome-package[test]", "my-awesome-package[lint]", "my-awesome-package[doc]"]
[project.urls]
homepage = "https://github.com/yourname/my-awesome-package"
repository = "https://github.com/yourname/my-awesome-package"
documentation = "https://my-awesome-package.readthedocs.io"
changelog = "https://github.com/yourname/my-awesome-package/blob/main/CHANGELOG.md"
[project.scripts]
my-cli = "my_package.cli:main"
[tool.setuptools]
package-dir = {"" = "src"}
packages = {find = {where = ["src"]}}
[tool.setuptools.dynamic]
version = {file = "VERSION.txt"}
[tool.pytest.ini_options]
minversion = "8.0"
addopts = "-ra -q --strict-markers"
testpaths = ["tests"]
[tool.ruff]
target-version = "py310"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
[tool.coverage.run]
source = ["my_package"]
branch = true
parallel = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if __name__ == '__main__':",
]
9.3 入口模块示例
# src/my_package/__init__.py
"""
my_awesome_package - 现代Python项目结构最佳实践示例。
本包演示了符合PEP 621标准的现代Python项目结构和配置方式。
"""
__version__ = "2025.4.0"
__version_info__ = (2025, 4, 0)
__all__ = [
"__version__",
"__version_info__",
"greet",
"Config",
]
from my_package.config import Config
from my_package.core import greet
# src/my_package/cli.py
"""命令行入口模块,使用 click 构建 CLI。"""
import click
from rich.console import Console
from rich.progress import track
from my_package import __version__
from my_package.core import greet
console = Console()
@click.group()
@click.version_option(__version__, prog_name="my-cli")
def cli():
"""my-awesome-package 命令行工具。"""
pass
@cli.command()
@click.argument("name", default="World")
@click.option("--count", "-c", default=1, help="重复次数")
def hello(name: str, count: int):
"""向指定名称问好。"""
for _ in track(range(count), description="处理中..."):
message = greet(name)
console.print(message, style="bold green")
@cli.command()
def info():
"""显示系统信息。"""
import platform
import sys
console.print(f"[bold]Python:[/bold] {sys.version}")
console.print(f"[bold]平台:[/bold] {platform.platform()}")
console.print(f"[bold]包版本:[/bold] {__version__}")
if __name__ == "__main__":
cli()
十、核心要点总结
- 标准演进路径: Python项目配置经历了 setup.py → setup.cfg → pyproject.toml 的演进,PEP 518/517/621/660 是关键的里程碑标准
- 项目布局选择: src 布局(推荐)通过在 src/ 下组织源码实现测试隔离和打包正确性;扁平布局适合小型项目和原型开发
- pyproject.toml 三大核心表: [build-system] 声明构建工具和后端;[project] 承载PEP 621标准化的项目元数据;[tool.*] 统一管理各工具配置
- 配置对比: pyproject.toml 声明式、构建后端无关、支持工具配置统一;setup.cfg 仅限setuptools;setup.py 仅在需要C扩展或自定义构建命令时保留
- 依赖分组管理: 通过 [project.optional-dependencies] 按场景(dev/test/lint/doc/ci)分组管理依赖,用户通过 pip install pkg[group] 安装
- 版本管理: 使用单一真实来源(__init__.py 或 VERSION.txt),结合动态版本声明,遵循SemVer或CalVer规范
- 最佳实践: 所有新项目应使用 pyproject.toml + src布局 + PEP 621元数据;仅保留必要的配置文件,减少项目根目录的文件冗余
十一、进一步思考
项目结构与打包配置是Python工程化的基础,掌握这些知识后,可以进一步探索以下方向:
扩展学习方向:
- 跨平台打包: 使用 cibuildwheel 构建多平台Wheel包
- 私有包管理: 搭建私有PyPI镜像(如使用 devpi 或 twine 上传到私有仓库)
- Monorepo管理: 使用 pdm 或 poetry 管理多包仓库
- 自动化发布: 使用 GitHub Actions / GitLab CI 实现自动构建、测试、发布流程
- 构建后端对比: 深入了解 setuptools、hatchling、flit_core、pdm-backend 的实现差异与性能对比
- 交叉编译: 为不同架构(x86_64、ARM64)和操作系统构建原生扩展
实践建议
- 尝试将现有的一个Python项目按照本文结构重新组织,采用src布局和 pyproject.toml 配置
- 配置 [tool.setuptools.dynamic] 实现版本号的单一真实来源
- 建立完整的依赖分组(dev/test/lint/doc/ci),并在CI中分别使用不同的组
- 删除不再需要的 setup.py、setup.cfg、pytest.ini 等冗余配置文件
- 添加 [project.scripts] 提供便捷的命令行入口