modern-python-guidance 0.3.4__tar.gz → 0.3.5__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.4 → modern_python_guidance-0.3.5}/.github/workflows/ci.yml +5 -2
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/CHANGELOG.md +14 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/CONTRIBUTING.md +15 -3
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/PKG-INFO +2 -1
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/score_v5.py +6 -3
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/pyproject.toml +16 -1
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/__init__.py +1 -1
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/cli.py +22 -8
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/frontmatter.py +1 -3
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/mcp_server.py +8 -5
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/search.py +25 -17
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/version_detect.py +2 -6
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/tests/test_cli_integration.py +27 -5
- modern_python_guidance-0.3.5/tests/test_guide_structure.py +129 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/tests/test_mcp_server.py +157 -70
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/tests/test_retrieve.py +10 -2
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/tests/test_search.py +6 -23
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/tests/test_setup.py +13 -6
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/tests/test_skill_sync.py +2 -6
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/tests/test_version_detect.py +6 -18
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/.github/workflows/check-python-release.yml +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/.gitignore +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/LICENSE +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/README.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/SECURITY.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompts/v5-a-detailed.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompts/v5-a-normal.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompts/v5-a-terse.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompts/v5-b-detailed.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompts/v5-b-normal.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompts/v5-b-terse.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompts/v5-c-detailed.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompts/v5-c-normal.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/prompts/v5-c-terse.txt +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/run-v5.sh +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/run.sh +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/score.sh +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/docs/benchmark-v5.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/docs/design.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/stdlib/template-strings.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/guide_index.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/retrieve.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/setup_cmd.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/uninstall_cmd.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/tests/test_frontmatter.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/tests/test_scorer_v5.py +0 -0
- {modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/tests/test_uninstall.py +0 -0
|
@@ -30,8 +30,11 @@ jobs:
|
|
|
30
30
|
- name: Install dependencies
|
|
31
31
|
run: uv pip install --system -e ".[dev]"
|
|
32
32
|
|
|
33
|
-
- name:
|
|
34
|
-
run:
|
|
33
|
+
- name: Check formatting
|
|
34
|
+
run: ruff format --check src/ tests/
|
|
35
35
|
|
|
36
36
|
- name: Run linter
|
|
37
37
|
run: ruff check src/ tests/
|
|
38
|
+
|
|
39
|
+
- name: Run tests
|
|
40
|
+
run: pytest --tb=short -q --cov --cov-report=term-missing
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.5] — 2026-05-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- CI format gate: `ruff format --check src/ tests/` runs before linter, catching formatting regressions at PR time (closes #19)
|
|
10
|
+
- Coverage reporting: `pytest-cov` with branch coverage and `fail_under = 59%` ratchet threshold (closes #15)
|
|
11
|
+
- Guide structure validation: 248 parametrized tests validating all 41 guides — frontmatter fields, section order, code fences, H1 title, no duplicate IDs (closes #16)
|
|
12
|
+
- CONTRIBUTING.md: documented CI checks, format fix command, and guide count update step
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Auto-formatted 12 existing source/test files with `ruff format` (whitespace only, no logic changes)
|
|
17
|
+
- CI step order: checkout → setup → install → **format check** → linter → tests (with `--cov`)
|
|
18
|
+
|
|
5
19
|
## [0.3.4] — 2026-05-30
|
|
6
20
|
|
|
7
21
|
### Fixed
|
|
@@ -37,13 +37,25 @@ See [docs/design.md](docs/design.md) for the full design document.
|
|
|
37
37
|
| `frequency` | string | `high` (LLMs do this often), `medium`, `low` |
|
|
38
38
|
|
|
39
39
|
3. Write BAD/GOOD/Why/Version Notes sections
|
|
40
|
-
4.
|
|
40
|
+
4. Update `EXPECTED_GUIDE_COUNT` in `tests/test_guide_structure.py`
|
|
41
|
+
5. Run `pytest` — the guide structure tests validate frontmatter, section order, and code fences automatically
|
|
41
42
|
|
|
42
43
|
## Running tests
|
|
43
44
|
|
|
44
45
|
```bash
|
|
45
46
|
uv venv && source .venv/bin/activate
|
|
46
47
|
uv pip install -e ".[dev]"
|
|
47
|
-
pytest
|
|
48
|
-
ruff check src/ tests/
|
|
48
|
+
pytest # 510+ tests including guide structure validation
|
|
49
|
+
ruff check src/ tests/ # lint
|
|
50
|
+
ruff format --check src/ tests/ # format check (CI enforced)
|
|
49
51
|
```
|
|
52
|
+
|
|
53
|
+
To auto-fix formatting: `ruff format src/ tests/`
|
|
54
|
+
|
|
55
|
+
## CI checks
|
|
56
|
+
|
|
57
|
+
All PRs run these checks on Python 3.11, 3.12, and 3.13:
|
|
58
|
+
|
|
59
|
+
1. `ruff format --check` — formatting
|
|
60
|
+
2. `ruff check` — linting
|
|
61
|
+
3. `pytest --cov` — tests with branch coverage (`fail_under = 59%`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modern-python-guidance
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
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
|
|
@@ -25,6 +25,7 @@ Classifier: Typing :: Typed
|
|
|
25
25
|
Requires-Python: >=3.11
|
|
26
26
|
Requires-Dist: packaging>=23.0
|
|
27
27
|
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
28
29
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
30
|
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
30
31
|
Description-Content-Type: text/markdown
|
|
@@ -1476,14 +1476,17 @@ def main() -> None:
|
|
|
1476
1476
|
"c": "C (pytest)",
|
|
1477
1477
|
}
|
|
1478
1478
|
v_label = variant_labels.get(args.variant, args.variant)
|
|
1479
|
-
|
|
1480
|
-
|
|
1479
|
+
|
|
1480
|
+
if args.output_format == "human":
|
|
1481
|
+
print(f"=== V5 Scoring: Variant {v_label}, Run {args.run_id} ===")
|
|
1482
|
+
print()
|
|
1481
1483
|
|
|
1482
1484
|
output = {}
|
|
1483
1485
|
for session_name in ("control", "treatment"):
|
|
1484
1486
|
session_dir = results_dir / session_name
|
|
1485
1487
|
if not session_dir.is_dir():
|
|
1486
|
-
|
|
1488
|
+
if args.output_format == "human":
|
|
1489
|
+
print(f" [{session_name}] Directory not found, skipping.")
|
|
1487
1490
|
continue
|
|
1488
1491
|
data = score_session(session_dir, args.variant)
|
|
1489
1492
|
output[session_name] = data
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "modern-python-guidance"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.5"
|
|
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"
|
|
@@ -34,6 +34,7 @@ dependencies = [
|
|
|
34
34
|
[project.optional-dependencies]
|
|
35
35
|
dev = [
|
|
36
36
|
"pytest>=7.0",
|
|
37
|
+
"pytest-cov>=4.0",
|
|
37
38
|
"ruff>=0.4.0",
|
|
38
39
|
]
|
|
39
40
|
|
|
@@ -66,3 +67,17 @@ select = ["E", "F", "W", "I", "UP", "FURB", "B", "SIM", "RUF"]
|
|
|
66
67
|
|
|
67
68
|
[tool.pytest.ini_options]
|
|
68
69
|
testpaths = ["tests"]
|
|
70
|
+
|
|
71
|
+
[tool.coverage.run]
|
|
72
|
+
source = ["modern_python_guidance"]
|
|
73
|
+
branch = true
|
|
74
|
+
|
|
75
|
+
[tool.coverage.report]
|
|
76
|
+
show_missing = true
|
|
77
|
+
skip_empty = true
|
|
78
|
+
fail_under = 59
|
|
79
|
+
exclude_lines = [
|
|
80
|
+
"pragma: no cover",
|
|
81
|
+
"if __name__ == .__main__.",
|
|
82
|
+
"if TYPE_CHECKING:",
|
|
83
|
+
]
|
{modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/cli.py
RENAMED
|
@@ -36,7 +36,9 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
36
36
|
p_search.add_argument("--category", help="Filter by category")
|
|
37
37
|
p_search.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
|
|
38
38
|
p_search.add_argument(
|
|
39
|
-
"--format",
|
|
39
|
+
"--format",
|
|
40
|
+
choices=["json", "human"],
|
|
41
|
+
default=None,
|
|
40
42
|
help="Output format (default: json when piped, human when TTY)",
|
|
41
43
|
)
|
|
42
44
|
|
|
@@ -45,7 +47,9 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
45
47
|
p_retrieve.add_argument("ids", help="Comma-separated guide IDs")
|
|
46
48
|
p_retrieve.add_argument("--python-version", help="Target Python version")
|
|
47
49
|
p_retrieve.add_argument(
|
|
48
|
-
"--format",
|
|
50
|
+
"--format",
|
|
51
|
+
choices=["json", "human"],
|
|
52
|
+
default=None,
|
|
49
53
|
help="Output format (default: json when piped, human when TTY)",
|
|
50
54
|
)
|
|
51
55
|
|
|
@@ -54,7 +58,9 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
54
58
|
p_list.add_argument("--category", help="Filter by category")
|
|
55
59
|
p_list.add_argument("--python-version", help="Filter by Python version")
|
|
56
60
|
p_list.add_argument(
|
|
57
|
-
"--format",
|
|
61
|
+
"--format",
|
|
62
|
+
choices=["json", "human"],
|
|
63
|
+
default=None,
|
|
58
64
|
help="Output format (default: json when piped, human when TTY)",
|
|
59
65
|
)
|
|
60
66
|
|
|
@@ -67,27 +73,35 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
67
73
|
|
|
68
74
|
# setup
|
|
69
75
|
p_setup = subparsers.add_parser(
|
|
70
|
-
"setup",
|
|
76
|
+
"setup",
|
|
77
|
+
help="Register MCP server and link Agent Skills",
|
|
71
78
|
)
|
|
72
79
|
p_setup.add_argument("--mcp-only", action="store_true", help="MCP registration only")
|
|
73
80
|
p_setup.add_argument("--skills-only", action="store_true", help="Skills symlink only")
|
|
74
81
|
p_setup.add_argument(
|
|
75
|
-
"--scope",
|
|
82
|
+
"--scope",
|
|
83
|
+
choices=["user", "local"],
|
|
84
|
+
default="user",
|
|
76
85
|
help="MCP scope (default: user)",
|
|
77
86
|
)
|
|
78
87
|
p_setup.add_argument(
|
|
79
|
-
"--project-dir",
|
|
88
|
+
"--project-dir",
|
|
89
|
+
type=Path,
|
|
90
|
+
help="Project directory for Skills symlink",
|
|
80
91
|
)
|
|
81
92
|
p_setup.add_argument("--dry-run", action="store_true", help="Show what would be done")
|
|
82
93
|
|
|
83
94
|
# uninstall
|
|
84
95
|
p_uninstall = subparsers.add_parser(
|
|
85
|
-
"uninstall",
|
|
96
|
+
"uninstall",
|
|
97
|
+
help="Reverse 'setup': deregister MCP server and unlink Agent Skills",
|
|
86
98
|
)
|
|
87
99
|
p_uninstall.add_argument("--mcp-only", action="store_true", help="MCP deregistration only")
|
|
88
100
|
p_uninstall.add_argument("--skills-only", action="store_true", help="Skills unlink only")
|
|
89
101
|
p_uninstall.add_argument(
|
|
90
|
-
"--project-dir",
|
|
102
|
+
"--project-dir",
|
|
103
|
+
type=Path,
|
|
104
|
+
help="Project directory for Skills symlink",
|
|
91
105
|
)
|
|
92
106
|
p_uninstall.add_argument("--dry-run", action="store_true", help="Show what would be done")
|
|
93
107
|
|
|
@@ -75,9 +75,7 @@ def _parse_raw(lines: list[str]) -> dict[str, Any]:
|
|
|
75
75
|
if current_key is None:
|
|
76
76
|
raise FrontmatterError("list item without preceding key", line=i)
|
|
77
77
|
if not isinstance(data[current_key], list):
|
|
78
|
-
raise FrontmatterError(
|
|
79
|
-
f"list item for non-list key '{current_key}'", line=i
|
|
80
|
-
)
|
|
78
|
+
raise FrontmatterError(f"list item for non-list key '{current_key}'", line=i)
|
|
81
79
|
data[current_key].append(_parse_scalar(list_match.group(1).strip()))
|
|
82
80
|
continue
|
|
83
81
|
|
|
@@ -340,11 +340,14 @@ def _handle_request(msg: dict) -> dict | None:
|
|
|
340
340
|
return None
|
|
341
341
|
|
|
342
342
|
if method == "initialize":
|
|
343
|
-
result = _result_response(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
343
|
+
result = _result_response(
|
|
344
|
+
req_id,
|
|
345
|
+
{
|
|
346
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
347
|
+
"capabilities": {"tools": {}},
|
|
348
|
+
"serverInfo": {"name": "modern-python-guidance", "version": __version__},
|
|
349
|
+
},
|
|
350
|
+
)
|
|
348
351
|
return None if is_notification else result
|
|
349
352
|
|
|
350
353
|
if method == "tools/list":
|
{modern_python_guidance-0.3.4 → modern_python_guidance-0.3.5}/src/modern_python_guidance/search.py
RENAMED
|
@@ -60,19 +60,25 @@ def search(
|
|
|
60
60
|
|
|
61
61
|
if score > 0:
|
|
62
62
|
score += FREQ_BOOST.get(meta.frequency, 0.0)
|
|
63
|
-
results.append(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
results.append(
|
|
64
|
+
SearchResult(
|
|
65
|
+
guide_id=guide_id,
|
|
66
|
+
score=score,
|
|
67
|
+
meta=meta,
|
|
68
|
+
token_estimate=token_estimate(guide.body),
|
|
69
|
+
snippet=guide.snippet,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
70
72
|
|
|
71
73
|
results.sort(key=lambda r: (-r.score, r.guide_id))
|
|
72
74
|
|
|
73
75
|
if not results:
|
|
74
76
|
return _fuzzy_fallback(
|
|
75
|
-
index,
|
|
77
|
+
index,
|
|
78
|
+
query,
|
|
79
|
+
python_version=python_version,
|
|
80
|
+
category=category,
|
|
81
|
+
limit=limit,
|
|
76
82
|
)
|
|
77
83
|
|
|
78
84
|
return results[:limit]
|
|
@@ -139,14 +145,16 @@ def _fuzzy_fallback(
|
|
|
139
145
|
continue
|
|
140
146
|
seen.add(guide_id)
|
|
141
147
|
guide = candidates[guide_id]
|
|
142
|
-
results.append(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
results.append(
|
|
149
|
+
SearchResult(
|
|
150
|
+
guide_id=guide_id,
|
|
151
|
+
score=round(ratio, 3),
|
|
152
|
+
meta=guide.meta,
|
|
153
|
+
token_estimate=token_estimate(guide.body),
|
|
154
|
+
fuzzy=True,
|
|
155
|
+
snippet=guide.snippet,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
150
158
|
|
|
151
159
|
results.sort(key=lambda r: (-r.score, r.guide_id))
|
|
152
|
-
return results[:min(limit, FUZZY_MAX)]
|
|
160
|
+
return results[: min(limit, FUZZY_MAX)]
|
|
@@ -21,9 +21,7 @@ log = logging.getLogger(__name__)
|
|
|
21
21
|
|
|
22
22
|
DEFAULT_VERSION = "3.11"
|
|
23
23
|
|
|
24
|
-
_KNOWN_MINORS = [
|
|
25
|
-
Version(f"3.{minor}") for minor in range(7, 20)
|
|
26
|
-
]
|
|
24
|
+
_KNOWN_MINORS = [Version(f"3.{minor}") for minor in range(7, 20)]
|
|
27
25
|
|
|
28
26
|
_POETRY_CARET_RE = re.compile(r"\^(\d+\.\d+)")
|
|
29
27
|
|
|
@@ -68,9 +66,7 @@ def _from_pyproject(path: Path) -> str | None:
|
|
|
68
66
|
if requires_python:
|
|
69
67
|
return _min_version_from_specifier(requires_python)
|
|
70
68
|
|
|
71
|
-
poetry_python = (
|
|
72
|
-
data.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python")
|
|
73
|
-
)
|
|
69
|
+
poetry_python = data.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python")
|
|
74
70
|
if poetry_python:
|
|
75
71
|
m = _POETRY_CARET_RE.search(str(poetry_python))
|
|
76
72
|
if m:
|
|
@@ -55,8 +55,17 @@ class TestSearch:
|
|
|
55
55
|
assert r.returncode == 0
|
|
56
56
|
data = json.loads(r.stdout)
|
|
57
57
|
expected_keys = {
|
|
58
|
-
"id",
|
|
59
|
-
"
|
|
58
|
+
"id",
|
|
59
|
+
"title",
|
|
60
|
+
"category",
|
|
61
|
+
"layer",
|
|
62
|
+
"tags",
|
|
63
|
+
"python",
|
|
64
|
+
"frequency",
|
|
65
|
+
"score",
|
|
66
|
+
"token_estimate",
|
|
67
|
+
"fuzzy",
|
|
68
|
+
"snippet",
|
|
60
69
|
}
|
|
61
70
|
assert set(data[0].keys()) == expected_keys
|
|
62
71
|
assert isinstance(data[0]["tags"], list)
|
|
@@ -89,14 +98,27 @@ class TestRetrieve:
|
|
|
89
98
|
r = run_cli("retrieve", "use-builtin-generics", "--format", "json")
|
|
90
99
|
data = json.loads(r.stdout)
|
|
91
100
|
expected_keys = {
|
|
92
|
-
"id",
|
|
93
|
-
"
|
|
101
|
+
"id",
|
|
102
|
+
"title",
|
|
103
|
+
"category",
|
|
104
|
+
"layer",
|
|
105
|
+
"python",
|
|
106
|
+
"frequency",
|
|
107
|
+
"version_match",
|
|
108
|
+
"content",
|
|
109
|
+
"token_estimate",
|
|
110
|
+
"source",
|
|
94
111
|
}
|
|
95
112
|
assert set(data[0].keys()) == expected_keys
|
|
96
113
|
|
|
97
114
|
def test_retrieve_version_match_flag(self):
|
|
98
115
|
r = run_cli(
|
|
99
|
-
"retrieve",
|
|
116
|
+
"retrieve",
|
|
117
|
+
"taskgroup-over-gather",
|
|
118
|
+
"--python-version",
|
|
119
|
+
"3.9",
|
|
120
|
+
"--format",
|
|
121
|
+
"json",
|
|
100
122
|
)
|
|
101
123
|
data = json.loads(r.stdout)
|
|
102
124
|
assert data[0]["version_match"] is False
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Structural validation for all guide files.
|
|
2
|
+
|
|
3
|
+
Ensures every guide in skills/modern-python-guidance/guides/ conforms to:
|
|
4
|
+
- Valid frontmatter (parsed by parse_frontmatter)
|
|
5
|
+
- id matches filename, category matches parent directory
|
|
6
|
+
- Exactly 5 ## sections in order: BAD, GOOD, Why, <any>, References
|
|
7
|
+
- Code fences in BAD and GOOD sections
|
|
8
|
+
- Body starts with H1 title
|
|
9
|
+
- No duplicate IDs across all guides
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from modern_python_guidance.frontmatter import parse_frontmatter
|
|
20
|
+
from modern_python_guidance.guide_index import _find_guides_dir
|
|
21
|
+
|
|
22
|
+
GUIDES_DIR = _find_guides_dir()
|
|
23
|
+
EXPECTED_GUIDE_COUNT = 41
|
|
24
|
+
|
|
25
|
+
REQUIRED_HEADING_ORDER = {
|
|
26
|
+
0: "BAD",
|
|
27
|
+
1: "GOOD",
|
|
28
|
+
2: "Why",
|
|
29
|
+
4: "References",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _collect_guides() -> list[Path]:
|
|
34
|
+
return sorted(GUIDES_DIR.rglob("*.md"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _headings_outside_fences(body: str) -> list[str]:
|
|
38
|
+
# Handles both ``` and ~~~ fences; guide_index._code_lines only handles ```
|
|
39
|
+
headings = []
|
|
40
|
+
in_fence = False
|
|
41
|
+
for line in body.splitlines():
|
|
42
|
+
stripped = line.strip()
|
|
43
|
+
if re.match(r"^(`{3,}|~{3,})", stripped):
|
|
44
|
+
in_fence = not in_fence
|
|
45
|
+
continue
|
|
46
|
+
if not in_fence and line.startswith("## "):
|
|
47
|
+
headings.append(line[3:].strip())
|
|
48
|
+
return headings
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _section_text(body: str, heading: str) -> str:
|
|
52
|
+
# Not fence-aware: splits on \n## which could appear inside code fences.
|
|
53
|
+
# Current guides have no ## at line start inside fences, so this is safe for now.
|
|
54
|
+
parts = body.split(f"## {heading}\n", 1)
|
|
55
|
+
if len(parts) < 2:
|
|
56
|
+
return ""
|
|
57
|
+
section = parts[1].split("\n## ", 1)[0]
|
|
58
|
+
return section
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_GUIDE_FILES = _collect_guides()
|
|
62
|
+
_GUIDE_IDS = [f"{f.parent.name}/{f.stem}" for f in _GUIDE_FILES]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture(params=_GUIDE_FILES, ids=_GUIDE_IDS)
|
|
66
|
+
def guide_file(request: pytest.FixtureRequest) -> Path:
|
|
67
|
+
return request.param
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestGuideStructure:
|
|
71
|
+
def test_parses_without_error(self, guide_file: Path):
|
|
72
|
+
text = guide_file.read_text(encoding="utf-8")
|
|
73
|
+
parse_frontmatter(text)
|
|
74
|
+
|
|
75
|
+
def test_id_matches_filename(self, guide_file: Path):
|
|
76
|
+
text = guide_file.read_text(encoding="utf-8")
|
|
77
|
+
meta, _ = parse_frontmatter(text)
|
|
78
|
+
assert meta.id == guide_file.stem, (
|
|
79
|
+
f"frontmatter id '{meta.id}' != filename '{guide_file.stem}'"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def test_category_matches_dirname(self, guide_file: Path):
|
|
83
|
+
text = guide_file.read_text(encoding="utf-8")
|
|
84
|
+
meta, _ = parse_frontmatter(text)
|
|
85
|
+
assert meta.category == guide_file.parent.name, (
|
|
86
|
+
f"frontmatter category '{meta.category}' != dir '{guide_file.parent.name}'"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def test_section_headings(self, guide_file: Path):
|
|
90
|
+
text = guide_file.read_text(encoding="utf-8")
|
|
91
|
+
_, body = parse_frontmatter(text)
|
|
92
|
+
headings = _headings_outside_fences(body)
|
|
93
|
+
|
|
94
|
+
assert len(headings) == 5, f"expected 5 ## headings, got {len(headings)}: {headings}"
|
|
95
|
+
for idx, expected in REQUIRED_HEADING_ORDER.items():
|
|
96
|
+
assert headings[idx] == expected, (
|
|
97
|
+
f"heading[{idx}] expected '{expected}', got '{headings[idx]}'"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def test_bad_good_have_code_fences(self, guide_file: Path):
|
|
101
|
+
text = guide_file.read_text(encoding="utf-8")
|
|
102
|
+
_, body = parse_frontmatter(text)
|
|
103
|
+
for section_name in ("BAD", "GOOD"):
|
|
104
|
+
section = _section_text(body, section_name)
|
|
105
|
+
assert re.search(r"^(`{3,}|~{3,})", section, re.MULTILINE), (
|
|
106
|
+
f"## {section_name} section has no code fence"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def test_body_starts_with_h1(self, guide_file: Path):
|
|
110
|
+
text = guide_file.read_text(encoding="utf-8")
|
|
111
|
+
_, body = parse_frontmatter(text)
|
|
112
|
+
assert body.startswith("# "), "body does not start with H1 heading"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestGuideInventory:
|
|
116
|
+
def test_no_duplicate_ids(self):
|
|
117
|
+
seen: dict[str, Path] = {}
|
|
118
|
+
for guide_file in _GUIDE_FILES:
|
|
119
|
+
text = guide_file.read_text(encoding="utf-8")
|
|
120
|
+
meta, _ = parse_frontmatter(text)
|
|
121
|
+
assert meta.id not in seen, (
|
|
122
|
+
f"duplicate id '{meta.id}': {seen[meta.id]} and {guide_file}"
|
|
123
|
+
)
|
|
124
|
+
seen[meta.id] = guide_file
|
|
125
|
+
|
|
126
|
+
def test_guide_count(self):
|
|
127
|
+
assert len(_GUIDE_FILES) == EXPECTED_GUIDE_COUNT, (
|
|
128
|
+
f"expected {EXPECTED_GUIDE_COUNT} guides, found {len(_GUIDE_FILES)}"
|
|
129
|
+
)
|