1. Python 工程化项目骨架搭建

Python 工程化项目骨架搭建 学习卡片

为什么需要工程骨架

一个可安装、可测试、可发布的 Python 工程,核心不在于代码写得多,而在于目录布局、构建元数据和包发现规则三者一致。本案例采用当前社区推荐的 src 布局:源码放在 src/ 下,测试放在 tests/ 下,构建配置集中在 pyproject.toml。这样做的直接好处是:测试必须针对“已安装的包”运行,而不是意外导入当前目录下的同名文件夹,从而提前暴露打包遗漏。

src 布局工程骨架与构建数据流 项目目录结构 myproj/ ├─ pyproject.toml ├─ README.md ├─ src/ │ └─ myproj/ │ ├─ __init__.py │ └─ core.py └─ tests/ └─ test_core.py 关键约束 • 包名 myproj 位于 src/ 之下 • tests/ 与 src/ 平级,不打包 • 构建后端读取 pyproject.toml 构建 构建后端 setuptools.build_meta 读取 [project] 元数据 package-dir 映射 "" → "src" find 包发现 定位 src/myproj 产出 可编辑安装 pip install -e . .pth 指向 src/ wheel 分发包 python -m build dist/*.whl 测试验证 pytest tests/ 导入已安装包 验证链:pyproject.toml → 构建后端 → src 包发现 → 可编辑安装 → pytest 通过 → 可发布 wheel

创建骨架文件

先建立目录与最小文件集。以下命令在空目录中执行,创建 myproj 工程:

mkdir -p myproj/src/myproj myproj/tests
cd myproj
touch src/myproj/__init__.py src/myproj/core.py tests/test_core.py README.md

写入包源码 src/myproj/core.py,提供一个可被测试的最小函数:

def add(a: int, b: int) -> int:
    """返回两数之和。"""
    return a + b

def slugify(text: str) -> str:
    """将文本转为小写连字符形式。"""
    return "-".join(text.lower().split())

src/myproj/__init__.py 暴露版本号,便于安装后验证:

__version__ = "0.1.0"

from .core import add, slugify

__all__ = ["add", "slugify", "__version__"]

编写 pyproject.toml

这是整个骨架的核心。它同时承担构建系统声明、项目元数据、依赖声明和包发现配置四项职责:

[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "myproj"
version = "0.1.0"
description = "一个可安装、可测试、可发布的 Python 工程骨架示例"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [{ name = "Example Author", email = "author@example.com" }]
dependencies = []

[project.optional-dependencies]
dev = ["pytest>=7.0", "build>=1.0"]

[tool.setuptools]
package-dir = { "" = "src" }

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"
package-dir = { "" = "src" } 与 packages.find.where = ["src"] 必须同时存在:前者告诉构建后端“根包在 src 下”,后者告诉它“去 src 里扫描包”。只写其中一个会导致安装后 import myproj 失败或打包为空。

编写测试

测试文件 tests/test_core.py 直接导入包名,而不是相对路径导入:

from myproj import add, slugify, __version__

def test_add():
    assert add(2, 3) == 5

def test_slugify():
    assert slugify("Hello World") == "hello-world"

def test_version():
    assert __version__ == "0.1.0"

可编辑安装与验证

在工程根目录创建虚拟环境并执行可编辑安装:

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
python -m pip install --upgrade pip
pip install -e ".[dev]"

安装完成后依次验证三件事:

  1. 包可导入:python -c "import myproj; print(myproj.__version__)" 应输出 0.1.0。
  2. 测试通过:pytest 应显示 3 passed。
  3. 可构建发布物:python -m build 应在 dist/ 下生成 myproj-0.1.0-py3-none-any.whl 与 .tar.gz。
若 pytest 报 ModuleNotFoundError: No module named 'myproj',通常是因为在未激活虚拟环境或未执行 pip install -e . 的情况下直接运行测试。src 布局的设计意图正是让这种错误暴露出来,而不是被当前目录的隐式导入掩盖。

验证清单

检查项命令通过标准
包可导入python -c "import myproj"无报错
版本正确python -c "import myproj; print(myproj.__version__)"输出 0.1.0
测试通过pytest3 passed
可构建python -m builddist/ 下生成 whl 与 tar.gz
wheel 内容正确python -m zipfile -l dist/*.whl包含 myproj/__init__.py 与 core.py

至此,一个具备标准目录结构、集中式构建配置、可编辑安装路径和测试入口的 Python 工程骨架已经就绪。后续添加命令行入口、依赖锁定或 CI 配置,都可以在这个骨架上直接扩展。