Skip to content

Commit 076c0ec

Browse files
committed
Migrate from Click to Typer + Rich
Python tools: - Replace click with typer for CLI framework - Use 'from rich import print' in all tools (shadows builtin) - Remove Console usage, just use print() for everything - Update all 9 Python scripts CI hooks: - Rename check_no_argparse.py -> check_typer_cli.py (bans argparse AND click) - Delete check_no_print.py (we now use print from rich) - Add check_rich_print.py (enforces 'from rich import print') Docs: - Update python/README.md with typer patterns and conventions - Update pyproject.toml comments - Update ci/README.md
1 parent 457a23f commit 076c0ec

17 files changed

Lines changed: 705 additions & 486 deletions

.pre-commit-config.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,16 @@ repos:
5555
exclude: ^(python/README\.md|bash/README\.md)
5656

5757
# Custom repository standards
58-
- id: check-no-argparse
59-
name: check no argparse (use click)
60-
entry: uv run --no-project python ci/check_no_argparse.py
58+
- id: check-typer-cli
59+
name: check typer CLI (no argparse/click)
60+
entry: uv run --no-project python ci/check_typer_cli.py
6161
language: system
6262
types: [python]
6363
files: ^python/
6464

65-
- id: check-no-print
66-
name: check no print() (use click.echo)
67-
entry: uv run --no-project python ci/check_no_print.py
65+
- id: check-rich-print
66+
name: check 'from rich import print'
67+
entry: uv run --no-project python ci/check_rich_print.py
6868
language: system
6969
types: [python]
7070
files: ^python/

ci/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44

55
### Python Tools
66
- `check_executable.py` - Scripts must be executable
7-
- `check_no_argparse.py` - Use `click`, not `argparse`
8-
- `check_no_print.py` - Use `click.echo()`, not `print()`
7+
- `check_typer_cli.py` - Use `typer`, not `argparse` or `click`
8+
- `check_rich_print.py` - Require `from rich import print`
99
- `check_main_guard.py` - Require `if __name__ == "__main__":`
1010
- `check_no_any.py` - Ban `typing.Any`
11-
- `check_help.py` - Ensure scripts support `--help`
1211

1312
### HTML Tools
1413
- `check_html_metadata.py` - Require frontmatter with category, `<title>`, and `<p class="subtitle">`
@@ -20,7 +19,8 @@
2019
uvx pre-commit run --all-files
2120

2221
# Test individual hook
23-
python ci/check_no_argparse.py python/tool.py
22+
python ci/check_typer_cli.py python/tool.py
23+
python ci/check_rich_print.py python/tool.py
2424
```
2525

2626
All hooks run automatically via `.pre-commit-config.yaml`.

ci/check_no_argparse.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

ci/check_no_print.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

ci/check_rich_print.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env python3
2+
"""Check that Python tools import print from rich."""
3+
4+
import ast
5+
import sys
6+
from pathlib import Path
7+
8+
9+
def has_rich_print_import(tree: ast.Module) -> bool:
10+
"""Check if file has 'from rich import print' statement."""
11+
for node in tree.body:
12+
if (
13+
isinstance(node, ast.ImportFrom)
14+
and node.module == "rich"
15+
and any(alias.name == "print" for alias in node.names)
16+
):
17+
return True
18+
return False
19+
20+
21+
def check_file(filepath: Path) -> bool:
22+
"""Check if file imports print from rich."""
23+
try:
24+
content = filepath.read_text()
25+
tree = ast.parse(content, filename=str(filepath))
26+
27+
if not has_rich_print_import(tree):
28+
print(f"{filepath}: missing 'from rich import print'")
29+
return False
30+
31+
return True
32+
except SyntaxError as e:
33+
print(f"{filepath}: syntax error: {e}")
34+
return False
35+
36+
37+
def main() -> int:
38+
"""Check all Python tool files passed as arguments."""
39+
files = [Path(f) for f in sys.argv[1:]]
40+
# Only check files in python/ directory
41+
python_tools = [
42+
f
43+
for f in files
44+
if f.suffix == ".py"
45+
and f.is_file()
46+
and "python/" in str(f)
47+
and not f.name.startswith("test_")
48+
]
49+
50+
if not python_tools:
51+
return 0
52+
53+
all_good = True
54+
for filepath in python_tools:
55+
if not check_file(filepath):
56+
all_good = False
57+
58+
return 0 if all_good else 1
59+
60+
61+
if __name__ == "__main__":
62+
sys.exit(main())
63+

ci/check_typer_cli.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env python3
2+
"""Check that Python files use typer instead of argparse or click."""
3+
4+
import ast
5+
import sys
6+
from pathlib import Path
7+
8+
BANNED_MODULES = {
9+
"argparse": "use typer instead",
10+
"click": "use typer instead",
11+
}
12+
13+
14+
def check_file(filepath: Path) -> bool:
15+
"""Check if file imports banned CLI modules."""
16+
try:
17+
content = filepath.read_text()
18+
tree = ast.parse(content, filename=str(filepath))
19+
20+
all_good = True
21+
for node in ast.walk(tree):
22+
if isinstance(node, ast.Import):
23+
for alias in node.names:
24+
if alias.name in BANNED_MODULES:
25+
reason = BANNED_MODULES[alias.name]
26+
print(f"{filepath}:{node.lineno}: imports {alias.name} ({reason})")
27+
all_good = False
28+
elif isinstance(node, ast.ImportFrom) and node.module in BANNED_MODULES:
29+
reason = BANNED_MODULES[node.module]
30+
print(f"{filepath}:{node.lineno}: imports from {node.module} ({reason})")
31+
all_good = False
32+
33+
return all_good
34+
except SyntaxError as e:
35+
print(f"{filepath}: syntax error: {e}")
36+
return False
37+
38+
39+
def main() -> int:
40+
"""Check all Python files passed as arguments."""
41+
files = [Path(f) for f in sys.argv[1:]]
42+
# Only check files in python/ directory (not ci/ scripts)
43+
python_files = [
44+
f
45+
for f in files
46+
if f.suffix == ".py"
47+
and f.is_file()
48+
and "python/" in str(f)
49+
]
50+
51+
if not python_files:
52+
return 0
53+
54+
all_good = True
55+
for filepath in python_files:
56+
if not check_file(filepath):
57+
all_good = False
58+
59+
return 0 if all_good else 1
60+
61+
62+
if __name__ == "__main__":
63+
sys.exit(main())

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ strict_concatenate = true
2626

2727
# Explicitly ban Any
2828
disallow_any_unimported = true
29-
disallow_any_expr = false # Too strict, would break click decorators
30-
disallow_any_decorated = false # Too strict for click
29+
disallow_any_expr = false # Too strict, would break typer Annotated types
30+
disallow_any_decorated = false # Too strict for typer
3131
disallow_any_explicit = true # Ban explicit Any usage
3232

3333
# Additional strictness

0 commit comments

Comments
 (0)