项目结构与pyproject.toml

现代Python项目规范与打包配置

专题: Python进阶编程

核心主题: 现代Python项目结构规范与pyproject.toml配置

主要内容: PEP 518/517/621/660标准演进、src布局与扁平布局对比、pyproject.toml完整字段解析、setuptools配置、项目版本管理与依赖分组

关键词: Python, pyproject.toml, 项目结构, setuptools, PEP 621, 打包, 依赖管理

一、概述

Python项目结构规范与打包配置是现代Python开发的基础技能。随着Python语言生态的演进,项目配置方式经历了从 setup.pysetup.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.pysetup.cfg 中的局面,使项目元数据与构建工具无关。

PEP 660 (2021)

定义可编辑安装(editable install)的标准协议,替代 pip install -e . 的旧有实现方式,使其适用于所有PEP 517兼容的构建后端。

2.2 各PEP的核心意义

PEP 518 - 构建系统依赖声明

pyproject.toml[build-system] 表中声明构建项目所需的工具和版本。例如,声明需要 setuptoolswheel,pip会在构建项目时自动安装这些依赖。这解决了"先有鸡还是先有蛋"的问题——不再需要手动安装构建工具。

PEP 517 - 构建后端接口

定义了构建后端的标准API,包括 build_wheelbuild_sdistget_requires_for_build_wheel 等钩子。任何实现了这些钩子的工具都可以作为构建后端,如 setuptools.build_metahatchlingflit_corepdm.backendpoetry.core.masonry.api 等。

PEP 621 - 项目元数据标准化

nameversiondescriptionauthorslicensedependenciesoptional-dependenciesscriptsentry-points 等元数据统一放在 [project] 表中,使元数据声明与构建工具解耦,实现打包配置的标准化。

PEP 660 - 可编辑安装标准化

规范了 pip install -e . 的行为,使所有PEP 517兼容的构建后端都能正确支持可编辑安装。之前只有 setuptools 能可靠支持此功能。

三、项目布局方案

Python项目有两种主流的目录布局方案:扁平布局(Flat Layout)src布局(src Layout)。选择合适的布局方案是项目组织的第一步。

3.1 扁平布局(Flat Layout)

将Python包目录直接放在项目根目录下,与 pyproject.tomlREADME.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_packagemy-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.cfgsetup.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特有配置(如 packagespackage-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项目配置建议

  1. 优先使用 pyproject.toml:将项目元数据、依赖和工具配置统一放在 pyproject.toml
  2. 利用 [tool.*] 统一管理工具配置:如 [tool.pytest][tool.ruff][tool.mypy],减少根目录下的配置文件数量
  3. 删除不必要的配置文件:如果 pyproject.toml 已覆盖所有配置,删除 setup.cfgsetup.pypytest.ini.mypy.ini.ruff.toml 等冗余文件
  4. 仅在必要时保留 setup.py:当你需要C扩展、自定义构建命令、编译优化等复杂操作时才保留
  5. 使用 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

7.2 日历化版本(CalVer)

格式:YYYY.MM.MICRO(如 2025.4.1

采用日历化版本的项目包括:pytest(如 8.3.0)、requestsblack 等。日历化版本的优势在于版本号直接反映发布时间,用户可以通过版本号了解包的时效性。

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__.pyVERSION.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工程化的基础,掌握这些知识后,可以进一步探索以下方向:

扩展学习方向:

  • 跨平台打包: 使用 cibuildwheel 构建多平台Wheel包
  • 私有包管理: 搭建私有PyPI镜像(如使用 devpitwine 上传到私有仓库)
  • Monorepo管理: 使用 pdmpoetry 管理多包仓库
  • 自动化发布: 使用 GitHub Actions / GitLab CI 实现自动构建、测试、发布流程
  • 构建后端对比: 深入了解 setuptools、hatchling、flit_core、pdm-backend 的实现差异与性能对比
  • 交叉编译: 为不同架构(x86_64、ARM64)和操作系统构建原生扩展

实践建议

  1. 尝试将现有的一个Python项目按照本文结构重新组织,采用src布局和 pyproject.toml 配置
  2. 配置 [tool.setuptools.dynamic] 实现版本号的单一真实来源
  3. 建立完整的依赖分组(dev/test/lint/doc/ci),并在CI中分别使用不同的组
  4. 删除不再需要的 setup.pysetup.cfgpytest.ini 等冗余配置文件
  5. 添加 [project.scripts] 提供便捷的命令行入口