modern-python-guidance 0.3.8__tar.gz → 0.4.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 (138) hide show
  1. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/CHANGELOG.md +10 -0
  2. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/PKG-INFO +6 -1
  3. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/README.md +5 -0
  4. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/pyproject.toml +1 -1
  5. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/async/async-timeout-context.md +2 -0
  6. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/async/exception-groups.md +1 -0
  7. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +2 -0
  8. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +1 -0
  9. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +1 -0
  10. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +1 -0
  11. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/django/django-async-views.md +2 -0
  12. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/django/django-check-constraints.md +1 -0
  13. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/django/django-json-field.md +2 -0
  14. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +2 -0
  15. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +2 -0
  16. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +1 -0
  17. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +2 -0
  18. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +1 -0
  19. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +2 -0
  20. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +4 -0
  21. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +2 -0
  22. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +3 -0
  23. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +1 -0
  24. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +1 -0
  25. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +2 -0
  26. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +2 -0
  27. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +1 -0
  28. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +3 -0
  29. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +3 -0
  30. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +2 -0
  31. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +1 -0
  32. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/stdlib/template-strings.md +1 -0
  33. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +2 -0
  34. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +3 -0
  35. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +2 -0
  36. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +1 -0
  37. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +3 -0
  38. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +1 -0
  39. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +2 -0
  40. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/override-decorator.md +1 -0
  41. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +2 -0
  42. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +2 -0
  43. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +2 -0
  44. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/union-syntax.md +2 -0
  45. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +2 -0
  46. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/__init__.py +1 -1
  47. modern_python_guidance-0.4.0/src/modern_python_guidance/check.py +146 -0
  48. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/cli.py +89 -0
  49. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/frontmatter.py +15 -0
  50. modern_python_guidance-0.4.0/tests/test_check.py +284 -0
  51. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_cli_integration.py +73 -0
  52. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_frontmatter.py +88 -0
  53. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_guide_structure.py +48 -1
  54. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/.github/workflows/check-python-release.yml +0 -0
  55. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/.github/workflows/ci.yml +0 -0
  56. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/.github/workflows/publish.yml +0 -0
  57. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/.gitignore +0 -0
  58. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/CONTRIBUTING.md +0 -0
  59. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/LICENSE +0 -0
  60. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/LICENSE-MIT +0 -0
  61. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/SECURITY.md +0 -0
  62. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
  63. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
  64. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
  65. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/app.py +0 -0
  66. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/config.py +0 -0
  67. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
  68. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/models.py +0 -0
  69. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
  70. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
  71. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
  72. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/setup.py +0 -0
  73. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
  74. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
  75. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
  76. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
  77. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
  78. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
  79. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
  80. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
  81. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
  82. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
  83. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
  84. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
  85. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/mcp-config.json +0 -0
  86. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompt-v2.txt +0 -0
  87. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompt-v3-mcp.txt +0 -0
  88. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompt-v3.txt +0 -0
  89. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompt-v4-a.txt +0 -0
  90. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompt-v4-b.txt +0 -0
  91. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompt-v4-c.txt +0 -0
  92. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompt.txt +0 -0
  93. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompts/v5-a-detailed.txt +0 -0
  94. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompts/v5-a-normal.txt +0 -0
  95. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompts/v5-a-terse.txt +0 -0
  96. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompts/v5-b-detailed.txt +0 -0
  97. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompts/v5-b-normal.txt +0 -0
  98. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompts/v5-b-terse.txt +0 -0
  99. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompts/v5-c-detailed.txt +0 -0
  100. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompts/v5-c-normal.txt +0 -0
  101. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/prompts/v5-c-terse.txt +0 -0
  102. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/run-mcp.sh +0 -0
  103. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/run-v4.sh +0 -0
  104. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/run-v5.sh +0 -0
  105. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/run.sh +0 -0
  106. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/score-v2.sh +0 -0
  107. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/score-v3.sh +0 -0
  108. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/score-v4.sh +0 -0
  109. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/score.sh +0 -0
  110. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/score_v5.py +0 -0
  111. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/bench/test-scorer.sh +0 -0
  112. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/docs/benchmark-evaluation.md +0 -0
  113. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/docs/benchmark-procedure.md +0 -0
  114. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/docs/benchmark-v5.md +0 -0
  115. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/docs/design.md +0 -0
  116. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/rules/modern-python.md +0 -0
  117. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/SKILL.md +0 -0
  118. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/__main__.py +0 -0
  119. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/compat.py +0 -0
  120. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/guide_index.py +0 -0
  121. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/mcp_server.py +0 -0
  122. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/retrieve.py +0 -0
  123. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/search.py +0 -0
  124. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/setup_cmd.py +0 -0
  125. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/uninstall_cmd.py +0 -0
  126. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/src/modern_python_guidance/version_detect.py +0 -0
  127. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_cli_unit.py +0 -0
  128. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_compat.py +0 -0
  129. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_guide_index.py +0 -0
  130. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_mcp_server.py +0 -0
  131. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_mcp_unit.py +0 -0
  132. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_retrieve.py +0 -0
  133. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_scorer_v5.py +0 -0
  134. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_search.py +0 -0
  135. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_setup.py +0 -0
  136. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_skill_sync.py +0 -0
  137. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_uninstall.py +0 -0
  138. {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.0}/tests/test_version_detect.py +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.4.0] — 2026-06-03
