核心目标 :能够区分脚本、应用与可发布包,使用 uv 创建结构清晰的项目,并理解
pyproject.toml、.python-version、.venv和uv.lock各自负责什么。前置知识:已完成 Part 1,能够运行
uv --version和uv python list。验证环境:uv 0.11.29、Python 3.12、Windows 11 PowerShell。最后复核日期:2026-07-20。
2.1 先选项目类型,再谈目录结构
Python 代码并不都需要被打成包。uv 的 init 将常见场景明确分开:
| 场景 | 推荐命令 | 是否默认构建 | 典型用途 |
|---|---|---|---|
| 单文件脚本 | uv init --script task.py |
否 | 自动化、数据处理、一次性任务 |
| 普通应用 | uv init --app app-name |
否 | Web 服务、内部应用 |
| 可安装 CLI | uv init --package cli-name |
是 | 命令行工具 |
| 可发布库 | uv init --lib lib-name |
是 | 被其他项目导入的库 |
应用不一定需要作为 Python 包安装;库则必须能被构建为 wheel/sdist。--package 可以理解为"创建一个带命令入口的打包项目"。
#mermaid-svg-xsephwyXJOOuYnTP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xsephwyXJOOuYnTP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xsephwyXJOOuYnTP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xsephwyXJOOuYnTP .error-icon{fill:#552222;}#mermaid-svg-xsephwyXJOOuYnTP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xsephwyXJOOuYnTP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xsephwyXJOOuYnTP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xsephwyXJOOuYnTP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xsephwyXJOOuYnTP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xsephwyXJOOuYnTP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xsephwyXJOOuYnTP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xsephwyXJOOuYnTP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xsephwyXJOOuYnTP .marker.cross{stroke:#333333;}#mermaid-svg-xsephwyXJOOuYnTP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xsephwyXJOOuYnTP p{margin:0;}#mermaid-svg-xsephwyXJOOuYnTP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xsephwyXJOOuYnTP .cluster-label text{fill:#333;}#mermaid-svg-xsephwyXJOOuYnTP .cluster-label span{color:#333;}#mermaid-svg-xsephwyXJOOuYnTP .cluster-label span p{background-color:transparent;}#mermaid-svg-xsephwyXJOOuYnTP .label text,#mermaid-svg-xsephwyXJOOuYnTP span{fill:#333;color:#333;}#mermaid-svg-xsephwyXJOOuYnTP .node rect,#mermaid-svg-xsephwyXJOOuYnTP .node circle,#mermaid-svg-xsephwyXJOOuYnTP .node ellipse,#mermaid-svg-xsephwyXJOOuYnTP .node polygon,#mermaid-svg-xsephwyXJOOuYnTP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xsephwyXJOOuYnTP .rough-node .label text,#mermaid-svg-xsephwyXJOOuYnTP .node .label text,#mermaid-svg-xsephwyXJOOuYnTP .image-shape .label,#mermaid-svg-xsephwyXJOOuYnTP .icon-shape .label{text-anchor:middle;}#mermaid-svg-xsephwyXJOOuYnTP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xsephwyXJOOuYnTP .rough-node .label,#mermaid-svg-xsephwyXJOOuYnTP .node .label,#mermaid-svg-xsephwyXJOOuYnTP .image-shape .label,#mermaid-svg-xsephwyXJOOuYnTP .icon-shape .label{text-align:center;}#mermaid-svg-xsephwyXJOOuYnTP .node.clickable{cursor:pointer;}#mermaid-svg-xsephwyXJOOuYnTP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xsephwyXJOOuYnTP .arrowheadPath{fill:#333333;}#mermaid-svg-xsephwyXJOOuYnTP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xsephwyXJOOuYnTP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xsephwyXJOOuYnTP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xsephwyXJOOuYnTP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xsephwyXJOOuYnTP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xsephwyXJOOuYnTP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xsephwyXJOOuYnTP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xsephwyXJOOuYnTP .cluster text{fill:#333;}#mermaid-svg-xsephwyXJOOuYnTP .cluster span{color:#333;}#mermaid-svg-xsephwyXJOOuYnTP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xsephwyXJOOuYnTP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xsephwyXJOOuYnTP rect.text{fill:none;stroke-width:0;}#mermaid-svg-xsephwyXJOOuYnTP .icon-shape,#mermaid-svg-xsephwyXJOOuYnTP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xsephwyXJOOuYnTP .icon-shape p,#mermaid-svg-xsephwyXJOOuYnTP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xsephwyXJOOuYnTP .icon-shape .label rect,#mermaid-svg-xsephwyXJOOuYnTP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xsephwyXJOOuYnTP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xsephwyXJOOuYnTP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xsephwyXJOOuYnTP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
是
否
代码只有一个文件?
--script
需要被其他项目安装或发布?
--app
提供终端命令?
--package
--lib
选错并非不可修复,但会带来误导。例如没有 [build-system] 的应用在 uv sync 时不会安装当前项目;此时配置 [project.scripts] 也不会凭空生成命令。
2.2 创建 weather-cli
本文使用一个可安装 CLI 作为贯穿示例:
powershell
uv init --package --name weather-cli --python 3.12 weather-cli
Set-Location weather-cli
uv 0.11.29 默认得到类似结构:
text
weather-cli/
├── .git/
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
└── src/
└── weather_cli/
└── __init__.py
首次执行项目命令后还会出现:
text
├── .venv/
└── uv.lock
发行名称 weather-cli 使用连字符,导入包 weather_cli 使用下划线,这是正常且常见的映射。
2.2.1 四个关键文件的职责
| 文件 | 谁维护 | 是否提交 Git | 职责 |
|---|---|---|---|
pyproject.toml |
开发者和 uv | 是 | 项目元数据、直接依赖、工具配置 |
uv.lock |
uv | 是 | 解析后的精确、跨平台依赖集合 |
.python-version |
开发者/uv | 通常是 | 本地默认 Python 请求 |
.venv/ |
uv | 否 | 当前机器可执行环境 |
一句话记忆:pyproject.toml 说"项目允许什么",uv.lock 说"这次解析选择什么",.venv 是"本机实际装了什么"。
2.3 读懂 pyproject.toml
整理后的配置如下:
toml
[project]
name = "weather-cli"
version = "0.1.0"
description = "A small weather query command-line application"
readme = "README.md"
requires-python = ">=3.12"
license = "MIT"
authors = [
{ name = "Example Maintainer", email = "maintainer@example.com" },
]
dependencies = []
[project.scripts]
weather = "weather_cli.cli:main"
[build-system]
requires = ["uv_build>=0.11.29,<0.12.0"]
build-backend = "uv_build"
2.3.1 [project] 是标准元数据
这一段来自 Python 打包标准(PEP 621),不是 uv 私有格式。因此构建后端、包索引和其他兼容工具都能理解它。
name:发行名称,在包索引和安装命令中使用;version:发行版本,必须符合 Python 版本规范;requires-python:消费者可使用的 Python 范围;dependencies:包运行所需的直接依赖;readme、license、authors:进入构建元数据。
不要把 Ruff、pytest 等开发工具写入 dependencies,否则用户安装 CLI 时也会被迫安装它们。
2.3.2 [project.scripts] 定义终端命令
toml
[project.scripts]
weather = "weather_cli.cli:main"
等号左侧是用户执行的命令,右侧是 模块:可调用对象。它不是 shell 别名;构建并安装项目后,安装器会创建跨平台入口脚本。
2.3.3 [build-system] 选择构建后端
toml
[build-system]
requires = ["uv_build>=0.11.29,<0.12.0"]
build-backend = "uv_build"
构建前端负责发起构建,构建后端负责把源代码变成 sdist/wheel。uv build 是前端命令,uv_build 是本文选择的后端。也可以在初始化时选择 Hatchling、Flit、PDM、setuptools、Maturin 等后端:
powershell
uv init --lib --build-backend hatch example-lib
选型应基于项目需求,不要仅为"流行"频繁更换后端。构建后端是发布链路的一部分。
2.3.4 [tool.uv] 只放 uv 扩展
标准元数据解决不了的 uv 行为放在 [tool.uv],例如默认依赖组:
toml
[tool.uv]
default-groups = ["dev", "test"]
其他工具使用各自命名空间:[tool.ruff]、[tool.pytest.ini_options]。不要把所有配置都堆进 [tool.uv]。
2.4 为什么可发布包优先使用 src/ 布局
如果包目录直接位于仓库根目录,开发时的当前工作目录可能让 import weather_cli 成功,即便项目根本没有正确安装。测试通过后,构建产物却可能缺文件。
src/ 布局要求解释器通过安装后的包导入,更接近用户真实环境:
text
weather-cli/
├── src/
│ └── weather_cli/
│ ├── __init__.py
│ ├── cli.py
│ └── service.py
└── tests/
└── test_service.py
它不能替代构建产物测试,但能提前暴露很多路径错误。
2.5 编写最小可运行 CLI
src/weather_cli/service.py:
python
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Weather:
city: str
temperature_c: float
condition: str
def format_weather(weather: Weather) -> str:
"""Format weather data for terminal output."""
return (
f"{weather.city}: {weather.temperature_c:.1f} °C, "
f"{weather.condition}"
)
src/weather_cli/cli.py:
python
from __future__ import annotations
import argparse
from collections.abc import Sequence
from weather_cli.service import Weather, format_weather
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Show example weather data")
parser.add_argument("city", nargs="?", default="Shanghai")
return parser
def main(argv: Sequence[str] | None = None) -> int:
args = build_parser().parse_args(argv)
weather = Weather(args.city, 26.0, "clear")
print(format_weather(weather))
return 0
if __name__ == "__main__":
raise SystemExit(main())
入口函数返回整数退出码,方便测试和 shell 自动化。业务格式化逻辑与参数解析分离,后续接入 HTTP 客户端时不会把所有代码塞进 main()。
运行:
powershell
uv run weather Beijing
第一次运行时,uv 会创建 uv.lock、创建 .venv、安装当前项目,然后执行入口命令。源代码以 editable 方式安装,修改 .py 文件后通常无需重复安装。
2.6 添加测试与质量工具
开发依赖使用标准依赖组:
powershell
uv add --group test pytest
uv add --dev ruff
配置结果类似:
toml
[dependency-groups]
dev = ["ruff>=0.12.0"]
test = ["pytest>=8.0"]
示例中的下界由实际 uv add 结果决定,不要从文章复制未来可能过时的工具版本。dev 默认会同步,其他组需显式请求,或者配置 default-groups。
tests/test_service.py:
python
from weather_cli.service import Weather, format_weather
def test_format_weather() -> None:
weather = Weather("Shanghai", 26.04, "clear")
assert format_weather(weather) == "Shanghai: 26.0 °C, clear"
执行:
powershell
uv run --group test pytest
uv run ruff format --check .
uv run ruff check .
如果希望测试组默认存在:
toml
[tool.uv]
default-groups = ["dev", "test"]
之后 uv sync 和 uv run pytest 会包含两组。团队应明确默认组,避免某位开发者"本地碰巧装过 pytest"掩盖配置缺失。
2.7 应用项目为何可能不安装自身
执行:
powershell
uv init --app simple-app
普通应用默认没有 [build-system]。uv 会管理它的依赖和环境,但 uv sync 不会把项目自身安装为包。这适合直接运行 main.py:
powershell
uv run main.py
如果应用后来需要稳定的包导入、命令入口或构建产物,应显式转为打包项目,而不是在 sys.path 上打补丁:
- 增加合适的
[build-system]; - 把包代码放进
src/<import_name>/; - 配置
[project.scripts]; - 运行
uv sync和uv build验证。
2.8 配置发现与优先级
uv 可以从项目级 pyproject.toml/uv.toml、用户级配置、环境变量和命令行读取设置。排障时采用以下思路:
- 命令行是否显式传入参数;
- 是否设置
UV_*环境变量; - 当前目录向上发现了哪个项目;
- 项目或用户配置是否覆盖默认值。
可用参数减少"在错误目录执行"的风险:
powershell
uv --project C:\work\weather-cli run weather Shanghai
uv --directory C:\work\weather-cli run pytest
--project 指定项目发现位置;--directory 先改变工作目录。两者语义不同,不要仅凭名字猜测。
2.9 常见错误
错误一:ModuleNotFoundError,但在仓库根目录直接运行又正常
这是典型的导入路径假阳性。使用 src/ 布局、声明构建系统,并通过 uv run 执行:
powershell
uv sync
uv run python -c "import weather_cli; print(weather_cli.__file__)"
错误二:配置了 [project.scripts],命令仍不存在
检查项目是否声明 [build-system],以及入口模块和函数是否真实存在。然后运行:
powershell
uv sync
uv run weather --help
错误三:把 pytest 放进 project.dependencies
使用 uv remove pytest,再执行 uv add --group test pytest。运行依赖描述用户安装项目所需的内容,测试工具只属于开发过程。
错误四:修改 pyproject.toml 后手工改 uv.lock
锁文件由 uv 维护:
powershell
uv lock
uv lock --check
错误五:提交 .venv
从 Git 索引中移除它,并确认 .gitignore 包含 .venv。不要把删除环境当作丢失依赖;依赖可以从声明和锁文件重建。
2.10 项目验收清单
-
uv run weather Shanghai在未激活环境时成功。 -
uv run --group test pytest通过。 -
uv run ruff format --check .和uv run ruff check .通过。 -
python -c "import weather_cli"只在正确环境中成功。 -
pyproject.toml只包含直接依赖,开发工具位于 dependency groups。 -
uv.lock已提交,.venv未提交。 - 包名、导入名、CLI 命令名的区别已记录在 README。
-
requires-python与真实 CI 测试范围一致。
2.11 本篇小结
现代 Python 项目的核心不是一个漂亮目录,而是让标准元数据、构建系统、源代码、锁文件和环境各司其职。src/ 布局与 uv run 让本地执行更接近用户安装后的真实情况,dependency groups 则防止开发工具污染运行依赖。
下一篇将深入 uv add → resolve → uv.lock → sync 的状态链路,并解释锁文件、依赖分组、升级和冲突诊断。