modern-python-guidance 0.4.5__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.5 → modern_python_guidance-0.5.0}/CHANGELOG.md +14 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/PKG-INFO +29 -1
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/README.md +28 -0
- {modern_python_guidance-0.4.5 → 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.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/__init__.py +1 -1
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/cli.py +74 -1
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/setup_cmd.py +38 -13
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_cli_integration.py +83 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_cli_unit.py +104 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_guide_structure.py +19 -13
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_setup.py +15 -0
- modern_python_guidance-0.4.5/rules/modern-python.md +0 -70
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/.github/workflows/check-python-release.yml +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/.github/workflows/ci.yml +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/.gitignore +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/CONTRIBUTING.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/LICENSE +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/SECURITY.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-a-detailed.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-a-normal.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-a-terse.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-b-detailed.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-b-normal.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-b-terse.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-c-detailed.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-c-normal.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-c-terse.txt +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/run-v5.sh +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/run.sh +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/score.sh +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/score_v5.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/docs/benchmark-v5.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/docs/design.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/template-strings.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/check.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/frontmatter.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/guide_index.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/mcp_server.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/retrieve.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/search.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/uninstall_cmd.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/version_detect.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_check.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_compat.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_frontmatter.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_guide_index.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_mcp_server.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_mcp_unit.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_retrieve.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_scorer_v5.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_search.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_skill_sync.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_uninstall.py +0 -0
- {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_version_detect.py +0 -0
|
@@ -2,6 +2,20 @@
|
|
|
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
|
+
|
|
5
19
|
## [0.4.5] — 2026-06-06
|
|
6
20
|
|
|
7
21
|
### Fixed
|
|
@@ -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
|
|
@@ -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.5 → 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)
|
|
@@ -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:
|
|
@@ -18,7 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
import pytest
|
|
19
19
|
|
|
20
20
|
from modern_python_guidance.frontmatter import parse_frontmatter
|
|
21
|
-
from modern_python_guidance.guide_index import _code_lines, _find_guides_dir
|
|
21
|
+
from modern_python_guidance.guide_index import _code_lines, _find_guides_dir, build_index
|
|
22
22
|
from modern_python_guidance.setup_cmd import _build_rule_text
|
|
23
23
|
|
|
24
24
|
GUIDES_DIR = _find_guides_dir()
|
|
@@ -115,12 +115,7 @@ class TestGuideStructure:
|
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
class TestRuleFileSync:
|
|
118
|
-
"""CI sync tests: rules/modern-python.md
|
|
119
|
-
|
|
120
|
-
def _skill_body(self) -> str:
|
|
121
|
-
skill_md = (GUIDES_DIR.parent / "SKILL.md").read_text(encoding="utf-8")
|
|
122
|
-
parts = skill_md.split("---", 2)
|
|
123
|
-
return parts[2].lstrip("\n")
|
|
118
|
+
"""CI sync tests: rules/modern-python.md is thin and matches _build_rule_text()."""
|
|
124
119
|
|
|
125
120
|
def _rule_path(self) -> Path:
|
|
126
121
|
return GUIDES_DIR.parent.parent.parent / "rules" / "modern-python.md"
|
|
@@ -130,12 +125,6 @@ class TestRuleFileSync:
|
|
|
130
125
|
parts = text.split("---", 2)
|
|
131
126
|
return parts[1].strip(), parts[2].lstrip("\n")
|
|
132
127
|
|
|
133
|
-
def test_body_matches_skill(self):
|
|
134
|
-
"""rules/modern-python.md body == SKILL.md body (content sync)."""
|
|
135
|
-
skill_body = self._skill_body()
|
|
136
|
-
_, rule_body = self._rule_parts()
|
|
137
|
-
assert rule_body == skill_body
|
|
138
|
-
|
|
139
128
|
def test_matches_build_rule_text(self):
|
|
140
129
|
"""rules/modern-python.md == _build_rule_text() output (SoT enforcement)."""
|
|
141
130
|
actual = self._rule_path().read_text(encoding="utf-8")
|
|
@@ -163,6 +152,23 @@ class TestRuleFileSync:
|
|
|
163
152
|
assert "name:" not in fm
|
|
164
153
|
assert "description:" not in fm
|
|
165
154
|
|
|
155
|
+
def test_thin_rule_has_guide_count(self):
|
|
156
|
+
"""Thin Rules body contains correct guide count."""
|
|
157
|
+
_, body = self._rule_parts()
|
|
158
|
+
assert f"All {EXPECTED_GUIDE_COUNT} guides" in body
|
|
159
|
+
|
|
160
|
+
def test_thin_rule_has_mcp_pointer(self):
|
|
161
|
+
"""Thin Rules body references MCP tool or CLI retrieve."""
|
|
162
|
+
_, body = self._rule_parts()
|
|
163
|
+
assert "retrieve_guides" in body or "mpg retrieve" in body
|
|
164
|
+
|
|
165
|
+
def test_thin_rule_has_all_guide_ids(self):
|
|
166
|
+
"""Every guide ID from the registry appears in thin Rules category index."""
|
|
167
|
+
_, body = self._rule_parts()
|
|
168
|
+
index = build_index()
|
|
169
|
+
for guide_id in index.guides:
|
|
170
|
+
assert f"`{guide_id}`" in body, f"guide ID missing from thin Rules: {guide_id}"
|
|
171
|
+
|
|
166
172
|
|
|
167
173
|
class TestDetectPatterns:
|
|
168
174
|
def test_field_present(self, guide_file: Path):
|
|
@@ -577,6 +577,21 @@ class TestRunSetup:
|
|
|
577
577
|
assert run_setup(dry_run=True) == 0
|
|
578
578
|
assert "Ready" not in capsys.readouterr().out
|
|
579
579
|
|
|
580
|
+
def test_success_shows_hook_hint(self, capsys: pytest.CaptureFixture[str]):
|
|
581
|
+
p_mcp, p_skills, p_rules = self._patch_all()
|
|
582
|
+
with p_mcp, p_skills, p_rules:
|
|
583
|
+
assert run_setup() == 0
|
|
584
|
+
out = capsys.readouterr().out
|
|
585
|
+
assert "PostToolUse" in out
|
|
586
|
+
|
|
587
|
+
def test_skills_only_shows_hook_hint(self, capsys: pytest.CaptureFixture[str]):
|
|
588
|
+
p_mcp, p_skills, p_rules = self._patch_all()
|
|
589
|
+
with p_mcp, p_skills, p_rules:
|
|
590
|
+
assert run_setup(skills_only=True) == 0
|
|
591
|
+
out = capsys.readouterr().out
|
|
592
|
+
assert "PostToolUse" in out
|
|
593
|
+
assert "Ready" not in out
|
|
594
|
+
|
|
580
595
|
def test_mutual_exclusion(self, capsys: pytest.CaptureFixture[str]):
|
|
581
596
|
code = run_setup(mcp_only=True, skills_only=True)
|
|
582
597
|
assert code == 1
|
|
@@ -1,70 +0,0 @@
|
|
|
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
|
-
## When to use
|
|
10
|
-
|
|
11
|
-
- Writing new Python code (use modern patterns from the start)
|
|
12
|
-
- Reviewing Python code (flag outdated patterns)
|
|
13
|
-
- Migrating from Pydantic V1 to V2
|
|
14
|
-
- Upgrading Python version (check which new features are available)
|
|
15
|
-
- Replacing legacy tooling (setup.py, flake8, pip)
|
|
16
|
-
|
|
17
|
-
## Embedded patterns (high-frequency, Ruff-uncovered)
|
|
18
|
-
|
|
19
|
-
### Pydantic V2 (>=3.9)
|
|
20
|
-
|
|
21
|
-
- `@validator("f")` → `@field_validator("f")`
|
|
22
|
-
- `@root_validator` → `@model_validator(mode="after")`
|
|
23
|
-
- `class Config:` → `model_config = ConfigDict(...)`
|
|
24
|
-
- `orm_mode` → `from_attributes`, `allow_population_by_field_name` → `populate_by_name`
|
|
25
|
-
- `.parse_obj(d)` → `.model_validate(d)`, `.parse_raw(j)` → `.model_validate_json(j)`
|
|
26
|
-
- `.dict()` → `.model_dump()`, `.json()` → `.model_dump_json()`
|
|
27
|
-
- `.schema()` → `.model_json_schema()`, `.copy()` → `.model_copy()`
|
|
28
|
-
|
|
29
|
-
### FastAPI (>=3.9)
|
|
30
|
-
|
|
31
|
-
- `@app.on_event("startup")`/`"shutdown"` → `@asynccontextmanager` lifespan + `FastAPI(lifespan=lifespan)`; yield dict becomes `request.state`
|
|
32
|
-
- `db: Session = Depends(get_db)` → `DbDep = Annotated[Session, Depends(get_db)]`; reusable type alias per PEP 593
|
|
33
|
-
|
|
34
|
-
### httpx
|
|
35
|
-
|
|
36
|
-
- Per-request `async with httpx.AsyncClient()` → shared `AsyncClient` with `base_url`
|
|
37
|
-
- Caveat: shared client must be closed via `async with` or lifespan management
|
|
38
|
-
|
|
39
|
-
### asyncio (>=3.11)
|
|
40
|
-
|
|
41
|
-
- `await asyncio.gather(a(), b())` → `async with asyncio.TaskGroup() as tg:` + `tg.create_task()`; access results via `task.result()`
|
|
42
|
-
- Caveat: 3.11+ only. `TaskGroup` cancels siblings on error and raises `ExceptionGroup`; `gather` preserves return order and supports `return_exceptions=True`
|
|
43
|
-
|
|
44
|
-
### SQLAlchemy 2.0 (>=3.9)
|
|
45
|
-
|
|
46
|
-
- `session.query(User).filter()` → `session.execute(select(User).where())`; use `session.scalars()` for ORM results
|
|
47
|
-
- `Column(Integer)` → `Mapped[int] = mapped_column()`; type inferred from annotation, nullability from `Optional`/`| None`
|
|
48
|
-
- Sync `Session` with `asyncio.to_thread` → `AsyncSession` + `create_async_engine` + `async_sessionmaker`
|
|
49
|
-
|
|
50
|
-
### Toolchain
|
|
51
|
-
|
|
52
|
-
- `setup.py` / `setup.cfg` → `pyproject.toml` with `[build-system]` + `[project]` (PEP 621)
|
|
53
|
-
- `subprocess.run(f"cmd {arg}", shell=True)` → `subprocess.run(["cmd", arg], check=True)`
|
|
54
|
-
- Caveat: `shell=True` is valid when pipes/globs are needed; use `shlex.quote()` for user input
|
|
55
|
-
|
|
56
|
-
## All 41 guides by category
|
|
57
|
-
|
|
58
|
-
- **typing** (7): `use-builtin-generics`, `union-syntax`, `type-parameter-syntax`, `override-decorator`, `typeis-vs-typeguard`, `paramspec-decorators`, `deferred-annotations`
|
|
59
|
-
- **async** (3): `taskgroup-over-gather`, `exception-groups`, `async-timeout-context`
|
|
60
|
-
- **stdlib** (5): `datetime-utc`, `pathlib-over-os-path`, `tomllib-builtin`, `removeprefix-removesuffix`, `template-strings`
|
|
61
|
-
- **data-structures** (3): `dict-merge-operator`, `match-case-patterns`, `dataclass-modern`
|
|
62
|
-
- **pydantic** (4): `pydantic-v2-validators`, `pydantic-v2-config`, `pydantic-v2-model-api`, `pydantic-v2-serialization`
|
|
63
|
-
- **fastapi** (3): `fastapi-lifespan`, `fastapi-annotated-depends`, `fastapi-typed-state`
|
|
64
|
-
- **httpx** (2): `httpx-async-client-reuse`, `httpx-streaming`
|
|
65
|
-
- **django** (3): `django-json-field`, `django-async-views`, `django-check-constraints`
|
|
66
|
-
- **sqlalchemy** (3): `sqlalchemy-2-style`, `sqlalchemy-mapped-column`, `sqlalchemy-async-session`
|
|
67
|
-
- **pytest** (3): `pytest-parametrize`, `pytest-tmp-path`, `pytest-raises-match`
|
|
68
|
-
- **toolchain** (5): `pyproject-toml-over-setup`, `uv-over-pip`, `ruff-over-flake8`, `no-pickle`, `safe-subprocess`
|
|
69
|
-
|
|
70
|
-
For full code examples, use `mpg retrieve <guide-id>` or MCP tool `retrieve_guides`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-a-detailed.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-b-detailed.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-c-detailed.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/__main__.py
RENAMED
|
File without changes
|
{modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/check.py
RENAMED
|
File without changes
|
{modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/compat.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/retrieve.py
RENAMED
|
File without changes
|
{modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|