Metadata-Version: 2.3
Name: dongtu_utils
Version: 0.1.0
Summary: Personal Python utility library
Author: aiden
Author-email: aiden <aidenpeng0504@gmail.com>
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# dongtu_utils

个人 Python 工具库 —— 用于学习 **包发布（打包 / 构建 / 上传 PyPI）** 与 **测试编写（pytest）**。

## 项目结构

```
dongtu_utils/
├── src/                        # src 布局：源码统一放 src/ 下
│   └── dongtu_utils/
│       ├── __init__.py         # 包入口，导出公共 API
│       ├── math.py             # 数学工具函数
│       └── py.typed            # 声明本包带类型标注（供 mypy / IDE 识别）
├── tests/
│   └── test_math.py            # pytest 测试
├── pyproject.toml              # 项目元数据 + 构建配置
├── uv.lock                     # 依赖锁文件
└── README.md
```

## 常用命令

```bash
uv sync                 # 安装依赖（含 dev 组）
uv run pytest           # 运行测试
uv build                                # 构建发行包（wheel + sdist 到 dist/）
uv run --env-file .env uv publish       # 发布到 PyPI（自动从 .env 读 token）
```

> 以上命令已封装进 `Makefile`，可用 `make <target>` 调用：`make sync` / `make test` / `make build` / `make publish`。运行 `make`（或 `make help`）查看全部 target。

---

## 一、发包流程

### 0. 前置：完善 pyproject.toml 元数据

`[project]` 表是包对外暴露的信息，发布前要填全：

| 字段 | 说明 |
|------|------|
| `name` | 包名，PyPI 全局唯一，决定 `pip install <name>` |
| `version` | 版本号（见下方「注意事项」，不可重复发布） |
| `description` | 一句话描述 |
| `authors` | 作者与邮箱 |
| `readme` | 指向 README.md（会渲染在 PyPI 项目页） |
| `requires-python` | 最低 Python 版本 |

构建后端在 `[build-system]` 声明，本项目用 `uv_build`：

```toml
[build-system]
requires = ["uv_build>=0.12.0,<0.13.0"]
build-backend = "uv_build"
```

### 1. 构建

```bash
uv build
```

执行后在 `dist/` 生成两种产物：

```
dist/
├── dongtu_utils-0.1.0-py3-none-any.whl   # 二进制发行包（wheel，安装快）
└── dongtu_utils-0.1.0.tar.gz             # 源码发行包（sdist）
```

> `uv build` 默认会在 `dist/` 里自动生成 `.gitignore`（内容 `*`），配合根目录 `.gitignore` 的 `dist/` 规则，构建产物不会被提交进 git。

### 2. 本地验证产物（发布前必做）

在隔离环境里安装刚构建的 wheel 并实际 import，确认打包内容正确：

```bash
# --no-project 排除当前项目，--with 临时安装这个 wheel（版本号按实际文件名替换）
uv run --no-project --with dist/dongtu_utils-0.1.0-py3-none-any.whl \
  python -c "from dongtu_utils import add; print(add(1, 2))"
```

### 3. 预览要发布的内容

```bash
uv publish --dry-run
```

只列出会上传的文件，不真正上传。

### 4. 先发到 Test PyPI 验证

```bash
uv publish --publish-url https://test.pypi.org/legacy/ --token <TEST_PYPI_TOKEN>
```

Test PyPI（`https://test.pypi.org`）与正式 PyPI 完全隔离，账号和 token 需单独注册。发布后用下面命令验证能否正常安装：

```bash
pip install -i https://test.pypi.org/simple/ dongtu_utils
```

### 5. 使用 API Token 发布到正式 PyPI

#### ① 获取 Token

1. 登录 <https://pypi.org> → 右上角账号 → **Account settings** → **API tokens** → **Add API token**。
2. **Scope（作用域）** 选「限制到指定项目」（只给 upload 权限），并填项目名 `dongtu_utils`，而不是「Entire account（整个账号）」——这样 token 一旦泄露，影响范围更小。
3. 生成后的 token 形如 `pypi-AgEIcHlwaS5vcmc...`，**只显示这一次**，当场复制保存。
4. Test PyPI 的 token 需要到 <https://test.pypi.org> 单独注册（入口相同）。

#### ② 把 Token 传给 uv publish

`uv publish` 只认两个来源：`--token` 参数，或 `UV_PUBLISH_TOKEN` 环境变量。

方式一：`--token` 参数（最直接，但 token 会留在 shell 历史里）

