modern-python-guidance 0.3.6__tar.gz → 0.3.7__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.6 → modern_python_guidance-0.3.7}/CHANGELOG.md +15 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/CONTRIBUTING.md +1 -1
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/PKG-INFO +1 -1
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/pyproject.toml +2 -2
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/__init__.py +1 -1
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/mcp_server.py +7 -6
- modern_python_guidance-0.3.7/tests/test_cli_unit.py +223 -0
- modern_python_guidance-0.3.7/tests/test_mcp_unit.py +379 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/.github/workflows/check-python-release.yml +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/.github/workflows/ci.yml +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/.gitignore +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/LICENSE +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/README.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/SECURITY.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-a-detailed.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-a-normal.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-a-terse.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-b-detailed.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-b-normal.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-b-terse.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-c-detailed.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-c-normal.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-c-terse.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/run-v5.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/run.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/score.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/score_v5.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/docs/benchmark-v5.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/docs/design.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/rules/modern-python.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/stdlib/template-strings.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/cli.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/frontmatter.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/guide_index.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/retrieve.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/search.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/setup_cmd.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/uninstall_cmd.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/version_detect.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_cli_integration.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_frontmatter.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_guide_structure.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_mcp_server.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_retrieve.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_scorer_v5.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_search.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_setup.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_skill_sync.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_uninstall.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/tests/test_version_detect.py +0 -0
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.7] — 2026-06-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `_read_message` CWE-674 recursion bug: ~1000 consecutive blank lines on MCP stdin would crash the server with `RecursionError`. Replaced recursive call with iterative `while` loop.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- 86 in-process unit tests for `cli.py` (33) and `mcp_server.py` (53), raising per-file coverage from 0% to 96%. Covers CLI dispatch, format auto-detection, search/retrieve/list subcommands, `_confine_path` security (8 patterns including symlink escape and CWD=/ guard), JSON-RPC framing, request handling, and serve loop recovery.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Coverage `fail_under` ratcheted from 59% to 92% (actual: 92.48%)
|
|
18
|
+
- CONTRIBUTING.md coverage gate updated to match
|
|
19
|
+
|
|
5
20
|
## [0.3.6] — 2026-05-31
|
|
6
21
|
|
|
7
22
|
### Added
|
|
@@ -58,4 +58,4 @@ All PRs run these checks on Python 3.11, 3.12, and 3.13:
|
|
|
58
58
|
|
|
59
59
|
1. `ruff format --check` — formatting
|
|
60
60
|
2. `ruff check` — linting
|
|
61
|
-
3. `pytest --cov` — tests with branch coverage (`fail_under =
|
|
61
|
+
3. `pytest --cov` — tests with branch coverage (`fail_under = 92%`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modern-python-guidance
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
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.7"
|
|
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"
|
|
@@ -76,7 +76,7 @@ branch = true
|
|
|
76
76
|
[tool.coverage.report]
|
|
77
77
|
show_missing = true
|
|
78
78
|
skip_empty = true
|
|
79
|
-
fail_under =
|
|
79
|
+
fail_under = 92
|
|
80
80
|
exclude_lines = [
|
|
81
81
|
"pragma: no cover",
|
|
82
82
|
"if __name__ == .__main__.",
|
|
@@ -35,12 +35,13 @@ class _Skip(Exception):
|
|
|
35
35
|
|
|
36
36
|
def _read_message(stream: object = None) -> dict | None:
|
|
37
37
|
buf = stream or sys.stdin
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
while True:
|
|
39
|
+
line = buf.readline()
|
|
40
|
+
if not line:
|
|
41
|
+
return None
|
|
42
|
+
line = line.strip()
|
|
43
|
+
if line:
|
|
44
|
+
break
|
|
44
45
|
try:
|
|
45
46
|
return json.loads(line)
|
|
46
47
|
except json.JSONDecodeError as exc:
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""In-process unit tests for cli.py — direct function calls for coverage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from modern_python_guidance.cli import _resolve_format, main
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestMainDispatch:
|
|
15
|
+
def test_no_command_exits_2(self, capsys):
|
|
16
|
+
with pytest.raises(SystemExit, match="2"):
|
|
17
|
+
main(argv=[])
|
|
18
|
+
|
|
19
|
+
def test_version_flag(self, capsys):
|
|
20
|
+
from modern_python_guidance import __version__
|
|
21
|
+
|
|
22
|
+
with pytest.raises(SystemExit, match="0"):
|
|
23
|
+
main(argv=["--version"])
|
|
24
|
+
assert __version__ in capsys.readouterr().out
|
|
25
|
+
|
|
26
|
+
def test_invalid_python_version_exits(self, capsys):
|
|
27
|
+
with pytest.raises(SystemExit, match="2"):
|
|
28
|
+
main(argv=["search", "typing", "--python-version", "abc"])
|
|
29
|
+
|
|
30
|
+
def test_broken_pipe_exits_0(self, monkeypatch):
|
|
31
|
+
def raise_broken_pipe(*_a, **_kw):
|
|
32
|
+
raise BrokenPipeError
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr("modern_python_guidance.cli.do_search", raise_broken_pipe)
|
|
35
|
+
with pytest.raises(SystemExit, match="0"):
|
|
36
|
+
main(argv=["search", "typing"])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestResolveFormat:
|
|
40
|
+
def test_explicit_json(self):
|
|
41
|
+
ns = _make_ns(format="json")
|
|
42
|
+
assert _resolve_format(ns) == "json"
|
|
43
|
+
|
|
44
|
+
def test_explicit_human(self):
|
|
45
|
+
ns = _make_ns(format="human")
|
|
46
|
+
assert _resolve_format(ns) == "human"
|
|
47
|
+
|
|
48
|
+
def test_auto_tty(self, monkeypatch):
|
|
49
|
+
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
|
50
|
+
ns = _make_ns(format=None)
|
|
51
|
+
assert _resolve_format(ns) == "human"
|
|
52
|
+
|
|
53
|
+
def test_auto_pipe(self, monkeypatch):
|
|
54
|
+
monkeypatch.setattr(sys.stdout, "isatty", lambda: False)
|
|
55
|
+
ns = _make_ns(format=None)
|
|
56
|
+
assert _resolve_format(ns) == "json"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestCmdSearch:
|
|
60
|
+
def test_json_output(self, capsys):
|
|
61
|
+
main(argv=["search", "typing list", "--format", "json"])
|
|
62
|
+
data = json.loads(capsys.readouterr().out)
|
|
63
|
+
assert isinstance(data, list)
|
|
64
|
+
assert len(data) >= 1
|
|
65
|
+
|
|
66
|
+
def test_human_output(self, capsys):
|
|
67
|
+
main(argv=["search", "typing", "--format", "human"])
|
|
68
|
+
out = capsys.readouterr().out
|
|
69
|
+
assert "use-builtin-generics" in out
|
|
70
|
+
|
|
71
|
+
def test_no_match_exits_1(self, capsys):
|
|
72
|
+
with pytest.raises(SystemExit, match="1"):
|
|
73
|
+
main(argv=["search", "qqqxxx999zzz", "--format", "json"])
|
|
74
|
+
|
|
75
|
+
def test_no_match_human_exits_1(self, capsys):
|
|
76
|
+
with pytest.raises(SystemExit, match="1"):
|
|
77
|
+
main(argv=["search", "qqqxxx999zzz", "--format", "human"])
|
|
78
|
+
assert "No guides found" in capsys.readouterr().out
|
|
79
|
+
|
|
80
|
+
def test_fuzzy_marker(self, capsys):
|
|
81
|
+
main(argv=["search", "typng", "--format", "human"])
|
|
82
|
+
out = capsys.readouterr().out
|
|
83
|
+
assert "(fuzzy)" in out or "use-builtin-generics" in out
|
|
84
|
+
|
|
85
|
+
def test_category_filter(self, capsys):
|
|
86
|
+
main(argv=["search", "typing", "--category", "typing", "--format", "json"])
|
|
87
|
+
data = json.loads(capsys.readouterr().out)
|
|
88
|
+
assert data
|
|
89
|
+
for item in data:
|
|
90
|
+
assert item["category"] == "typing"
|
|
91
|
+
|
|
92
|
+
def test_limit(self, capsys):
|
|
93
|
+
main(argv=["search", "python", "--limit", "2", "--format", "json"])
|
|
94
|
+
data = json.loads(capsys.readouterr().out)
|
|
95
|
+
assert len(data) <= 2
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestCmdRetrieve:
|
|
99
|
+
def test_json_output(self, capsys):
|
|
100
|
+
main(argv=["retrieve", "use-builtin-generics", "--format", "json"])
|
|
101
|
+
data = json.loads(capsys.readouterr().out)
|
|
102
|
+
assert isinstance(data, list)
|
|
103
|
+
assert data[0]["id"] == "use-builtin-generics"
|
|
104
|
+
|
|
105
|
+
def test_human_output(self, capsys):
|
|
106
|
+
main(argv=["retrieve", "use-builtin-generics", "--format", "human"])
|
|
107
|
+
out = capsys.readouterr().out
|
|
108
|
+
assert "use-builtin-generics" in out
|
|
109
|
+
assert "version match:" in out
|
|
110
|
+
|
|
111
|
+
def test_comma_split_ids(self, capsys):
|
|
112
|
+
main(argv=["retrieve", "use-builtin-generics,union-syntax", "--format", "json"])
|
|
113
|
+
data = json.loads(capsys.readouterr().out)
|
|
114
|
+
ids = {r["id"] for r in data}
|
|
115
|
+
assert "use-builtin-generics" in ids
|
|
116
|
+
assert "union-syntax" in ids
|
|
117
|
+
|
|
118
|
+
def test_no_match_exits_1(self, capsys):
|
|
119
|
+
with pytest.raises(SystemExit, match="1"):
|
|
120
|
+
main(argv=["retrieve", "nonexistent-guide-id", "--format", "json"])
|
|
121
|
+
|
|
122
|
+
def test_no_match_human_exits_1(self, capsys):
|
|
123
|
+
with pytest.raises(SystemExit, match="1"):
|
|
124
|
+
main(argv=["retrieve", "nonexistent-guide-id", "--format", "human"])
|
|
125
|
+
assert "No guides found" in capsys.readouterr().out
|
|
126
|
+
|
|
127
|
+
def test_version_match_yes(self, capsys):
|
|
128
|
+
main(
|
|
129
|
+
argv=[
|
|
130
|
+
"retrieve",
|
|
131
|
+
"use-builtin-generics",
|
|
132
|
+
"--python-version",
|
|
133
|
+
"3.12",
|
|
134
|
+
"--format",
|
|
135
|
+
"human",
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
out = capsys.readouterr().out
|
|
139
|
+
assert "YES" in out
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class TestCmdList:
|
|
143
|
+
def test_json_output(self, capsys):
|
|
144
|
+
main(argv=["list", "--format", "json"])
|
|
145
|
+
data = json.loads(capsys.readouterr().out)
|
|
146
|
+
assert isinstance(data, list)
|
|
147
|
+
assert len(data) >= 10
|
|
148
|
+
|
|
149
|
+
def test_human_output(self, capsys):
|
|
150
|
+
main(argv=["list", "--format", "human"])
|
|
151
|
+
out = capsys.readouterr().out
|
|
152
|
+
assert "layer" in out
|
|
153
|
+
|
|
154
|
+
def test_category_filter(self, capsys):
|
|
155
|
+
main(argv=["list", "--category", "stdlib", "--format", "json"])
|
|
156
|
+
data = json.loads(capsys.readouterr().out)
|
|
157
|
+
assert data
|
|
158
|
+
for item in data:
|
|
159
|
+
assert item["category"] == "stdlib"
|
|
160
|
+
|
|
161
|
+
def test_version_filter(self, capsys):
|
|
162
|
+
main(argv=["list", "--python-version", "3.11", "--format", "json"])
|
|
163
|
+
data = json.loads(capsys.readouterr().out)
|
|
164
|
+
assert isinstance(data, list)
|
|
165
|
+
assert len(data) >= 1
|
|
166
|
+
|
|
167
|
+
def test_empty_exits_1(self, capsys):
|
|
168
|
+
with pytest.raises(SystemExit, match="1"):
|
|
169
|
+
main(argv=["list", "--category", "nonexistent_cat", "--format", "json"])
|
|
170
|
+
|
|
171
|
+
def test_category_grouping_human(self, capsys):
|
|
172
|
+
main(argv=["list", "--format", "human"])
|
|
173
|
+
out = capsys.readouterr().out
|
|
174
|
+
assert "[" in out and "]" in out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TestCmdDetectVersion:
|
|
178
|
+
def test_basic_output(self, capsys):
|
|
179
|
+
main(argv=["detect-version"])
|
|
180
|
+
out = capsys.readouterr().out.strip()
|
|
181
|
+
assert out # should return some version string
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TestCmdSetupUninstall:
|
|
185
|
+
def test_setup_dispatch(self):
|
|
186
|
+
with patch("modern_python_guidance.setup_cmd.run_setup", return_value=0) as mock:
|
|
187
|
+
with pytest.raises(SystemExit, match="0"):
|
|
188
|
+
main(argv=["setup", "--dry-run"])
|
|
189
|
+
mock.assert_called_once()
|
|
190
|
+
call_kwargs = mock.call_args
|
|
191
|
+
assert call_kwargs[1]["dry_run"] is True
|
|
192
|
+
|
|
193
|
+
def test_uninstall_dispatch(self):
|
|
194
|
+
with patch("modern_python_guidance.uninstall_cmd.run_uninstall", return_value=0) as mock:
|
|
195
|
+
with pytest.raises(SystemExit, match="0"):
|
|
196
|
+
main(argv=["uninstall", "--dry-run"])
|
|
197
|
+
mock.assert_called_once()
|
|
198
|
+
call_kwargs = mock.call_args
|
|
199
|
+
assert call_kwargs[1]["dry_run"] is True
|
|
200
|
+
|
|
201
|
+
def test_setup_nonzero_exit(self):
|
|
202
|
+
with (
|
|
203
|
+
patch("modern_python_guidance.setup_cmd.run_setup", return_value=1),
|
|
204
|
+
pytest.raises(SystemExit, match="1"),
|
|
205
|
+
):
|
|
206
|
+
main(argv=["setup", "--dry-run"])
|
|
207
|
+
|
|
208
|
+
def test_uninstall_nonzero_exit(self):
|
|
209
|
+
with (
|
|
210
|
+
patch("modern_python_guidance.uninstall_cmd.run_uninstall", return_value=1),
|
|
211
|
+
pytest.raises(SystemExit, match="1"),
|
|
212
|
+
):
|
|
213
|
+
main(argv=["uninstall", "--dry-run"])
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# --- helpers ---
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _make_ns(**kwargs):
|
|
220
|
+
"""Build a minimal argparse.Namespace for _resolve_format tests."""
|
|
221
|
+
import argparse
|
|
222
|
+
|
|
223
|
+
return argparse.Namespace(**kwargs)
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""In-process unit tests for mcp_server.py — direct function calls for coverage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import modern_python_guidance.mcp_server as mcp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(autouse=True)
|
|
15
|
+
def _reset_index():
|
|
16
|
+
"""Reset the global singleton between tests."""
|
|
17
|
+
mcp._index = None
|
|
18
|
+
yield
|
|
19
|
+
mcp._index = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# --- Framing ---
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestFraming:
|
|
26
|
+
def test_read_message_eof(self):
|
|
27
|
+
stream = io.StringIO("")
|
|
28
|
+
assert mcp._read_message(stream) is None
|
|
29
|
+
|
|
30
|
+
def test_read_message_valid_json(self):
|
|
31
|
+
stream = io.StringIO('{"key": "value"}\n')
|
|
32
|
+
result = mcp._read_message(stream)
|
|
33
|
+
assert result == {"key": "value"}
|
|
34
|
+
|
|
35
|
+
def test_read_message_skips_blank_lines(self):
|
|
36
|
+
stream = io.StringIO('\n\n\n{"key": "value"}\n')
|
|
37
|
+
result = mcp._read_message(stream)
|
|
38
|
+
assert result == {"key": "value"}
|
|
39
|
+
|
|
40
|
+
def test_read_message_many_blank_lines_no_recursion(self):
|
|
41
|
+
lines = "\n" * 2000 + '{"ok": true}\n'
|
|
42
|
+
stream = io.StringIO(lines)
|
|
43
|
+
result = mcp._read_message(stream)
|
|
44
|
+
assert result == {"ok": True}
|
|
45
|
+
|
|
46
|
+
def test_read_message_blank_then_eof(self):
|
|
47
|
+
stream = io.StringIO("\n\n\n")
|
|
48
|
+
assert mcp._read_message(stream) is None
|
|
49
|
+
|
|
50
|
+
def test_read_message_invalid_json_raises_skip(self):
|
|
51
|
+
stream = io.StringIO("not-json\n")
|
|
52
|
+
with pytest.raises(mcp._Skip, match="invalid JSON"):
|
|
53
|
+
mcp._read_message(stream)
|
|
54
|
+
|
|
55
|
+
def test_write_message(self):
|
|
56
|
+
out = io.StringIO()
|
|
57
|
+
mcp._write_message({"result": 42}, out)
|
|
58
|
+
written = out.getvalue()
|
|
59
|
+
assert json.loads(written.strip()) == {"result": 42}
|
|
60
|
+
assert written.endswith("\n")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# --- Response builders ---
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestResponseBuilders:
|
|
67
|
+
def test_error_response(self):
|
|
68
|
+
resp = mcp._error_response(1, -32600, "bad request")
|
|
69
|
+
assert resp["jsonrpc"] == "2.0"
|
|
70
|
+
assert resp["id"] == 1
|
|
71
|
+
assert resp["error"]["code"] == -32600
|
|
72
|
+
assert resp["error"]["message"] == "bad request"
|
|
73
|
+
|
|
74
|
+
def test_result_response(self):
|
|
75
|
+
resp = mcp._result_response(2, {"data": "ok"})
|
|
76
|
+
assert resp["jsonrpc"] == "2.0"
|
|
77
|
+
assert resp["id"] == 2
|
|
78
|
+
assert resp["result"] == {"data": "ok"}
|
|
79
|
+
|
|
80
|
+
def test_tool_result_success(self):
|
|
81
|
+
r = mcp._tool_result("hello")
|
|
82
|
+
assert r["content"][0]["text"] == "hello"
|
|
83
|
+
assert "isError" not in r
|
|
84
|
+
|
|
85
|
+
def test_tool_result_error(self):
|
|
86
|
+
r = mcp._tool_result("fail", is_error=True)
|
|
87
|
+
assert r["content"][0]["text"] == "fail"
|
|
88
|
+
assert r["isError"] is True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- _confine_path ---
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestConfinePath:
|
|
95
|
+
def test_none_returns_cwd(self, tmp_path, monkeypatch):
|
|
96
|
+
monkeypatch.chdir(tmp_path)
|
|
97
|
+
result = mcp._confine_path(None)
|
|
98
|
+
assert result == tmp_path
|
|
99
|
+
|
|
100
|
+
def test_valid_subdir(self, tmp_path, monkeypatch):
|
|
101
|
+
sub = tmp_path / "sub"
|
|
102
|
+
sub.mkdir()
|
|
103
|
+
monkeypatch.chdir(tmp_path)
|
|
104
|
+
result = mcp._confine_path("sub")
|
|
105
|
+
assert isinstance(result, Path)
|
|
106
|
+
assert result == sub
|
|
107
|
+
|
|
108
|
+
def test_nonexistent_dir(self, tmp_path, monkeypatch):
|
|
109
|
+
monkeypatch.chdir(tmp_path)
|
|
110
|
+
result = mcp._confine_path("does-not-exist")
|
|
111
|
+
assert isinstance(result, str)
|
|
112
|
+
assert "not found" in result
|
|
113
|
+
|
|
114
|
+
def test_absolute_path_rejected(self, tmp_path, monkeypatch):
|
|
115
|
+
monkeypatch.chdir(tmp_path)
|
|
116
|
+
result = mcp._confine_path("/etc")
|
|
117
|
+
assert isinstance(result, str)
|
|
118
|
+
assert "relative" in result
|
|
119
|
+
|
|
120
|
+
def test_traversal_rejected(self, tmp_path, monkeypatch):
|
|
121
|
+
monkeypatch.chdir(tmp_path)
|
|
122
|
+
result = mcp._confine_path("../../..")
|
|
123
|
+
assert isinstance(result, str)
|
|
124
|
+
|
|
125
|
+
def test_nested_traversal_rejected(self, tmp_path, monkeypatch):
|
|
126
|
+
sub = tmp_path / "sub"
|
|
127
|
+
sub.mkdir()
|
|
128
|
+
monkeypatch.chdir(tmp_path)
|
|
129
|
+
result = mcp._confine_path("sub/../../..")
|
|
130
|
+
assert isinstance(result, str)
|
|
131
|
+
|
|
132
|
+
def test_symlink_escape_rejected(self, tmp_path, monkeypatch):
|
|
133
|
+
monkeypatch.chdir(tmp_path)
|
|
134
|
+
link = tmp_path / "escape"
|
|
135
|
+
link.symlink_to("/tmp")
|
|
136
|
+
result = mcp._confine_path("escape")
|
|
137
|
+
assert isinstance(result, str)
|
|
138
|
+
|
|
139
|
+
def test_cwd_is_root(self, monkeypatch):
|
|
140
|
+
monkeypatch.chdir("/")
|
|
141
|
+
result = mcp._confine_path(None)
|
|
142
|
+
assert isinstance(result, str)
|
|
143
|
+
assert "root" in result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# --- _validate_python_version ---
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestValidateVersion:
|
|
150
|
+
def test_none_is_valid(self):
|
|
151
|
+
assert mcp._validate_python_version(None) is None
|
|
152
|
+
|
|
153
|
+
def test_valid_version(self):
|
|
154
|
+
assert mcp._validate_python_version("3.12") is None
|
|
155
|
+
|
|
156
|
+
def test_invalid_version(self):
|
|
157
|
+
err = mcp._validate_python_version("abc")
|
|
158
|
+
assert err is not None
|
|
159
|
+
assert "Invalid" in err
|
|
160
|
+
|
|
161
|
+
def test_three_part_version(self):
|
|
162
|
+
err = mcp._validate_python_version("3.12.1")
|
|
163
|
+
assert err is not None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# --- Tool functions ---
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TestToolFunctions:
|
|
170
|
+
def test_search_empty_query(self):
|
|
171
|
+
r = mcp._tool_search({"query": ""})
|
|
172
|
+
assert r["isError"] is True
|
|
173
|
+
|
|
174
|
+
def test_search_valid(self):
|
|
175
|
+
r = mcp._tool_search({"query": "typing"})
|
|
176
|
+
data = json.loads(r["content"][0]["text"])
|
|
177
|
+
assert isinstance(data, list)
|
|
178
|
+
assert len(data) >= 1
|
|
179
|
+
|
|
180
|
+
def test_search_with_filters(self):
|
|
181
|
+
r = mcp._tool_search(
|
|
182
|
+
{
|
|
183
|
+
"query": "typing",
|
|
184
|
+
"python_version": "3.12",
|
|
185
|
+
"category": "typing",
|
|
186
|
+
"limit": 2,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
data = json.loads(r["content"][0]["text"])
|
|
190
|
+
assert len(data) <= 2
|
|
191
|
+
|
|
192
|
+
def test_search_invalid_version(self):
|
|
193
|
+
r = mcp._tool_search({"query": "typing", "python_version": "bad"})
|
|
194
|
+
assert r["isError"] is True
|
|
195
|
+
|
|
196
|
+
def test_retrieve_empty_ids(self):
|
|
197
|
+
r = mcp._tool_retrieve({"guide_ids": []})
|
|
198
|
+
assert r["isError"] is True
|
|
199
|
+
|
|
200
|
+
def test_retrieve_valid(self):
|
|
201
|
+
r = mcp._tool_retrieve({"guide_ids": ["use-builtin-generics"]})
|
|
202
|
+
data = json.loads(r["content"][0]["text"])
|
|
203
|
+
assert isinstance(data, list)
|
|
204
|
+
|
|
205
|
+
def test_retrieve_exactly_41_allowed(self):
|
|
206
|
+
ids = [f"fake-{i}" for i in range(41)]
|
|
207
|
+
r = mcp._tool_retrieve({"guide_ids": ids})
|
|
208
|
+
assert r.get("isError") is not True
|
|
209
|
+
|
|
210
|
+
def test_retrieve_42_rejected(self):
|
|
211
|
+
ids = [f"fake-{i}" for i in range(42)]
|
|
212
|
+
r = mcp._tool_retrieve({"guide_ids": ids})
|
|
213
|
+
assert r["isError"] is True
|
|
214
|
+
assert "41" in r["content"][0]["text"]
|
|
215
|
+
|
|
216
|
+
def test_retrieve_invalid_version(self):
|
|
217
|
+
r = mcp._tool_retrieve({"guide_ids": ["use-builtin-generics"], "python_version": "x"})
|
|
218
|
+
assert r["isError"] is True
|
|
219
|
+
|
|
220
|
+
def test_list_all(self):
|
|
221
|
+
r = mcp._tool_list({})
|
|
222
|
+
data = json.loads(r["content"][0]["text"])
|
|
223
|
+
assert isinstance(data, list)
|
|
224
|
+
assert len(data) >= 10
|
|
225
|
+
|
|
226
|
+
def test_list_category_filter(self):
|
|
227
|
+
r = mcp._tool_list({"category": "stdlib"})
|
|
228
|
+
data = json.loads(r["content"][0]["text"])
|
|
229
|
+
assert data
|
|
230
|
+
for item in data:
|
|
231
|
+
assert item["category"] == "stdlib"
|
|
232
|
+
|
|
233
|
+
def test_list_version_filter(self):
|
|
234
|
+
r = mcp._tool_list({"python_version": "3.11"})
|
|
235
|
+
data = json.loads(r["content"][0]["text"])
|
|
236
|
+
assert isinstance(data, list)
|
|
237
|
+
assert data
|
|
238
|
+
|
|
239
|
+
def test_list_invalid_version(self):
|
|
240
|
+
r = mcp._tool_list({"python_version": "nope"})
|
|
241
|
+
assert r["isError"] is True
|
|
242
|
+
|
|
243
|
+
def test_detect_version_valid(self, tmp_path, monkeypatch):
|
|
244
|
+
monkeypatch.chdir(tmp_path)
|
|
245
|
+
(tmp_path / "pyproject.toml").write_text('[project]\nrequires-python = ">=3.12"\n')
|
|
246
|
+
r = mcp._tool_detect_version({"project_dir": None})
|
|
247
|
+
data = json.loads(r["content"][0]["text"])
|
|
248
|
+
assert "python_version" in data
|
|
249
|
+
|
|
250
|
+
def test_detect_version_confined(self, tmp_path, monkeypatch):
|
|
251
|
+
monkeypatch.chdir(tmp_path)
|
|
252
|
+
r = mcp._tool_detect_version({"project_dir": "/etc"})
|
|
253
|
+
assert r["isError"] is True
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# --- _handle_tool_call ---
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class TestHandleToolCall:
|
|
260
|
+
def test_unknown_tool(self):
|
|
261
|
+
r = mcp._handle_tool_call("nonexistent", {})
|
|
262
|
+
assert r["isError"] is True
|
|
263
|
+
assert "Unknown tool" in r["content"][0]["text"]
|
|
264
|
+
|
|
265
|
+
def test_dispatch_search(self):
|
|
266
|
+
r = mcp._handle_tool_call("search_guides", {"query": "typing"})
|
|
267
|
+
assert "isError" not in r
|
|
268
|
+
|
|
269
|
+
def test_exception_handling(self, monkeypatch):
|
|
270
|
+
def boom(*_a, **_kw):
|
|
271
|
+
raise RuntimeError("kaboom")
|
|
272
|
+
|
|
273
|
+
monkeypatch.setattr(mcp, "_tool_search", boom)
|
|
274
|
+
r = mcp._handle_tool_call("search_guides", {"query": "x"})
|
|
275
|
+
assert r["isError"] is True
|
|
276
|
+
assert "Internal error" in r["content"][0]["text"]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# --- _handle_request ---
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class TestHandleRequest:
|
|
283
|
+
def test_initialize(self):
|
|
284
|
+
msg = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
|
|
285
|
+
resp = mcp._handle_request(msg)
|
|
286
|
+
assert resp is not None
|
|
287
|
+
assert resp["id"] == 1
|
|
288
|
+
assert "protocolVersion" in resp["result"]
|
|
289
|
+
|
|
290
|
+
def test_initialize_notification_returns_none(self):
|
|
291
|
+
msg = {"jsonrpc": "2.0", "method": "initialize", "params": {}}
|
|
292
|
+
resp = mcp._handle_request(msg)
|
|
293
|
+
assert resp is None
|
|
294
|
+
|
|
295
|
+
def test_notifications_initialized(self):
|
|
296
|
+
msg = {"jsonrpc": "2.0", "method": "notifications/initialized"}
|
|
297
|
+
assert mcp._handle_request(msg) is None
|
|
298
|
+
|
|
299
|
+
def test_tools_list(self):
|
|
300
|
+
msg = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
|
|
301
|
+
resp = mcp._handle_request(msg)
|
|
302
|
+
assert resp is not None
|
|
303
|
+
assert "tools" in resp["result"]
|
|
304
|
+
assert len(resp["result"]["tools"]) == 4
|
|
305
|
+
|
|
306
|
+
def test_tools_call(self):
|
|
307
|
+
msg = {
|
|
308
|
+
"jsonrpc": "2.0",
|
|
309
|
+
"id": 3,
|
|
310
|
+
"method": "tools/call",
|
|
311
|
+
"params": {"name": "search_guides", "arguments": {"query": "typing"}},
|
|
312
|
+
}
|
|
313
|
+
resp = mcp._handle_request(msg)
|
|
314
|
+
assert resp is not None
|
|
315
|
+
assert resp["id"] == 3
|
|
316
|
+
|
|
317
|
+
def test_unknown_method(self):
|
|
318
|
+
msg = {"jsonrpc": "2.0", "id": 4, "method": "nonexistent"}
|
|
319
|
+
resp = mcp._handle_request(msg)
|
|
320
|
+
assert resp is not None
|
|
321
|
+
assert "error" in resp
|
|
322
|
+
assert resp["error"]["code"] == -32601
|
|
323
|
+
|
|
324
|
+
def test_unknown_notification_ignored(self):
|
|
325
|
+
msg = {"jsonrpc": "2.0", "method": "custom/notification"}
|
|
326
|
+
resp = mcp._handle_request(msg)
|
|
327
|
+
assert resp is None
|
|
328
|
+
|
|
329
|
+
def test_tools_call_notification_mode(self):
|
|
330
|
+
msg = {
|
|
331
|
+
"jsonrpc": "2.0",
|
|
332
|
+
"method": "tools/call",
|
|
333
|
+
"params": {"name": "search_guides", "arguments": {"query": "typing"}},
|
|
334
|
+
}
|
|
335
|
+
resp = mcp._handle_request(msg)
|
|
336
|
+
assert resp is None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# --- serve ---
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class TestServe:
|
|
343
|
+
def test_empty_stdin_eof(self):
|
|
344
|
+
sin = io.StringIO("")
|
|
345
|
+
sout = io.StringIO()
|
|
346
|
+
mcp.serve(stdin=sin, stdout=sout)
|
|
347
|
+
assert sout.getvalue() == ""
|
|
348
|
+
|
|
349
|
+
def test_single_request(self):
|
|
350
|
+
req = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
|
|
351
|
+
sin = io.StringIO(json.dumps(req) + "\n")
|
|
352
|
+
sout = io.StringIO()
|
|
353
|
+
mcp.serve(stdin=sin, stdout=sout)
|
|
354
|
+
resp = json.loads(sout.getvalue().strip())
|
|
355
|
+
assert resp["id"] == 1
|
|
356
|
+
assert "protocolVersion" in resp["result"]
|
|
357
|
+
|
|
358
|
+
def test_malformed_json_recovery(self):
|
|
359
|
+
lines = (
|
|
360
|
+
"not-valid-json\n"
|
|
361
|
+
+ json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}})
|
|
362
|
+
+ "\n"
|
|
363
|
+
)
|
|
364
|
+
sin = io.StringIO(lines)
|
|
365
|
+
sout = io.StringIO()
|
|
366
|
+
mcp.serve(stdin=sin, stdout=sout)
|
|
367
|
+
resp = json.loads(sout.getvalue().strip())
|
|
368
|
+
assert resp["id"] == 1
|
|
369
|
+
|
|
370
|
+
def test_multiple_requests(self):
|
|
371
|
+
req1 = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}})
|
|
372
|
+
req2 = json.dumps({"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}})
|
|
373
|
+
sin = io.StringIO(req1 + "\n" + req2 + "\n")
|
|
374
|
+
sout = io.StringIO()
|
|
375
|
+
mcp.serve(stdin=sin, stdout=sout)
|
|
376
|
+
responses = [json.loads(line) for line in sout.getvalue().strip().split("\n")]
|
|
377
|
+
assert len(responses) == 2
|
|
378
|
+
assert responses[0]["id"] == 1
|
|
379
|
+
assert responses[1]["id"] == 2
|
|
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.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-a-detailed.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/bench/prompts/v5-b-detailed.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/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.6 → modern_python_guidance-0.3.7}/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.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/__main__.py
RENAMED
|
File without changes
|
{modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/cli.py
RENAMED
|
File without changes
|
{modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/compat.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/src/modern_python_guidance/retrieve.py
RENAMED
|
File without changes
|
{modern_python_guidance-0.3.6 → modern_python_guidance-0.3.7}/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
|
|
File without changes
|
|
File without changes
|