modern-python-guidance 0.3.7__tar.gz → 0.3.8__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.3.8}/CHANGELOG.md +10 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/PKG-INFO +1 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/pyproject.toml +1 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/__init__.py +1 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/cli.py +26 -10
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/mcp_server.py +11 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/retrieve.py +13 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_cli_unit.py +44 -2
- modern_python_guidance-0.3.8/tests/test_compat.py +82 -0
- modern_python_guidance-0.3.8/tests/test_guide_index.py +280 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_mcp_server.py +3 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_mcp_unit.py +45 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_retrieve.py +34 -1
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/.github/workflows/check-python-release.yml +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/.github/workflows/ci.yml +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/.gitignore +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/CONTRIBUTING.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/LICENSE +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/README.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/SECURITY.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-a-detailed.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-a-normal.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-a-terse.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-b-detailed.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-b-normal.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-b-terse.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-c-detailed.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-c-normal.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-c-terse.txt +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/run-v5.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/run.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/score.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/score_v5.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/docs/benchmark-v5.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/docs/design.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/rules/modern-python.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/stdlib/template-strings.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/frontmatter.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/guide_index.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/search.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/setup_cmd.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/uninstall_cmd.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/version_detect.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_cli_integration.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_frontmatter.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_guide_structure.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_scorer_v5.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_search.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_setup.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_skill_sync.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/tests/test_uninstall.py +0 -0
- {modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/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.3.8] — 2026-06-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- 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)
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `_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)
|
|
14
|
+
|
|
5
15
|
## [0.3.7] — 2026-06-02
|
|
6
16
|
|
|
7
17
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modern-python-guidance
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
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
|
|
@@ -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.8"
|
|
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"
|
{modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/cli.py
RENAMED
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from modern_python_guidance import __version__
|
|
13
13
|
from modern_python_guidance.compat import VERSION_RE, version_compatible
|
|
14
14
|
from modern_python_guidance.guide_index import build_index
|
|
15
|
-
from modern_python_guidance.retrieve import retrieve
|
|
15
|
+
from modern_python_guidance.retrieve import retrieve, suggest_ids
|
|
16
16
|
from modern_python_guidance.search import search as do_search
|
|
17
17
|
from modern_python_guidance.version_detect import detect_version
|
|
18
18
|
|
|
@@ -191,26 +191,42 @@ def _cmd_search(args: argparse.Namespace) -> None:
|
|
|
191
191
|
|
|
192
192
|
def _cmd_retrieve(args: argparse.Namespace) -> None:
|
|
193
193
|
index = build_index()
|
|
194
|
-
guide_ids = [gid.strip() for gid in args.ids.split(",")]
|
|
194
|
+
guide_ids = [gid.strip() for gid in args.ids.split(",") if gid.strip()]
|
|
195
|
+
if not guide_ids:
|
|
196
|
+
print("No guide IDs provided.")
|
|
197
|
+
sys.exit(1)
|
|
195
198
|
results = retrieve(index, guide_ids, python_version=args.python_version)
|
|
196
199
|
|
|
197
|
-
|
|
200
|
+
found_ids = {r["id"] for r in results}
|
|
201
|
+
missing = [gid for gid in guide_ids if gid not in found_ids]
|
|
198
202
|
|
|
199
|
-
|
|
200
|
-
if fmt == "human":
|
|
201
|
-
print("No guides found.")
|
|
202
|
-
else:
|
|
203
|
-
print("[]")
|
|
204
|
-
sys.exit(1)
|
|
203
|
+
fmt = _resolve_format(args)
|
|
205
204
|
|
|
206
205
|
if fmt == "json":
|
|
207
|
-
|
|
206
|
+
if missing:
|
|
207
|
+
not_found = [{"id": gid, "suggestions": suggest_ids(index, gid)} for gid in missing]
|
|
208
|
+
envelope = {"results": results, "not_found": not_found}
|
|
209
|
+
print(json.dumps(envelope, indent=2, ensure_ascii=False))
|
|
210
|
+
else:
|
|
211
|
+
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
208
212
|
else:
|
|
209
213
|
for r in results:
|
|
210
214
|
match_str = "YES" if r["version_match"] else "NO"
|
|
211
215
|
print(f"--- {r['id']} (version match: {match_str}) ---")
|
|
212
216
|
print(r["content"])
|
|
213
217
|
print()
|
|
218
|
+
for gid in missing:
|
|
219
|
+
suggestions = suggest_ids(index, gid)
|
|
220
|
+
if suggestions:
|
|
221
|
+
print(f"No guide found for '{gid}'. Did you mean:")
|
|
222
|
+
for s in suggestions:
|
|
223
|
+
print(f" {s}")
|
|
224
|
+
else:
|
|
225
|
+
print(f"No guide found for '{gid}'.")
|
|
226
|
+
print("Run 'mpg list' to see available guides.")
|
|
227
|
+
|
|
228
|
+
if missing:
|
|
229
|
+
sys.exit(1)
|
|
214
230
|
|
|
215
231
|
|
|
216
232
|
def _cmd_list(args: argparse.Namespace) -> None:
|
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from modern_python_guidance import __version__
|
|
11
11
|
from modern_python_guidance.compat import VERSION_RE
|
|
12
12
|
from modern_python_guidance.guide_index import GuideIndex, build_index
|
|
13
|
-
from modern_python_guidance.retrieve import retrieve
|
|
13
|
+
from modern_python_guidance.retrieve import retrieve, suggest_ids
|
|
14
14
|
from modern_python_guidance.search import search
|
|
15
15
|
from modern_python_guidance.version_detect import detect_version
|
|
16
16
|
|
|
@@ -280,6 +280,14 @@ def _tool_retrieve(arguments: dict) -> dict:
|
|
|
280
280
|
|
|
281
281
|
index = _get_index()
|
|
282
282
|
results = retrieve(index, guide_ids, python_version=pv)
|
|
283
|
+
|
|
284
|
+
found_ids = {r["id"] for r in results}
|
|
285
|
+
missing = [gid for gid in guide_ids if gid not in found_ids]
|
|
286
|
+
if missing:
|
|
287
|
+
not_found = [{"id": gid, "suggestions": suggest_ids(index, gid)} for gid in missing]
|
|
288
|
+
envelope = {"results": results, "not_found": not_found}
|
|
289
|
+
return _tool_result(json.dumps(envelope, indent=2, ensure_ascii=False))
|
|
290
|
+
|
|
283
291
|
return _tool_result(json.dumps(results, indent=2, ensure_ascii=False))
|
|
284
292
|
|
|
285
293
|
|
|
@@ -332,6 +340,8 @@ PROTOCOL_VERSION = "2024-11-05"
|
|
|
332
340
|
|
|
333
341
|
|
|
334
342
|
def _handle_request(msg: dict) -> dict | None:
|
|
343
|
+
if not isinstance(msg, dict):
|
|
344
|
+
return _error_response(None, -32600, "Invalid Request: expected JSON object")
|
|
335
345
|
method = msg.get("method", "")
|
|
336
346
|
req_id = msg.get("id")
|
|
337
347
|
params = msg.get("params", {})
|
{modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/retrieve.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import difflib
|
|
5
6
|
import json
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -9,6 +10,18 @@ from modern_python_guidance import __version__
|
|
|
9
10
|
from modern_python_guidance.compat import token_estimate, version_compatible
|
|
10
11
|
from modern_python_guidance.guide_index import Guide, GuideIndex
|
|
11
12
|
|
|
13
|
+
MAX_ID_LEN = 200
|
|
14
|
+
_SUGGEST_CUTOFF = 0.5
|
|
15
|
+
_SUGGEST_MAX = 3
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def suggest_ids(index: GuideIndex, missing_id: str) -> list[str]:
|
|
19
|
+
if not isinstance(missing_id, str):
|
|
20
|
+
return []
|
|
21
|
+
truncated = missing_id[:MAX_ID_LEN].lower()
|
|
22
|
+
all_ids = list(index.guides.keys())
|
|
23
|
+
return difflib.get_close_matches(truncated, all_ids, n=_SUGGEST_MAX, cutoff=_SUGGEST_CUTOFF)
|
|
24
|
+
|
|
12
25
|
|
|
13
26
|
def retrieve(
|
|
14
27
|
index: GuideIndex,
|
|
@@ -115,14 +115,56 @@ class TestCmdRetrieve:
|
|
|
115
115
|
assert "use-builtin-generics" in ids
|
|
116
116
|
assert "union-syntax" in ids
|
|
117
117
|
|
|
118
|
-
def
|
|
118
|
+
def test_no_match_exits_1_json_envelope(self, capsys):
|
|
119
119
|
with pytest.raises(SystemExit, match="1"):
|
|
120
120
|
main(argv=["retrieve", "nonexistent-guide-id", "--format", "json"])
|
|
121
|
+
data = json.loads(capsys.readouterr().out)
|
|
122
|
+
assert "not_found" in data
|
|
123
|
+
assert data["results"] == []
|
|
124
|
+
assert data["not_found"][0]["id"] == "nonexistent-guide-id"
|
|
121
125
|
|
|
122
126
|
def test_no_match_human_exits_1(self, capsys):
|
|
123
127
|
with pytest.raises(SystemExit, match="1"):
|
|
124
128
|
main(argv=["retrieve", "nonexistent-guide-id", "--format", "human"])
|
|
125
|
-
assert "No
|
|
129
|
+
assert "No guide found for" in capsys.readouterr().out
|
|
130
|
+
|
|
131
|
+
def test_suggestion_human(self, capsys):
|
|
132
|
+
with pytest.raises(SystemExit, match="1"):
|
|
133
|
+
main(argv=["retrieve", "builtin-generics", "--format", "human"])
|
|
134
|
+
out = capsys.readouterr().out
|
|
135
|
+
assert "Did you mean" in out
|
|
136
|
+
assert "use-builtin-generics" in out
|
|
137
|
+
|
|
138
|
+
def test_suggestion_json(self, capsys):
|
|
139
|
+
with pytest.raises(SystemExit, match="1"):
|
|
140
|
+
main(argv=["retrieve", "builtin-generics", "--format", "json"])
|
|
141
|
+
data = json.loads(capsys.readouterr().out)
|
|
142
|
+
assert "use-builtin-generics" in data["not_found"][0]["suggestions"]
|
|
143
|
+
|
|
144
|
+
def test_mixed_valid_and_invalid(self, capsys):
|
|
145
|
+
with pytest.raises(SystemExit, match="1"):
|
|
146
|
+
main(argv=["retrieve", "use-builtin-generics,zzz-fake", "--format", "json"])
|
|
147
|
+
data = json.loads(capsys.readouterr().out)
|
|
148
|
+
assert len(data["results"]) == 1
|
|
149
|
+
assert data["results"][0]["id"] == "use-builtin-generics"
|
|
150
|
+
assert data["not_found"][0]["id"] == "zzz-fake"
|
|
151
|
+
|
|
152
|
+
def test_all_found_bare_list(self, capsys):
|
|
153
|
+
main(argv=["retrieve", "use-builtin-generics", "--format", "json"])
|
|
154
|
+
data = json.loads(capsys.readouterr().out)
|
|
155
|
+
assert isinstance(data, list)
|
|
156
|
+
assert data[0]["id"] == "use-builtin-generics"
|
|
157
|
+
|
|
158
|
+
def test_trailing_comma_ignored(self, capsys):
|
|
159
|
+
main(argv=["retrieve", "use-builtin-generics,", "--format", "json"])
|
|
160
|
+
data = json.loads(capsys.readouterr().out)
|
|
161
|
+
assert isinstance(data, list)
|
|
162
|
+
assert data[0]["id"] == "use-builtin-generics"
|
|
163
|
+
|
|
164
|
+
def test_all_commas_exits_1(self, capsys):
|
|
165
|
+
with pytest.raises(SystemExit, match="1"):
|
|
166
|
+
main(argv=["retrieve", ",,,", "--format", "human"])
|
|
167
|
+
assert "No guide IDs provided" in capsys.readouterr().out
|
|
126
168
|
|
|
127
169
|
def test_version_match_yes(self, capsys):
|
|
128
170
|
main(
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from modern_python_guidance.compat import VERSION_RE, token_estimate, version_compatible
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestVersionCompatible:
|
|
9
|
+
def test_compatible(self):
|
|
10
|
+
assert version_compatible(">=3.9", "3.12") is True
|
|
11
|
+
|
|
12
|
+
def test_incompatible(self):
|
|
13
|
+
assert version_compatible(">=3.11", "3.9") is False
|
|
14
|
+
|
|
15
|
+
def test_boundary_exact(self):
|
|
16
|
+
assert version_compatible(">=3.11", "3.11") is True
|
|
17
|
+
|
|
18
|
+
def test_compound_specifier_pass(self):
|
|
19
|
+
assert version_compatible(">=3.9,<3.13", "3.12") is True
|
|
20
|
+
|
|
21
|
+
def test_compound_specifier_fail(self):
|
|
22
|
+
assert version_compatible(">=3.9,<3.13", "3.13") is False
|
|
23
|
+
|
|
24
|
+
def test_empty_specifier_matches_all(self):
|
|
25
|
+
assert version_compatible("", "3.12") is True
|
|
26
|
+
|
|
27
|
+
def test_invalid_specifier_fail_open(self):
|
|
28
|
+
assert version_compatible("not-valid", "3.12") is True
|
|
29
|
+
|
|
30
|
+
def test_invalid_target_fail_open(self):
|
|
31
|
+
assert version_compatible(">=3.9", "abc") is True
|
|
32
|
+
|
|
33
|
+
def test_patch_level_target(self):
|
|
34
|
+
assert version_compatible(">=3.9", "3.11.1") is True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestTokenEstimate:
|
|
38
|
+
def test_empty_string(self):
|
|
39
|
+
assert token_estimate("") == 0
|
|
40
|
+
|
|
41
|
+
def test_one_char(self):
|
|
42
|
+
assert token_estimate("x") == 0
|
|
43
|
+
|
|
44
|
+
def test_three_chars(self):
|
|
45
|
+
assert token_estimate("xxx") == 0
|
|
46
|
+
|
|
47
|
+
def test_four_chars(self):
|
|
48
|
+
assert token_estimate("abcd") == 1
|
|
49
|
+
|
|
50
|
+
def test_five_chars(self):
|
|
51
|
+
assert token_estimate("xxxxx") == 1
|
|
52
|
+
|
|
53
|
+
def test_hundred_chars(self):
|
|
54
|
+
assert token_estimate("x" * 100) == 25
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestVersionRe:
|
|
58
|
+
@pytest.mark.parametrize(
|
|
59
|
+
("value", "expected"),
|
|
60
|
+
[
|
|
61
|
+
("3.12", True),
|
|
62
|
+
("3.12.1", False),
|
|
63
|
+
("abc", False),
|
|
64
|
+
("", False),
|
|
65
|
+
("v3.12", False),
|
|
66
|
+
("3.", False),
|
|
67
|
+
(".11", False),
|
|
68
|
+
("03.011", True),
|
|
69
|
+
],
|
|
70
|
+
ids=[
|
|
71
|
+
"major_minor",
|
|
72
|
+
"patch_no_match",
|
|
73
|
+
"alpha_no_match",
|
|
74
|
+
"empty_no_match",
|
|
75
|
+
"v_prefix_no_match",
|
|
76
|
+
"trailing_dot_no_match",
|
|
77
|
+
"leading_dot_no_match",
|
|
78
|
+
"leading_zeros_match",
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
def test_pattern(self, value: str, expected: bool):
|
|
82
|
+
assert (VERSION_RE.match(value) is not None) is expected
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from modern_python_guidance.frontmatter import GuideMeta
|
|
8
|
+
from modern_python_guidance.guide_index import (
|
|
9
|
+
Guide,
|
|
10
|
+
GuideIndex,
|
|
11
|
+
_code_lines,
|
|
12
|
+
_find_guides_dir,
|
|
13
|
+
build_index,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _raise_type_error(_pkg):
|
|
18
|
+
raise TypeError("mocked")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _make_guide_md(
|
|
22
|
+
*,
|
|
23
|
+
guide_id: str = "test-guide",
|
|
24
|
+
title: str = "Test Guide",
|
|
25
|
+
category: str = "testing",
|
|
26
|
+
layer: int = 1,
|
|
27
|
+
python: str = ">=3.9",
|
|
28
|
+
frequency: str = "high",
|
|
29
|
+
bad_code: str = "old_code()",
|
|
30
|
+
good_code: str = "new_code()",
|
|
31
|
+
) -> str:
|
|
32
|
+
return (
|
|
33
|
+
f"---\n"
|
|
34
|
+
f"id: {guide_id}\n"
|
|
35
|
+
f"title: {title}\n"
|
|
36
|
+
f"category: {category}\n"
|
|
37
|
+
f"layer: {layer}\n"
|
|
38
|
+
f"tags:\n"
|
|
39
|
+
f" - test\n"
|
|
40
|
+
f'python: "{python}"\n'
|
|
41
|
+
f"frequency: {frequency}\n"
|
|
42
|
+
f"---\n"
|
|
43
|
+
f"\n"
|
|
44
|
+
f"## BAD\n"
|
|
45
|
+
f"```python\n"
|
|
46
|
+
f"{bad_code}\n"
|
|
47
|
+
f"```\n"
|
|
48
|
+
f"\n"
|
|
49
|
+
f"## GOOD\n"
|
|
50
|
+
f"```python\n"
|
|
51
|
+
f"{good_code}\n"
|
|
52
|
+
f"```\n"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# GuideIndex dataclass methods
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestGuideIndex:
|
|
62
|
+
def test_len_empty(self):
|
|
63
|
+
idx = GuideIndex()
|
|
64
|
+
assert len(idx) == 0
|
|
65
|
+
|
|
66
|
+
def test_len_with_entries(self):
|
|
67
|
+
meta = GuideMeta(
|
|
68
|
+
id="a",
|
|
69
|
+
title="A",
|
|
70
|
+
category="c",
|
|
71
|
+
layer=1,
|
|
72
|
+
tags=["t"],
|
|
73
|
+
python=">=3.9",
|
|
74
|
+
frequency="high",
|
|
75
|
+
)
|
|
76
|
+
idx = GuideIndex(guides={"a": Guide(meta=meta, body="", source_path="a.md")})
|
|
77
|
+
assert len(idx) == 1
|
|
78
|
+
|
|
79
|
+
def test_get_existing(self):
|
|
80
|
+
meta = GuideMeta(
|
|
81
|
+
id="a",
|
|
82
|
+
title="A",
|
|
83
|
+
category="c",
|
|
84
|
+
layer=1,
|
|
85
|
+
tags=["t"],
|
|
86
|
+
python=">=3.9",
|
|
87
|
+
frequency="high",
|
|
88
|
+
)
|
|
89
|
+
guide = Guide(meta=meta, body="body", source_path="a.md")
|
|
90
|
+
idx = GuideIndex(guides={"a": guide})
|
|
91
|
+
assert idx.get("a") is guide
|
|
92
|
+
|
|
93
|
+
def test_get_missing(self):
|
|
94
|
+
idx = GuideIndex()
|
|
95
|
+
assert idx.get("nonexistent") is None
|
|
96
|
+
|
|
97
|
+
def test_all_meta(self):
|
|
98
|
+
meta = GuideMeta(
|
|
99
|
+
id="a",
|
|
100
|
+
title="A",
|
|
101
|
+
category="c",
|
|
102
|
+
layer=1,
|
|
103
|
+
tags=["t"],
|
|
104
|
+
python=">=3.9",
|
|
105
|
+
frequency="high",
|
|
106
|
+
)
|
|
107
|
+
idx = GuideIndex(guides={"a": Guide(meta=meta, body="", source_path="a.md")})
|
|
108
|
+
result = idx.all_meta()
|
|
109
|
+
assert len(result) == 1
|
|
110
|
+
assert result[0] is meta
|
|
111
|
+
|
|
112
|
+
def test_categories_sorted_unique(self):
|
|
113
|
+
def _meta(guide_id: str, cat: str) -> GuideMeta:
|
|
114
|
+
return GuideMeta(
|
|
115
|
+
id=guide_id,
|
|
116
|
+
title=guide_id,
|
|
117
|
+
category=cat,
|
|
118
|
+
layer=1,
|
|
119
|
+
tags=["t"],
|
|
120
|
+
python=">=3.9",
|
|
121
|
+
frequency="high",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
idx = GuideIndex(
|
|
125
|
+
guides={
|
|
126
|
+
"b": Guide(meta=_meta("b", "zeta"), body="", source_path="b.md"),
|
|
127
|
+
"a": Guide(meta=_meta("a", "alpha"), body="", source_path="a.md"),
|
|
128
|
+
"c": Guide(meta=_meta("c", "alpha"), body="", source_path="c.md"),
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
assert idx.categories() == ["alpha", "zeta"]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# build_index
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TestBuildIndex:
|
|
140
|
+
def test_happy_path(self, tmp_path: Path):
|
|
141
|
+
(tmp_path / "guide-a.md").write_text(_make_guide_md(guide_id="guide-a"))
|
|
142
|
+
(tmp_path / "guide-b.md").write_text(_make_guide_md(guide_id="guide-b", category="async"))
|
|
143
|
+
idx = build_index(tmp_path)
|
|
144
|
+
assert len(idx) == 2
|
|
145
|
+
assert idx.get("guide-a") is not None
|
|
146
|
+
assert idx.get("guide-b") is not None
|
|
147
|
+
|
|
148
|
+
def test_nonexistent_directory(self, tmp_path: Path, caplog):
|
|
149
|
+
missing = tmp_path / "does-not-exist"
|
|
150
|
+
with caplog.at_level(logging.WARNING):
|
|
151
|
+
idx = build_index(missing)
|
|
152
|
+
assert len(idx) == 0
|
|
153
|
+
assert "not found" in caplog.text
|
|
154
|
+
|
|
155
|
+
def test_empty_directory(self, tmp_path: Path):
|
|
156
|
+
idx = build_index(tmp_path)
|
|
157
|
+
assert len(idx) == 0
|
|
158
|
+
|
|
159
|
+
def test_duplicate_id_first_wins(self, tmp_path: Path, caplog):
|
|
160
|
+
(tmp_path / "aaa.md").write_text(_make_guide_md(guide_id="dup", title="First"))
|
|
161
|
+
(tmp_path / "bbb.md").write_text(_make_guide_md(guide_id="dup", title="Second"))
|
|
162
|
+
(tmp_path / "ccc.md").write_text(_make_guide_md(guide_id="survivor", title="Survivor"))
|
|
163
|
+
with caplog.at_level(logging.WARNING):
|
|
164
|
+
idx = build_index(tmp_path)
|
|
165
|
+
assert len(idx) == 2
|
|
166
|
+
assert idx.get("dup").meta.title == "First"
|
|
167
|
+
assert idx.get("survivor") is not None
|
|
168
|
+
assert "Duplicate" in caplog.text
|
|
169
|
+
|
|
170
|
+
def test_frontmatter_error_skipped(self, tmp_path: Path, caplog):
|
|
171
|
+
(tmp_path / "bad.md").write_text("no frontmatter here")
|
|
172
|
+
(tmp_path / "good.md").write_text(_make_guide_md(guide_id="good"))
|
|
173
|
+
with caplog.at_level(logging.WARNING):
|
|
174
|
+
idx = build_index(tmp_path)
|
|
175
|
+
assert len(idx) == 1
|
|
176
|
+
assert idx.get("good") is not None
|
|
177
|
+
assert "Skipping" in caplog.text
|
|
178
|
+
|
|
179
|
+
def test_generic_exception_skipped(self, tmp_path: Path, caplog):
|
|
180
|
+
(tmp_path / "err.md").write_text("placeholder")
|
|
181
|
+
(tmp_path / "ok.md").write_text(_make_guide_md(guide_id="ok"))
|
|
182
|
+
|
|
183
|
+
original_read_text = Path.read_text
|
|
184
|
+
|
|
185
|
+
def _patched_read_text(self, *args, **kwargs):
|
|
186
|
+
if self.name == "err.md":
|
|
187
|
+
raise RuntimeError("simulated read failure")
|
|
188
|
+
return original_read_text(self, *args, **kwargs)
|
|
189
|
+
|
|
190
|
+
with patch.object(Path, "read_text", _patched_read_text), caplog.at_level(logging.WARNING):
|
|
191
|
+
idx = build_index(tmp_path)
|
|
192
|
+
assert len(idx) == 1
|
|
193
|
+
assert idx.get("ok") is not None
|
|
194
|
+
assert "Unexpected error" in caplog.text
|
|
195
|
+
|
|
196
|
+
def test_id_filename_mismatch(self, tmp_path: Path, caplog):
|
|
197
|
+
(tmp_path / "wrong-name.md").write_text(_make_guide_md(guide_id="real-id"))
|
|
198
|
+
with caplog.at_level(logging.WARNING):
|
|
199
|
+
idx = build_index(tmp_path)
|
|
200
|
+
assert len(idx) == 1
|
|
201
|
+
assert idx.get("real-id") is not None
|
|
202
|
+
assert idx.get("wrong-name") is None
|
|
203
|
+
assert "mismatch" in caplog.text
|
|
204
|
+
|
|
205
|
+
def test_nested_subdirectories(self, tmp_path: Path):
|
|
206
|
+
sub = tmp_path / "subdir"
|
|
207
|
+
sub.mkdir()
|
|
208
|
+
(sub / "nested.md").write_text(_make_guide_md(guide_id="nested"))
|
|
209
|
+
idx = build_index(tmp_path)
|
|
210
|
+
assert len(idx) == 1
|
|
211
|
+
assert idx.get("nested") is not None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# _code_lines (boundary cases only — _extract_snippet covered by test_search.py)
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class TestCodeLines:
|
|
220
|
+
def test_basic_extraction(self):
|
|
221
|
+
body = "## BAD\n```python\nold()\nnew()\n```\n"
|
|
222
|
+
assert _code_lines(body, "## BAD") == ["old()", "new()"]
|
|
223
|
+
|
|
224
|
+
def test_tilde_fence_unsupported(self):
|
|
225
|
+
body = "## BAD\n~~~python\nold()\n~~~\n"
|
|
226
|
+
assert _code_lines(body, "## BAD") == []
|
|
227
|
+
|
|
228
|
+
def test_heading_trailing_space_no_match(self):
|
|
229
|
+
body = "## BAD \n```python\nold()\n```\n"
|
|
230
|
+
assert _code_lines(body, "## BAD") == []
|
|
231
|
+
|
|
232
|
+
def test_unclosed_fence_returns_all_lines(self):
|
|
233
|
+
body = "## BAD\n```python\nline1\nline2\n"
|
|
234
|
+
assert _code_lines(body, "## BAD") == ["line1", "line2"]
|
|
235
|
+
|
|
236
|
+
def test_empty_code_block(self):
|
|
237
|
+
body = "## BAD\n```python\n```\n"
|
|
238
|
+
assert _code_lines(body, "## BAD") == []
|
|
239
|
+
|
|
240
|
+
def test_multiple_fences_first_only(self):
|
|
241
|
+
body = "## BAD\n```python\nfirst()\n```\n```python\nsecond()\n```\n"
|
|
242
|
+
assert _code_lines(body, "## BAD") == ["first()"]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
# _find_guides_dir fallbacks
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class TestFindGuidesDir:
|
|
251
|
+
def test_dev_fallback(self, monkeypatch):
|
|
252
|
+
monkeypatch.setattr(
|
|
253
|
+
"modern_python_guidance.guide_index.importlib.resources.files",
|
|
254
|
+
_raise_type_error,
|
|
255
|
+
)
|
|
256
|
+
import modern_python_guidance.guide_index as gi_module
|
|
257
|
+
|
|
258
|
+
repo_root = Path(__file__).resolve().parent.parent
|
|
259
|
+
monkeypatch.setattr(
|
|
260
|
+
gi_module,
|
|
261
|
+
"__file__",
|
|
262
|
+
str(repo_root / "src" / "modern_python_guidance" / "guide_index.py"),
|
|
263
|
+
)
|
|
264
|
+
result = _find_guides_dir()
|
|
265
|
+
expected = repo_root / "skills" / "modern-python-guidance" / "guides"
|
|
266
|
+
assert result == expected
|
|
267
|
+
|
|
268
|
+
def test_relative_fallback(self, monkeypatch, tmp_path):
|
|
269
|
+
monkeypatch.setattr(
|
|
270
|
+
"modern_python_guidance.guide_index.importlib.resources.files",
|
|
271
|
+
_raise_type_error,
|
|
272
|
+
)
|
|
273
|
+
fake_file = tmp_path / "pkg" / "sub" / "guide_index.py"
|
|
274
|
+
fake_file.parent.mkdir(parents=True)
|
|
275
|
+
fake_file.touch()
|
|
276
|
+
import modern_python_guidance.guide_index as gi_module
|
|
277
|
+
|
|
278
|
+
monkeypatch.setattr(gi_module, "__file__", str(fake_file))
|
|
279
|
+
result = _find_guides_dir()
|
|
280
|
+
assert result == Path("skills") / "modern-python-guidance" / "guides"
|
|
@@ -262,7 +262,9 @@ class TestRetrieveGuides:
|
|
|
262
262
|
result = responses[1]["result"]
|
|
263
263
|
assert "isError" not in result
|
|
264
264
|
data = json.loads(result["content"][0]["text"])
|
|
265
|
-
assert
|
|
265
|
+
assert "not_found" in data
|
|
266
|
+
assert data["results"] == []
|
|
267
|
+
assert data["not_found"][0]["id"] == "nonexistent-guide-xyz"
|
|
266
268
|
|
|
267
269
|
|
|
268
270
|
class TestListGuides:
|
|
@@ -197,10 +197,25 @@ class TestToolFunctions:
|
|
|
197
197
|
r = mcp._tool_retrieve({"guide_ids": []})
|
|
198
198
|
assert r["isError"] is True
|
|
199
199
|
|
|
200
|
-
def
|
|
200
|
+
def test_retrieve_valid_bare_list(self):
|
|
201
201
|
r = mcp._tool_retrieve({"guide_ids": ["use-builtin-generics"]})
|
|
202
202
|
data = json.loads(r["content"][0]["text"])
|
|
203
203
|
assert isinstance(data, list)
|
|
204
|
+
assert data[0]["id"] == "use-builtin-generics"
|
|
205
|
+
|
|
206
|
+
def test_retrieve_nonexistent_envelope(self):
|
|
207
|
+
r = mcp._tool_retrieve({"guide_ids": ["builtin-generics"]})
|
|
208
|
+
data = json.loads(r["content"][0]["text"])
|
|
209
|
+
assert "not_found" in data
|
|
210
|
+
assert data["not_found"][0]["id"] == "builtin-generics"
|
|
211
|
+
assert "use-builtin-generics" in data["not_found"][0]["suggestions"]
|
|
212
|
+
|
|
213
|
+
def test_retrieve_mixed_envelope(self):
|
|
214
|
+
r = mcp._tool_retrieve({"guide_ids": ["use-builtin-generics", "zzz-fake"]})
|
|
215
|
+
data = json.loads(r["content"][0]["text"])
|
|
216
|
+
assert len(data["results"]) == 1
|
|
217
|
+
assert data["results"][0]["id"] == "use-builtin-generics"
|
|
218
|
+
assert data["not_found"][0]["id"] == "zzz-fake"
|
|
204
219
|
|
|
205
220
|
def test_retrieve_exactly_41_allowed(self):
|
|
206
221
|
ids = [f"fake-{i}" for i in range(41)]
|
|
@@ -335,6 +350,18 @@ class TestHandleRequest:
|
|
|
335
350
|
resp = mcp._handle_request(msg)
|
|
336
351
|
assert resp is None
|
|
337
352
|
|
|
353
|
+
@pytest.mark.parametrize(
|
|
354
|
+
"msg",
|
|
355
|
+
[[1, 2, 3], "hello", 42, True],
|
|
356
|
+
ids=["list", "string", "number", "bool"],
|
|
357
|
+
)
|
|
358
|
+
def test_non_dict_returns_invalid_request(self, msg):
|
|
359
|
+
resp = mcp._handle_request(msg)
|
|
360
|
+
assert resp["jsonrpc"] == "2.0"
|
|
361
|
+
assert resp["id"] is None
|
|
362
|
+
assert resp["error"]["code"] == -32600
|
|
363
|
+
assert "expected JSON object" in resp["error"]["message"]
|
|
364
|
+
|
|
338
365
|
|
|
339
366
|
# --- serve ---
|
|
340
367
|
|
|
@@ -377,3 +404,20 @@ class TestServe:
|
|
|
377
404
|
assert len(responses) == 2
|
|
378
405
|
assert responses[0]["id"] == 1
|
|
379
406
|
assert responses[1]["id"] == 2
|
|
407
|
+
|
|
408
|
+
def test_non_dict_json_recovery(self):
|
|
409
|
+
lines = (
|
|
410
|
+
json.dumps([1, 2, 3])
|
|
411
|
+
+ "\n"
|
|
412
|
+
+ json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}})
|
|
413
|
+
+ "\n"
|
|
414
|
+
)
|
|
415
|
+
sin = io.StringIO(lines)
|
|
416
|
+
sout = io.StringIO()
|
|
417
|
+
mcp.serve(stdin=sin, stdout=sout)
|
|
418
|
+
responses = [json.loads(line) for line in sout.getvalue().strip().split("\n")]
|
|
419
|
+
assert len(responses) == 2
|
|
420
|
+
assert responses[0]["error"]["code"] == -32600
|
|
421
|
+
assert responses[0]["id"] is None
|
|
422
|
+
assert responses[1]["id"] == 1
|
|
423
|
+
assert "protocolVersion" in responses[1]["result"]
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
8
|
from modern_python_guidance.guide_index import build_index
|
|
9
|
-
from modern_python_guidance.retrieve import retrieve, retrieve_json
|
|
9
|
+
from modern_python_guidance.retrieve import retrieve, retrieve_json, suggest_ids
|
|
10
10
|
|
|
11
11
|
GUIDES_DIR = Path(__file__).parent.parent / "skills" / "modern-python-guidance" / "guides"
|
|
12
12
|
|
|
@@ -84,3 +84,36 @@ class TestRetrieveJSON:
|
|
|
84
84
|
"source",
|
|
85
85
|
}
|
|
86
86
|
assert set(parsed[0].keys()) == expected_keys
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestSuggestIds:
|
|
90
|
+
def test_close_match(self, index):
|
|
91
|
+
suggestions = suggest_ids(index, "builtin-generics")
|
|
92
|
+
assert "use-builtin-generics" in suggestions
|
|
93
|
+
|
|
94
|
+
def test_no_match(self, index):
|
|
95
|
+
suggestions = suggest_ids(index, "zzz-totally-unknown")
|
|
96
|
+
assert suggestions == []
|
|
97
|
+
|
|
98
|
+
def test_max_three(self, index):
|
|
99
|
+
suggestions = suggest_ids(index, "pydantic")
|
|
100
|
+
assert len(suggestions) <= 3
|
|
101
|
+
|
|
102
|
+
def test_case_insensitive(self, index):
|
|
103
|
+
suggestions = suggest_ids(index, "USE-BUILTIN-GENERICS")
|
|
104
|
+
assert "use-builtin-generics" in suggestions
|
|
105
|
+
|
|
106
|
+
def test_long_id_truncated(self, index):
|
|
107
|
+
long_id = "use-builtin-generics" + "-x" * 200
|
|
108
|
+
suggestions = suggest_ids(index, long_id)
|
|
109
|
+
assert isinstance(suggestions, list)
|
|
110
|
+
|
|
111
|
+
def test_non_string_returns_empty(self, index):
|
|
112
|
+
assert suggest_ids(index, 123) == []
|
|
113
|
+
|
|
114
|
+
def test_empty_index(self):
|
|
115
|
+
from modern_python_guidance.guide_index import GuideIndex
|
|
116
|
+
|
|
117
|
+
empty = GuideIndex()
|
|
118
|
+
suggestions = suggest_ids(empty, "anything")
|
|
119
|
+
assert suggestions == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-a-detailed.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-b-detailed.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/bench/prompts/v5-c-detailed.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/__main__.py
RENAMED
|
File without changes
|
{modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/compat.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.3.7 → modern_python_guidance-0.3.8}/src/modern_python_guidance/search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|