6.2 如何擴展共用模組
6.2 如何擴展共用模組
步驟 3:更新
本章介紹如何為 .claude/lib/ 共用程式庫添加新功能。這是維護和擴展 Hook 系統的關鍵技能。
前置知識
建議先閱讀:
共用模組架構
目前的 .claude/lib/ 結構:
1.claude/lib/
2├── __init__.py # 模組初始化與匯出
3├── git_utils.py # Git 操作工具
4├── hook_logging.py # 日誌系統
5├── hook_io.py # 輸入輸出處理
6├── config_loader.py # 配置載入器
7├── hook_validator.py # Hook 驗證器
8└── markdown_link_checker.py # Markdown 連結檢查步驟 1:規劃新模組
決定放置位置
| 情況 | 建議 |
|---|---|
| 新的獨立功能 | 建立新檔案 |
| 擴展現有功能 | 修改現有檔案 |
| 工具函式 | 加入相關現有模組 |
設計介面
在寫程式碼之前,先規劃公開介面:
1# 思考要提供什麼功能給使用者
2
3# 函式:簡單操作
4def validate_yaml(content: str) -> bool:
5 """驗證 YAML 格式"""
6 pass
7
8# 類別:複雜操作或需要狀態
9class YamlValidator:
10 """YAML 驗證器"""
11
12 def __init__(self, strict: bool = True):
13 """初始化"""
14 pass
15
16 def validate(self, content: str) -> ValidationResult:
17 """驗證內容"""
18 pass步驟 2:實作新模組
範例:建立 YAML 工具模組
1# .claude/lib/yaml_utils.py
2"""
3YAML 工具模組
4
5提供 YAML 檔案的讀取、驗證和處理功能。
6"""
7
8from pathlib import Path
9from typing import Optional, Any
10from dataclasses import dataclass
11
12# 嘗試導入 yaml
13try:
14 import yaml
15 HAS_YAML = True
16except ImportError:
17 HAS_YAML = False
18
19
20@dataclass
21class YamlResult:
22 """YAML 處理結果"""
23 success: bool
24 data: Optional[dict] = None
25 error: Optional[str] = None
26
27
28def load_yaml(path: str, encoding: str = "utf-8") -> YamlResult:
29 """
30 載入 YAML 檔案
31
32 Args:
33 path: 檔案路徑
34 encoding: 檔案編碼
35
36 Returns:
37 YamlResult: 載入結果
38
39 Example:
40 result = load_yaml("config.yaml")
41 if result.success:
42 print(result.data)
43 else:
44 print(f"錯誤: {result.error}")
45 """
46 if not HAS_YAML:
47 return YamlResult(
48 success=False,
49 error="PyYAML 未安裝。請執行: pip install pyyaml"
50 )
51
52 file_path = Path(path)
53
54 if not file_path.exists():
55 return YamlResult(
56 success=False,
57 error=f"檔案不存在: {path}"
58 )
59
60 try:
61 content = file_path.read_text(encoding=encoding)
62 data = yaml.safe_load(content)
63 return YamlResult(success=True, data=data or {})
64 except yaml.YAMLError as e:
65 return YamlResult(success=False, error=f"YAML 解析錯誤: {e}")
66
67
68def validate_yaml(content: str) -> bool:
69 """
70 驗證 YAML 格式
71
72 Args:
73 content: YAML 內容字串
74
75 Returns:
76 bool: 格式正確返回 True
77 """
78 if not HAS_YAML:
79 return False
80
81 try:
82 yaml.safe_load(content)
83 return True
84 except yaml.YAMLError:
85 return False
86
87
88def merge_yaml_configs(*configs: dict) -> dict:
89 """
90 合併多個 YAML 配置
91
92 後面的配置會覆蓋前面的。
93
94 Args:
95 *configs: 要合併的配置字典
96
97 Returns:
98 dict: 合併後的配置
99
100 Example:
101 base = {"a": 1, "b": 2}
102 override = {"b": 3, "c": 4}
103 result = merge_yaml_configs(base, override)
104 # result = {"a": 1, "b": 3, "c": 4}
105 """
106 result = {}
107 for config in configs:
108 if config:
109 _deep_merge(result, config)
110 return result
111
112
113def _deep_merge(base: dict, override: dict) -> None:
114 """深度合併字典(就地修改 base)"""
115 for key, value in override.items():
116 if (
117 key in base
118 and isinstance(base[key], dict)
119 and isinstance(value, dict)
120 ):
121 _deep_merge(base[key], value)
122 else:
123 base[key] = value步驟 3:更新 __init__.py
在 __init__.py 中註冊新模組:
1# .claude/lib/__init__.py
2"""
3Claude Hooks 共用程式庫
4"""
5
6# 現有匯入
7from .git_utils import (
8 run_git_command,
9 get_current_branch,
10 # ...
11)
12
13from .hook_logging import setup_hook_logging
14from .hook_io import read_hook_input, write_hook_output
15
16# 新增:YAML 工具
17from .yaml_utils import (
18 load_yaml,
19 validate_yaml,
20 merge_yaml_configs,
21 YamlResult,
22)
23
24__all__ = [
25 # 現有匯出
26 "run_git_command",
27 "get_current_branch",
28 "setup_hook_logging",
29 "read_hook_input",
30 "write_hook_output",
31 # 新增
32 "load_yaml",
33 "validate_yaml",
34 "merge_yaml_configs",
35 "YamlResult",
36]
37
38__version__ = "0.29.0" # 更新版本步驟 4:撰寫測試
1# tests/lib/test_yaml_utils.py
2"""
3YAML 工具模組測試
4"""
5
6import unittest
7from unittest.mock import patch, mock_open
8import sys
9from pathlib import Path
10
11# 添加 lib 目錄到路徑
12sys.path.insert(0, str(Path(__file__).parent.parent.parent / ".claude" / "lib"))
13
14from yaml_utils import load_yaml, validate_yaml, merge_yaml_configs, YamlResult
15
16
17class TestLoadYaml(unittest.TestCase):
18 """測試 load_yaml 函式"""
19
20 def test_load_valid_yaml(self):
21 """測試載入有效的 YAML 檔案"""
22 yaml_content = "key: value\nlist:\n - item1\n - item2"
23
24 with patch("pathlib.Path.exists", return_value=True):
25 with patch("pathlib.Path.read_text", return_value=yaml_content):
26 result = load_yaml("test.yaml")
27
28 self.assertTrue(result.success)
29 self.assertEqual(result.data["key"], "value")
30 self.assertEqual(len(result.data["list"]), 2)
31
32 def test_load_nonexistent_file(self):
33 """測試載入不存在的檔案"""
34 with patch("pathlib.Path.exists", return_value=False):
35 result = load_yaml("nonexistent.yaml")
36
37 self.assertFalse(result.success)
38 self.assertIn("不存在", result.error)
39
40 def test_load_invalid_yaml(self):
41 """測試載入無效的 YAML"""
42 invalid_content = "key: [invalid yaml"
43
44 with patch("pathlib.Path.exists", return_value=True):
45 with patch("pathlib.Path.read_text", return_value=invalid_content):
46 result = load_yaml("invalid.yaml")
47
48 self.assertFalse(result.success)
49 self.assertIn("解析錯誤", result.error)
50
51
52class TestValidateYaml(unittest.TestCase):
53 """測試 validate_yaml 函式"""
54
55 def test_valid_yaml(self):
56 """測試有效的 YAML"""
57 self.assertTrue(validate_yaml("key: value"))
58 self.assertTrue(validate_yaml("list:\n - a\n - b"))
59
60 def test_invalid_yaml(self):
61 """測試無效的 YAML"""
62 self.assertFalse(validate_yaml("key: [unclosed"))
63 self.assertFalse(validate_yaml(" bad indent\nkey: value"))
64
65
66class TestMergeYamlConfigs(unittest.TestCase):
67 """測試 merge_yaml_configs 函式"""
68
69 def test_simple_merge(self):
70 """測試簡單合併"""
71 base = {"a": 1, "b": 2}
72 override = {"b": 3, "c": 4}
73
74 result = merge_yaml_configs(base, override)
75
76 self.assertEqual(result["a"], 1)
77 self.assertEqual(result["b"], 3) # 被覆蓋
78 self.assertEqual(result["c"], 4)
79
80 def test_deep_merge(self):
81 """測試深度合併"""
82 base = {
83 "database": {
84 "host": "localhost",
85 "port": 5432
86 }
87 }
88 override = {
89 "database": {
90 "port": 3306,
91 "name": "mydb"
92 }
93 }
94
95 result = merge_yaml_configs(base, override)
96
97 self.assertEqual(result["database"]["host"], "localhost")
98 self.assertEqual(result["database"]["port"], 3306)
99 self.assertEqual(result["database"]["name"], "mydb")
100
101 def test_multiple_configs(self):
102 """測試多個配置合併"""
103 config1 = {"a": 1}
104 config2 = {"b": 2}
105 config3 = {"c": 3}
106
107 result = merge_yaml_configs(config1, config2, config3)
108
109 self.assertEqual(result, {"a": 1, "b": 2, "c": 3})
110
111
112if __name__ == "__main__":
113 unittest.main()步驟 5:更新文件
在模組文檔中說明
在模組開頭加入詳細的 docstring:
1"""
2YAML 工具模組
3
4提供 YAML 檔案的讀取、驗證和處理功能。
5
6主要功能:
7- load_yaml: 載入 YAML 檔案
8- validate_yaml: 驗證 YAML 格式
9- merge_yaml_configs: 合併配置
10
11依賴:
12- PyYAML (可選,但建議安裝)
13
14使用方式:
15 from lib.yaml_utils import load_yaml, validate_yaml
16
17 # 載入配置
18 result = load_yaml("config.yaml")
19 if result.success:
20 config = result.data
21
22 # 驗證格式
23 is_valid = validate_yaml(content)
24
25版本: 0.29.0
26"""擴展現有模組
範例:為 git_utils 添加新功能
1# 在 .claude/lib/git_utils.py 中添加
2
3def get_uncommitted_changes() -> list[str]:
4 """
5 取得未提交的變更檔案列表
6
7 Returns:
8 list[str]: 變更檔案的路徑列表
9
10 Example:
11 changes = get_uncommitted_changes()
12 if changes:
13 print(f"有 {len(changes)} 個未提交的變更")
14 """
15 success, output = run_git_command(["status", "--porcelain"])
16
17 if not success:
18 return []
19
20 files = []
21 for line in output.strip().split("\n"):
22 if line.strip():
23 # 格式: "XY filename" 或 "XY filename -> newname"
24 parts = line[3:].split(" -> ")
25 files.append(parts[-1])
26
27 return files
28
29
30def has_staged_changes() -> bool:
31 """
32 檢查是否有已暫存的變更
33
34 Returns:
35 bool: 有暫存變更返回 True
36 """
37 success, output = run_git_command(["diff", "--cached", "--name-only"])
38 return success and bool(output.strip())然後在 __init__.py 中更新匯出:
1from .git_utils import (
2 run_git_command,
3 get_current_branch,
4 get_project_root,
5 get_worktree_list,
6 is_protected_branch,
7 is_allowed_branch,
8 # 新增
9 get_uncommitted_changes,
10 has_staged_changes,
11)
12
13__all__ = [
14 # ... 現有匯出 ...
15 "get_uncommitted_changes",
16 "has_staged_changes",
17]設計原則
1. 單一職責
每個模組專注一個領域:
1# 好:專注於 Git 操作
2# git_utils.py
3def get_current_branch(): ...
4def run_git_command(): ...
5def is_protected_branch(): ...
6
7# 不好:混合不同功能
8# utils.py
9def get_current_branch(): ...
10def validate_yaml(): ...
11def send_notification(): ...2. 統一的返回值模式
使用一致的返回值設計:
1# 簡單操作:返回 (bool, str)
2def run_command(cmd: str) -> tuple[bool, str]:
3 """返回 (成功與否, 輸出或錯誤訊息)"""
4 pass
5
6# 複雜操作:返回 dataclass
7@dataclass
8class OperationResult:
9 success: bool
10 data: Optional[Any] = None
11 error: Optional[str] = None
12
13def complex_operation() -> OperationResult:
14 pass3. 優雅的依賴處理
1# 處理可選依賴
2try:
3 import yaml
4 HAS_YAML = True
5except ImportError:
6 HAS_YAML = False
7
8def validate_yaml(content: str) -> bool:
9 if not HAS_YAML:
10 raise ImportError("需要安裝 PyYAML: pip install pyyaml")
11 # ...4. 完整的文檔字串
1def load_config(name: str) -> dict:
2 """
3 載入配置檔案
4
5 Args:
6 name: 配置名稱(不含副檔名)
7
8 Returns:
9 dict: 配置內容
10
11 Raises:
12 FileNotFoundError: 配置檔案不存在
13
14 Example:
15 config = load_config("agents")
16 print(config["known_agents"])
17 """完整檢查清單
擴展共用模組時的檢查項目:
- 設計清晰的公開介面
- 使用 Type Hints
- 撰寫完整的 docstring
- 處理可選依賴
- 統一返回值模式
- 更新
__init__.py - 更新
__all__匯出 - 更新版本號
- 撰寫單元測試
- 測試與現有 Hook 的整合
思考題
- 什麼時候應該建立新模組,什麼時候應該擴展現有模組?
- 如何設計 API 使其易於測試?
__all__的作用是什麼?為什麼要維護它?
上一章:如何新增一個 Hook 下一章:如何新增語言解析器 回到首頁:Python 維護工程師實戰指南