modern-python-guidance 0.3.7__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.
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/CHANGELOG.md +20 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/PKG-INFO +6 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/README.md +5 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/pyproject.toml +1 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/async/async-timeout-context.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/async/exception-groups.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/django/django-async-views.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/django/django-check-constraints.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/django/django-json-field.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +4 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +3 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +3 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +3 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/stdlib/template-strings.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +3 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +3 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/override-decorator.md +1 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/union-syntax.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +2 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/__init__.py +1 -1
- modern_python_guidance-0.4.0/src/modern_python_guidance/check.py +146 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/cli.py +115 -10
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/frontmatter.py +15 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/mcp_server.py +11 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/retrieve.py +13 -0
- modern_python_guidance-0.4.0/tests/test_check.py +284 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_cli_integration.py +73 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_cli_unit.py +44 -2
- modern_python_guidance-0.4.0/tests/test_compat.py +82 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_frontmatter.py +88 -0
- modern_python_guidance-0.4.0/tests/test_guide_index.py +280 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_guide_structure.py +48 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_mcp_server.py +3 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_mcp_unit.py +45 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_retrieve.py +34 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/.github/workflows/check-python-release.yml +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/.github/workflows/ci.yml +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/.gitignore +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/CONTRIBUTING.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/LICENSE +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/SECURITY.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompts/v5-a-detailed.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompts/v5-a-normal.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompts/v5-a-terse.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompts/v5-b-detailed.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompts/v5-b-normal.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompts/v5-b-terse.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompts/v5-c-detailed.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompts/v5-c-normal.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/prompts/v5-c-terse.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/run-v5.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/run.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/score.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/score_v5.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/docs/benchmark-v5.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/docs/design.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/rules/modern-python.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/guide_index.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/search.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/setup_cmd.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/uninstall_cmd.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/src/modern_python_guidance/version_detect.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_scorer_v5.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_search.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_setup.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_skill_sync.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_uninstall.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.4.0}/tests/test_version_detect.py +0 -0
|
@@ -2,6 +2,26 @@
|
|
|
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
|
+
|
|
15
|
+
## [0.3.8] — 2026-06-02
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Fuzzy suggestions on retrieve miss: when a guide ID is not found, `difflib.get_close_matches` suggests up to 3 similar IDs (cutoff=0.5, case-insensitive). CLI shows "Did you mean:" in human format; JSON format and MCP tool return an envelope `{"results": [...], "not_found": [{"id": ..., "suggestions": [...]}]}`. Bare list preserved on all-found for backward compatibility. Exit code 1 when any ID is not found. (closes #14)
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `_handle_request` crash on non-dict JSON input (list, string, number, bool): now returns JSON-RPC -32600 "Invalid Request" error instead of `AttributeError`. Server continues processing subsequent requests. (closes #82)
|
|
24
|
+
|
|
5
25
|
## [0.3.7] — 2026-06-02
|
|
6
26
|
|
|
7
27
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modern-python-guidance
|
|
3
|
-
Version: 0.
|
|
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.
|
|
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"
|
|
@@ -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)
|