modern-python-guidance 0.3.8__tar.gz → 0.4.1__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.8 → modern_python_guidance-0.4.1}/CHANGELOG.md +17 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/PKG-INFO +6 -1
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/README.md +5 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/pyproject.toml +1 -1
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/async/async-timeout-context.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/async/exception-groups.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/django/django-async-views.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/django/django-check-constraints.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/django/django-json-field.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +4 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +3 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +3 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +3 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/stdlib/template-strings.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +3 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +3 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/typing/override-decorator.md +1 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/typing/union-syntax.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +2 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/__init__.py +1 -1
- modern_python_guidance-0.4.1/src/modern_python_guidance/check.py +146 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/cli.py +89 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/frontmatter.py +15 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/guide_index.py +25 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/search.py +15 -2
- modern_python_guidance-0.4.1/tests/test_check.py +284 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_cli_integration.py +73 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_frontmatter.py +88 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_guide_index.py +90 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_guide_structure.py +48 -1
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_search.py +98 -2
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/.github/workflows/check-python-release.yml +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/.github/workflows/ci.yml +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/.gitignore +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/CONTRIBUTING.md +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/LICENSE +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/SECURITY.md +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompts/v5-a-detailed.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompts/v5-a-normal.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompts/v5-a-terse.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompts/v5-b-detailed.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompts/v5-b-normal.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompts/v5-b-terse.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompts/v5-c-detailed.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompts/v5-c-normal.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/prompts/v5-c-terse.txt +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/run-v5.sh +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/run.sh +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/score.sh +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/score_v5.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/docs/benchmark-v5.md +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/docs/design.md +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/rules/modern-python.md +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/mcp_server.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/retrieve.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/setup_cmd.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/uninstall_cmd.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/src/modern_python_guidance/version_detect.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_cli_unit.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_compat.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_mcp_server.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_mcp_unit.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_retrieve.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_scorer_v5.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_setup.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_skill_sync.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_uninstall.py +0 -0
- {modern_python_guidance-0.3.8 → modern_python_guidance-0.4.1}/tests/test_version_detect.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.1] — 2026-06-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Body text search indexing: API names and identifiers appearing only in guide body text (e.g. `aiter_bytes`, `from_attributes`, `serialize_timestamp`) are now discoverable via `mpg search`. Body matches score at `WEIGHT_BODY=2`, below all frontmatter weights (TAG=10, ALIAS=8, TITLE=5, CATEGORY=3), preserving existing metadata-dominant ranking. Two-tier query tokenization handles code fragments like `aiter_bytes()` and `from_attributes=True`. (closes #22)
|
|
10
|
+
- 25 new tests (911 total).
|
|
11
|
+
|
|
12
|
+
## [0.4.0] — 2026-06-03
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `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)
|
|
17
|
+
- `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`.
|
|
18
|
+
- `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.
|
|
19
|
+
- 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.
|
|
20
|
+
- 205 new tests (886 total). Coverage: 92%+.
|
|
21
|
+
|
|
5
22
|
## [0.3.8] — 2026-06-02
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modern-python-guidance
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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.1"
|
|
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)
|