```bash
uv publish --token "pypi-AgEIcHlwaS5vcmc..."
```

方式二：环境变量（推荐，不落进 shell 历史）

```bash
export UV_PUBLISH_TOKEN="pypi-AgEIcHlwaS5vcmc..."
uv publish
```

#### ③ 用 .env 存 Token（本项目已配置好）

`uv publish` 本身**不会**自动读取 `.env`，但可以借 `uv run --env-file` 先把 `.env` 加载进环境再执行发布，一条命令搞定：

```bash
uv run --env-file .env uv publish
```

`.env` 里变量名要叫 `UV_PUBLISH_TOKEN`（本项目已配好）：

```
# .env
UV_PUBLISH_TOKEN=pypi-AgEIcHlwaS5vcmc...
```

不用 `uv run` 时的等价手动写法：

```bash
set -a; source .env; set +a   # 把 .env 里的变量加载进当前 shell
uv publish
```

> 安全提醒：`.env` 已被 `.gitignore` 忽略，但务必确认它**从未被提交进 git**。若 token 曾泄露（截图、贴进聊天、误提交仓库），立刻去 PyPI 后台 **Revoke** 作废并重新生成。

发布成功后，任何人都可以 `pip install dongtu_utils`。

---

## 二、发包注意事项

1. **版本号不可覆盖** —— PyPI 不允许删除或覆盖已发布的版本，同一版本号只能上传一次。每次发布前必须递增 `version`（如 `0.1.0 → 0.1.1`）。本地反复测试时用预发布版本号：`0.1.0.dev1`、`0.1.0rc1`。

2. **用 API Token，不要用密码** —— PyPI 已不支持用户名 + 密码上传，只支持 API token（或受信任发布者）。Token 形如 `pypi-...`，在 <https://pypi.org/manage/account/token/> 生成，**只显示一次，务必当场保存**。不要写进代码或提交到 git。具体用法见上方「使用 API Token 发布」。

3. **先测 Test PyPI** —— 正式发布前先走一遍 Test PyPI，验证打包、上传、安装全链路都没问题。

4. **别提交构建产物与虚拟环境** —— `dist/`、`.venv/`、`__pycache__/` 都应被 `.gitignore` 忽略（本项目根 `.gitignore` 已覆盖）。

5. **`py.typed` 要随包发布** —— 本项目带 `py.typed`（表示有类型标注），在 src 布局下会被自动打进包；后续若新增无标注的子模块，其类型信息会连带缺失。

6. **构建与发布分离** —— `uv build` 负责生成产物，`uv publish` 负责上传；上传前先 `uv build` 保证 `dist/` 是最新的（`uv publish` 默认上传 `dist/*`）。

---

## 三、测试编写

### 1. 添加 pytest

```bash
uv add --dev pytest
```

pytest 会被加进 `[dependency-groups].dev`：

```toml
[dependency-groups]
dev = [
    "build>=1.6.1",
    "pytest>=9.1.1",
]
```

### 2. 写测试文件

测试放 `tests/` 目录，文件名用 `test_*.py`。src 布局下，测试里**直接从包名导入**（uv 会以 editable 方式把项目装进环境）：

```python
# tests/test_math.py
import pytest

from dongtu_utils import add


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


@pytest.mark.parametrize(("a", "b", "expected"), [(1, 2, 3), (0, 0, 0), (-1, 1, 0)])
def test_add_parametrized(a, b, expected):
    assert add(a, b) == expected


def test_add_raises():
    with pytest.raises(TypeError):
        add("1", 2)
```

### 3. 运行

```bash
uv run pytest                          # 跑全部测试
uv run pytest -v                       # 详细输出（显示每个用例名）
uv run pytest tests/test_math.py       # 只跑某个文件
uv run pytest -k add                   # 按名称过滤（跑名字含 "add" 的用例）
```

### 4. 常用写法速查

| 需求 | 写法 |
|------|------|
| 参数化（同一测试跑多组输入） | `@pytest.mark.parametrize("a,b,expected", [...])` |
| 共享前置数据 / 资源 | `@pytest.fixture` |
| 断言抛异常 | `with pytest.raises(XxxError):` |
| 浮点近似比较 | `assert result == pytest.approx(3.14)` |
| 跳过某测试 | `@pytest.mark.skip(reason="...")` |

### 5. 命名约定

- 测试文件：`test_*.py`
- 测试函数：`test_*`
- 共享 fixture：`tests/conftest.py`（pytest 自动发现，无需手动 import）
