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.
Files changed (139) hide show
  1. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/CHANGELOG.md +14 -0
  2. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/PKG-INFO +29 -1
  3. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/README.md +28 -0
  4. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/pyproject.toml +2 -1
  5. modern_python_guidance-0.5.0/rules/modern-python.md +31 -0
  6. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/__init__.py +1 -1
  7. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/cli.py +74 -1
  8. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/setup_cmd.py +38 -13
  9. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_cli_integration.py +83 -0
  10. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_cli_unit.py +104 -0
  11. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_guide_structure.py +19 -13
  12. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_setup.py +15 -0
  13. modern_python_guidance-0.4.5/rules/modern-python.md +0 -70
  14. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/.github/workflows/check-python-release.yml +0 -0
  15. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/.github/workflows/ci.yml +0 -0
  16. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/.github/workflows/publish.yml +0 -0
  17. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/.gitignore +0 -0
  18. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/CONTRIBUTING.md +0 -0
  19. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/LICENSE +0 -0
  20. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/LICENSE-MIT +0 -0
  21. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/SECURITY.md +0 -0
  22. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
  23. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
  24. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
  25. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/app.py +0 -0
  26. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/config.py +0 -0
  27. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
  28. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/models.py +0 -0
  29. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
  30. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
  31. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
  32. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/setup.py +0 -0
  33. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
  34. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
  35. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
  36. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
  37. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
  38. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
  39. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
  40. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
  41. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
  42. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
  43. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
  44. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
  45. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/mcp-config.json +0 -0
  46. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v2.txt +0 -0
  47. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v3-mcp.txt +0 -0
  48. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v3.txt +0 -0
  49. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v4-a.txt +0 -0
  50. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v4-b.txt +0 -0
  51. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt-v4-c.txt +0 -0
  52. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompt.txt +0 -0
  53. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-a-detailed.txt +0 -0
  54. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-a-normal.txt +0 -0
  55. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-a-terse.txt +0 -0
  56. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-b-detailed.txt +0 -0
  57. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-b-normal.txt +0 -0
  58. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-b-terse.txt +0 -0
  59. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-c-detailed.txt +0 -0
  60. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-c-normal.txt +0 -0
  61. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/prompts/v5-c-terse.txt +0 -0
  62. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/run-mcp.sh +0 -0
  63. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/run-v4.sh +0 -0
  64. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/run-v5.sh +0 -0
  65. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/run.sh +0 -0
  66. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/score-v2.sh +0 -0
  67. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/score-v3.sh +0 -0
  68. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/score-v4.sh +0 -0
  69. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/score.sh +0 -0
  70. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/score_v5.py +0 -0
  71. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/bench/test-scorer.sh +0 -0
  72. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/docs/benchmark-evaluation.md +0 -0
  73. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/docs/benchmark-procedure.md +0 -0
  74. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/docs/benchmark-v5.md +0 -0
  75. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/docs/design.md +0 -0
  76. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/SKILL.md +0 -0
  77. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
  78. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
  79. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
  80. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
  81. {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
  82. {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
  83. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
  84. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
  85. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
  86. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
  87. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
  88. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
  89. {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
  90. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
  91. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
  92. {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
  93. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
  94. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
  95. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
  96. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
  97. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
  98. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
  99. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
  100. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
  101. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
  102. {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
  103. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
  104. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/template-strings.md +0 -0
  105. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
  106. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
  107. {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
  108. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
  109. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
  110. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
  111. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +0 -0
  112. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
  113. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
  114. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
  115. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
  116. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
  117. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
  118. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/__main__.py +0 -0
  119. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/check.py +0 -0
  120. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/compat.py +0 -0
  121. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/frontmatter.py +0 -0
  122. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/guide_index.py +0 -0
  123. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/mcp_server.py +0 -0
  124. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/retrieve.py +0 -0
  125. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/search.py +0 -0
  126. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/uninstall_cmd.py +0 -0
  127. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/src/modern_python_guidance/version_detect.py +0 -0
  128. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_check.py +0 -0
  129. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_compat.py +0 -0
  130. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_frontmatter.py +0 -0
  131. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_guide_index.py +0 -0
  132. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_mcp_server.py +0 -0
  133. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_mcp_unit.py +0 -0
  134. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_retrieve.py +0 -0
  135. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_scorer_v5.py +0 -0
  136. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_search.py +0 -0
  137. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_skill_sync.py +0 -0
  138. {modern_python_guidance-0.4.5 → modern_python_guidance-0.5.0}/tests/test_uninstall.py +0 -0
  139. {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.4.5
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.4.5"
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`.
@@ -1,3 +1,3 @@
1
1
  """Modern Python Guidance — version-aware BAD/GOOD pattern guides for AI coding agents."""
2
2
 
3
- __version__ = "0.4.5"
3
+ __version__ = "0.5.0"
@@ -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
- else:
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
- def _build_rule_text() -> str:
99
- """Generate rule file content from SKILL.md body with rule-specific frontmatter.
98
+ _THIN_RULE_BODY = """\
99
+ # Modern Python Guidance
100
100
 
101
- Used by CI sync tests to verify the bundled ``rules/modern-python.md`` matches
102
- what would be generated from SKILL.md. Strips SKILL.md frontmatter and prepends
103
- rule-only frontmatter (no name/description keys).
104
- """
105
- skills_dir = _find_skills_dir()
106
- skill_md = (skills_dir / "SKILL.md").read_text(encoding="utf-8")
107
- parts = skill_md.split("---", 2)
108
- body = parts[2].lstrip("\n")
109
- return RULE_FRONTMATTER + "\n\n" + body
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 and do_mcp and do_skills:
289
- print("Ready. Start Claude Code to use mpg guides.")
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 body matches SKILL.md body."""
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`.