modern-python-guidance 0.3.6__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.6 → modern_python_guidance-0.3.8}/CHANGELOG.md +25 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/CONTRIBUTING.md +1 -1
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/PKG-INFO +1 -1
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/pyproject.toml +2 -2
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/__init__.py +1 -1
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/cli.py +26 -10
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/mcp_server.py +18 -7
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/retrieve.py +13 -0
- modern_python_guidance-0.3.8/tests/test_cli_unit.py +265 -0
- 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.6 → modern_python_guidance-0.3.8}/tests/test_mcp_server.py +3 -1
- modern_python_guidance-0.3.8/tests/test_mcp_unit.py +423 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/tests/test_retrieve.py +34 -1
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/.github/workflows/check-python-release.yml +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/.github/workflows/ci.yml +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/.gitignore +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/LICENSE +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/README.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/SECURITY.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompts/v5-a-detailed.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompts/v5-a-normal.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompts/v5-a-terse.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompts/v5-b-detailed.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompts/v5-b-normal.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompts/v5-b-terse.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompts/v5-c-detailed.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompts/v5-c-normal.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/prompts/v5-c-terse.txt +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/run-v5.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/run.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/score.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/score_v5.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/docs/benchmark-v5.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/docs/design.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/rules/modern-python.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/stdlib/template-strings.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/frontmatter.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/guide_index.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/search.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/setup_cmd.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/uninstall_cmd.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/src/modern_python_guidance/version_detect.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/tests/test_cli_integration.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/tests/test_frontmatter.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/tests/test_guide_structure.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/tests/test_scorer_v5.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/tests/test_search.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/tests/test_setup.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/tests/test_skill_sync.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/tests/test_uninstall.py +0 -0
- {modern_python_guidance-0.3.6 → modern_python_guidance-0.3.8}/tests/test_version_detect.py +0 -0
|
@@ -2,6 +2,31 @@
|
|
|
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
|
+
|
|
15
|
+
## [0.3.7] — 2026-06-02
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `_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.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Coverage `fail_under` ratcheted from 59% to 92% (actual: 92.48%)
|
|
28
|
+
- CONTRIBUTING.md coverage gate updated to match
|
|
29
|
+
|
|
5
30
|
## [0.3.6] — 2026-05-31
|
|
6
31
|
|
|
7
32
|
### 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.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"
|
|
@@ -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__.",
|
{modern_python_guidance-0.3.6 → 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
|
|
|
@@ -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:
|
|
@@ -279,6 +280,14 @@ def _tool_retrieve(arguments: dict) -> dict:
|
|
|
279
280
|
|
|
280
281
|
index = _get_index()
|
|
281
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
|
+
|
|
282
291
|
return _tool_result(json.dumps(results, indent=2, ensure_ascii=False))
|
|
283
292
|
|
|
284
293
|
|
|
@@ -331,6 +340,8 @@ PROTOCOL_VERSION = "2024-11-05"
|
|
|
331
340
|
|
|
332
341
|
|
|
333
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")
|
|
334
345
|
method = msg.get("method", "")
|
|
335
346
|
req_id = msg.get("id")
|
|
336
347
|
params = msg.get("params", {})
|
{modern_python_guidance-0.3.6 → 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,
|
|
@@ -0,0 +1,265 @@
|
|
|
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_json_envelope(self, capsys):
|
|
119
|
+
with pytest.raises(SystemExit, match="1"):
|
|
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"
|
|
125
|
+
|
|
126
|
+
def test_no_match_human_exits_1(self, capsys):
|
|
127
|
+
with pytest.raises(SystemExit, match="1"):
|
|
128
|
+
main(argv=["retrieve", "nonexistent-guide-id", "--format", "human"])
|
|
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
|
|
168
|
+
|
|
169
|
+
def test_version_match_yes(self, capsys):
|
|
170
|
+
main(
|
|
171
|
+
argv=[
|
|
172
|
+
"retrieve",
|
|
173
|
+
"use-builtin-generics",
|
|
174
|
+
"--python-version",
|
|
175
|
+
"3.12",
|
|
176
|
+
"--format",
|
|
177
|
+
"human",
|
|
178
|
+
]
|
|
179
|
+
)
|
|
180
|
+
out = capsys.readouterr().out
|
|
181
|
+
assert "YES" in out
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TestCmdList:
|
|
185
|
+
def test_json_output(self, capsys):
|
|
186
|
+
main(argv=["list", "--format", "json"])
|
|
187
|
+
data = json.loads(capsys.readouterr().out)
|
|
188
|
+
assert isinstance(data, list)
|
|
189
|
+
assert len(data) >= 10
|
|
190
|
+
|
|
191
|
+
def test_human_output(self, capsys):
|
|
192
|
+
main(argv=["list", "--format", "human"])
|
|
193
|
+
out = capsys.readouterr().out
|
|
194
|
+
assert "layer" in out
|
|
195
|
+
|
|
196
|
+
def test_category_filter(self, capsys):
|
|
197
|
+
main(argv=["list", "--category", "stdlib", "--format", "json"])
|
|
198
|
+
data = json.loads(capsys.readouterr().out)
|
|
199
|
+
assert data
|
|
200
|
+
for item in data:
|
|
201
|
+
assert item["category"] == "stdlib"
|
|
202
|
+
|
|
203
|
+
def test_version_filter(self, capsys):
|
|
204
|
+
main(argv=["list", "--python-version", "3.11", "--format", "json"])
|
|
205
|
+
data = json.loads(capsys.readouterr().out)
|
|
206
|
+
assert isinstance(data, list)
|
|
207
|
+
assert len(data) >= 1
|
|
208
|
+
|
|
209
|
+
def test_empty_exits_1(self, capsys):
|
|
210
|
+
with pytest.raises(SystemExit, match="1"):
|
|
211
|
+
main(argv=["list", "--category", "nonexistent_cat", "--format", "json"])
|
|
212
|
+
|
|
213
|
+
def test_category_grouping_human(self, capsys):
|
|
214
|
+
main(argv=["list", "--format", "human"])
|
|
215
|
+
out = capsys.readouterr().out
|
|
216
|
+
assert "[" in out and "]" in out
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class TestCmdDetectVersion:
|
|
220
|
+
def test_basic_output(self, capsys):
|
|
221
|
+
main(argv=["detect-version"])
|
|
222
|
+
out = capsys.readouterr().out.strip()
|
|
223
|
+
assert out # should return some version string
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TestCmdSetupUninstall:
|
|
227
|
+
def test_setup_dispatch(self):
|
|
228
|
+
with patch("modern_python_guidance.setup_cmd.run_setup", return_value=0) as mock:
|
|
229
|
+
with pytest.raises(SystemExit, match="0"):
|
|
230
|
+
main(argv=["setup", "--dry-run"])
|
|
231
|
+
mock.assert_called_once()
|
|
232
|
+
call_kwargs = mock.call_args
|
|
233
|
+
assert call_kwargs[1]["dry_run"] is True
|
|
234
|
+
|
|
235
|
+
def test_uninstall_dispatch(self):
|
|
236
|
+
with patch("modern_python_guidance.uninstall_cmd.run_uninstall", return_value=0) as mock:
|
|
237
|
+
with pytest.raises(SystemExit, match="0"):
|
|
238
|
+
main(argv=["uninstall", "--dry-run"])
|
|
239
|
+
mock.assert_called_once()
|
|
240
|
+
call_kwargs = mock.call_args
|
|
241
|
+
assert call_kwargs[1]["dry_run"] is True
|
|
242
|
+
|
|
243
|
+
def test_setup_nonzero_exit(self):
|
|
244
|
+
with (
|
|
245
|
+
patch("modern_python_guidance.setup_cmd.run_setup", return_value=1),
|
|
246
|
+
pytest.raises(SystemExit, match="1"),
|
|
247
|
+
):
|
|
248
|
+
main(argv=["setup", "--dry-run"])
|
|
249
|
+
|
|
250
|
+
def test_uninstall_nonzero_exit(self):
|
|
251
|
+
with (
|
|
252
|
+
patch("modern_python_guidance.uninstall_cmd.run_uninstall", return_value=1),
|
|
253
|
+
pytest.raises(SystemExit, match="1"),
|
|
254
|
+
):
|
|
255
|
+
main(argv=["uninstall", "--dry-run"])
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# --- helpers ---
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _make_ns(**kwargs):
|
|
262
|
+
"""Build a minimal argparse.Namespace for _resolve_format tests."""
|
|
263
|
+
import argparse
|
|
264
|
+
|
|
265
|
+
return argparse.Namespace(**kwargs)
|
|
@@ -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
|