案例:打包共用庫
案例:打包共用庫
本案例基於 .claude/lib 整體結構,展示如何將內部共用庫打包成可重用的 Python 套件。
先備知識
- 模組六:打包與發布
- Python 模組與套件基礎
問題背景
現有設計
.claude/lib 目錄結構:
1.claude/lib/
2├── __init__.py # Package entry point with version
3├── config_loader.py # YAML/JSON configuration loader
4├── git_utils.py # Git operations (branch, worktree)
5├── hook_io.py # Hook I/O standardization
6├── hook_logging.py # Logging system for hooks
7├── hook_validator.py # Hook compliance validator
8├── markdown_link_checker.py # Markdown link validation
9├── README.md # API documentation
10└── tests/ # Unit tests
11 ├── __init__.py
12 ├── test_config_loader.py
13 ├── test_git_utils.py
14 ├── test_hook_io.py
15 └── test_hook_logging.py這是一個典型的內部工具庫,包含四個核心模組:
| 模組 | 功能 | 相依性 |
|---|---|---|
config_loader | YAML/JSON 配置載入 | PyYAML (optional) |
git_utils | Git 命令執行與分支管理 | 無(使用 subprocess) |
hook_io | Hook 輸入輸出標準化 | 無(使用標準庫) |
hook_logging | 日誌系統設定 | 無(使用標準庫) |
現有限制
作為內部目錄的問題:
- 無法跨專案重用:程式碼被鎖定在單一專案中
- 沒有版本管理:無法追蹤 API 變更
- 無法透過 pip 安裝:其他專案必須複製程式碼
- 相依性管理不明確:PyYAML 是可選還是必要?
進階解決方案
設計目標
- 建立標準的 Python 套件結構
- 使用 pyproject.toml 管理元資料
- 支援 pip install
- 建立 CI/CD 發布流程
實作步驟
步驟 1:重組目錄結構
從內部目錄結構轉換為標準的 src layout:
1claude-hooks-lib/
2├── pyproject.toml # Package metadata and build config
3├── README.md # Package documentation
4├── LICENSE # License file (MIT recommended)
5├── CHANGELOG.md # Version history
6├── src/
7│ └── claude_hooks_lib/ # Package directory (underscore for import)
8│ ├── __init__.py # Public API exports
9│ ├── config_loader.py
10│ ├── git_utils.py
11│ ├── hook_io.py
12│ ├── hook_logging.py
13│ ├── hook_validator.py
14│ └── py.typed # PEP 561 marker for type hints
15└── tests/
16 ├── __init__.py
17 ├── conftest.py # Pytest fixtures
18 ├── test_config_loader.py
19 ├── test_git_utils.py
20 ├── test_hook_io.py
21 └── test_hook_logging.py為什麼選擇 src layout?
1# Flat layout (不推薦用於套件發布)
2my-package/
3├── my_package/ # Package 直接在根目錄
4│ └── __init__.py
5└── tests/
6
7# Src layout (推薦)
8my-package/
9├── src/
10│ └── my_package/ # Package 在 src/ 下
11│ └── __init__.py
12└── tests/| 特性 | Flat Layout | Src Layout |
|---|---|---|
| 測試環境 | 可能意外導入本地版本 | 強制安裝後測試 |
| 套件發布 | 容易遺漏檔案 | 明確的套件邊界 |
| 複雜度 | 較低 | 稍高 |
| 推薦場景 | 簡單專案、應用程式 | 套件發布、函式庫 |
步驟 2:建立 pyproject.toml
1[build-system]
2requires = ["hatchling"]
3build-backend = "hatchling.build"
4
5[project]
6name = "claude-hooks-lib"
7version = "0.28.0"
8description = "Shared utilities for Claude Code hooks"
9readme = "README.md"
10license = "MIT"
11requires-python = ">=3.10"
12authors = [
13 { name = "Your Name", email = "your.email@example.com" }
14]
15keywords = ["claude", "hooks", "utilities", "git"]
16classifiers = [
17 "Development Status :: 4 - Beta",
18 "Intended Audience :: Developers",
19 "License :: OSI Approved :: MIT License",
20 "Operating System :: OS Independent",
21 "Programming Language :: Python :: 3",
22 "Programming Language :: Python :: 3.10",
23 "Programming Language :: Python :: 3.11",
24 "Programming Language :: Python :: 3.12",
25 "Programming Language :: Python :: 3.13",
26 "Typing :: Typed",
27]
28
29# Core dependencies (minimal)
30dependencies = []
31
32[project.optional-dependencies]
33# YAML support
34yaml = ["PyYAML>=6.0"]
35
36# Development dependencies
37dev = [
38 "pytest>=8.0",
39 "pytest-cov>=4.0",
40 "mypy>=1.0",
41 "ruff>=0.4",
42]
43
44# All optional features
45all = ["claude-hooks-lib[yaml]"]
46
47[project.urls]
48Homepage = "https://github.com/yourname/claude-hooks-lib"
49Documentation = "https://github.com/yourname/claude-hooks-lib#readme"
50Repository = "https://github.com/yourname/claude-hooks-lib.git"
51Changelog = "https://github.com/yourname/claude-hooks-lib/blob/main/CHANGELOG.md"
52
53[project.scripts]
54# Command-line entry points
55hook-validator = "claude_hooks_lib.hook_validator:main"
56
57[tool.hatch.build.targets.sdist]
58include = [
59 "/src",
60 "/tests",
61]
62
63[tool.hatch.build.targets.wheel]
64packages = ["src/claude_hooks_lib"]關鍵設定說明
- build-system:使用 Hatch 作為建構後端(現代、快速)
- requires-python:指定最低 Python 版本
- dependencies:核心相依性保持為空(僅使用標準庫)
- optional-dependencies:將 PyYAML 設為可選
- project.scripts:定義命令列工具入口點
步驟 3:處理相依性
相依性分層策略:
1[project]
2# 核心相依性:僅標準庫
3dependencies = []
4
5[project.optional-dependencies]
6# 功能性相依性(用戶根據需求安裝)
7yaml = ["PyYAML>=6.0"]
8
9# 開發相依性(僅開發者需要)
10dev = [
11 "pytest>=8.0",
12 "pytest-cov>=4.0",
13 "mypy>=1.0",
14 "ruff>=0.4",
15]
16
17# 測試相依性(CI/CD 需要)
18test = [
19 "pytest>=8.0",
20 "pytest-cov>=4.0",
21]
22
23# 文件相依性
24docs = [
25 "mkdocs>=1.5",
26 "mkdocs-material>=9.0",
27]
28
29# 完整安裝
30all = [
31 "claude-hooks-lib[yaml,dev,docs]",
32]安裝方式範例
1# 基本安裝(無可選相依性)
2pip install claude-hooks-lib
3
4# 包含 YAML 支援
5pip install "claude-hooks-lib[yaml]"
6
7# 開發者安裝
8pip install -e ".[dev]"
9
10# 完整安裝
11pip install "claude-hooks-lib[all]"步驟 4:版本管理策略
方法 A:單一來源版本(推薦)
在 __init__.py 中定義版本:
1# src/claude_hooks_lib/__init__.py
2"""
3Claude Hooks Library
4
5Shared utilities for building Claude Code hooks.
6"""
7
8__version__ = "0.28.0"
9__all__ = [
10 # Version
11 "__version__",
12 # git_utils
13 "run_git_command",
14 "get_current_branch",
15 "get_project_root",
16 "get_worktree_list",
17 "is_protected_branch",
18 "is_allowed_branch",
19 # hook_logging
20 "setup_hook_logging",
21 # hook_io
22 "read_hook_input",
23 "write_hook_output",
24 "create_pretooluse_output",
25 "create_posttooluse_output",
26 # config_loader
27 "load_config",
28 "load_agents_config",
29 "load_quality_rules",
30 "clear_config_cache",
31]
32
33from .git_utils import (
34 run_git_command,
35 get_current_branch,
36 get_project_root,
37 get_worktree_list,
38 is_protected_branch,
39 is_allowed_branch,
40)
41
42from .hook_logging import setup_hook_logging
43
44from .hook_io import (
45 read_hook_input,
46 write_hook_output,
47 create_pretooluse_output,
48 create_posttooluse_output,
49)
50
51from .config_loader import (
52 load_config,
53 load_agents_config,
54 load_quality_rules,
55 clear_config_cache,
56)在 pyproject.toml 中使用動態版本:
1[project]
2name = "claude-hooks-lib"
3dynamic = ["version"]
4
5[tool.hatch.version]
6path = "src/claude_hooks_lib/__init__.py"方法 B:使用 hatch-vcs(Git tag 版本)
1[build-system]
2requires = ["hatchling", "hatch-vcs"]
3build-backend = "hatchling.build"
4
5[project]
6dynamic = ["version"]
7
8[tool.hatch.version]
9source = "vcs"
10
11[tool.hatch.build.hooks.vcs]
12version-file = "src/claude_hooks_lib/_version.py"1# Create version tag
2git tag v0.28.0
3git push --tags步驟 5:建立發布流程
.github/workflows/publish.yml:
1name: Publish to PyPI
2
3on:
4 release:
5 types: [published]
6
7permissions:
8 contents: read
9 id-token: write # Required for trusted publishing
10
11jobs:
12 build:
13 runs-on: ubuntu-latest
14 steps:
15 - uses: actions/checkout@v4
16
17 - name: Set up Python
18 uses: actions/setup-python@v5
19 with:
20 python-version: "3.12"
21
22 - name: Install build tools
23 run: |
24 python -m pip install --upgrade pip
25 pip install build
26
27 - name: Build package
28 run: python -m build
29
30 - name: Upload artifacts
31 uses: actions/upload-artifact@v4
32 with:
33 name: dist
34 path: dist/
35
36 test:
37 runs-on: ubuntu-latest
38 strategy:
39 matrix:
40 python-version: ["3.10", "3.11", "3.12", "3.13"]
41 steps:
42 - uses: actions/checkout@v4
43
44 - name: Set up Python ${{ matrix.python-version }}
45 uses: actions/setup-python@v5
46 with:
47 python-version: ${{ matrix.python-version }}
48
49 - name: Install dependencies
50 run: |
51 python -m pip install --upgrade pip
52 pip install -e ".[dev,yaml]"
53
54 - name: Run tests
55 run: pytest tests/ -v --cov=src/claude_hooks_lib
56
57 - name: Type check
58 run: mypy src/claude_hooks_lib
59
60 publish-testpypi:
61 needs: [build, test]
62 runs-on: ubuntu-latest
63 environment: testpypi
64 steps:
65 - name: Download artifacts
66 uses: actions/download-artifact@v4
67 with:
68 name: dist
69 path: dist/
70
71 - name: Publish to TestPyPI
72 uses: pypa/gh-action-pypi-publish@release/v1
73 with:
74 repository-url: https://test.pypi.org/legacy/
75
76 publish-pypi:
77 needs: [publish-testpypi]
78 runs-on: ubuntu-latest
79 environment: pypi
80 steps:
81 - name: Download artifacts
82 uses: actions/download-artifact@v4
83 with:
84 name: dist
85 path: dist/
86
87 - name: Publish to PyPI
88 uses: pypa/gh-action-pypi-publish@release/v1CI 測試工作流程(.github/workflows/test.yml):
1name: Tests
2
3on:
4 push:
5 branches: [main]
6 pull_request:
7 branches: [main]
8
9jobs:
10 test:
11 runs-on: ${{ matrix.os }}
12 strategy:
13 fail-fast: false
14 matrix:
15 os: [ubuntu-latest, macos-latest, windows-latest]
16 python-version: ["3.10", "3.11", "3.12", "3.13"]
17
18 steps:
19 - uses: actions/checkout@v4
20
21 - name: Set up Python ${{ matrix.python-version }}
22 uses: actions/setup-python@v5
23 with:
24 python-version: ${{ matrix.python-version }}
25
26 - name: Install dependencies
27 run: |
28 python -m pip install --upgrade pip
29 pip install -e ".[dev,yaml]"
30
31 - name: Lint with ruff
32 run: ruff check src/ tests/
33
34 - name: Format check with ruff
35 run: ruff format --check src/ tests/
36
37 - name: Type check with mypy
38 run: mypy src/claude_hooks_lib
39
40 - name: Run tests with coverage
41 run: |
42 pytest tests/ -v --cov=src/claude_hooks_lib --cov-report=xml
43
44 - name: Upload coverage
45 uses: codecov/codecov-action@v4
46 with:
47 files: ./coverage.xml完整程式碼
完整的 pyproject.toml
1[build-system]
2requires = ["hatchling"]
3build-backend = "hatchling.build"
4
5[project]
6name = "claude-hooks-lib"
7dynamic = ["version"]
8description = "Shared utilities for Claude Code hooks"
9readme = "README.md"
10license = "MIT"
11requires-python = ">=3.10"
12authors = [
13 { name = "Your Name", email = "your.email@example.com" }
14]
15keywords = ["claude", "hooks", "utilities", "git", "automation"]
16classifiers = [
17 "Development Status :: 4 - Beta",
18 "Intended Audience :: Developers",
19 "License :: OSI Approved :: MIT License",
20 "Operating System :: OS Independent",
21 "Programming Language :: Python :: 3",
22 "Programming Language :: Python :: 3.10",
23 "Programming Language :: Python :: 3.11",
24 "Programming Language :: Python :: 3.12",
25 "Programming Language :: Python :: 3.13",
26 "Typing :: Typed",
27 "Topic :: Software Development :: Libraries :: Python Modules",
28]
29
30dependencies = []
31
32[project.optional-dependencies]
33yaml = ["PyYAML>=6.0"]
34dev = [
35 "pytest>=8.0",
36 "pytest-cov>=4.0",
37 "mypy>=1.0",
38 "ruff>=0.4",
39 "PyYAML>=6.0",
40]
41docs = [
42 "mkdocs>=1.5",
43 "mkdocs-material>=9.0",
44]
45all = ["claude-hooks-lib[yaml,dev,docs]"]
46
47[project.urls]
48Homepage = "https://github.com/yourname/claude-hooks-lib"
49Documentation = "https://github.com/yourname/claude-hooks-lib#readme"
50Repository = "https://github.com/yourname/claude-hooks-lib.git"
51Changelog = "https://github.com/yourname/claude-hooks-lib/blob/main/CHANGELOG.md"
52
53[project.scripts]
54hook-validator = "claude_hooks_lib.hook_validator:main"
55
56# ===== Build Configuration =====
57
58[tool.hatch.version]
59path = "src/claude_hooks_lib/__init__.py"
60
61[tool.hatch.build.targets.sdist]
62include = ["/src", "/tests", "/README.md", "/LICENSE"]
63
64[tool.hatch.build.targets.wheel]
65packages = ["src/claude_hooks_lib"]
66
67# ===== Tool Configuration =====
68
69[tool.ruff]
70src = ["src"]
71line-length = 88
72target-version = "py310"
73
74[tool.ruff.lint]
75select = [
76 "E", # pycodestyle errors
77 "W", # pycodestyle warnings
78 "F", # pyflakes
79 "I", # isort
80 "B", # flake8-bugbear
81 "C4", # flake8-comprehensions
82 "UP", # pyupgrade
83]
84ignore = ["E501"] # Line too long (handled by formatter)
85
86[tool.ruff.lint.isort]
87known-first-party = ["claude_hooks_lib"]
88
89[tool.mypy]
90python_version = "3.10"
91warn_return_any = true
92warn_unused_ignores = true
93disallow_untyped_defs = true
94strict = true
95
96[[tool.mypy.overrides]]
97module = "yaml"
98ignore_missing_imports = true
99
100[tool.pytest.ini_options]
101testpaths = ["tests"]
102python_files = "test_*.py"
103python_functions = "test_*"
104addopts = "-v --tb=short"
105
106[tool.coverage.run]
107source = ["src/claude_hooks_lib"]
108branch = true
109
110[tool.coverage.report]
111exclude_lines = [
112 "pragma: no cover",
113 "if TYPE_CHECKING:",
114 "raise NotImplementedError",
115]使用範例
安裝套件
1# From PyPI (after publishing)
2pip install claude-hooks-lib
3
4# With YAML support
5pip install "claude-hooks-lib[yaml]"
6
7# Development installation (from source)
8git clone https://github.com/yourname/claude-hooks-lib
9cd claude-hooks-lib
10pip install -e ".[dev]"Python 使用範例
1# Basic usage
2from claude_hooks_lib import (
3 get_current_branch,
4 is_protected_branch,
5 setup_hook_logging,
6 read_hook_input,
7 write_hook_output,
8 create_pretooluse_output,
9)
10
11# Initialize logging
12logger = setup_hook_logging("my-custom-hook")
13
14# Check branch protection
15branch = get_current_branch()
16if branch and is_protected_branch(branch):
17 logger.warning(f"Operating on protected branch: {branch}")
18
19# Process hook input
20input_data = read_hook_input()
21tool_name = input_data.get("tool_name", "")
22
23# Generate output
24output = create_pretooluse_output(
25 decision="allow",
26 reason="All checks passed"
27)
28write_hook_output(output)命令列工具使用
1# Validate a single hook
2hook-validator .claude/hooks/my-hook.py
3
4# Validate all hooks
5hook-validator --all
6
7# Output as JSON
8hook-validator --all --json
9
10# Strict mode (warnings as errors)
11hook-validator --all --strict設計權衡
| 面向 | 內部目錄 | 獨立套件 |
|---|---|---|
| 重用性 | 僅限單專案 | 跨專案共用 |
| 版本管理 | 與專案綁定 | 獨立語意化版本 |
| 維護成本 | 低(無發布流程) | 中(需維護 CI/CD) |
| 相依管理 | 隱式(需手動追蹤) | 顯式(pyproject.toml) |
| 安裝方式 | 複製程式碼或 sys.path | pip install |
| 測試隔離 | 可能測試到本地版本 | 強制測試安裝版本 |
| API 穩定性 | 無保證 | 版本號約束 |
專案結構比較
| Layout | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| Flat Layout | 簡單應用、腳本 | 簡單直覺 | 測試可能導入錯誤版本 |
| Src Layout | 函式庫、套件發布 | 測試隔離、明確邊界 | 額外一層目錄 |
建構工具比較
| 工具 | 特點 | 推薦場景 |
|---|---|---|
| setuptools | 成熟穩定、生態最大 | 需要相容舊專案 |
| Hatch | 現代、快速、功能完整 | 新專案首選 |
| Poetry | 依賴鎖定、虛擬環境管理 | 需要嚴格依賴控制 |
| Flit | 極簡、僅純 Python | 簡單函式庫 |
什麼時候該打包成套件?
適合打包
- 多個專案需要使用相同程式碼
- 程式碼相對穩定,API 不常變動
- 需要版本控制和變更追蹤
- 希望其他人能 pip install 使用
- 需要明確的相依性管理
不建議打包
- 僅單一專案使用
- 程式碼還在快速迭代
- 與專案緊密耦合(如特定的配置路徑)
- 維護成本超過重用收益
決策流程圖
1程式碼需要跨專案使用?
2├── 否 → 保持內部目錄
3└── 是 → API 穩定嗎?
4 ├── 否 → 等待穩定後再打包
5 └── 是 → 有維護能力嗎?
6 ├── 否 → 考慮 monorepo
7 └── 是 → 打包發布練習
基礎練習:建立最小的 pyproject.toml
目標:為一個簡單的工具函式庫建立 pyproject.toml
1# src/my_utils/__init__.py
2"""Simple utilities."""
3
4__version__ = "0.1.0"
5
6def greet(name: str) -> str:
7 """Return a greeting message."""
8 return f"Hello, {name}!"要求:
- 使用 hatchling 作為建構後端
- 設定專案名稱、版本、描述
- 指定 Python 版本要求(>=3.10)
參考答案
1[build-system]
2requires = ["hatchling"]
3build-backend = "hatchling.build"
4
5[project]
6name = "my-utils"
7version = "0.1.0"
8description = "Simple utility functions"
9requires-python = ">=3.10"
10
11[tool.hatch.build.targets.wheel]
12packages = ["src/my_utils"]進階練習:新增 optional dependencies
目標:擴展上面的套件,加入可選的功能
要求:
- 新增一個需要
requests的函式 - 將
requests設為可選相依性 - 加入開發相依性(pytest, ruff)
參考答案
1[build-system]
2requires = ["hatchling"]
3build-backend = "hatchling.build"
4
5[project]
6name = "my-utils"
7version = "0.1.0"
8description = "Simple utility functions"
9requires-python = ">=3.10"
10dependencies = []
11
12[project.optional-dependencies]
13http = ["requests>=2.28"]
14dev = [
15 "pytest>=8.0",
16 "ruff>=0.4",
17 "my-utils[http]", # Include http for testing
18]
19
20[tool.hatch.build.targets.wheel]
21packages = ["src/my_utils"] 1# src/my_utils/http.py
2"""HTTP utilities (requires requests)."""
3
4try:
5 import requests
6 HAS_REQUESTS = True
7except ImportError:
8 HAS_REQUESTS = False
9
10def fetch_json(url: str) -> dict:
11 """Fetch JSON from URL."""
12 if not HAS_REQUESTS:
13 raise ImportError(
14 "requests is required for this feature. "
15 "Install with: pip install my-utils[http]"
16 )
17 response = requests.get(url)
18 response.raise_for_status()
19 return response.json()挑戰題:設定 GitHub Actions 自動發布到 PyPI
目標:建立完整的 CI/CD 流程
要求:
- Pull Request 時執行測試
- 建立 Release 時自動發布到 PyPI
- 使用 Trusted Publishing(不需要 API Token)
- 多版本 Python 測試矩陣
提示:
- 需要在 PyPI 設定 Trusted Publisher
- 使用
pypa/gh-action-pypi-publish@release/v1 - 設定
id-token: write權限
參考答案
先在 PyPI 設定 Trusted Publisher:
- 前往 pypi.org
- 新增 GitHub publisher
- 填入 repository owner、name、workflow 路徑
建立工作流程檔案:
1# .github/workflows/ci.yml
2name: CI
3
4on:
5 push:
6 branches: [main]
7 pull_request:
8 branches: [main]
9
10jobs:
11 test:
12 runs-on: ubuntu-latest
13 strategy:
14 matrix:
15 python-version: ["3.10", "3.11", "3.12", "3.13"]
16 steps:
17 - uses: actions/checkout@v4
18 - uses: actions/setup-python@v5
19 with:
20 python-version: ${{ matrix.python-version }}
21 - run: pip install -e ".[dev]"
22 - run: ruff check src/ tests/
23 - run: pytest tests/ -v 1# .github/workflows/publish.yml
2name: Publish
3
4on:
5 release:
6 types: [published]
7
8permissions:
9 contents: read
10 id-token: write
11
12jobs:
13 publish:
14 runs-on: ubuntu-latest
15 environment: pypi
16 steps:
17 - uses: actions/checkout@v4
18 - uses: actions/setup-python@v5
19 with:
20 python-version: "3.12"
21 - run: pip install build
22 - run: python -m build
23 - uses: pypa/gh-action-pypi-publish@release/v1延伸閱讀
返回:模組六:打包與發布