案例:自動註冊機制
案例:自動註冊機制替代方案:
本案例基於 .claude/lib/hook_validator.py 的實際程式碼,展示如何用 Metaclass 實現檢查器的自動註冊。
先備知識
問題背景
現有設計
hook_validator.py 的 HookValidator 類別包含多個 check_* 方法,需要在 validate_hook() 中手動呼叫:
1class HookValidator:
2 """Hook 合規性驗證器"""
3
4 def validate_hook(self, hook_path: str) -> ValidationResult:
5 """驗證單個 Hook 檔案"""
6 # ... 前置處理 ...
7
8 # 執行各項檢查 - 手動呼叫每個 check 方法
9 issues = []
10 issues.extend(self.check_naming_convention(hook_path))
11 issues.extend(self.check_lib_imports(content, hook_path))
12 issues.extend(self.check_output_format(content))
13 issues.extend(self.check_test_exists(hook_path))
14
15 return ValidationResult(hook_path=str(hook_path), issues=issues)
16
17 def check_naming_convention(self, hook_path: Path) -> List[ValidationIssue]:
18 """檢查命名規範"""
19 # ... 實作 ...
20
21 def check_lib_imports(self, content: str, hook_path: Path) -> List[ValidationIssue]:
22 """檢查共用模組導入"""
23 # ... 實作 ...
24
25 def check_output_format(self, content: str) -> List[ValidationIssue]:
26 """檢查 Hook 輸出格式"""
27 # ... 實作 ...
28
29 def check_test_exists(self, hook_path: Path) -> List[ValidationIssue]:
30 """檢查對應的測試檔案是否存在"""
31 # ... 實作 ...這個設計的優點
- 明確的執行順序:可以精確控制檢查項的執行順序
- 容易理解呼叫流程:閱讀
validate_hook()就知道會執行哪些檢查 - 簡單直覺:不需要學習額外的抽象概念
這個設計的限制
新增檢查項時:
- 需要修改兩處程式碼:新增
check_*方法 + 修改validate_hook()呼叫 - 容易忘記註冊:新增方法後忘記在
validate_hook()中呼叫 - 無法動態控制:無法在執行時期啟用/停用特定檢查項
- 難以擴展:子類別新增檢查項也需要覆寫
validate_hook()
進階解決方案
設計目標
- 定義檢查方法時自動註冊到執行清單
- 支援優先順序控制
- 支援動態啟用/停用特定檢查項
- 子類別的檢查項自動繼承
實作步驟
步驟 1:用裝飾器標記檢查方法
首先,我們需要一個方式來標記哪些方法是「檢查器」。裝飾器是最自然的選擇:
1from functools import wraps
2from typing import Callable, Optional
3
4def check(
5 priority: int = 100,
6 enabled: bool = True,
7 description: str = ""
8) -> Callable:
9 """
10 Decorator to mark a method as a checker.
11
12 Args:
13 priority: Execution order (lower runs first)
14 enabled: Whether this check is enabled by default
15 description: Human-readable description
16
17 Example:
18 @check(priority=10, description="Validate filename format")
19 def check_naming(self, hook_path):
20 ...
21 """
22 def decorator(func: Callable) -> Callable:
23 # Store metadata on the function object
24 func._is_checker = True
25 func._checker_priority = priority
26 func._checker_enabled = enabled
27 func._checker_description = description or func.__doc__ or func.__name__
28
29 @wraps(func)
30 def wrapper(*args, **kwargs):
31 return func(*args, **kwargs)
32
33 # Copy metadata to wrapper
34 wrapper._is_checker = True
35 wrapper._checker_priority = priority
36 wrapper._checker_enabled = enabled
37 wrapper._checker_description = description or func.__doc__ or func.__name__
38
39 return wrapper
40 return decorator使用方式:
1class Validator:
2 @check(priority=10, description="Check filename format")
3 def check_naming_convention(self, hook_path):
4 """檢查命名規範"""
5 ...
6
7 @check(priority=20, description="Check library imports")
8 def check_lib_imports(self, content, hook_path):
9 """檢查共用模組導入"""
10 ...步驟 2:用 Metaclass 收集標記的方法
接下來,用 Metaclass 在類別建立時自動收集所有被 @check 標記的方法:
1class CheckerMeta(type):
2 """
3 Metaclass that automatically collects methods marked with @check.
4
5 When a class is created, this metaclass:
6 1. Scans all methods for the _is_checker attribute
7 2. Collects them into a _checkers registry
8 3. Sorts by priority for execution order
9 """
10
11 def __new__(mcs, name: str, bases: tuple, namespace: dict):
12 # Create the class first
13 cls = super().__new__(mcs, name, bases, namespace)
14
15 # Collect checkers from parent classes
16 inherited_checkers = {}
17 for base in bases:
18 if hasattr(base, '_checkers'):
19 inherited_checkers.update(base._checkers)
20
21 # Collect checkers from current class
22 current_checkers = {}
23 for attr_name, attr_value in namespace.items():
24 if callable(attr_value) and getattr(attr_value, '_is_checker', False):
25 current_checkers[attr_name] = {
26 'method': attr_name,
27 'priority': attr_value._checker_priority,
28 'enabled': attr_value._checker_enabled,
29 'description': attr_value._checker_description,
30 }
31
32 # Merge: current class can override parent checkers
33 all_checkers = {**inherited_checkers, **current_checkers}
34
35 # Store as class attribute
36 cls._checkers = all_checkers
37
38 return cls步驟 3:實作優先順序
在 Metaclass 中已經收集了優先順序資訊,現在需要一個方法按順序執行:
1class CheckerBase(metaclass=CheckerMeta):
2 """
3 Base class for validators with auto-registration.
4
5 Provides run_all_checks() to execute registered checkers.
6 """
7
8 def get_sorted_checkers(self) -> list:
9 """
10 Get all enabled checkers sorted by priority.
11
12 Returns:
13 List of (method_name, checker_info) tuples
14 """
15 checkers = [
16 (name, info)
17 for name, info in self._checkers.items()
18 if info['enabled']
19 ]
20 # Sort by priority (lower number = higher priority)
21 return sorted(checkers, key=lambda x: x[1]['priority'])
22
23 def run_all_checks(self, *args, **kwargs) -> list:
24 """
25 Run all enabled checkers in priority order.
26
27 Args:
28 *args, **kwargs: Arguments passed to each checker
29
30 Returns:
31 Combined list of issues from all checkers
32 """
33 all_issues = []
34
35 for method_name, info in self.get_sorted_checkers():
36 method = getattr(self, method_name)
37 try:
38 issues = method(*args, **kwargs)
39 if issues:
40 all_issues.extend(issues)
41 except Exception as e:
42 # Optionally handle checker errors
43 all_issues.append({
44 'level': 'error',
45 'message': f"Checker {method_name} failed: {e}"
46 })
47
48 return all_issues步驟 4:實作啟用/停用機制
允許在執行時期動態控制檢查項:
1class CheckerBase(metaclass=CheckerMeta):
2 """
3 Base class for validators with auto-registration.
4 """
5
6 def __init__(self):
7 # Instance-level override for checker states
8 self._checker_overrides = {}
9
10 def enable_checker(self, checker_name: str) -> None:
11 """
12 Enable a specific checker for this instance.
13
14 Args:
15 checker_name: Name of the checker method
16 """
17 if checker_name not in self._checkers:
18 raise ValueError(f"Unknown checker: {checker_name}")
19 self._checker_overrides[checker_name] = True
20
21 def disable_checker(self, checker_name: str) -> None:
22 """
23 Disable a specific checker for this instance.
24
25 Args:
26 checker_name: Name of the checker method
27 """
28 if checker_name not in self._checkers:
29 raise ValueError(f"Unknown checker: {checker_name}")
30 self._checker_overrides[checker_name] = False
31
32 def is_checker_enabled(self, checker_name: str) -> bool:
33 """
34 Check if a specific checker is enabled.
35
36 Instance overrides take precedence over class defaults.
37 """
38 if checker_name in self._checker_overrides:
39 return self._checker_overrides[checker_name]
40 return self._checkers.get(checker_name, {}).get('enabled', False)
41
42 def get_sorted_checkers(self) -> list:
43 """Get all enabled checkers sorted by priority."""
44 checkers = [
45 (name, info)
46 for name, info in self._checkers.items()
47 if self.is_checker_enabled(name)
48 ]
49 return sorted(checkers, key=lambda x: x[1]['priority'])
50
51 def list_checkers(self) -> list:
52 """
53 List all available checkers with their status.
54
55 Returns:
56 List of dicts with checker information
57 """
58 result = []
59 for name, info in sorted(
60 self._checkers.items(),
61 key=lambda x: x[1]['priority']
62 ):
63 result.append({
64 'name': name,
65 'priority': info['priority'],
66 'enabled': self.is_checker_enabled(name),
67 'description': info['description'],
68 })
69 return result完整程式碼
1#!/usr/bin/env python3
2"""
3Auto-registration pattern using Metaclass.
4
5This module demonstrates how to automatically register checker methods
6using a combination of decorators and metaclasses.
7"""
8
9from dataclasses import dataclass, field
10from functools import wraps
11from pathlib import Path
12from typing import Callable, List, Optional, Any
13import re
14
15# ===== Data Classes =====
16
17@dataclass
18class ValidationIssue:
19 """Represents a validation issue."""
20 level: str # "error" | "warning" | "info"
21 message: str
22 line: Optional[int] = None
23 suggestion: Optional[str] = None
24
25@dataclass
26class ValidationResult:
27 """Validation result for a single target."""
28 target: str
29 issues: List[ValidationIssue] = field(default_factory=list)
30
31 @property
32 def is_valid(self) -> bool:
33 """True if no errors found."""
34 return not any(issue.level == "error" for issue in self.issues)
35
36# ===== Decorator =====
37
38def check(
39 priority: int = 100,
40 enabled: bool = True,
41 description: str = ""
42) -> Callable:
43 """
44 Decorator to mark a method as a checker.
45
46 Args:
47 priority: Execution order (lower runs first)
48 enabled: Whether this check is enabled by default
49 description: Human-readable description
50 """
51 def decorator(func: Callable) -> Callable:
52 @wraps(func)
53 def wrapper(*args, **kwargs):
54 return func(*args, **kwargs)
55
56 # Store metadata
57 wrapper._is_checker = True
58 wrapper._checker_priority = priority
59 wrapper._checker_enabled = enabled
60 wrapper._checker_description = description or func.__doc__ or func.__name__
61
62 return wrapper
63 return decorator
64
65# ===== Metaclass =====
66
67class CheckerMeta(type):
68 """
69 Metaclass that automatically collects @check decorated methods.
70 """
71
72 def __new__(mcs, name: str, bases: tuple, namespace: dict):
73 cls = super().__new__(mcs, name, bases, namespace)
74
75 # Inherit checkers from parent classes
76 inherited_checkers = {}
77 for base in bases:
78 if hasattr(base, '_checkers'):
79 inherited_checkers.update(base._checkers)
80
81 # Collect checkers from current class
82 current_checkers = {}
83 for attr_name, attr_value in namespace.items():
84 if callable(attr_value) and getattr(attr_value, '_is_checker', False):
85 current_checkers[attr_name] = {
86 'method': attr_name,
87 'priority': attr_value._checker_priority,
88 'enabled': attr_value._checker_enabled,
89 'description': attr_value._checker_description,
90 }
91
92 # Merge checkers
93 cls._checkers = {**inherited_checkers, **current_checkers}
94
95 return cls
96
97# ===== Base Class =====
98
99class CheckerBase(metaclass=CheckerMeta):
100 """
101 Base class providing auto-registration functionality.
102 """
103
104 def __init__(self):
105 self._checker_overrides = {}
106
107 def enable_checker(self, name: str) -> None:
108 """Enable a checker for this instance."""
109 if name not in self._checkers:
110 raise ValueError(f"Unknown checker: {name}")
111 self._checker_overrides[name] = True
112
113 def disable_checker(self, name: str) -> None:
114 """Disable a checker for this instance."""
115 if name not in self._checkers:
116 raise ValueError(f"Unknown checker: {name}")
117 self._checker_overrides[name] = False
118
119 def is_checker_enabled(self, name: str) -> bool:
120 """Check if a checker is enabled."""
121 if name in self._checker_overrides:
122 return self._checker_overrides[name]
123 return self._checkers.get(name, {}).get('enabled', False)
124
125 def get_sorted_checkers(self) -> list:
126 """Get enabled checkers sorted by priority."""
127 return sorted(
128 [(n, i) for n, i in self._checkers.items() if self.is_checker_enabled(n)],
129 key=lambda x: x[1]['priority']
130 )
131
132 def list_checkers(self) -> list:
133 """List all checkers with their status."""
134 return [
135 {
136 'name': name,
137 'priority': info['priority'],
138 'enabled': self.is_checker_enabled(name),
139 'description': info['description'],
140 }
141 for name, info in sorted(
142 self._checkers.items(),
143 key=lambda x: x[1]['priority']
144 )
145 ]
146
147# ===== Implementation Example =====
148
149class HookValidator(CheckerBase):
150 """
151 Hook validator with auto-registered checkers.
152
153 Checkers are automatically discovered and executed in priority order.
154 """
155
156 # Pattern constants
157 VALID_NAME_PATTERN = r"^[a-z0-9](/python-advanced/02-metaprogramming/case-studies/auto-registration/[a-z0-9\-_]*[a-z0-9])?\.py$"
158 HOOK_IO_PATTERNS = [
159 r"from\s+hook_io\s+import",
160 r"from\s+lib\.hook_io\s+import",
161 ]
162 BAD_OUTPUT_PATTERNS = [
163 r'print\s*\(\s*json\.dumps\s*\(',
164 ]
165
166 def __init__(self, project_root: Optional[str] = None):
167 super().__init__()
168 self.project_root = Path(project_root) if project_root else Path.cwd()
169
170 def validate(self, hook_path: str) -> ValidationResult:
171 """
172 Validate a hook file by running all enabled checkers.
173
174 Args:
175 hook_path: Path to the hook file
176
177 Returns:
178 ValidationResult with all issues found
179 """
180 path = Path(hook_path)
181 if not path.is_absolute():
182 path = self.project_root / path
183
184 # Read file content
185 try:
186 content = path.read_text(encoding='utf-8')
187 except Exception as e:
188 return ValidationResult(
189 target=str(path),
190 issues=[ValidationIssue(
191 level="error",
192 message=f"Cannot read file: {e}"
193 )]
194 )
195
196 # Prepare context for checkers
197 context = {
198 'path': path,
199 'content': content,
200 'filename': path.name,
201 }
202
203 # Run all enabled checkers
204 all_issues = []
205 for method_name, _ in self.get_sorted_checkers():
206 method = getattr(self, method_name)
207 try:
208 issues = method(context)
209 if issues:
210 all_issues.extend(issues)
211 except Exception as e:
212 all_issues.append(ValidationIssue(
213 level="error",
214 message=f"Checker '{method_name}' crashed: {e}"
215 ))
216
217 return ValidationResult(target=str(path), issues=all_issues)
218
219 @check(priority=10, description="Check filename format")
220 def check_naming_convention(self, ctx: dict) -> List[ValidationIssue]:
221 """Validate hook filename follows naming convention."""
222 issues = []
223 filename = ctx['filename']
224
225 if not re.match(self.VALID_NAME_PATTERN, filename):
226 issues.append(ValidationIssue(
227 level="warning",
228 message=f"Filename '{filename}' doesn't follow naming convention",
229 suggestion="Use snake_case or kebab-case, e.g., check_format.py"
230 ))
231
232 return issues
233
234 @check(priority=20, description="Check hook_io import")
235 def check_lib_imports(self, ctx: dict) -> List[ValidationIssue]:
236 """Check if hook_io module is properly imported."""
237 issues = []
238 content = ctx['content']
239
240 has_import = any(
241 re.search(pattern, content)
242 for pattern in self.HOOK_IO_PATTERNS
243 )
244
245 if not has_import:
246 issues.append(ValidationIssue(
247 level="warning",
248 message="hook_io module not imported",
249 suggestion="Add: from hook_io import read_hook_input, write_hook_output"
250 ))
251
252 return issues
253
254 @check(priority=30, description="Check output format")
255 def check_output_format(self, ctx: dict) -> List[ValidationIssue]:
256 """Check if proper output functions are used."""
257 issues = []
258 content = ctx['content']
259
260 for pattern in self.BAD_OUTPUT_PATTERNS:
261 if re.search(pattern, content):
262 issues.append(ValidationIssue(
263 level="warning",
264 message="Using print(json.dumps(...)) instead of write_hook_output()",
265 suggestion="Replace with: write_hook_output(output_dict)"
266 ))
267 break
268
269 return issues
270
271 @check(priority=40, enabled=False, description="Check test file exists")
272 def check_test_exists(self, ctx: dict) -> List[ValidationIssue]:
273 """Check if corresponding test file exists."""
274 issues = []
275 hook_name = ctx['path'].stem
276 test_name = f"test_{hook_name.replace('-', '_')}.py"
277
278 test_path = self.project_root / ".claude" / "lib" / "tests" / test_name
279 if not test_path.exists():
280 issues.append(ValidationIssue(
281 level="info",
282 message=f"No test file found: {test_name}",
283 suggestion=f"Create test at: {test_path}"
284 ))
285
286 return issues
287
288# ===== Extended Example: Subclass =====
289
290class StrictHookValidator(HookValidator):
291 """
292 Extended validator with additional checks.
293
294 Inherits all checks from HookValidator and adds more.
295 """
296
297 @check(priority=15, description="Check shebang line")
298 def check_shebang(self, ctx: dict) -> List[ValidationIssue]:
299 """Check if file starts with proper shebang."""
300 issues = []
301 content = ctx['content']
302
303 if not content.startswith('#!/usr/bin/env python'):
304 issues.append(ValidationIssue(
305 level="info",
306 message="Missing shebang line",
307 suggestion="Add: #!/usr/bin/env python3"
308 ))
309
310 return issues
311
312 @check(priority=25, description="Check docstring exists")
313 def check_docstring(self, ctx: dict) -> List[ValidationIssue]:
314 """Check if module has a docstring."""
315 issues = []
316 content = ctx['content']
317
318 # Simple check: look for triple quotes near the start
319 lines = content.split('\n')[:10] # First 10 lines
320 has_docstring = any('"""' in line or "'''" in line for line in lines)
321
322 if not has_docstring:
323 issues.append(ValidationIssue(
324 level="info",
325 message="Module docstring not found",
326 suggestion="Add a docstring at the top of the file"
327 ))
328
329 return issues
330
331# ===== Demo =====
332
333if __name__ == "__main__":
334 print("=" * 60)
335 print("Auto-Registration Demo")
336 print("=" * 60)
337
338 # Create validator
339 validator = HookValidator()
340
341 # List all registered checkers
342 print("\n[Registered Checkers]")
343 for checker in validator.list_checkers():
344 status = "ON" if checker['enabled'] else "OFF"
345 print(f" [{status}] {checker['name']} (priority: {checker['priority']})")
346 print(f" {checker['description']}")
347
348 # Enable disabled checker
349 print("\n[Enable check_test_exists]")
350 validator.enable_checker('check_test_exists')
351 for checker in validator.list_checkers():
352 if checker['name'] == 'check_test_exists':
353 status = "ON" if checker['enabled'] else "OFF"
354 print(f" [{status}] {checker['name']}")
355
356 # Demo with extended validator
357 print("\n[Extended Validator - StrictHookValidator]")
358 strict_validator = StrictHookValidator()
359 for checker in strict_validator.list_checkers():
360 status = "ON" if checker['enabled'] else "OFF"
361 print(f" [{status}] {checker['name']} (priority: {checker['priority']})")
362
363 print("\n" + "=" * 60)使用範例
1# Create validator instance
2validator = HookValidator(project_root="/path/to/project")
3
4# List available checkers
5for checker in validator.list_checkers():
6 print(f"[{'ON' if checker['enabled'] else 'OFF'}] {checker['name']}")
7
8# Output:
9# [ON] check_naming_convention
10# [ON] check_lib_imports
11# [ON] check_output_format
12# [OFF] check_test_exists
13
14# Enable/disable checkers dynamically
15validator.enable_checker('check_test_exists')
16validator.disable_checker('check_output_format')
17
18# Validate a hook file
19result = validator.validate(".claude/hooks/my-hook.py")
20print(f"Valid: {result.is_valid}")
21for issue in result.issues:
22 print(f" [{issue.level}] {issue.message}")
23
24# Create extended validator with more checks
25strict = StrictHookValidator()
26print(f"Total checkers: {len(strict.list_checkers())}") # Inherits parent's checkers替代方案:__init_subclass__
Python 3.6 引入了 __init_subclass__,可以在不使用 Metaclass 的情況下實現部分功能:
1class CheckerBase:
2 """
3 Base class using __init_subclass__ for auto-registration.
4
5 Simpler than metaclass, but less powerful.
6 """
7 _checkers = {}
8
9 def __init_subclass__(cls, **kwargs):
10 super().__init_subclass__(**kwargs)
11
12 # Collect checkers from this subclass
13 for attr_name in dir(cls):
14 attr = getattr(cls, attr_name, None)
15 if callable(attr) and getattr(attr, '_is_checker', False):
16 cls._checkers[attr_name] = {
17 'method': attr_name,
18 'priority': attr._checker_priority,
19 'enabled': attr._checker_enabled,
20 'description': attr._checker_description,
21 }
22
23class HookValidator(CheckerBase):
24 """Validator using __init_subclass__."""
25
26 @check(priority=10)
27 def check_naming(self, ctx):
28 """Check naming convention."""
29 pass
30
31 @check(priority=20)
32 def check_imports(self, ctx):
33 """Check imports."""
34 pass__init_subclass__ vs Metaclass
| 面向 | __init_subclass__ | Metaclass |
|---|---|---|
| 複雜度 | 低 | 高 |
| 適用場景 | 子類別註冊 | 完整控制類別建立 |
| 效能 | 較好 | 略差 |
| 修改類別 | 有限 | 完整 |
| 繼承處理 | 需手動 | 自動 |
| 推薦情況 | 優先使用 | 需要進階功能時 |
原則:如果 __init_subclass__ 能滿足需求,優先使用它。
設計權衡
| 面向 | 手動註冊 | Metaclass 自動註冊 |
|---|---|---|
| 程式碼重複 | 高(每次新增都要改兩處) | 低(只需加裝飾器) |
| 理解難度 | 低 | 中(需理解 Metaclass) |
| 擴展性 | 差(子類別需覆寫) | 好(自動繼承) |
| 除錯難度 | 低 | 中(執行流程隱含) |
| 動態控制 | 需額外實作 | 內建支援 |
| 執行順序 | 明確可見 | 由 priority 決定 |
什麼時候該用這個技術?
適合使用:
- 插件系統:需要自動發現和載入插件
- 框架開發:Django admin、pytest fixtures 等
- 大量相似元件:多個檢查器/處理器需統一管理
- 需要動態控制:執行時期啟用/停用功能
不建議使用:
- 只有少數幾個檢查項:手動維護更簡單
- 團隊不熟悉 Metaclass:增加維護負擔
- 簡單的應用程式:過度工程
- 執行順序非常重要:手動呼叫更明確
練習
基礎練習
實作一個簡單的命令註冊系統:
1class CommandRegistry:
2 """
3 命令註冊系統
4
5 需求:
6 1. 用 @command(name="xxx") 裝飾器註冊命令
7 2. 提供 execute(name, *args) 執行指定命令
8 3. 提供 list_commands() 列出所有命令
9 """
10 pass
11
12# 使用範例
13class MyApp(CommandRegistry):
14 @command(name="greet")
15 def say_hello(self, name):
16 return f"Hello, {name}!"
17
18 @command(name="add")
19 def add_numbers(self, a, b):
20 return a + b
21
22app = MyApp()
23print(app.execute("greet", "World")) # Hello, World!
24print(app.list_commands()) # ['add', 'greet']進階練習
新增檢查項的相依性管理:
1@check(priority=10, depends_on=['check_file_exists'])
2def check_content(self, ctx):
3 """只有在檔案存在檢查通過後才執行"""
4 pass提示:
- 在
@check裝飾器加入depends_on參數 - 在
run_all_checks()中追蹤每個檢查項的結果 - 檢查相依項是否通過再執行
挑戰題
實作跨模組的檢查項發現(類似 pytest):
1# checkers/naming.py
2@check(priority=10)
3def check_naming(ctx):
4 pass
5
6# checkers/imports.py
7@check(priority=20)
8def check_imports(ctx):
9 pass
10
11# main.py
12validator = HookValidator()
13validator.discover_checkers('checkers/') # 自動載入目錄下的所有檢查項提示:
- 使用
importlib動態載入模組 - 掃描模組中帶有
_is_checker標記的函式 - 將函式綁定為實例方法
延伸閱讀
上一章:宣告式驗證 下一章:類似 Django Field
#python #python-advanced #metaprogramming #metaclass #case-study