6
+
7
+ ### Added
8
+
9
+ - `mpg check <file>` command: scan a Python file for outdated patterns using regex matching against guide definitions. Reports matches with line numbers, guide IDs, and inline snippets. Linter exit-code convention (0=clean, 1=findings, 2=error). Supports `--python-version` filtering, `--format json|human`, and `--exit-zero`. JSON envelope includes `file`, `mpg_version`, `matches`, and `summary` with `guide_ids` for batched `mpg retrieve`. (closes #21)
10
+ - `detect_patterns` field in guide frontmatter: 3-value semantics — curated regex list (26 guides), explicit opt-out `[]` (15 guides), or absent `None` (auto-extraction fallback for future guides). All patterns validated at parse time via `re.compile`.
11
+ - `CheckError` exception in check module for clean library-level error handling (file not found, binary file, read errors). CLI catches and converts to exit code 2.
12
+ - Structural tests: all 41 guides must have `detect_patterns` present, patterns must compile, must match at least one BAD line, and must NOT match any GOOD line.
13
+ - 205 new tests (886 total). Coverage: 92%+.
14
+
5
15
  ## [0.3.8] — 2026-06-02
6
16
 
7
17
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modern-python-guidance
3
- Version: 0.3.8
3
+ Version: 0.4.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
@@ -139,6 +139,11 @@ mpg detect-version
139
139
  # Filter by category
140
140
  mpg search "timeout" --category async
141
141
 
142
+ # Scan a file for outdated patterns
143
+ mpg check app.py
144
+ mpg check app.py --format json | jq '.summary.guide_ids'
145
+ mpg check app.py --exit-zero # always exit 0
146
+
142
147
  # JSON output (default when piped, explicit with --format)
143
148
  mpg search "typing" --format json | jq '.[0].id'
144
149
  ```
@@ -107,6 +107,11 @@ mpg detect-version
107
107
  # Filter by category
108
108
  mpg search "timeout" --category async
109
109
 
110
+ # Scan a file for outdated patterns
111
+ mpg check app.py
112
+ mpg check app.py --format json | jq '.summary.guide_ids'
113
+ mpg check app.py --exit-zero # always exit 0
114
+
110
115
  # JSON output (default when piped, explicit with --format)
111
116
  mpg search "typing" --format json | jq '.[0].id'
112
117
  ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "modern-python-guidance"
7
- version = "0.3.8"
7
+ version = "0.4.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"
@@ -11,6 +11,8 @@ aliases:
11
11
  - asyncio.timeout
12
12
  python: ">=3.11"
13
13
  frequency: medium
14
+ detect-patterns:
15
+ - "asyncio\.wait_for\("
14
16
  ---
15
17
 
16
18
  # Use asyncio.timeout Instead of wait_for
@@ -14,6 +14,7 @@ aliases:
14
14
  python: ">=3.11"
15
15
  frequency: medium
16
16
  pep: 654
17
+ detect-patterns:
17
18
  ---
18
19
 
19
20
  # Use except* for Exception Groups
@@ -12,6 +12,8 @@ aliases:
12
12
  - gather
13
13
  python: ">=3.11"
14
14
  frequency: high
15
+ detect-patterns:
16
+ - "asyncio\.gather\("
15
17
  ---
16
18
 
17
19
  # Use asyncio.TaskGroup Instead of asyncio.gather
@@ -14,6 +14,7 @@ aliases:
14
14
  - dataclasses
15
15
  python: ">=3.10"
16
16
  frequency: high
17
+ detect-patterns:
17
18
  ---
18
19
 
19
20
  # Use Modern Dataclass Features
@@ -14,6 +14,7 @@ aliases:
14
14
  python: ">=3.9"
15
15
  frequency: medium
16
16
  pep: 584
17
+ detect-patterns:
17
18
  ---
18
19
 
19
20
  # Use | Operator for Dict Merging
@@ -14,6 +14,7 @@ aliases:
14
14
  python: ">=3.10"
15
15
  frequency: medium
16
16
  pep: 634
17
+ detect-patterns:
17
18
  ---
18
19
 
19
20
  # Use Structural Pattern Matching
@@ -13,6 +13,8 @@ aliases:
13
13
  - async-orm
14
14
  python: ">=3.9"
15
15
  frequency: medium
16
+ detect-patterns:
17
+ - "from asgiref\.sync import sync_to_async"
16
18
  ---
17
19
 
18
20
  # Use Native Async Views
@@ -12,6 +12,7 @@ aliases:
12
12
  - check-constraint
13
13
  python: ">=3.9"
14
14
  frequency: low
15
+ detect-patterns:
15
16
  ---
16
17
 
17
18
  # Use condition Instead of check in CheckConstraint
@@ -13,6 +13,8 @@ aliases:
13
13
  - contrib-jsonfield
14
14
  python: ">=3.9"
15
15
  frequency: high
16
+ detect-patterns:
17
+ - "from django\.contrib\.postgres\.fields import.*JSONField"
16
18
  ---
17
19
 
18
20
  # Use Built-in JSONField
@@ -13,6 +13,8 @@ aliases:
13
13
  - dependency injection
14
14
  python: ">=3.9"
15
15
  frequency: high
16
+ detect-patterns:
17
+ - "= Depends\("
16
18
  ---
17
19
 
18
20
  # Use Annotated for Dependency Injection
@@ -14,6 +14,8 @@ aliases:
14
14
  - shutdown
15
15
  python: ">=3.9"
16
16
  frequency: high
17
+ detect-patterns:
18
+ - "\.on_event\("
17
19
  ---
18
20
 
19
21
  # Use Lifespan Context Manager
@@ -13,6 +13,7 @@ aliases:
13
13
  - typed state
14
14
  python: ">=3.9"
15
15
  frequency: medium
16
+ detect-patterns:
16
17
  ---
17
18
 
18
19
  # Use TypedDict or dataclass for App State
@@ -14,6 +14,8 @@ aliases:
14
14
  - requests
15
15
  python: ">=3.9"
16
16
  frequency: high
17
+ detect-patterns:
18
+ - "async with httpx\.AsyncClient\(\) as"
17
19
  ---
18
20
 
19
21
  # Reuse httpx.AsyncClient
@@ -13,6 +13,7 @@ aliases:
13
13
  - large response
14
14
  python: ">=3.9"
15
15
  frequency: medium
16
+ detect-patterns:
16
17
  ---
17
18
 
18
19
  # Use httpx Streaming for Large Responses
@@ -13,6 +13,8 @@ aliases:
13
13
  - ConfigDict
14
14
  python: ">=3.9"
15
15
  frequency: high
16
+ detect-patterns:
17
+ - "class Config:"
16
18
  ---
17
19
 
18
20
  # Use model_config Instead of class Config
@@ -14,6 +14,10 @@ aliases:
14
14
  - model_validate
15
15
  python: ">=3.9"
16
16
  frequency: high
17
+ detect-patterns:
18
+ - "\.parse_obj\("
19
+ - "\.parse_raw\("
20
+ - "\.from_orm\("
17
21
  ---
18
22
 
19
23
  # Use Pydantic V2 Model API
@@ -13,6 +13,8 @@ aliases:
13
13
  - json_encoders
14
14
  python: ">=3.9"
15
15
  frequency: medium
16
+ detect-patterns:
17
+ - "json_encoders"
16
18
  ---
17
19
 
18
20
  # Use field_serializer for Custom Serialization
@@ -13,6 +13,9 @@ aliases:
13
13
  - model_validator
14
14
  python: ">=3.9"
15
15
  frequency: high
16
+ detect-patterns:
17
+ - "@validator\("
18
+ - "@root_validator"
16
19
  ---
17
20
 
18
21
  # Use Pydantic V2 Validators
@@ -12,6 +12,7 @@ aliases:
12
12
  - test-loop
13
13
  python: ">=3.9"
14
14
  frequency: high
15
+ detect-patterns:
15
16
  ---
16
17
 
17
18
  # Use pytest.mark.parametrize
@@ -13,6 +13,7 @@ aliases:
13
13
  - exception-match
14
14
  python: ">=3.9"
15
15
  frequency: medium
16
+ detect-patterns:
16
17
  ---
17
18
 
18
19
  # Use pytest.raises with match
@@ -13,6 +13,8 @@ aliases:
13
13
  - temp-directory
14
14
  python: ">=3.9"
15
15
  frequency: medium
16
+ detect-patterns:
17
+ - "def test_.*\btmpdir\b"
16
18
  ---
17
19
 
18
20
  # Use tmp_path Instead of tmpdir
@@ -13,6 +13,8 @@ aliases:
13
13
  - legacy-query
14
14
  python: ">=3.9"
15
15
  frequency: high
16
+ detect-patterns:
17
+ - "\.query\([A-Z]"
16
18
  ---
17
19
 
18
20
  # Use SQLAlchemy 2.0 Query Style
@@ -13,6 +13,7 @@ aliases:
13
13
  - async-sessionmaker
14
14
  python: ">=3.9"
15
15
  frequency: medium
16
+ detect-patterns:
16
17
  ---
17
18
 
18
19
  # Use AsyncSession for Async Database Access
@@ -13,6 +13,9 @@ aliases:
13
13
  - declarative-base
14
14
  python: ">=3.9"
15
15
  frequency: high
16
+ detect-patterns:
17
+ - "from sqlalchemy import .*\bColumn\b"
18
+ - "declarative_base\("
16
19
  ---
17
20
 
18
21
  # Use Mapped and mapped_column
@@ -13,6 +13,9 @@ aliases:
13
13
  - datetime.utcfromtimestamp
14
14
  python: ">=3.11"
15
15
  frequency: high
16
+ detect-patterns:
17
+ - "datetime\.utcnow\("
18
+ - "datetime\.utcfromtimestamp\("
16
19
  ---
17
20
 
18
21
  # Use datetime.now(UTC) Instead of utcnow()
@@ -13,6 +13,8 @@ aliases:
13
13
  - os.path.exists
14
14
  python: ">=3.9"
15
15
  frequency: high
16
+ detect-patterns:
17
+ - "os\.path\."
16
18
  ---
17
19
 
18
20
  # Use pathlib.Path Instead of os.path
@@ -14,6 +14,7 @@ aliases:
14
14
  python: ">=3.9"
15
15
  frequency: medium
16
16
  pep: 616
17
+ detect-patterns:
17
18
  ---
18
19
 
19
20
  # Use str.removeprefix/removesuffix
@@ -16,6 +16,7 @@ aliases:
16
16
  python: ">=3.14"
17
17
  frequency: medium
18
18
  pep: 750
19
+ detect-patterns:
19
20
  ---
20
21
 
21
22
  # Use Template Strings for Structured String Processing
@@ -14,6 +14,8 @@ aliases:
14
14
  python: ">=3.11"
15
15
  frequency: medium
16
16
  pep: 680
17
+ detect-patterns:
18
+ - "import toml\b"
17
19
  ---
18
20
 
19
21
  # Use Built-in tomllib
@@ -14,6 +14,9 @@ aliases:
14
14
  - shelve
15
15
  python: ">=3.9"
16
16
  frequency: medium
17
+ detect-patterns:
18
+ - "import pickle"
19
+ - "pickle\.(load|loads|dump|dumps)\("
17
20
  ---
18
21
 
19
22
  # Avoid pickle for Untrusted Data
@@ -15,6 +15,8 @@ aliases:
15
15
  python: ">=3.7"
16
16
  frequency: high
17
17
  pep: 621
18
+ detect-patterns:
19
+ - "from setuptools import"
18
20
  ---
19
21
 
20
22
  # Use pyproject.toml Instead of setup.py
@@ -19,6 +19,7 @@ aliases:
19
19
  - formatter
20
20
  python: ">=3.7"
21
21
  frequency: high
22
+ detect-patterns:
22
23
  ---
23
24
 
24
25
  # Use Ruff Instead of Flake8 + isort + Black
@@ -14,6 +14,9 @@ aliases:
14
14
  - subprocess.run
15
15
  python: ">=3.9"
16
16
  frequency: high
17
+ detect-patterns:
18
+ - "os\.system\("
19
+ - "shell\s*=\s*True"
17
20
  ---
18
21
 
19
22
  # Use subprocess Safely
@@ -16,6 +16,7 @@ aliases:
16
16
  - venv
17
17
  python: ">=3.8"
18
18
  frequency: high
19
+ detect-patterns:
19
20
  ---
20
21
 
21
22
  # Use uv Instead of pip
@@ -14,6 +14,8 @@ aliases:
14
14
  python: ">=3.14"
15
15
  frequency: high
16
16
  pep: 649
17
+ detect-patterns:
18
+ - "from __future__ import annotations"
17
19
  ---
18
20
 
19
21
  # Drop `from __future__ import annotations` on Python 3.14+
@@ -12,6 +12,7 @@ aliases:
12
12
  python: ">=3.12"
13
13
  frequency: medium
14
14
  pep: 698
15
+ detect-patterns:
15
16
  ---
16
17
 
17
18
  # Use @override to Mark Method Overrides
@@ -14,6 +14,8 @@ aliases:
14
14
  python: ">=3.10"
15
15
  frequency: medium
16
16
  pep: 612
17
+ detect-patterns:
18
+ - "Callable\[\.\.\."
17
19
  ---
18
20
 
19
21
  # Use ParamSpec for Typed Decorators
@@ -14,6 +14,8 @@ aliases:
14
14
  python: ">=3.12"
15
15
  frequency: medium
16
16
  pep: 695
17
+ detect-patterns:
18
+ - "from typing import .*\bGeneric\b"
17
19
  ---
18
20
 
19
21
  # Use PEP 695 Type Parameter Syntax
@@ -14,6 +14,8 @@ aliases:
14
14
  python: ">=3.13"
15
15
  frequency: low
16
16
  pep: 742
17
+ detect-patterns:
18
+ - "-> TypeGuard\["
17
19
  ---
18
20
 
19
21
  # Use TypeIs for Precise Type Narrowing
@@ -14,6 +14,8 @@ aliases:
14
14
  python: ">=3.10"
15
15
  frequency: high
16
16
  pep: 604
17
+ detect-patterns:
18
+ - "from typing import .*\b(Optional|Union)\b"
17
19
  ---
18
20
 
19
21
  # Use X | Y Union Syntax
@@ -16,6 +16,8 @@ aliases:
16
16
  python: ">=3.9"
17
17
  frequency: high
18
18
  pep: 585
19
+ detect-patterns:
20
+ - "from typing import .*\b(List|Dict|Set|Tuple|FrozenSet|Type|Deque|DefaultDict|OrderedDict|Counter|ChainMap)\b"
19
21
  ---
20
22
 
21
23
  # Use Built-in Generic Types
@@ -1,3 +1,3 @@
1
1
  """Modern Python Guidance — version-aware BAD/GOOD pattern guides for AI coding agents."""
2
2
 
3
- __version__ = "0.3.8"
3
+ __version__ = "0.4.0"
@@ -0,0 +1,146 @@
1
+ """Scan a Python file for outdated patterns against guide definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from modern_python_guidance.compat import version_compatible
10
+ from modern_python_guidance.guide_index import Guide, GuideIndex, _code_lines
11
+
12
+ FREQ_RANK = {"high": 0, "medium": 1, "low": 2}
13
+
14
+
15
+ class CheckError(Exception):
16
+ """Raised for unrecoverable file-level errors (not found, binary, unreadable)."""
17
+
18
+
19
+ _MAX_LINE_LEN = 10_240
20
+ _BINARY_PROBE_SIZE = 8192
21
+ _ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
22
+ _CTRL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
23
+
24
+
25
+ @dataclass
26
+ class CheckMatch:
27
+ line: int
28
+ source_line: str
29
+ guide_id: str
30
+ guide_title: str
31
+ category: str
32
+ frequency: str
33
+ snippet: str
34
+
35
+
36
+ def check_file(
37
+ path: Path,
38
+ index: GuideIndex,
39
+ *,
40
+ python_version: str | None = None,
41
+ ) -> list[CheckMatch]:
42
+ _validate_file(path)
43
+ text = _read_file(path)
44
+ if not text:
45
+ return []
46
+
47
+ patterns = _build_patterns(index, python_version=python_version)
48
+ if not patterns:
49
+ return []
50
+
51
+ matches: list[CheckMatch] = []
52
+ for lineno, line in enumerate(text.splitlines(), 1):
53
+ stripped = line.strip()
54
+ if not stripped or stripped.startswith("#"):
55
+ continue
56
+ if len(line) > _MAX_LINE_LEN:
57
+ continue
58
+
59
+ for compiled, guide in patterns:
60
+ if compiled.search(line):
61
+ matches.append(
62
+ CheckMatch(
63
+ line=lineno,
64
+ source_line=line,
65
+ guide_id=guide.meta.id,
66
+ guide_title=guide.meta.title,
67
+ category=guide.meta.category,
68
+ frequency=guide.meta.frequency,
69
+ snippet=guide.snippet,
70
+ )
71
+ )
72
+ break
73
+
74
+ return matches
75
+
76
+
77
+ def _validate_file(path: Path) -> None:
78
+ if not path.exists():
79
+ raise CheckError(f"file not found: {path}")
80
+ if not path.is_file():
81
+ raise CheckError(f"not a file: {path}")
82
+
83
+
84
+ def _read_file(path: Path) -> str:
85
+ try:
86
+ raw = path.read_bytes()
87
+ except OSError as e:
88
+ raise CheckError(f"cannot read {path}: {e}") from e
89
+
90
+ probe = raw[:_BINARY_PROBE_SIZE]
91
+ if b"\x00" in probe:
92
+ raise CheckError(f"binary file: {path}")
93
+
94
+ try:
95
+ return raw.decode("utf-8")
96
+ except UnicodeDecodeError:
97
+ return raw.decode("utf-8", errors="replace")
98
+
99
+
100
+ def _build_patterns(
101
+ index: GuideIndex,
102
+ *,
103
+ python_version: str | None = None,
104
+ ) -> list[tuple[re.Pattern[str], Guide]]:
105
+ entries: list[tuple[re.Pattern[str], Guide]] = []
106
+
107
+ for guide in index.guides.values():
108
+ if python_version and not version_compatible(guide.meta.python, python_version):
109
+ continue
110
+
111
+ raw_patterns = _get_patterns(guide)
112
+ for pat_str in raw_patterns:
113
+ try:
114
+ compiled = re.compile(pat_str)
115
+ entries.append((compiled, guide))
116
+ except re.error:
117
+ pass
118
+
119
+ entries.sort(key=lambda e: (e[1].meta.layer, FREQ_RANK.get(e[1].meta.frequency, 2)))
120
+ return entries
121
+
122
+
123
+ def _get_patterns(guide: Guide) -> list[str]:
124
+ if guide.meta.detect_patterns is not None:
125
+ return guide.meta.detect_patterns
126
+ return _auto_extract_patterns(guide)
127
+
128
+
129
+ def _auto_extract_patterns(guide: Guide) -> list[str]:
130
+ bad_lines = _code_lines(guide.body, "## BAD")
131
+ patterns: list[str] = []
132
+ for line in bad_lines:
133
+ stripped = line.strip()
134
+ if stripped.startswith(("from ", "import ")):
135
+ escaped = re.escape(stripped)
136
+ patterns.append(escaped)
137
+ elif stripped.startswith("@"):
138
+ parts = stripped.split("(", 1)
139
+ escaped = re.escape(parts[0])
140
+ patterns.append(escaped)
141
+ return patterns
142
+
143
+
144
+ def sanitize_line(text: str) -> str:
145
+ text = _ANSI_RE.sub("", text)
146
+ return _CTRL_RE.sub("", text)