modern-python-guidance 0.2.1__tar.gz → 0.2.3__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.2.1 → modern_python_guidance-0.2.3}/CHANGELOG.md +23 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/PKG-INFO +2 -2
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/README.md +1 -1
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/pyproject.toml +1 -1
- modern_python_guidance-0.2.3/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +85 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +1 -1
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +5 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/__init__.py +1 -1
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/cli.py +4 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/guide_index.py +39 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/mcp_server.py +4 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/search.py +3 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/tests/test_cli_integration.py +12 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/tests/test_mcp_server.py +20 -0
- modern_python_guidance-0.2.3/tests/test_search.py +223 -0
- modern_python_guidance-0.2.1/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -73
- modern_python_guidance-0.2.1/tests/test_search.py +0 -89
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/.github/workflows/ci.yml +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/.gitignore +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/CONTRIBUTING.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/LICENSE +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/SECURITY.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/run.sh +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/score.sh +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/docs/design.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/frontmatter.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/retrieve.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/version_detect.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/tests/test_frontmatter.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/tests/test_retrieve.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/tests/test_skill_sync.py +0 -0
- {modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/tests/test_version_detect.py +0 -0
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.3] — 2026-05-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `fastapi-typed-state` guide: added missing Version Notes section (closes #13)
|
|
10
|
+
- `fastapi-typed-state` and `fastapi-lifespan` guides: corrected minimum version from FastAPI >= 0.93.0 to >= 0.94.0 (lifespan state dict requires Starlette >= 0.26.0, which FastAPI 0.93.0 excludes)
|
|
11
|
+
|
|
12
|
+
## [0.2.2] — 2026-05-28
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Search response (MCP + CLI) now includes `tags`, `python`, `frequency`, and `snippet` fields for richer agent decision-making without requiring a follow-up retrieve call
|
|
17
|
+
- `dataclass-modern` guide rewritten: BAD/GOOD examples now center on immutable value objects (`frozen=True, slots=True, kw_only=True`), with decision criteria for when to use each flag; frequency upgraded to `high`
|
|
18
|
+
- README benchmark highlight now specifies "via Agent Skills" to accurately reflect the delivery method used in the A/B evaluation
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Snippet extraction: every guide produces a one-liner BAD → GOOD transformation preview (e.g. `@dataclass → @dataclass(frozen=True, slots=True, kw_only=True)`)
|
|
23
|
+
- 6 new tests: snippet non-empty invariant, exact fixture assertions, MCP/CLI enriched key validation
|
|
24
|
+
|
|
5
25
|
## [0.2.1] — 2026-05-27
|
|
6
26
|
|
|
7
27
|
### Changed
|
|
@@ -68,6 +88,9 @@ Initial release.
|
|
|
68
88
|
- Strict YAML-subset frontmatter parser (no PyYAML dependency)
|
|
69
89
|
- GitHub Actions CI (pytest + ruff on Python 3.11, 3.12, 3.13)
|
|
70
90
|
|
|
91
|
+
[0.2.3]: https://github.com/yottayoshida/modern-python-guidance/releases/tag/v0.2.3
|
|
92
|
+
[0.2.2]: https://github.com/yottayoshida/modern-python-guidance/releases/tag/v0.2.2
|
|
93
|
+
[0.2.1]: https://github.com/yottayoshida/modern-python-guidance/releases/tag/v0.2.1
|
|
71
94
|
[0.2.0]: https://github.com/yottayoshida/modern-python-guidance/releases/tag/v0.2.0
|
|
72
95
|
[0.1.2]: https://github.com/yottayoshida/modern-python-guidance/releases/tag/v0.1.2
|
|
73
96
|
[0.1.1]: https://github.com/yottayoshida/modern-python-guidance/releases/tag/v0.1.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modern-python-guidance
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -40,7 +40,7 @@ Stop your AI from writing `typing.List`, `@validator`, and `setup.py`. 39 versio
|
|
|
40
40
|
|
|
41
41
|
## Highlights
|
|
42
42
|
|
|
43
|
-
- **Measurable impact**: +14.7pp overall improvement in A/B benchmark
|
|
43
|
+
- **Measurable impact**: +14.7pp overall improvement in A/B benchmark via Agent Skills (38 scored items, [details](docs/benchmark-evaluation.md)). Largest variant (FastAPI, 32 items): Control 60.4% → Treatment 82.3%
|
|
44
44
|
- **39 guides** across stdlib, Pydantic, FastAPI, Django, SQLAlchemy, pytest, and toolchain
|
|
45
45
|
- **Version-aware**: auto-detects your project's Python version and filters guides accordingly
|
|
46
46
|
- **3 delivery methods**: MCP server, CLI, Agent Skills plugin
|
|
@@ -9,7 +9,7 @@ Stop your AI from writing `typing.List`, `@validator`, and `setup.py`. 39 versio
|
|
|
9
9
|
|
|
10
10
|
## Highlights
|
|
11
11
|
|
|
12
|
-
- **Measurable impact**: +14.7pp overall improvement in A/B benchmark
|
|
12
|
+
- **Measurable impact**: +14.7pp overall improvement in A/B benchmark via Agent Skills (38 scored items, [details](docs/benchmark-evaluation.md)). Largest variant (FastAPI, 32 items): Control 60.4% → Treatment 82.3%
|
|
13
13
|
- **39 guides** across stdlib, Pydantic, FastAPI, Django, SQLAlchemy, pytest, and toolchain
|
|
14
14
|
- **Version-aware**: auto-detects your project's Python version and filters guides accordingly
|
|
15
15
|
- **3 delivery methods**: MCP server, CLI, Agent Skills plugin
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "modern-python-guidance"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Version-aware BAD/GOOD pattern guides that help AI coding agents generate modern Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0 OR MIT"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: dataclass-modern
|
|
3
|
+
title: Use Modern Dataclass Features (frozen, slots, kw_only)
|
|
4
|
+
category: data-structures
|
|
5
|
+
layer: 1
|
|
6
|
+
tags:
|
|
7
|
+
- dataclass
|
|
8
|
+
- slots
|
|
9
|
+
- kw_only
|
|
10
|
+
- frozen
|
|
11
|
+
- immutable
|
|
12
|
+
aliases:
|
|
13
|
+
- dataclass
|
|
14
|
+
- dataclasses
|
|
15
|
+
python: ">=3.10"
|
|
16
|
+
frequency: high
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Use Modern Dataclass Features
|
|
20
|
+
|
|
21
|
+
Since Python 3.10, dataclasses support `frozen=True`, `slots=True`, and `kw_only=True` for immutable value objects with better performance.
|
|
22
|
+
|
|
23
|
+
## BAD
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class AppConfig:
|
|
30
|
+
db_host: str
|
|
31
|
+
db_port: int
|
|
32
|
+
debug: bool = False
|
|
33
|
+
|
|
34
|
+
config = AppConfig("localhost", 5432)
|
|
35
|
+
config.db_host = "evil.example.com" # mutable — accidental or malicious mutation
|
|
36
|
+
config.typo_field = True # silently creates new attribute
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## GOOD
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
45
|
+
class AppConfig:
|
|
46
|
+
db_host: str
|
|
47
|
+
db_port: int
|
|
48
|
+
debug: bool = False
|
|
49
|
+
|
|
50
|
+
config = AppConfig(db_host="localhost", db_port=5432)
|
|
51
|
+
config.db_host = "evil" # FrozenInstanceError
|
|
52
|
+
config.typo_field = True # AttributeError
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Why
|
|
56
|
+
|
|
57
|
+
### When to use each flag
|
|
58
|
+
|
|
59
|
+
| Flag | Use when | Effect |
|
|
60
|
+
|------|----------|--------|
|
|
61
|
+
| `frozen=True` | Value objects, configs, DTOs, dict keys | Immutable + hashable |
|
|
62
|
+
| `slots=True` | Always (unless you need `__dict__`) | 20-35% less memory, faster access, blocks typo attrs |
|
|
63
|
+
| `kw_only=True` | 3+ fields, or fields of same type | Forces named args, prevents ordering bugs |
|
|
64
|
+
|
|
65
|
+
### When NOT to use
|
|
66
|
+
|
|
67
|
+
- **`frozen`**: Skip when you need mutable builder pattern or in-place updates in tight loops
|
|
68
|
+
- **`slots`**: Skip when you need `__dict__` introspection, multiple inheritance with conflicting slots, or dynamic attribute assignment
|
|
69
|
+
- **`kw_only`**: Skip for 1-2 field classes where positional is unambiguous (e.g., `Point(x, y)`)
|
|
70
|
+
|
|
71
|
+
### Decision checklist
|
|
72
|
+
|
|
73
|
+
1. Is this a value object, config, or DTO? → Add `frozen=True`
|
|
74
|
+
2. Do you need `__dict__` or multiple inheritance? → If no, add `slots=True`
|
|
75
|
+
3. Are there 3+ fields or fields of the same type? → Add `kw_only=True`
|
|
76
|
+
|
|
77
|
+
## Version Notes
|
|
78
|
+
|
|
79
|
+
- 3.10+: `slots=True`, `kw_only=True`
|
|
80
|
+
- 3.10+: Per-field `kw_only` via `field(kw_only=True)`
|
|
81
|
+
- 3.7-3.9: Basic `@dataclass` and `frozen=True` only (no slots/kw_only)
|
|
82
|
+
|
|
83
|
+
## References
|
|
84
|
+
|
|
85
|
+
- [dataclasses documentation](https://docs.python.org/3/library/dataclasses.html)
|
|
@@ -68,7 +68,7 @@ async def root(request: Request):
|
|
|
68
68
|
|
|
69
69
|
## Version Notes
|
|
70
70
|
|
|
71
|
-
- Works on Python 3.9+ with FastAPI >= 0.
|
|
71
|
+
- Works on Python 3.9+ with FastAPI >= 0.94.0 (lifespan state dict requires Starlette >= 0.26.0)
|
|
72
72
|
- `AsyncIterator` moved from `typing` to `collections.abc` in 3.9
|
|
73
73
|
|
|
74
74
|
## References
|
|
@@ -70,6 +70,11 @@ async def root(request: Request):
|
|
|
70
70
|
- `dataclass` or `TypedDict` documents the expected shape
|
|
71
71
|
- Resource cleanup is guaranteed by the context manager
|
|
72
72
|
|
|
73
|
+
## Version Notes
|
|
74
|
+
|
|
75
|
+
- Lifespan state dict requires FastAPI >= 0.94.0 (Starlette >= 0.26.0)
|
|
76
|
+
- `@dataclass(slots=True)` requires Python 3.10+; use plain `@dataclass` on 3.9
|
|
77
|
+
|
|
73
78
|
## References
|
|
74
79
|
|
|
75
80
|
- [FastAPI Lifespan State](https://fastapi.tiangolo.com/advanced/events/#lifespan-state)
|
{modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/cli.py
RENAMED
|
@@ -122,9 +122,13 @@ def _cmd_search(args: argparse.Namespace) -> None:
|
|
|
122
122
|
"title": r.meta.title,
|
|
123
123
|
"category": r.meta.category,
|
|
124
124
|
"layer": r.meta.layer,
|
|
125
|
+
"tags": r.meta.tags,
|
|
126
|
+
"python": r.meta.python,
|
|
127
|
+
"frequency": r.meta.frequency,
|
|
125
128
|
"score": r.score,
|
|
126
129
|
"token_estimate": r.token_estimate,
|
|
127
130
|
"fuzzy": r.fuzzy,
|
|
131
|
+
"snippet": r.snippet,
|
|
128
132
|
}
|
|
129
133
|
for r in results
|
|
130
134
|
]
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import importlib.resources
|
|
6
6
|
import logging
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
+
from itertools import zip_longest
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
from modern_python_guidance.frontmatter import FrontmatterError, GuideMeta, parse_frontmatter
|
|
@@ -17,6 +18,7 @@ class Guide:
|
|
|
17
18
|
meta: GuideMeta
|
|
18
19
|
body: str
|
|
19
20
|
source_path: str
|
|
21
|
+
snippet: str = ""
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
@dataclass
|
|
@@ -68,6 +70,7 @@ def build_index(guides_dir: Path | None = None) -> GuideIndex:
|
|
|
68
70
|
meta=meta,
|
|
69
71
|
body=body,
|
|
70
72
|
source_path=str(md_file),
|
|
73
|
+
snippet=_extract_snippet(body),
|
|
71
74
|
)
|
|
72
75
|
except FrontmatterError as e:
|
|
73
76
|
log.warning("Skipping %s: %s", md_file, e)
|
|
@@ -78,6 +81,42 @@ def build_index(guides_dir: Path | None = None) -> GuideIndex:
|
|
|
78
81
|
return index
|
|
79
82
|
|
|
80
83
|
|
|
84
|
+
def _extract_snippet(body: str) -> str:
|
|
85
|
+
"""Extract a BAD → GOOD one-liner from guide body.
|
|
86
|
+
|
|
87
|
+
Finds the first pair of lines from BAD and GOOD code blocks that differ,
|
|
88
|
+
which best conveys the transformation the guide teaches.
|
|
89
|
+
"""
|
|
90
|
+
bad_lines = _code_lines(body, "## BAD")
|
|
91
|
+
good_lines = _code_lines(body, "## GOOD")
|
|
92
|
+
if not bad_lines or not good_lines:
|
|
93
|
+
return ""
|
|
94
|
+
for b, g in zip_longest(bad_lines, good_lines, fillvalue=""):
|
|
95
|
+
if b != g:
|
|
96
|
+
return f"{b} → {g}" if b and g else (b or g)
|
|
97
|
+
return f"{bad_lines[0]} → {good_lines[0]}"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _code_lines(body: str, heading: str) -> list[str]:
|
|
101
|
+
"""Return all non-empty code lines from the first fence under a heading."""
|
|
102
|
+
parts = body.split(heading + "\n")
|
|
103
|
+
if len(parts) < 2:
|
|
104
|
+
return []
|
|
105
|
+
section = parts[1].split("\n## ")[0]
|
|
106
|
+
in_fence = False
|
|
107
|
+
lines: list[str] = []
|
|
108
|
+
for line in section.splitlines():
|
|
109
|
+
stripped = line.strip()
|
|
110
|
+
if stripped.startswith("```"):
|
|
111
|
+
if not in_fence:
|
|
112
|
+
in_fence = True
|
|
113
|
+
continue
|
|
114
|
+
break
|
|
115
|
+
if in_fence and stripped:
|
|
116
|
+
lines.append(stripped)
|
|
117
|
+
return lines
|
|
118
|
+
|
|
119
|
+
|
|
81
120
|
def _find_guides_dir() -> Path:
|
|
82
121
|
try:
|
|
83
122
|
skills_pkg = importlib.resources.files("modern_python_guidance") / "skills"
|
|
@@ -252,9 +252,13 @@ def _tool_search(arguments: dict) -> dict:
|
|
|
252
252
|
"title": r.meta.title,
|
|
253
253
|
"category": r.meta.category,
|
|
254
254
|
"layer": r.meta.layer,
|
|
255
|
+
"tags": r.meta.tags,
|
|
256
|
+
"python": r.meta.python,
|
|
257
|
+
"frequency": r.meta.frequency,
|
|
255
258
|
"score": r.score,
|
|
256
259
|
"token_estimate": r.token_estimate,
|
|
257
260
|
"fuzzy": r.fuzzy,
|
|
261
|
+
"snippet": r.snippet,
|
|
258
262
|
}
|
|
259
263
|
for r in results
|
|
260
264
|
]
|
{modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/search.py
RENAMED
|
@@ -28,6 +28,7 @@ class SearchResult:
|
|
|
28
28
|
meta: GuideMeta
|
|
29
29
|
token_estimate: int
|
|
30
30
|
fuzzy: bool = False
|
|
31
|
+
snippet: str = ""
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def search(
|
|
@@ -64,6 +65,7 @@ def search(
|
|
|
64
65
|
score=score,
|
|
65
66
|
meta=meta,
|
|
66
67
|
token_estimate=token_estimate(guide.body),
|
|
68
|
+
snippet=guide.snippet,
|
|
67
69
|
))
|
|
68
70
|
|
|
69
71
|
results.sort(key=lambda r: (-r.score, r.guide_id))
|
|
@@ -143,6 +145,7 @@ def _fuzzy_fallback(
|
|
|
143
145
|
meta=guide.meta,
|
|
144
146
|
token_estimate=token_estimate(guide.body),
|
|
145
147
|
fuzzy=True,
|
|
148
|
+
snippet=guide.snippet,
|
|
146
149
|
))
|
|
147
150
|
|
|
148
151
|
results.sort(key=lambda r: (-r.score, r.guide_id))
|
|
@@ -50,6 +50,18 @@ class TestSearch:
|
|
|
50
50
|
data = json.loads(r.stdout)
|
|
51
51
|
assert all(d["category"] == "fastapi" for d in data)
|
|
52
52
|
|
|
53
|
+
def test_search_enriched_keys(self):
|
|
54
|
+
r = run_cli("search", "pydantic validator", "--format", "json")
|
|
55
|
+
assert r.returncode == 0
|
|
56
|
+
data = json.loads(r.stdout)
|
|
57
|
+
expected_keys = {
|
|
58
|
+
"id", "title", "category", "layer", "tags", "python",
|
|
59
|
+
"frequency", "score", "token_estimate", "fuzzy", "snippet",
|
|
60
|
+
}
|
|
61
|
+
assert set(data[0].keys()) == expected_keys
|
|
62
|
+
assert isinstance(data[0]["tags"], list)
|
|
63
|
+
assert "→" in data[0]["snippet"]
|
|
64
|
+
|
|
53
65
|
|
|
54
66
|
class TestRetrieve:
|
|
55
67
|
def test_retrieve_single(self):
|
|
@@ -99,6 +99,26 @@ class TestSearchGuides:
|
|
|
99
99
|
assert isinstance(data, list)
|
|
100
100
|
assert len(data) >= 1
|
|
101
101
|
|
|
102
|
+
def test_search_enriched_keys(self):
|
|
103
|
+
responses = _run_mcp(
|
|
104
|
+
*_init_handshake(),
|
|
105
|
+
{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {
|
|
106
|
+
"name": "search_guides",
|
|
107
|
+
"arguments": {"query": "pydantic validator"},
|
|
108
|
+
}},
|
|
109
|
+
)
|
|
110
|
+
data = json.loads(responses[1]["result"]["content"][0]["text"])
|
|
111
|
+
expected_keys = {
|
|
112
|
+
"id", "title", "category", "layer", "tags", "python",
|
|
113
|
+
"frequency", "score", "token_estimate", "fuzzy", "snippet",
|
|
114
|
+
}
|
|
115
|
+
assert set(data[0].keys()) == expected_keys
|
|
116
|
+
assert isinstance(data[0]["tags"], list)
|
|
117
|
+
assert isinstance(data[0]["python"], str)
|
|
118
|
+
assert isinstance(data[0]["frequency"], str)
|
|
119
|
+
assert isinstance(data[0]["snippet"], str)
|
|
120
|
+
assert "→" in data[0]["snippet"]
|
|
121
|
+
|
|
102
122
|
def test_search_empty_query(self):
|
|
103
123
|
responses = _run_mcp(
|
|
104
124
|
*_init_handshake(),
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from modern_python_guidance.guide_index import _extract_snippet, build_index
|
|
8
|
+
from modern_python_guidance.search import search
|
|
9
|
+
|
|
10
|
+
GUIDES_DIR = Path(__file__).parent.parent / "skills" / "modern-python-guidance" / "guides"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def index():
|
|
15
|
+
return build_index(GUIDES_DIR)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestBasicSearch:
|
|
19
|
+
def test_search_by_tag(self, index):
|
|
20
|
+
results = search(index, "typing")
|
|
21
|
+
assert len(results) >= 1
|
|
22
|
+
assert results[0].guide_id == "use-builtin-generics"
|
|
23
|
+
|
|
24
|
+
def test_search_by_alias(self, index):
|
|
25
|
+
results = search(index, "typing.List")
|
|
26
|
+
assert len(results) >= 1
|
|
27
|
+
assert results[0].guide_id == "use-builtin-generics"
|
|
28
|
+
|
|
29
|
+
def test_search_by_title_word(self, index):
|
|
30
|
+
results = search(index, "lifespan")
|
|
31
|
+
assert len(results) >= 1
|
|
32
|
+
assert results[0].guide_id == "fastapi-lifespan"
|
|
33
|
+
|
|
34
|
+
def test_search_by_category(self, index):
|
|
35
|
+
results = search(index, "asyncio", category="async")
|
|
36
|
+
assert all(r.meta.category == "async" for r in results)
|
|
37
|
+
|
|
38
|
+
def test_search_returns_token_estimate(self, index):
|
|
39
|
+
results = search(index, "typing")
|
|
40
|
+
assert all(r.token_estimate > 0 for r in results)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestVersionFilter:
|
|
44
|
+
def test_version_excludes_incompatible(self, index):
|
|
45
|
+
results = search(index, "asyncio taskgroup", python_version="3.9")
|
|
46
|
+
ids = [r.guide_id for r in results]
|
|
47
|
+
assert "taskgroup-over-gather" not in ids
|
|
48
|
+
|
|
49
|
+
def test_version_includes_compatible(self, index):
|
|
50
|
+
results = search(index, "asyncio taskgroup", python_version="3.11")
|
|
51
|
+
ids = [r.guide_id for r in results]
|
|
52
|
+
assert "taskgroup-over-gather" in ids
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestFuzzyFallback:
|
|
56
|
+
def test_fuzzy_on_no_match(self, index):
|
|
57
|
+
results = search(index, "genercs")
|
|
58
|
+
assert len(results) > 0
|
|
59
|
+
assert results[0].fuzzy is True
|
|
60
|
+
|
|
61
|
+
def test_truly_irrelevant_query(self, index):
|
|
62
|
+
results = search(index, "javascript react angular")
|
|
63
|
+
assert all(r.fuzzy for r in results) or len(results) == 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestDeterminism:
|
|
67
|
+
def test_same_score_sorted_by_id(self, index):
|
|
68
|
+
results = search(index, "pydantic")
|
|
69
|
+
assert len(results) >= 2
|
|
70
|
+
assert not any(r.fuzzy for r in results)
|
|
71
|
+
same_score_groups: dict[float, list[str]] = {}
|
|
72
|
+
for r in results:
|
|
73
|
+
same_score_groups.setdefault(r.score, []).append(r.guide_id)
|
|
74
|
+
for group in same_score_groups.values():
|
|
75
|
+
assert group == sorted(group)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestSnippet:
|
|
79
|
+
def test_all_guides_have_snippet(self, index):
|
|
80
|
+
results = search(index, "python", limit=50)
|
|
81
|
+
for r in results:
|
|
82
|
+
guide = index.get(r.guide_id)
|
|
83
|
+
assert guide is not None
|
|
84
|
+
assert guide.snippet, f"{r.guide_id} has empty snippet"
|
|
85
|
+
|
|
86
|
+
def test_all_guides_non_empty_snippet(self):
|
|
87
|
+
idx = build_index(GUIDES_DIR)
|
|
88
|
+
for guide_id, guide in idx.guides.items():
|
|
89
|
+
assert guide.snippet, f"{guide_id} has empty snippet"
|
|
90
|
+
|
|
91
|
+
def test_snippet_in_search_result(self, index):
|
|
92
|
+
results = search(index, "pydantic validator")
|
|
93
|
+
assert len(results) >= 1
|
|
94
|
+
r = results[0]
|
|
95
|
+
assert r.snippet
|
|
96
|
+
assert "→" in r.snippet
|
|
97
|
+
|
|
98
|
+
def test_snippet_exact_fixtures(self, index):
|
|
99
|
+
fixtures = {
|
|
100
|
+
"pydantic-v2-validators": (
|
|
101
|
+
"from pydantic import BaseModel, validator, root_validator"
|
|
102
|
+
" → "
|
|
103
|
+
"from pydantic import BaseModel, field_validator, model_validator"
|
|
104
|
+
),
|
|
105
|
+
"dataclass-modern": "@dataclass → @dataclass(frozen=True, slots=True, kw_only=True)",
|
|
106
|
+
"use-builtin-generics": (
|
|
107
|
+
"from typing import Dict, List, Optional, Set, Tuple"
|
|
108
|
+
" → "
|
|
109
|
+
"def process(items: list[str]) -> dict[str, int]:"
|
|
110
|
+
),
|
|
111
|
+
"taskgroup-over-gather": (
|
|
112
|
+
"results = await asyncio.gather("
|
|
113
|
+
" → "
|
|
114
|
+
"async with asyncio.TaskGroup() as tg:"
|
|
115
|
+
),
|
|
116
|
+
}
|
|
117
|
+
for guide_id, expected in fixtures.items():
|
|
118
|
+
guide = index.get(guide_id)
|
|
119
|
+
assert guide is not None, f"{guide_id} not found"
|
|
120
|
+
assert guide.snippet == expected, (
|
|
121
|
+
f"{guide_id}: expected {expected!r}, got {guide.snippet!r}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestSnippetExtraction:
|
|
126
|
+
def test_unequal_bad_longer(self):
|
|
127
|
+
body = (
|
|
128
|
+
"## BAD\n```python\nline_a\nline_b\nline_c\n```\n"
|
|
129
|
+
"## GOOD\n```python\nline_x\n```\n"
|
|
130
|
+
)
|
|
131
|
+
snippet = _extract_snippet(body)
|
|
132
|
+
assert snippet == "line_a → line_x"
|
|
133
|
+
|
|
134
|
+
def test_unequal_good_longer(self):
|
|
135
|
+
body = (
|
|
136
|
+
"## BAD\n```python\nline_a\n```\n"
|
|
137
|
+
"## GOOD\n```python\nline_x\nline_y\nline_z\n```\n"
|
|
138
|
+
)
|
|
139
|
+
snippet = _extract_snippet(body)
|
|
140
|
+
assert snippet == "line_a → line_x"
|
|
141
|
+
|
|
142
|
+
def test_late_differing_line(self):
|
|
143
|
+
body = (
|
|
144
|
+
"## BAD\n```python\nimport foo\nresult = old_call()\n```\n"
|
|
145
|
+
"## GOOD\n```python\nimport foo\nresult = new_call()\n```\n"
|
|
146
|
+
)
|
|
147
|
+
snippet = _extract_snippet(body)
|
|
148
|
+
assert snippet == "result = old_call() → result = new_call()"
|
|
149
|
+
|
|
150
|
+
def test_diff_beyond_eight_lines(self):
|
|
151
|
+
shared = "\n".join(f"line_{i}" for i in range(9))
|
|
152
|
+
body = (
|
|
153
|
+
f"## BAD\n```python\n{shared}\nold_call()\n```\n"
|
|
154
|
+
f"## GOOD\n```python\n{shared}\nnew_call()\n```\n"
|
|
155
|
+
)
|
|
156
|
+
snippet = _extract_snippet(body)
|
|
157
|
+
assert snippet == "old_call() → new_call()"
|
|
158
|
+
|
|
159
|
+
def test_trailing_only_in_bad(self):
|
|
160
|
+
body = (
|
|
161
|
+
"## BAD\n```python\nshared\nextra_bad\n```\n"
|
|
162
|
+
"## GOOD\n```python\nshared\n```\n"
|
|
163
|
+
)
|
|
164
|
+
snippet = _extract_snippet(body)
|
|
165
|
+
assert snippet == "extra_bad"
|
|
166
|
+
|
|
167
|
+
def test_trailing_only_in_good(self):
|
|
168
|
+
body = (
|
|
169
|
+
"## BAD\n```python\nshared\n```\n"
|
|
170
|
+
"## GOOD\n```python\nshared\nextra_good\n```\n"
|
|
171
|
+
)
|
|
172
|
+
snippet = _extract_snippet(body)
|
|
173
|
+
assert snippet == "extra_good"
|
|
174
|
+
|
|
175
|
+
def test_all_lines_identical(self):
|
|
176
|
+
body = (
|
|
177
|
+
"## BAD\n```python\nsame\n```\n"
|
|
178
|
+
"## GOOD\n```python\nsame\n```\n"
|
|
179
|
+
)
|
|
180
|
+
snippet = _extract_snippet(body)
|
|
181
|
+
assert snippet == "same → same"
|
|
182
|
+
|
|
183
|
+
def test_heading_boundary_not_prefix_match(self):
|
|
184
|
+
body = (
|
|
185
|
+
"## BADLY_NAMED\n```python\nwrong\n```\n"
|
|
186
|
+
"## BAD\n```python\ncorrect_bad\n```\n"
|
|
187
|
+
"## GOOD\n```python\ncorrect_good\n```\n"
|
|
188
|
+
)
|
|
189
|
+
snippet = _extract_snippet(body)
|
|
190
|
+
assert snippet == "correct_bad → correct_good"
|
|
191
|
+
|
|
192
|
+
def test_first_fence_only(self):
|
|
193
|
+
body = (
|
|
194
|
+
"## BAD\n```python\nfirst_bad\n```\n"
|
|
195
|
+
"```python\nsecond_bad\n```\n"
|
|
196
|
+
"## GOOD\n```python\nfirst_good\n```\n"
|
|
197
|
+
)
|
|
198
|
+
snippet = _extract_snippet(body)
|
|
199
|
+
assert snippet == "first_bad → first_good"
|
|
200
|
+
|
|
201
|
+
def test_no_bad_section(self):
|
|
202
|
+
body = "## GOOD\n```python\ncode\n```\n"
|
|
203
|
+
snippet = _extract_snippet(body)
|
|
204
|
+
assert snippet == ""
|
|
205
|
+
|
|
206
|
+
def test_no_good_section(self):
|
|
207
|
+
body = "## BAD\n```python\ncode\n```\n"
|
|
208
|
+
snippet = _extract_snippet(body)
|
|
209
|
+
assert snippet == ""
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestEdgeCases:
|
|
213
|
+
def test_empty_query(self, index):
|
|
214
|
+
results = search(index, "")
|
|
215
|
+
assert results == []
|
|
216
|
+
|
|
217
|
+
def test_long_query_truncated(self, index):
|
|
218
|
+
results = search(index, "x " * 1000)
|
|
219
|
+
assert isinstance(results, list)
|
|
220
|
+
|
|
221
|
+
def test_limit(self, index):
|
|
222
|
+
results = search(index, "python", limit=2)
|
|
223
|
+
assert len(results) <= 2
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
id: dataclass-modern
|
|
3
|
-
title: Use Modern Dataclass Features (slots, kw_only)
|
|
4
|
-
category: data-structures
|
|
5
|
-
layer: 1
|
|
6
|
-
tags:
|
|
7
|
-
- dataclass
|
|
8
|
-
- slots
|
|
9
|
-
- kw_only
|
|
10
|
-
aliases:
|
|
11
|
-
- dataclass
|
|
12
|
-
- dataclasses
|
|
13
|
-
python: ">=3.10"
|
|
14
|
-
frequency: medium
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
# Use Modern Dataclass Features
|
|
18
|
-
|
|
19
|
-
Since Python 3.10, dataclasses support `slots=True` and `kw_only=True` for better performance and safer APIs.
|
|
20
|
-
|
|
21
|
-
## BAD
|
|
22
|
-
|
|
23
|
-
```python
|
|
24
|
-
from dataclasses import dataclass
|
|
25
|
-
|
|
26
|
-
@dataclass
|
|
27
|
-
class Point:
|
|
28
|
-
x: float
|
|
29
|
-
y: float
|
|
30
|
-
z: float = 0.0
|
|
31
|
-
|
|
32
|
-
# No slots: allows typos on attributes
|
|
33
|
-
p = Point(1.0, 2.0)
|
|
34
|
-
p.w = 3.0 # silently creates new attribute (typo for 'z')
|
|
35
|
-
|
|
36
|
-
# Positional args: easy to mix up x and y
|
|
37
|
-
p = Point(2.0, 1.0) # is this (x=2, y=1) or (x=1, y=2)?
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## GOOD
|
|
41
|
-
|
|
42
|
-
```python
|
|
43
|
-
from dataclasses import dataclass
|
|
44
|
-
|
|
45
|
-
@dataclass(slots=True, kw_only=True)
|
|
46
|
-
class Point:
|
|
47
|
-
x: float
|
|
48
|
-
y: float
|
|
49
|
-
z: float = 0.0
|
|
50
|
-
|
|
51
|
-
p = Point(x=1.0, y=2.0)
|
|
52
|
-
p.w = 3.0 # AttributeError: 'Point' has no attribute 'w'
|
|
53
|
-
|
|
54
|
-
# kw_only forces explicit names — no positional confusion
|
|
55
|
-
p = Point(x=2.0, y=1.0) # intent is clear
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Why
|
|
59
|
-
|
|
60
|
-
- `slots=True`: 20-35% less memory, faster attribute access, prevents typo attributes
|
|
61
|
-
- `kw_only=True`: forces named arguments, eliminates positional ordering bugs
|
|
62
|
-
- `frozen=True` + `slots=True`: fast immutable value objects
|
|
63
|
-
- Combine for production data classes: `@dataclass(slots=True, frozen=True, kw_only=True)`
|
|
64
|
-
|
|
65
|
-
## Version Notes
|
|
66
|
-
|
|
67
|
-
- 3.10+: `slots=True`, `kw_only=True`
|
|
68
|
-
- 3.10+: Per-field `kw_only` via `field(kw_only=True)`
|
|
69
|
-
- 3.7-3.9: Basic `@dataclass` without slots/kw_only
|
|
70
|
-
|
|
71
|
-
## References
|
|
72
|
-
|
|
73
|
-
- [dataclasses documentation](https://docs.python.org/3/library/dataclasses.html)
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from modern_python_guidance.guide_index import build_index
|
|
8
|
-
from modern_python_guidance.search import search
|
|
9
|
-
|
|
10
|
-
GUIDES_DIR = Path(__file__).parent.parent / "skills" / "modern-python-guidance" / "guides"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@pytest.fixture
|
|
14
|
-
def index():
|
|
15
|
-
return build_index(GUIDES_DIR)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class TestBasicSearch:
|
|
19
|
-
def test_search_by_tag(self, index):
|
|
20
|
-
results = search(index, "typing")
|
|
21
|
-
assert len(results) >= 1
|
|
22
|
-
assert results[0].guide_id == "use-builtin-generics"
|
|
23
|
-
|
|
24
|
-
def test_search_by_alias(self, index):
|
|
25
|
-
results = search(index, "typing.List")
|
|
26
|
-
assert len(results) >= 1
|
|
27
|
-
assert results[0].guide_id == "use-builtin-generics"
|
|
28
|
-
|
|
29
|
-
def test_search_by_title_word(self, index):
|
|
30
|
-
results = search(index, "lifespan")
|
|
31
|
-
assert len(results) >= 1
|
|
32
|
-
assert results[0].guide_id == "fastapi-lifespan"
|
|
33
|
-
|
|
34
|
-
def test_search_by_category(self, index):
|
|
35
|
-
results = search(index, "asyncio", category="async")
|
|
36
|
-
assert all(r.meta.category == "async" for r in results)
|
|
37
|
-
|
|
38
|
-
def test_search_returns_token_estimate(self, index):
|
|
39
|
-
results = search(index, "typing")
|
|
40
|
-
assert all(r.token_estimate > 0 for r in results)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class TestVersionFilter:
|
|
44
|
-
def test_version_excludes_incompatible(self, index):
|
|
45
|
-
results = search(index, "asyncio taskgroup", python_version="3.9")
|
|
46
|
-
ids = [r.guide_id for r in results]
|
|
47
|
-
assert "taskgroup-over-gather" not in ids
|
|
48
|
-
|
|
49
|
-
def test_version_includes_compatible(self, index):
|
|
50
|
-
results = search(index, "asyncio taskgroup", python_version="3.11")
|
|
51
|
-
ids = [r.guide_id for r in results]
|
|
52
|
-
assert "taskgroup-over-gather" in ids
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class TestFuzzyFallback:
|
|
56
|
-
def test_fuzzy_on_no_match(self, index):
|
|
57
|
-
results = search(index, "genercs")
|
|
58
|
-
assert len(results) > 0
|
|
59
|
-
assert results[0].fuzzy is True
|
|
60
|
-
|
|
61
|
-
def test_truly_irrelevant_query(self, index):
|
|
62
|
-
results = search(index, "javascript react angular")
|
|
63
|
-
assert all(r.fuzzy for r in results) or len(results) == 0
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class TestDeterminism:
|
|
67
|
-
def test_same_score_sorted_by_id(self, index):
|
|
68
|
-
results = search(index, "pydantic")
|
|
69
|
-
assert len(results) >= 2
|
|
70
|
-
assert not any(r.fuzzy for r in results)
|
|
71
|
-
same_score_groups: dict[float, list[str]] = {}
|
|
72
|
-
for r in results:
|
|
73
|
-
same_score_groups.setdefault(r.score, []).append(r.guide_id)
|
|
74
|
-
for group in same_score_groups.values():
|
|
75
|
-
assert group == sorted(group)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class TestEdgeCases:
|
|
79
|
-
def test_empty_query(self, index):
|
|
80
|
-
results = search(index, "")
|
|
81
|
-
assert results == []
|
|
82
|
-
|
|
83
|
-
def test_long_query_truncated(self, index):
|
|
84
|
-
results = search(index, "x " * 1000)
|
|
85
|
-
assert isinstance(results, list)
|
|
86
|
-
|
|
87
|
-
def test_limit(self, index):
|
|
88
|
-
results = search(index, "python", limit=2)
|
|
89
|
-
assert len(results) <= 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
|
|
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.2.1 → modern_python_guidance-0.2.3}/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
|
{modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/__main__.py
RENAMED
|
File without changes
|
{modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/compat.py
RENAMED
|
File without changes
|
|
File without changes
|
{modern_python_guidance-0.2.1 → modern_python_guidance-0.2.3}/src/modern_python_guidance/retrieve.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|