modern-python-guidance 0.4.4__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/CHANGELOG.md +25 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/PKG-INFO +29 -1
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/README.md +28 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/docs/design.md +8 -4
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/pyproject.toml +2 -1
- modern_python_guidance-0.5.0/rules/modern-python.md +31 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/__init__.py +1 -1
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/cli.py +74 -1
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/mcp_server.py +29 -5
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/setup_cmd.py +38 -13
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_cli_integration.py +83 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_cli_unit.py +104 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_guide_structure.py +19 -13
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_mcp_unit.py +15 -5
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_setup.py +15 -0
- modern_python_guidance-0.4.4/rules/modern-python.md +0 -70
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/.github/workflows/check-python-release.yml +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/.github/workflows/ci.yml +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/.gitignore +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/CONTRIBUTING.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/LICENSE +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/SECURITY.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompts/v5-a-detailed.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompts/v5-a-normal.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompts/v5-a-terse.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompts/v5-b-detailed.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompts/v5-b-normal.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompts/v5-b-terse.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompts/v5-c-detailed.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompts/v5-c-normal.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/prompts/v5-c-terse.txt +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/run-v5.sh +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/run.sh +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/score.sh +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/score_v5.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/docs/benchmark-v5.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/template-strings.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/check.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/frontmatter.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/guide_index.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/retrieve.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/search.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/uninstall_cmd.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/version_detect.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_check.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_compat.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_frontmatter.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_guide_index.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_mcp_server.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_retrieve.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_scorer_v5.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_search.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_skill_sync.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_uninstall.py +0 -0
- {modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/tests/test_version_detect.py +0 -0
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.0] — 2026-06-06
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Rules file (`rules/modern-python.md`) rewritten from full content (70 lines) to thin format (~30 lines): category index with all 41 guide IDs, top-5 high-frequency one-liner patterns, and MCP/CLI call-to-action. When the Rules file freezes as a static copy in git-tracked workspaces (symlink-to-file degradation), the thin format causes minimal stale-content damage — guide IDs rarely change. Run `mpg setup` to update the Rules file. (closes #109)
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `mpg hook claude-post-tool-use` subcommand: PostToolUse hook that reads stdin JSON from Claude Code, checks `.py` files for outdated patterns via `check_file()`, and surfaces findings as stderr feedback (exit 2). Non-Python files, missing files, and clean files produce no output (exit 0). No jq or shell wrapper required.
|
|
14
|
+
- `mpg check --quiet` flag: suppresses "No outdated patterns found." output on clean files in human format. JSON format is unaffected.
|
|
15
|
+
- `mpg setup` now prints a PostToolUse hook hint after successful setup.
|
|
16
|
+
- README: new "Recommended hooks" section with copy-pasteable `.claude/settings.json` example.
|
|
17
|
+
- 29 new tests (1028 total).
|
|
18
|
+
|
|
19
|
+
## [0.4.5] — 2026-06-06
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- MCP `retrieve_guides` maxItems hardcoded to 41: replaced with `_guide_limit()` that derives the limit from actual guide count at runtime. `_get_tools()` dynamically injects `maxItems` and description into the `tools/list` schema. Adding new guides no longer requires updating `mcp_server.py`. (closes #98)
|
|
24
|
+
- `docs/design.md` out of sync with v0.4.4 implementation: consolidated overlapping non-goals, added `check`/`setup`/`uninstall` to CLI architecture diagram, added `check.py`, `setup_cmd.py`, `uninstall_cmd.py`, `mcp_server.py` to module responsibility table, fixed Layer 1 guide count from 16 to 18. (closes #99)
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- 1 new test (1000 total).
|
|
29
|
+
|
|
5
30
|
## [0.4.4] — 2026-06-05
|
|
6
31
|
|
|
7
32
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modern-python-guidance
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Version-aware BAD/GOOD pattern guides that help AI coding agents generate modern Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/yottayoshida/modern-python-guidance
|
|
6
6
|
Project-URL: Repository, https://github.com/yottayoshida/modern-python-guidance
|
|
@@ -176,6 +176,34 @@ mpg list --python-version 3.9
|
|
|
176
176
|
# Excludes: TaskGroup (3.11+), match/case (3.10+), etc.
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
## Recommended hooks
|
|
180
|
+
|
|
181
|
+
Add a [PostToolUse hook](https://docs.anthropic.com/en/docs/claude-code/hooks) to auto-check Python files whenever Claude edits them. Create or update `.claude/settings.json` in your project:
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"hooks": {
|
|
186
|
+
"PostToolUse": [
|
|
187
|
+
{
|
|
188
|
+
"matcher": "tool == \"Edit\" || tool == \"Write\" || tool == \"MultiEdit\"",
|
|
189
|
+
"hooks": [
|
|
190
|
+
{
|
|
191
|
+
"type": "command",
|
|
192
|
+
"command": "mpg hook claude-post-tool-use"
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The hook reads stdin from Claude Code, checks any `.py` file for outdated patterns, and surfaces findings as inline feedback. Non-Python files and clean files produce no output.
|
|
202
|
+
|
|
203
|
+
Verify with `/hooks` in Claude Code to confirm the hook is active.
|
|
204
|
+
|
|
205
|
+
For manual CLI use, `mpg check --quiet <file>` provides the same check without the "no patterns found" message.
|
|
206
|
+
|
|
179
207
|
## Development
|
|
180
208
|
|
|
181
209
|
```bash
|
|
@@ -144,6 +144,34 @@ mpg list --python-version 3.9
|
|
|
144
144
|
# Excludes: TaskGroup (3.11+), match/case (3.10+), etc.
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
## Recommended hooks
|
|
148
|
+
|
|
149
|
+
Add a [PostToolUse hook](https://docs.anthropic.com/en/docs/claude-code/hooks) to auto-check Python files whenever Claude edits them. Create or update `.claude/settings.json` in your project:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"hooks": {
|
|
154
|
+
"PostToolUse": [
|
|
155
|
+
{
|
|
156
|
+
"matcher": "tool == \"Edit\" || tool == \"Write\" || tool == \"MultiEdit\"",
|
|
157
|
+
"hooks": [
|
|
158
|
+
{
|
|
159
|
+
"type": "command",
|
|
160
|
+
"command": "mpg hook claude-post-tool-use"
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The hook reads stdin from Claude Code, checks any `.py` file for outdated patterns, and surfaces findings as inline feedback. Non-Python files and clean files produce no output.
|
|
170
|
+
|
|
171
|
+
Verify with `/hooks` in Claude Code to confirm the hook is active.
|
|
172
|
+
|
|
173
|
+
For manual CLI use, `mpg check --quiet <file>` provides the same check without the "no patterns found" message.
|
|
174
|
+
|
|
147
175
|
## Development
|
|
148
176
|
|
|
149
177
|
```bash
|
|
@@ -14,16 +14,16 @@ LLMs frequently generate outdated Python patterns: `typing.List` instead of `lis
|
|
|
14
14
|
|
|
15
15
|
## Non-goals
|
|
16
16
|
|
|
17
|
-
- Automated code transformation (
|
|
17
|
+
- Automated code transformation / codemod (the `check` command scans for outdated patterns via regex + tokenize, but does not rewrite code)
|
|
18
18
|
- Language Server Protocol integration
|
|
19
|
-
- Pattern detection in source code (no AST analysis)
|
|
20
19
|
|
|
21
20
|
## Architecture
|
|
22
21
|
|
|
23
22
|
```
|
|
24
23
|
┌─────────────────────────────────────────────────────────┐
|
|
25
24
|
│ CLI (cli.py) │
|
|
26
|
-
│ search │ retrieve │ list │ detect-version
|
|
25
|
+
│ search │ retrieve │ list │ detect-version │ check │
|
|
26
|
+
│ setup │ uninstall │
|
|
27
27
|
├─────────┴──────────┴──────┴─────────────────────────────┤
|
|
28
28
|
│ │
|
|
29
29
|
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
|
|
@@ -67,6 +67,10 @@ LLMs frequently generate outdated Python patterns: `typing.List` instead of `lis
|
|
|
67
67
|
| `retrieve.py` | Renders guide content as JSON with version-match flag and token estimate |
|
|
68
68
|
| `version_detect.py` | Detects target Python version from `--python-version` flag, `pyproject.toml` (`requires-python`), `.python-version` file, or default (3.11) |
|
|
69
69
|
| `compat.py` | `version_compatible()` using `packaging.specifiers` and `token_estimate()` (chars / 4) |
|
|
70
|
+
| `check.py` | Scan a Python file for outdated patterns against guide definitions (regex + tokenize, not AST) |
|
|
71
|
+
| `setup_cmd.py` | Automate MCP server registration and Agent Skills symlink creation |
|
|
72
|
+
| `uninstall_cmd.py` | Reverse `mpg setup`: deregister the MCP server and remove the Skills symlink |
|
|
73
|
+
| `mcp_server.py` | MCP server — JSON-RPC 2.0 over stdio, zero external dependencies |
|
|
70
74
|
|
|
71
75
|
## Guide format
|
|
72
76
|
|
|
@@ -197,7 +201,7 @@ The CLI defaults to JSON when piped and human-readable when attached to a TTY. T
|
|
|
197
201
|
|
|
198
202
|
| Layer | Scope | Categories | Count |
|
|
199
203
|
|-------|-------|-----------|-------|
|
|
200
|
-
| 1 — stdlib | Python standard library | typing, async, stdlib, data-structures |
|
|
204
|
+
| 1 — stdlib | Python standard library | typing, async, stdlib, data-structures | 18 |
|
|
201
205
|
| 2 — frameworks | Third-party frameworks | pydantic, fastapi, httpx, django, sqlalchemy, pytest | 18 |
|
|
202
206
|
| 3 — toolchain | Development tools | toolchain | 5 |
|
|
203
207
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "modern-python-guidance"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Version-aware BAD/GOOD pattern guides that help AI coding agents generate modern Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0 OR MIT"
|
|
@@ -65,6 +65,7 @@ select = ["E", "F", "W", "I", "UP", "FURB", "B", "SIM", "RUF"]
|
|
|
65
65
|
|
|
66
66
|
[tool.ruff.lint.per-file-ignores]
|
|
67
67
|
"bench/*.py" = ["SIM102", "SIM110"]
|
|
68
|
+
"src/modern_python_guidance/setup_cmd.py" = ["E501"]
|
|
68
69
|
|
|
69
70
|
[tool.pytest.ini_options]
|
|
70
71
|
testpaths = ["tests"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths: ["**/*.py", "*.py", "**/pyproject.toml", "**/requirements*.txt", "**/setup.py", "**/setup.cfg", "**/.python-version", "**/Pipfile"]
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Modern Python Guidance
|
|
6
|
+
|
|
7
|
+
Pre-generation design patterns that Ruff cannot auto-fix. Apply these transformations when writing or reviewing Python code.
|
|
8
|
+
|
|
9
|
+
## Embedded patterns (high-frequency, Ruff-uncovered)
|
|
10
|
+
|
|
11
|
+
- `from typing import List, Dict` → `list[str]`, `dict[str, int]` (>=3.9)
|
|
12
|
+
- `@validator("f")` → `@field_validator("f")` (Pydantic V2)
|
|
13
|
+
- `datetime.utcnow()` → `datetime.now(UTC)` (>=3.11)
|
|
14
|
+
- `session.query(User).filter()` → `session.execute(select(User).where())` (SQLAlchemy 2.0)
|
|
15
|
+
- `subprocess.run(f"cmd {arg}", shell=True)` → `subprocess.run(["cmd", arg], check=True)`
|
|
16
|
+
|
|
17
|
+
## All 41 guides by category
|
|
18
|
+
|
|
19
|
+
- **async** (3): `async-timeout-context`, `exception-groups`, `taskgroup-over-gather`
|
|
20
|
+
- **data-structures** (3): `dataclass-modern`, `dict-merge-operator`, `match-case-patterns`
|
|
21
|
+
- **django** (3): `django-async-views`, `django-check-constraints`, `django-json-field`
|
|
22
|
+
- **fastapi** (3): `fastapi-annotated-depends`, `fastapi-lifespan`, `fastapi-typed-state`
|
|
23
|
+
- **httpx** (2): `httpx-async-client-reuse`, `httpx-streaming`
|
|
24
|
+
- **pydantic** (4): `pydantic-v2-config`, `pydantic-v2-model-api`, `pydantic-v2-serialization`, `pydantic-v2-validators`
|
|
25
|
+
- **pytest** (3): `pytest-parametrize`, `pytest-raises-match`, `pytest-tmp-path`
|
|
26
|
+
- **sqlalchemy** (3): `sqlalchemy-2-style`, `sqlalchemy-async-session`, `sqlalchemy-mapped-column`
|
|
27
|
+
- **stdlib** (5): `datetime-utc`, `pathlib-over-os-path`, `removeprefix-removesuffix`, `template-strings`, `tomllib-builtin`
|
|
28
|
+
- **toolchain** (5): `no-pickle`, `pyproject-toml-over-setup`, `ruff-over-flake8`, `safe-subprocess`, `uv-over-pip`
|
|
29
|
+
- **typing** (7): `deferred-annotations`, `override-decorator`, `paramspec-decorators`, `type-parameter-syntax`, `typeis-vs-typeguard`, `union-syntax`, `use-builtin-generics`
|
|
30
|
+
|
|
31
|
+
For full code examples, use `mpg retrieve <guide-id>` or MCP tool `retrieve_guides`.
|
{modern_python_guidance-0.4.4 → modern_python_guidance-0.5.0}/src/modern_python_guidance/cli.py
RENAMED
|
@@ -130,6 +130,22 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
130
130
|
action="store_true",
|
|
131
131
|
help="Always exit 0 even when patterns are found",
|
|
132
132
|
)
|
|
133
|
+
p_check.add_argument(
|
|
134
|
+
"--quiet",
|
|
135
|
+
action="store_true",
|
|
136
|
+
help="Suppress output when no patterns are found (human format only)",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# hook
|
|
140
|
+
p_hook = subparsers.add_parser(
|
|
141
|
+
"hook",
|
|
142
|
+
help="Claude Code hook subcommands",
|
|
143
|
+
)
|
|
144
|
+
hook_sub = p_hook.add_subparsers(dest="hook_name")
|
|
145
|
+
hook_sub.add_parser(
|
|
146
|
+
"claude-post-tool-use",
|
|
147
|
+
help="PostToolUse hook: check .py files from stdin JSON",
|
|
148
|
+
)
|
|
133
149
|
|
|
134
150
|
args = parser.parse_args(argv)
|
|
135
151
|
|
|
@@ -158,6 +174,8 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
158
174
|
_cmd_uninstall(args)
|
|
159
175
|
elif args.command == "check":
|
|
160
176
|
_cmd_check(args)
|
|
177
|
+
elif args.command == "hook":
|
|
178
|
+
_cmd_hook(args)
|
|
161
179
|
except BrokenPipeError:
|
|
162
180
|
sys.exit(0)
|
|
163
181
|
|
|
@@ -341,7 +359,7 @@ def _cmd_check(args: argparse.Namespace) -> None:
|
|
|
341
359
|
|
|
342
360
|
if fmt == "json":
|
|
343
361
|
_check_json(matches, args.file)
|
|
344
|
-
|
|
362
|
+
elif not (args.quiet and not matches):
|
|
345
363
|
_check_human(matches)
|
|
346
364
|
|
|
347
365
|
if matches and not args.exit_zero:
|
|
@@ -395,3 +413,58 @@ def _check_human(matches: list[CheckMatch]) -> None:
|
|
|
395
413
|
f"\n{len(matches)} outdated pattern{ps} found ({unique} guide{gs}). "
|
|
396
414
|
f"Run `mpg retrieve {ids}` for details."
|
|
397
415
|
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _cmd_hook(args: argparse.Namespace) -> None:
|
|
419
|
+
if not args.hook_name:
|
|
420
|
+
print("usage: modern-python-guidance hook <name>", file=sys.stderr)
|
|
421
|
+
print("available hooks: claude-post-tool-use", file=sys.stderr)
|
|
422
|
+
sys.exit(2)
|
|
423
|
+
if args.hook_name == "claude-post-tool-use":
|
|
424
|
+
_hook_post_tool_use()
|
|
425
|
+
else:
|
|
426
|
+
print(f"unknown hook: {args.hook_name}", file=sys.stderr)
|
|
427
|
+
print("available hooks: claude-post-tool-use", file=sys.stderr)
|
|
428
|
+
sys.exit(2)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _hook_post_tool_use() -> None:
|
|
432
|
+
try:
|
|
433
|
+
data = json.load(sys.stdin)
|
|
434
|
+
except (json.JSONDecodeError, ValueError):
|
|
435
|
+
sys.exit(0)
|
|
436
|
+
|
|
437
|
+
try:
|
|
438
|
+
file_path = data["tool_input"]["file_path"]
|
|
439
|
+
except (KeyError, TypeError):
|
|
440
|
+
sys.exit(0)
|
|
441
|
+
|
|
442
|
+
if not isinstance(file_path, str) or not file_path.lower().endswith(".py"):
|
|
443
|
+
sys.exit(0)
|
|
444
|
+
|
|
445
|
+
path = Path(file_path)
|
|
446
|
+
if not path.is_file():
|
|
447
|
+
sys.exit(0)
|
|
448
|
+
|
|
449
|
+
index = build_index()
|
|
450
|
+
try:
|
|
451
|
+
matches = check_file(path, index)
|
|
452
|
+
except CheckError:
|
|
453
|
+
sys.exit(0)
|
|
454
|
+
|
|
455
|
+
if not matches:
|
|
456
|
+
sys.exit(0)
|
|
457
|
+
|
|
458
|
+
for m in matches:
|
|
459
|
+
src = sanitize_line(m.source_line.strip())
|
|
460
|
+
print(
|
|
461
|
+
f"mpg: {m.guide_id} (line {m.line}): {src}",
|
|
462
|
+
file=sys.stderr,
|
|
463
|
+
)
|
|
464
|
+
guide_ids = sorted({m.guide_id for m in matches})
|
|
465
|
+
print(
|
|
466
|
+
f"mpg: {len(matches)} outdated pattern(s). "
|
|
467
|
+
f"Run `mpg retrieve {','.join(guide_ids)}` for modern alternatives.",
|
|
468
|
+
file=sys.stderr,
|
|
469
|
+
)
|
|
470
|
+
sys.exit(2)
|
|
@@ -26,6 +26,30 @@ def _get_index() -> GuideIndex:
|
|
|
26
26
|
return _index
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _guide_limit() -> int:
|
|
30
|
+
return len(_get_index())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_tools() -> list[dict]:
|
|
34
|
+
n = _guide_limit()
|
|
35
|
+
tools = list(TOOLS)
|
|
36
|
+
tools[1] = {
|
|
37
|
+
**TOOLS[1],
|
|
38
|
+
"inputSchema": {
|
|
39
|
+
**TOOLS[1]["inputSchema"],
|
|
40
|
+
"properties": {
|
|
41
|
+
**TOOLS[1]["inputSchema"]["properties"],
|
|
42
|
+
"guide_ids": {
|
|
43
|
+
**TOOLS[1]["inputSchema"]["properties"]["guide_ids"],
|
|
44
|
+
"maxItems": n,
|
|
45
|
+
"description": f"Guide IDs to retrieve (max {n})",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
return tools
|
|
51
|
+
|
|
52
|
+
|
|
29
53
|
# --- JSON-RPC framing (newline-delimited JSON) ---
|
|
30
54
|
|
|
31
55
|
|
|
@@ -122,8 +146,7 @@ TOOLS = [
|
|
|
122
146
|
"guide_ids": {
|
|
123
147
|
"type": "array",
|
|
124
148
|
"items": {"type": "string"},
|
|
125
|
-
"description": "Guide IDs to retrieve
|
|
126
|
-
"maxItems": 41,
|
|
149
|
+
"description": "Guide IDs to retrieve",
|
|
127
150
|
},
|
|
128
151
|
"python_version": {
|
|
129
152
|
"type": "string",
|
|
@@ -270,8 +293,9 @@ def _tool_retrieve(arguments: dict) -> dict:
|
|
|
270
293
|
guide_ids = arguments.get("guide_ids", [])
|
|
271
294
|
if not guide_ids:
|
|
272
295
|
return _tool_result("guide_ids is required and must not be empty", is_error=True)
|
|
273
|
-
|
|
274
|
-
|
|
296
|
+
limit = _guide_limit()
|
|
297
|
+
if len(guide_ids) > limit:
|
|
298
|
+
return _tool_result(f"guide_ids exceeds maximum of {limit}", is_error=True)
|
|
275
299
|
|
|
276
300
|
pv = arguments.get("python_version")
|
|
277
301
|
err = _validate_python_version(pv)
|
|
@@ -371,7 +395,7 @@ def _handle_request(msg: dict) -> dict | None:
|
|
|
371
395
|
return None if is_notification else result
|
|
372
396
|
|
|
373
397
|
if method == "tools/list":
|
|
374
|
-
result = _result_response(req_id, {"tools":
|
|
398
|
+
result = _result_response(req_id, {"tools": _get_tools()})
|
|
375
399
|
return None if is_notification else result
|
|
376
400
|
|
|
377
401
|
if method == "tools/call":
|
|
@@ -95,18 +95,40 @@ def _rules_file_path(project_dir: Path | None = None) -> Path:
|
|
|
95
95
|
return root / ".claude" / "rules" / RULE_FILE_NAME
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
_THIN_RULE_BODY = """\
|
|
99
|
+
# Modern Python Guidance
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
Pre-generation design patterns that Ruff cannot auto-fix. Apply these transformations when writing or reviewing Python code.
|
|
102
|
+
|
|
103
|
+
## Embedded patterns (high-frequency, Ruff-uncovered)
|
|
104
|
+
|
|
105
|
+
- `from typing import List, Dict` → `list[str]`, `dict[str, int]` (>=3.9)
|
|
106
|
+
- `@validator("f")` → `@field_validator("f")` (Pydantic V2)
|
|
107
|
+
- `datetime.utcnow()` → `datetime.now(UTC)` (>=3.11)
|
|
108
|
+
- `session.query(User).filter()` → `session.execute(select(User).where())` (SQLAlchemy 2.0)
|
|
109
|
+
- `subprocess.run(f"cmd {arg}", shell=True)` → `subprocess.run(["cmd", arg], check=True)`
|
|
110
|
+
|
|
111
|
+
## All 41 guides by category
|
|
112
|
+
|
|
113
|
+
- **async** (3): `async-timeout-context`, `exception-groups`, `taskgroup-over-gather`
|
|
114
|
+
- **data-structures** (3): `dataclass-modern`, `dict-merge-operator`, `match-case-patterns`
|
|
115
|
+
- **django** (3): `django-async-views`, `django-check-constraints`, `django-json-field`
|
|
116
|
+
- **fastapi** (3): `fastapi-annotated-depends`, `fastapi-lifespan`, `fastapi-typed-state`
|
|
117
|
+
- **httpx** (2): `httpx-async-client-reuse`, `httpx-streaming`
|
|
118
|
+
- **pydantic** (4): `pydantic-v2-config`, `pydantic-v2-model-api`, `pydantic-v2-serialization`, `pydantic-v2-validators`
|
|
119
|
+
- **pytest** (3): `pytest-parametrize`, `pytest-raises-match`, `pytest-tmp-path`
|
|
120
|
+
- **sqlalchemy** (3): `sqlalchemy-2-style`, `sqlalchemy-async-session`, `sqlalchemy-mapped-column`
|
|
121
|
+
- **stdlib** (5): `datetime-utc`, `pathlib-over-os-path`, `removeprefix-removesuffix`, `template-strings`, `tomllib-builtin`
|
|
122
|
+
- **toolchain** (5): `no-pickle`, `pyproject-toml-over-setup`, `ruff-over-flake8`, `safe-subprocess`, `uv-over-pip`
|
|
123
|
+
- **typing** (7): `deferred-annotations`, `override-decorator`, `paramspec-decorators`, `type-parameter-syntax`, `typeis-vs-typeguard`, `union-syntax`, `use-builtin-generics`
|
|
124
|
+
|
|
125
|
+
For full code examples, use `mpg retrieve <guide-id>` or MCP tool `retrieve_guides`.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _build_rule_text() -> str:
|
|
130
|
+
"""Generate thin rule file content with category index and MCP pointer."""
|
|
131
|
+
return RULE_FRONTMATTER + "\n\n" + _THIN_RULE_BODY
|
|
110
132
|
|
|
111
133
|
|
|
112
134
|
def setup_mcp(
|
|
@@ -285,8 +307,11 @@ def run_setup(
|
|
|
285
307
|
rules_ok = setup_rules(project_dir=project_dir, dry_run=dry_run)
|
|
286
308
|
|
|
287
309
|
if mcp_ok and skills_ok and rules_ok:
|
|
288
|
-
if not dry_run
|
|
289
|
-
|
|
310
|
+
if not dry_run:
|
|
311
|
+
if do_mcp and do_skills:
|
|
312
|
+
print("Ready. Start Claude Code to use mpg guides.")
|
|
313
|
+
print("Tip: Add a PostToolUse hook to auto-check Python files.")
|
|
314
|
+
print("See: https://github.com/yottayoshida/modern-python-guidance#recommended-hooks")
|
|
290
315
|
return 0
|
|
291
316
|
|
|
292
317
|
return 1
|
|
@@ -250,6 +250,89 @@ class TestCheck:
|
|
|
250
250
|
old_matches = json.loads(r_old.stdout)["summary"]["total_matches"]
|
|
251
251
|
assert old_matches <= all_matches
|
|
252
252
|
|
|
253
|
+
def test_check_quiet_clean_file(self, tmp_path):
|
|
254
|
+
p = tmp_path / "clean.py"
|
|
255
|
+
p.write_text("x: list[str] = []\n")
|
|
256
|
+
r = run_cli("check", str(p), "--quiet", "--format", "human")
|
|
257
|
+
assert r.returncode == 0
|
|
258
|
+
assert r.stdout == ""
|
|
259
|
+
assert r.stderr == ""
|
|
260
|
+
|
|
261
|
+
def test_check_quiet_with_matches(self, tmp_path):
|
|
262
|
+
p = tmp_path / "bad.py"
|
|
263
|
+
p.write_text("from typing import List\n")
|
|
264
|
+
r = run_cli("check", str(p), "--quiet", "--format", "human")
|
|
265
|
+
assert r.returncode == 1
|
|
266
|
+
assert "outdated pattern" in r.stdout
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TestHook:
|
|
270
|
+
def _run_hook(self, stdin_data: str) -> subprocess.CompletedProcess[str]:
|
|
271
|
+
return subprocess.run(
|
|
272
|
+
[*BIN, "hook", "claude-post-tool-use"],
|
|
273
|
+
input=stdin_data,
|
|
274
|
+
capture_output=True,
|
|
275
|
+
text=True,
|
|
276
|
+
timeout=10,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def test_hook_py_with_matches(self, tmp_path):
|
|
280
|
+
p = tmp_path / "bad.py"
|
|
281
|
+
p.write_text("from typing import List\n")
|
|
282
|
+
stdin = json.dumps({"tool_input": {"file_path": str(p)}})
|
|
283
|
+
r = self._run_hook(stdin)
|
|
284
|
+
assert r.returncode == 2
|
|
285
|
+
assert "mpg:" in r.stderr
|
|
286
|
+
assert r.stdout == ""
|
|
287
|
+
|
|
288
|
+
def test_hook_py_clean(self, tmp_path):
|
|
289
|
+
p = tmp_path / "clean.py"
|
|
290
|
+
p.write_text("x: list[str] = []\n")
|
|
291
|
+
stdin = json.dumps({"tool_input": {"file_path": str(p)}})
|
|
292
|
+
r = self._run_hook(stdin)
|
|
293
|
+
assert r.returncode == 0
|
|
294
|
+
assert r.stdout == ""
|
|
295
|
+
assert r.stderr == ""
|
|
296
|
+
|
|
297
|
+
def test_hook_non_py(self, tmp_path):
|
|
298
|
+
p = tmp_path / "file.js"
|
|
299
|
+
p.write_text("const x = 1;\n")
|
|
300
|
+
stdin = json.dumps({"tool_input": {"file_path": str(p)}})
|
|
301
|
+
r = self._run_hook(stdin)
|
|
302
|
+
assert r.returncode == 0
|
|
303
|
+
|
|
304
|
+
def test_hook_missing_file(self):
|
|
305
|
+
stdin = json.dumps({"tool_input": {"file_path": "/nonexistent/test.py"}})
|
|
306
|
+
r = self._run_hook(stdin)
|
|
307
|
+
assert r.returncode == 0
|
|
308
|
+
|
|
309
|
+
def test_hook_malformed_json(self):
|
|
310
|
+
r = self._run_hook("{bad json")
|
|
311
|
+
assert r.returncode == 0
|
|
312
|
+
|
|
313
|
+
def test_hook_missing_keys(self):
|
|
314
|
+
r = self._run_hook(json.dumps({"other": "data"}))
|
|
315
|
+
assert r.returncode == 0
|
|
316
|
+
|
|
317
|
+
def test_hook_uppercase_py(self, tmp_path):
|
|
318
|
+
p = tmp_path / "bad.PY"
|
|
319
|
+
p.write_text("from typing import List\n")
|
|
320
|
+
stdin = json.dumps({"tool_input": {"file_path": str(p)}})
|
|
321
|
+
r = self._run_hook(stdin)
|
|
322
|
+
assert r.returncode == 2
|
|
323
|
+
assert "mpg:" in r.stderr
|
|
324
|
+
|
|
325
|
+
def test_hook_bare_no_subcommand(self):
|
|
326
|
+
r = subprocess.run(
|
|
327
|
+
[*BIN, "hook"],
|
|
328
|
+
input="",
|
|
329
|
+
capture_output=True,
|
|
330
|
+
text=True,
|
|
331
|
+
timeout=10,
|
|
332
|
+
)
|
|
333
|
+
assert r.returncode == 2
|
|
334
|
+
assert "available hooks" in r.stderr
|
|
335
|
+
|
|
253
336
|
|
|
254
337
|
class TestVersion:
|
|
255
338
|
def test_version_flag(self):
|
|
@@ -223,6 +223,110 @@ class TestCmdDetectVersion:
|
|
|
223
223
|
assert out # should return some version string
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
class TestCmdCheck:
|
|
227
|
+
def test_json_output(self, tmp_path, capsys):
|
|
228
|
+
p = tmp_path / "bad.py"
|
|
229
|
+
p.write_text("from typing import List\n")
|
|
230
|
+
with pytest.raises(SystemExit, match="1"):
|
|
231
|
+
main(argv=["check", str(p), "--format", "json"])
|
|
232
|
+
data = json.loads(capsys.readouterr().out)
|
|
233
|
+
assert data["summary"]["total_matches"] >= 1
|
|
234
|
+
|
|
235
|
+
def test_human_output(self, tmp_path, capsys):
|
|
236
|
+
p = tmp_path / "bad.py"
|
|
237
|
+
p.write_text("from typing import List\n")
|
|
238
|
+
with pytest.raises(SystemExit, match="1"):
|
|
239
|
+
main(argv=["check", str(p), "--format", "human"])
|
|
240
|
+
assert "outdated pattern" in capsys.readouterr().out
|
|
241
|
+
|
|
242
|
+
def test_clean_file(self, tmp_path, capsys):
|
|
243
|
+
p = tmp_path / "clean.py"
|
|
244
|
+
p.write_text("x: list[str] = []\n")
|
|
245
|
+
main(argv=["check", str(p), "--format", "human"])
|
|
246
|
+
assert "No outdated patterns" in capsys.readouterr().out
|
|
247
|
+
|
|
248
|
+
def test_file_not_found(self, tmp_path, capsys):
|
|
249
|
+
with pytest.raises(SystemExit, match="2"):
|
|
250
|
+
main(argv=["check", str(tmp_path / "gone.py"), "--format", "json"])
|
|
251
|
+
|
|
252
|
+
def test_quiet_suppresses_clean(self, tmp_path, capsys):
|
|
253
|
+
p = tmp_path / "clean.py"
|
|
254
|
+
p.write_text("x: list[str] = []\n")
|
|
255
|
+
main(argv=["check", str(p), "--quiet", "--format", "human"])
|
|
256
|
+
captured = capsys.readouterr()
|
|
257
|
+
assert captured.out == ""
|
|
258
|
+
|
|
259
|
+
def test_exit_zero(self, tmp_path, capsys):
|
|
260
|
+
p = tmp_path / "bad.py"
|
|
261
|
+
p.write_text("from typing import List\n")
|
|
262
|
+
main(argv=["check", str(p), "--exit-zero", "--format", "json"])
|
|
263
|
+
data = json.loads(capsys.readouterr().out)
|
|
264
|
+
assert data["summary"]["total_matches"] >= 1
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class TestCmdHook:
|
|
268
|
+
def test_bare_hook_exits_2(self, capsys):
|
|
269
|
+
with pytest.raises(SystemExit, match="2"):
|
|
270
|
+
main(argv=["hook"])
|
|
271
|
+
assert "available hooks" in capsys.readouterr().err
|
|
272
|
+
|
|
273
|
+
def test_unknown_hook_exits_2(self, capsys):
|
|
274
|
+
with pytest.raises(SystemExit, match="2"):
|
|
275
|
+
main(argv=["hook", "nonexistent"])
|
|
276
|
+
assert "invalid choice" in capsys.readouterr().err
|
|
277
|
+
|
|
278
|
+
def test_post_tool_use_py_match(self, tmp_path, capsys, monkeypatch):
|
|
279
|
+
p = tmp_path / "bad.py"
|
|
280
|
+
p.write_text("from typing import List\n")
|
|
281
|
+
import io
|
|
282
|
+
|
|
283
|
+
stdin_data = json.dumps({"tool_input": {"file_path": str(p)}})
|
|
284
|
+
monkeypatch.setattr("sys.stdin", io.StringIO(stdin_data))
|
|
285
|
+
with pytest.raises(SystemExit, match="2"):
|
|
286
|
+
main(argv=["hook", "claude-post-tool-use"])
|
|
287
|
+
assert "mpg:" in capsys.readouterr().err
|
|
288
|
+
|
|
289
|
+
def test_post_tool_use_py_clean(self, tmp_path, monkeypatch):
|
|
290
|
+
p = tmp_path / "clean.py"
|
|
291
|
+
p.write_text("x: list[str] = []\n")
|
|
292
|
+
import io
|
|
293
|
+
|
|
294
|
+
stdin_data = json.dumps({"tool_input": {"file_path": str(p)}})
|
|
295
|
+
monkeypatch.setattr("sys.stdin", io.StringIO(stdin_data))
|
|
296
|
+
with pytest.raises(SystemExit, match="0"):
|
|
297
|
+
main(argv=["hook", "claude-post-tool-use"])
|
|
298
|
+
|
|
299
|
+
def test_post_tool_use_non_py(self, monkeypatch):
|
|
300
|
+
import io
|
|
301
|
+
|
|
302
|
+
stdin_data = json.dumps({"tool_input": {"file_path": "/tmp/x.js"}})
|
|
303
|
+
monkeypatch.setattr("sys.stdin", io.StringIO(stdin_data))
|
|
304
|
+
with pytest.raises(SystemExit, match="0"):
|
|
305
|
+
main(argv=["hook", "claude-post-tool-use"])
|
|
306
|
+
|
|
307
|
+
def test_post_tool_use_malformed(self, monkeypatch):
|
|
308
|
+
import io
|
|
309
|
+
|
|
310
|
+
monkeypatch.setattr("sys.stdin", io.StringIO("{bad"))
|
|
311
|
+
with pytest.raises(SystemExit, match="0"):
|
|
312
|
+
main(argv=["hook", "claude-post-tool-use"])
|
|
313
|
+
|
|
314
|
+
def test_post_tool_use_missing_keys(self, monkeypatch):
|
|
315
|
+
import io
|
|
316
|
+
|
|
317
|
+
monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps({"x": 1})))
|
|
318
|
+
with pytest.raises(SystemExit, match="0"):
|
|
319
|
+
main(argv=["hook", "claude-post-tool-use"])
|
|
320
|
+
|
|
321
|
+
def test_post_tool_use_missing_file(self, monkeypatch):
|
|
322
|
+
import io
|
|
323
|
+
|
|
324
|
+
stdin_data = json.dumps({"tool_input": {"file_path": "/nonexistent/z.py"}})
|
|
325
|
+
monkeypatch.setattr("sys.stdin", io.StringIO(stdin_data))
|
|
326
|
+
with pytest.raises(SystemExit, match="0"):
|
|
327
|
+
main(argv=["hook", "claude-post-tool-use"])
|
|
328
|
+
|
|
329
|
+
|
|
226
330
|
class TestCmdSetupUninstall:
|
|
227
331
|
def test_setup_dispatch(self):
|
|
228
332
|
with patch("modern_python_guidance.setup_cmd.run_setup", return_value=0) as mock:
|