modern-python-guidance 0.3.5__tar.gz → 0.3.6__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.5 → modern_python_guidance-0.3.6}/CHANGELOG.md +16 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/PKG-INFO +11 -11
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/README.md +10 -10
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/pyproject.toml +3 -2
- modern_python_guidance-0.3.6/rules/modern-python.md +70 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/__init__.py +1 -1
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/cli.py +12 -6
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/setup_cmd.py +106 -2
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/uninstall_cmd.py +49 -2
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_guide_structure.py +51 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_setup.py +192 -37
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_uninstall.py +117 -35
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/.github/workflows/check-python-release.yml +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/.github/workflows/ci.yml +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/.gitignore +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/CONTRIBUTING.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/LICENSE +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/SECURITY.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-a-detailed.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-a-normal.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-a-terse.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-b-detailed.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-b-normal.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-b-terse.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-c-detailed.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-c-normal.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-c-terse.txt +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/run-v5.sh +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/run.sh +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/score.sh +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/score_v5.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/docs/benchmark-v5.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/docs/design.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/stdlib/template-strings.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/frontmatter.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/guide_index.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/mcp_server.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/retrieve.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/search.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/version_detect.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_cli_integration.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_frontmatter.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_mcp_server.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_retrieve.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_scorer_v5.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_search.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_skill_sync.py +0 -0
- {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_version_detect.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.6] — 2026-05-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Rule-based delivery via symlink: `mpg setup` creates `.claude/rules/modern-python.md` that auto-injects modern Python guidance whenever Python-related files are touched, replacing reliance on probabilistic skill matching (closes #79)
|
|
10
|
+
- `setup_rules()` / `uninstall_rules()` mirroring skills symlink pattern
|
|
11
|
+
- `source.is_symlink()` security check to refuse symlink-to-symlink chains
|
|
12
|
+
- CI sync test enforcing SKILL.md body == rule body consistency
|
|
13
|
+
- 21 new tests (V-037 to V-060) for setup, uninstall, CI sync, and security
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `--skills-only` now includes Rules (both are project-local artifacts)
|
|
18
|
+
- README updated to document 4 delivery methods (was 3)
|
|
19
|
+
- `--project-dir` help text updated to mention Skills/Rules symlinks
|
|
20
|
+
|
|
5
21
|
## [0.3.5] — 2026-05-30
|
|
6
22
|
|
|
7
23
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modern-python-guidance
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
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
|
|
@@ -44,7 +44,7 @@ Stop your AI from writing `typing.List`, `@validator`, and `setup.py`. 41 versio
|
|
|
44
44
|
- **Measurable impact**: AI writes modern Python 98% of the time with mpg, vs 79% without — even with vague prompts (Opus 4.8, [V5 benchmark details](docs/benchmark-v5.md))
|
|
45
45
|
- **41 guides** across stdlib, Pydantic, FastAPI, Django, SQLAlchemy, pytest, and toolchain
|
|
46
46
|
- **Version-aware**: auto-detects your project's Python version and filters guides accordingly
|
|
47
|
-
- **
|
|
47
|
+
- **4 delivery methods**: MCP server, CLI, Agent Skills, and Rules (auto-injects on `.py` file touch)
|
|
48
48
|
- **Not Ruff**: Ruff auto-fixes syntax (`List` → `list`). mpg guides design decisions that Ruff can't touch — `TaskGroup` over `gather`, Pydantic V2 migration, SQLAlchemy 2.0 style
|
|
49
49
|
|
|
50
50
|
> **Note:** The tool itself requires Python 3.11+ to run. Guides cover patterns from Python 3.9 onward, and `--python-version` filters guides for your target environment.
|
|
@@ -58,7 +58,7 @@ pip install modern-python-guidance
|
|
|
58
58
|
mpg setup
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
This registers the MCP server
|
|
61
|
+
This registers the MCP server, links Agent Skills, and creates a Rules file (`.claude/rules/modern-python.md`) in one command. The Rules file auto-injects modern Python guidance whenever you touch Python-related files. Start a new Claude Code session afterwards — newly registered MCP servers, skills, and rules take effect on the next launch.
|
|
62
62
|
|
|
63
63
|
### CLI
|
|
64
64
|
|
|
@@ -90,7 +90,7 @@ claude mcp add mpg -- mpg mcp
|
|
|
90
90
|
}
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
**Agent Skills
|
|
93
|
+
**Agent Skills + Rules only (Claude Code):**
|
|
94
94
|
```bash
|
|
95
95
|
mpg setup --skills-only
|
|
96
96
|
```
|
|
@@ -99,25 +99,25 @@ mpg setup --skills-only
|
|
|
99
99
|
| Flag | Purpose |
|
|
100
100
|
|------|---------|
|
|
101
101
|
| `--mcp-only` | MCP registration only |
|
|
102
|
-
| `--skills-only` |
|
|
102
|
+
| `--skills-only` | Project-local artifacts only (Skills + Rules) |
|
|
103
103
|
| `--scope {user,local}` | MCP scope (default: user) |
|
|
104
|
-
| `--project-dir PATH` | Target project for Skills
|
|
104
|
+
| `--project-dir PATH` | Target project for Skills/Rules symlinks |
|
|
105
105
|
| `--dry-run` | Show what would be done |
|
|
106
106
|
|
|
107
|
-
**Uninstall** — reverse `mpg setup` (deregister the MCP server and unlink Agent Skills):
|
|
107
|
+
**Uninstall** — reverse `mpg setup` (deregister the MCP server and unlink Agent Skills + Rules):
|
|
108
108
|
```bash
|
|
109
|
-
mpg uninstall # remove
|
|
109
|
+
mpg uninstall # remove all
|
|
110
110
|
mpg uninstall --dry-run # preview what would be removed
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
| Flag | Purpose |
|
|
114
114
|
|------|---------|
|
|
115
115
|
| `--mcp-only` | MCP deregistration only |
|
|
116
|
-
| `--skills-only` |
|
|
117
|
-
| `--project-dir PATH` | Target project for
|
|
116
|
+
| `--skills-only` | Project-local artifacts only (Skills + Rules) |
|
|
117
|
+
| `--project-dir PATH` | Target project for Skills/Rules symlinks |
|
|
118
118
|
| `--dry-run` | Show what would be done |
|
|
119
119
|
|
|
120
|
-
`mpg uninstall` clears the MCP registration from every scope `setup` can write to (user and local), removes only the
|
|
120
|
+
`mpg uninstall` clears the MCP registration from every scope `setup` can write to (user and local), removes only the symlinks mpg created (never their targets or other files), and is idempotent — running it on an already-clean state is a harmless no-op.
|
|
121
121
|
|
|
122
122
|
</details>
|
|
123
123
|
|
|
@@ -12,7 +12,7 @@ Stop your AI from writing `typing.List`, `@validator`, and `setup.py`. 41 versio
|
|
|
12
12
|
- **Measurable impact**: AI writes modern Python 98% of the time with mpg, vs 79% without — even with vague prompts (Opus 4.8, [V5 benchmark details](docs/benchmark-v5.md))
|
|
13
13
|
- **41 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
|
+
- **4 delivery methods**: MCP server, CLI, Agent Skills, and Rules (auto-injects on `.py` file touch)
|
|
16
16
|
- **Not Ruff**: Ruff auto-fixes syntax (`List` → `list`). mpg guides design decisions that Ruff can't touch — `TaskGroup` over `gather`, Pydantic V2 migration, SQLAlchemy 2.0 style
|
|
17
17
|
|
|
18
18
|
> **Note:** The tool itself requires Python 3.11+ to run. Guides cover patterns from Python 3.9 onward, and `--python-version` filters guides for your target environment.
|
|
@@ -26,7 +26,7 @@ pip install modern-python-guidance
|
|
|
26
26
|
mpg setup
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
This registers the MCP server
|
|
29
|
+
This registers the MCP server, links Agent Skills, and creates a Rules file (`.claude/rules/modern-python.md`) in one command. The Rules file auto-injects modern Python guidance whenever you touch Python-related files. Start a new Claude Code session afterwards — newly registered MCP servers, skills, and rules take effect on the next launch.
|
|
30
30
|
|
|
31
31
|
### CLI
|
|
32
32
|
|
|
@@ -58,7 +58,7 @@ claude mcp add mpg -- mpg mcp
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
**Agent Skills
|
|
61
|
+
**Agent Skills + Rules only (Claude Code):**
|
|
62
62
|
```bash
|
|
63
63
|
mpg setup --skills-only
|
|
64
64
|
```
|
|
@@ -67,25 +67,25 @@ mpg setup --skills-only
|
|
|
67
67
|
| Flag | Purpose |
|
|
68
68
|
|------|---------|
|
|
69
69
|
| `--mcp-only` | MCP registration only |
|
|
70
|
-
| `--skills-only` |
|
|
70
|
+
| `--skills-only` | Project-local artifacts only (Skills + Rules) |
|
|
71
71
|
| `--scope {user,local}` | MCP scope (default: user) |
|
|
72
|
-
| `--project-dir PATH` | Target project for Skills
|
|
72
|
+
| `--project-dir PATH` | Target project for Skills/Rules symlinks |
|
|
73
73
|
| `--dry-run` | Show what would be done |
|
|
74
74
|
|
|
75
|
-
**Uninstall** — reverse `mpg setup` (deregister the MCP server and unlink Agent Skills):
|
|
75
|
+
**Uninstall** — reverse `mpg setup` (deregister the MCP server and unlink Agent Skills + Rules):
|
|
76
76
|
```bash
|
|
77
|
-
mpg uninstall # remove
|
|
77
|
+
mpg uninstall # remove all
|
|
78
78
|
mpg uninstall --dry-run # preview what would be removed
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
| Flag | Purpose |
|
|
82
82
|
|------|---------|
|
|
83
83
|
| `--mcp-only` | MCP deregistration only |
|
|
84
|
-
| `--skills-only` |
|
|
85
|
-
| `--project-dir PATH` | Target project for
|
|
84
|
+
| `--skills-only` | Project-local artifacts only (Skills + Rules) |
|
|
85
|
+
| `--project-dir PATH` | Target project for Skills/Rules symlinks |
|
|
86
86
|
| `--dry-run` | Show what would be done |
|
|
87
87
|
|
|
88
|
-
`mpg uninstall` clears the MCP registration from every scope `setup` can write to (user and local), removes only the
|
|
88
|
+
`mpg uninstall` clears the MCP registration from every scope `setup` can write to (user and local), removes only the symlinks mpg created (never their targets or other files), and is idempotent — running it on an already-clean state is a harmless no-op.
|
|
89
89
|
|
|
90
90
|
</details>
|
|
91
91
|
|
|
@@ -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.6"
|
|
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"
|
|
@@ -49,11 +49,12 @@ Issues = "https://github.com/yottayoshida/modern-python-guidance/issues"
|
|
|
49
49
|
|
|
50
50
|
[tool.hatch.build.targets.wheel]
|
|
51
51
|
packages = ["src/modern_python_guidance"]
|
|
52
|
-
only-include = ["src/modern_python_guidance", "skills"]
|
|
52
|
+
only-include = ["src/modern_python_guidance", "skills", "rules"]
|
|
53
53
|
|
|
54
54
|
[tool.hatch.build.targets.wheel.sources]
|
|
55
55
|
"src" = ""
|
|
56
56
|
"skills" = "modern_python_guidance/skills"
|
|
57
|
+
"rules" = "modern_python_guidance/rules"
|
|
57
58
|
|
|
58
59
|
[tool.ruff]
|
|
59
60
|
target-version = "py311"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths: ["**/*.py", "*.py", "**/pyproject.toml", "**/requirements*.txt", "**/setup.py", "**/setup.cfg", "**/.python-version", "**/Pipfile"]
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Modern Python Guidance
|
|
6
|
+
|
|
7
|
+
Pre-generation design patterns that Ruff cannot auto-fix. Apply these transformations when writing or reviewing Python code.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- Writing new Python code (use modern patterns from the start)
|
|
12
|
+
- Reviewing Python code (flag outdated patterns)
|
|
13
|
+
- Migrating from Pydantic V1 to V2
|
|
14
|
+
- Upgrading Python version (check which new features are available)
|
|
15
|
+
- Replacing legacy tooling (setup.py, flake8, pip)
|
|
16
|
+
|
|
17
|
+
## Embedded patterns (high-frequency, Ruff-uncovered)
|
|
18
|
+
|
|
19
|
+
### Pydantic V2 (>=3.9)
|
|
20
|
+
|
|
21
|
+
- `@validator("f")` → `@field_validator("f")`
|
|
22
|
+
- `@root_validator` → `@model_validator(mode="after")`
|
|
23
|
+
- `class Config:` → `model_config = ConfigDict(...)`
|
|
24
|
+
- `orm_mode` → `from_attributes`, `allow_population_by_field_name` → `populate_by_name`
|
|
25
|
+
- `.parse_obj(d)` → `.model_validate(d)`, `.parse_raw(j)` → `.model_validate_json(j)`
|
|
26
|
+
- `.dict()` → `.model_dump()`, `.json()` → `.model_dump_json()`
|
|
27
|
+
- `.schema()` → `.model_json_schema()`, `.copy()` → `.model_copy()`
|
|
28
|
+
|
|
29
|
+
### FastAPI (>=3.9)
|
|
30
|
+
|
|
31
|
+
- `@app.on_event("startup")`/`"shutdown"` → `@asynccontextmanager` lifespan + `FastAPI(lifespan=lifespan)`; yield dict becomes `request.state`
|
|
32
|
+
- `db: Session = Depends(get_db)` → `DbDep = Annotated[Session, Depends(get_db)]`; reusable type alias per PEP 593
|
|
33
|
+
|
|
34
|
+
### httpx
|
|
35
|
+
|
|
36
|
+
- Per-request `async with httpx.AsyncClient()` → shared `AsyncClient` with `base_url`
|
|
37
|
+
- Caveat: shared client must be closed via `async with` or lifespan management
|
|
38
|
+
|
|
39
|
+
### asyncio (>=3.11)
|
|
40
|
+
|
|
41
|
+
- `await asyncio.gather(a(), b())` → `async with asyncio.TaskGroup() as tg:` + `tg.create_task()`; access results via `task.result()`
|
|
42
|
+
- Caveat: 3.11+ only. `TaskGroup` cancels siblings on error and raises `ExceptionGroup`; `gather` preserves return order and supports `return_exceptions=True`
|
|
43
|
+
|
|
44
|
+
### SQLAlchemy 2.0 (>=3.9)
|
|
45
|
+
|
|
46
|
+
- `session.query(User).filter()` → `session.execute(select(User).where())`; use `session.scalars()` for ORM results
|
|
47
|
+
- `Column(Integer)` → `Mapped[int] = mapped_column()`; type inferred from annotation, nullability from `Optional`/`| None`
|
|
48
|
+
- Sync `Session` with `asyncio.to_thread` → `AsyncSession` + `create_async_engine` + `async_sessionmaker`
|
|
49
|
+
|
|
50
|
+
### Toolchain
|
|
51
|
+
|
|
52
|
+
- `setup.py` / `setup.cfg` → `pyproject.toml` with `[build-system]` + `[project]` (PEP 621)
|
|
53
|
+
- `subprocess.run(f"cmd {arg}", shell=True)` → `subprocess.run(["cmd", arg], check=True)`
|
|
54
|
+
- Caveat: `shell=True` is valid when pipes/globs are needed; use `shlex.quote()` for user input
|
|
55
|
+
|
|
56
|
+
## All 41 guides by category
|
|
57
|
+
|
|
58
|
+
- **typing** (7): `use-builtin-generics`, `union-syntax`, `type-parameter-syntax`, `override-decorator`, `typeis-vs-typeguard`, `paramspec-decorators`, `deferred-annotations`
|
|
59
|
+
- **async** (3): `taskgroup-over-gather`, `exception-groups`, `async-timeout-context`
|
|
60
|
+
- **stdlib** (5): `datetime-utc`, `pathlib-over-os-path`, `tomllib-builtin`, `removeprefix-removesuffix`, `template-strings`
|
|
61
|
+
- **data-structures** (3): `dict-merge-operator`, `match-case-patterns`, `dataclass-modern`
|
|
62
|
+
- **pydantic** (4): `pydantic-v2-validators`, `pydantic-v2-config`, `pydantic-v2-model-api`, `pydantic-v2-serialization`
|
|
63
|
+
- **fastapi** (3): `fastapi-lifespan`, `fastapi-annotated-depends`, `fastapi-typed-state`
|
|
64
|
+
- **httpx** (2): `httpx-async-client-reuse`, `httpx-streaming`
|
|
65
|
+
- **django** (3): `django-json-field`, `django-async-views`, `django-check-constraints`
|
|
66
|
+
- **sqlalchemy** (3): `sqlalchemy-2-style`, `sqlalchemy-mapped-column`, `sqlalchemy-async-session`
|
|
67
|
+
- **pytest** (3): `pytest-parametrize`, `pytest-tmp-path`, `pytest-raises-match`
|
|
68
|
+
- **toolchain** (5): `pyproject-toml-over-setup`, `uv-over-pip`, `ruff-over-flake8`, `no-pickle`, `safe-subprocess`
|
|
69
|
+
|
|
70
|
+
For full code examples, use `mpg retrieve <guide-id>` or MCP tool `retrieve_guides`.
|
{modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/cli.py
RENAMED
|
@@ -74,10 +74,12 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
74
74
|
# setup
|
|
75
75
|
p_setup = subparsers.add_parser(
|
|
76
76
|
"setup",
|
|
77
|
-
help="Register MCP server and link Agent Skills",
|
|
77
|
+
help="Register MCP server and link Agent Skills + Rules",
|
|
78
78
|
)
|
|
79
79
|
p_setup.add_argument("--mcp-only", action="store_true", help="MCP registration only")
|
|
80
|
-
p_setup.add_argument(
|
|
80
|
+
p_setup.add_argument(
|
|
81
|
+
"--skills-only", action="store_true", help="Project-local artifacts only (Skills + Rules)"
|
|
82
|
+
)
|
|
81
83
|
p_setup.add_argument(
|
|
82
84
|
"--scope",
|
|
83
85
|
choices=["user", "local"],
|
|
@@ -87,21 +89,25 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
87
89
|
p_setup.add_argument(
|
|
88
90
|
"--project-dir",
|
|
89
91
|
type=Path,
|
|
90
|
-
help="Project directory for Skills
|
|
92
|
+
help="Project directory for Skills/Rules symlinks",
|
|
91
93
|
)
|
|
92
94
|
p_setup.add_argument("--dry-run", action="store_true", help="Show what would be done")
|
|
93
95
|
|
|
94
96
|
# uninstall
|
|
95
97
|
p_uninstall = subparsers.add_parser(
|
|
96
98
|
"uninstall",
|
|
97
|
-
help="Reverse 'setup': deregister MCP server and unlink Agent Skills",
|
|
99
|
+
help="Reverse 'setup': deregister MCP server and unlink Agent Skills + Rules",
|
|
98
100
|
)
|
|
99
101
|
p_uninstall.add_argument("--mcp-only", action="store_true", help="MCP deregistration only")
|
|
100
|
-
p_uninstall.add_argument(
|
|
102
|
+
p_uninstall.add_argument(
|
|
103
|
+
"--skills-only",
|
|
104
|
+
action="store_true",
|
|
105
|
+
help="Project-local artifacts only (Skills + Rules)",
|
|
106
|
+
)
|
|
101
107
|
p_uninstall.add_argument(
|
|
102
108
|
"--project-dir",
|
|
103
109
|
type=Path,
|
|
104
|
-
help="Project directory for Skills
|
|
110
|
+
help="Project directory for Skills/Rules symlinks",
|
|
105
111
|
)
|
|
106
112
|
p_uninstall.add_argument("--dry-run", action="store_true", help="Show what would be done")
|
|
107
113
|
|
|
@@ -11,8 +11,16 @@ import sys
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
SKILLS_LINK_NAME = "modern-python-guidance"
|
|
14
|
+
RULE_FILE_NAME = "modern-python.md"
|
|
14
15
|
MCP_SERVER_NAME = "mpg"
|
|
15
16
|
|
|
17
|
+
RULE_FRONTMATTER = (
|
|
18
|
+
"---\n"
|
|
19
|
+
'paths: ["**/*.py", "*.py", "**/pyproject.toml", "**/requirements*.txt",'
|
|
20
|
+
' "**/setup.py", "**/setup.cfg", "**/.python-version", "**/Pipfile"]\n'
|
|
21
|
+
"---"
|
|
22
|
+
)
|
|
23
|
+
|
|
16
24
|
|
|
17
25
|
def _find_skills_dir() -> Path:
|
|
18
26
|
"""Resolve the bundled skills directory (package install or editable)."""
|
|
@@ -33,6 +41,25 @@ def _find_skills_dir() -> Path:
|
|
|
33
41
|
raise FileNotFoundError(msg)
|
|
34
42
|
|
|
35
43
|
|
|
44
|
+
def _find_rule_source() -> Path:
|
|
45
|
+
"""Resolve the bundled rule file (package install or editable)."""
|
|
46
|
+
try:
|
|
47
|
+
pkg = importlib.resources.files("modern_python_guidance") / "rules"
|
|
48
|
+
rule_path = Path(str(pkg)) / RULE_FILE_NAME
|
|
49
|
+
if rule_path.is_file():
|
|
50
|
+
return rule_path
|
|
51
|
+
except (TypeError, FileNotFoundError):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
src_root = Path(__file__).resolve().parent.parent.parent
|
|
55
|
+
dev_path = src_root / "rules" / RULE_FILE_NAME
|
|
56
|
+
if dev_path.is_file():
|
|
57
|
+
return dev_path
|
|
58
|
+
|
|
59
|
+
msg = "Cannot locate bundled rule file"
|
|
60
|
+
raise FileNotFoundError(msg)
|
|
61
|
+
|
|
62
|
+
|
|
36
63
|
def _find_project_root(start: Path | None = None) -> Path:
|
|
37
64
|
"""Walk upward from *start* to find the project root."""
|
|
38
65
|
current = (start or Path.cwd()).resolve()
|
|
@@ -63,6 +90,26 @@ def _skills_link_path(project_dir: Path | None = None) -> Path:
|
|
|
63
90
|
return root / ".claude" / "skills" / SKILLS_LINK_NAME
|
|
64
91
|
|
|
65
92
|
|
|
93
|
+
def _rules_file_path(project_dir: Path | None = None) -> Path:
|
|
94
|
+
"""Resolve the rule file symlink path: ``<root>/.claude/rules/modern-python.md``."""
|
|
95
|
+
root = project_dir or _find_project_root()
|
|
96
|
+
return root / ".claude" / "rules" / RULE_FILE_NAME
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _build_rule_text() -> str:
|
|
100
|
+
"""Generate rule file content from SKILL.md body with rule-specific frontmatter.
|
|
101
|
+
|
|
102
|
+
Used by CI sync tests to verify the bundled ``rules/modern-python.md`` matches
|
|
103
|
+
what would be generated from SKILL.md. Strips SKILL.md frontmatter and prepends
|
|
104
|
+
rule-only frontmatter (no name/description keys).
|
|
105
|
+
"""
|
|
106
|
+
skills_dir = _find_skills_dir()
|
|
107
|
+
skill_md = (skills_dir / "SKILL.md").read_text(encoding="utf-8")
|
|
108
|
+
parts = skill_md.split("---", 2)
|
|
109
|
+
body = parts[2].lstrip("\n")
|
|
110
|
+
return RULE_FRONTMATTER + "\n\n" + body
|
|
111
|
+
|
|
112
|
+
|
|
66
113
|
def setup_mcp(
|
|
67
114
|
*,
|
|
68
115
|
scope: str = "user",
|
|
@@ -80,7 +127,7 @@ def setup_mcp(
|
|
|
80
127
|
print("Error: 'claude' command not found.", file=sys.stderr)
|
|
81
128
|
print("Install Claude Code: https://claude.ai/download", file=sys.stderr)
|
|
82
129
|
print(
|
|
83
|
-
"Run 'mpg setup --skills-only' to set up
|
|
130
|
+
"Run 'mpg setup --skills-only' to set up project-local artifacts without MCP.",
|
|
84
131
|
file=sys.stderr,
|
|
85
132
|
)
|
|
86
133
|
return False
|
|
@@ -156,6 +203,58 @@ def setup_skills(
|
|
|
156
203
|
return True
|
|
157
204
|
|
|
158
205
|
|
|
206
|
+
def setup_rules(
|
|
207
|
+
*,
|
|
208
|
+
project_dir: Path | None = None,
|
|
209
|
+
dry_run: bool = False,
|
|
210
|
+
) -> bool:
|
|
211
|
+
"""Create a rule file symlink. Returns True on success."""
|
|
212
|
+
try:
|
|
213
|
+
source = _find_rule_source()
|
|
214
|
+
except FileNotFoundError as e:
|
|
215
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
if source.is_symlink():
|
|
219
|
+
print("Error: rule source is itself a symlink (unexpected).", file=sys.stderr)
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
root = project_dir or _find_project_root()
|
|
223
|
+
link_path = _rules_file_path(project_dir)
|
|
224
|
+
rules_parent = link_path.parent
|
|
225
|
+
|
|
226
|
+
if dry_run:
|
|
227
|
+
print(f"Would link: {link_path} -> {source}")
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
if link_path.is_symlink():
|
|
231
|
+
current_target = Path(os.readlink(link_path))
|
|
232
|
+
if current_target == source or link_path.resolve() == source.resolve():
|
|
233
|
+
print(f"Rule already linked at {link_path.relative_to(root)}")
|
|
234
|
+
return True
|
|
235
|
+
link_path.unlink()
|
|
236
|
+
elif link_path.exists():
|
|
237
|
+
print(
|
|
238
|
+
f"Error: {link_path.relative_to(root)} exists and is not a symlink.",
|
|
239
|
+
file=sys.stderr,
|
|
240
|
+
)
|
|
241
|
+
print(
|
|
242
|
+
f"Remove it manually: rm {shlex.quote(str(link_path))}",
|
|
243
|
+
file=sys.stderr,
|
|
244
|
+
)
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
rules_parent.mkdir(parents=True, exist_ok=True)
|
|
249
|
+
os.symlink(source, link_path)
|
|
250
|
+
except OSError as e:
|
|
251
|
+
print(f"Error creating symlink: {e}", file=sys.stderr)
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
print(f"Rule linked to {link_path.relative_to(root)}")
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
|
|
159
258
|
def run_setup(
|
|
160
259
|
*,
|
|
161
260
|
scope: str = "user",
|
|
@@ -171,9 +270,11 @@ def run_setup(
|
|
|
171
270
|
|
|
172
271
|
do_mcp = not skills_only
|
|
173
272
|
do_skills = not mcp_only
|
|
273
|
+
do_rules = not mcp_only
|
|
174
274
|
|
|
175
275
|
mcp_ok = True
|
|
176
276
|
skills_ok = True
|
|
277
|
+
rules_ok = True
|
|
177
278
|
|
|
178
279
|
if do_mcp:
|
|
179
280
|
mcp_ok = setup_mcp(scope=scope, dry_run=dry_run)
|
|
@@ -181,7 +282,10 @@ def run_setup(
|
|
|
181
282
|
if do_skills:
|
|
182
283
|
skills_ok = setup_skills(project_dir=project_dir, dry_run=dry_run)
|
|
183
284
|
|
|
184
|
-
if
|
|
285
|
+
if do_rules:
|
|
286
|
+
rules_ok = setup_rules(project_dir=project_dir, dry_run=dry_run)
|
|
287
|
+
|
|
288
|
+
if mcp_ok and skills_ok and rules_ok:
|
|
185
289
|
if not dry_run and do_mcp and do_skills:
|
|
186
290
|
print("Ready. Start Claude Code to use mpg guides.")
|
|
187
291
|
return 0
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from modern_python_guidance.setup_cmd import (
|
|
12
12
|
MCP_SERVER_NAME,
|
|
13
13
|
_find_project_root,
|
|
14
|
+
_rules_file_path,
|
|
14
15
|
_skills_link_path,
|
|
15
16
|
)
|
|
16
17
|
|
|
@@ -48,7 +49,7 @@ def uninstall_mcp(*, dry_run: bool = False) -> bool:
|
|
|
48
49
|
print("Error: 'claude' command not found.", file=sys.stderr)
|
|
49
50
|
print("Install Claude Code: https://claude.ai/download", file=sys.stderr)
|
|
50
51
|
print(
|
|
51
|
-
"Run 'mpg uninstall --skills-only' to remove
|
|
52
|
+
"Run 'mpg uninstall --skills-only' to remove project-local artifacts without MCP.",
|
|
52
53
|
file=sys.stderr,
|
|
53
54
|
)
|
|
54
55
|
return False
|
|
@@ -139,6 +140,47 @@ def uninstall_skills(
|
|
|
139
140
|
return True
|
|
140
141
|
|
|
141
142
|
|
|
143
|
+
def uninstall_rules(
|
|
144
|
+
*,
|
|
145
|
+
project_dir: Path | None = None,
|
|
146
|
+
dry_run: bool = False,
|
|
147
|
+
) -> bool:
|
|
148
|
+
"""Remove the rule file symlink. Returns True on success.
|
|
149
|
+
|
|
150
|
+
Only a symlink is removed. A non-symlink entity at the path is refused.
|
|
151
|
+
Idempotent: if no symlink is present, this is a no-op success.
|
|
152
|
+
"""
|
|
153
|
+
root = project_dir or _find_project_root()
|
|
154
|
+
link_path = _rules_file_path(project_dir)
|
|
155
|
+
|
|
156
|
+
if not link_path.is_symlink():
|
|
157
|
+
if link_path.exists():
|
|
158
|
+
print(
|
|
159
|
+
f"Error: {link_path.relative_to(root)} exists and is not a symlink.",
|
|
160
|
+
file=sys.stderr,
|
|
161
|
+
)
|
|
162
|
+
print(
|
|
163
|
+
f"Remove it manually: rm {shlex.quote(str(link_path))}",
|
|
164
|
+
file=sys.stderr,
|
|
165
|
+
)
|
|
166
|
+
return False
|
|
167
|
+
print(f"Rule not linked at {link_path.relative_to(root)} — nothing to remove.")
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
if dry_run:
|
|
171
|
+
print(f"Would remove: {link_path}")
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
link_path.unlink()
|
|
176
|
+
except OSError as e:
|
|
177
|
+
print(f"Error removing symlink: {e}", file=sys.stderr)
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
print(f"Rule unlinked from {link_path.relative_to(root)}")
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
|
|
142
184
|
def run_uninstall(
|
|
143
185
|
*,
|
|
144
186
|
mcp_only: bool = False,
|
|
@@ -153,9 +195,11 @@ def run_uninstall(
|
|
|
153
195
|
|
|
154
196
|
do_mcp = not skills_only
|
|
155
197
|
do_skills = not mcp_only
|
|
198
|
+
do_rules = not mcp_only
|
|
156
199
|
|
|
157
200
|
mcp_ok = True
|
|
158
201
|
skills_ok = True
|
|
202
|
+
rules_ok = True
|
|
159
203
|
|
|
160
204
|
if do_mcp:
|
|
161
205
|
mcp_ok = uninstall_mcp(dry_run=dry_run)
|
|
@@ -163,7 +207,10 @@ def run_uninstall(
|
|
|
163
207
|
if do_skills:
|
|
164
208
|
skills_ok = uninstall_skills(project_dir=project_dir, dry_run=dry_run)
|
|
165
209
|
|
|
166
|
-
if
|
|
210
|
+
if do_rules:
|
|
211
|
+
rules_ok = uninstall_rules(project_dir=project_dir, dry_run=dry_run)
|
|
212
|
+
|
|
213
|
+
if mcp_ok and skills_ok and rules_ok:
|
|
167
214
|
if not dry_run and do_mcp and do_skills:
|
|
168
215
|
print("Done. mpg has been removed.")
|
|
169
216
|
return 0
|
|
@@ -18,6 +18,7 @@ import pytest
|
|
|
18
18
|
|
|
19
19
|
from modern_python_guidance.frontmatter import parse_frontmatter
|
|
20
20
|
from modern_python_guidance.guide_index import _find_guides_dir
|
|
21
|
+
from modern_python_guidance.setup_cmd import _build_rule_text
|
|
21
22
|
|
|
22
23
|
GUIDES_DIR = _find_guides_dir()
|
|
23
24
|
EXPECTED_GUIDE_COUNT = 41
|
|
@@ -112,6 +113,56 @@ class TestGuideStructure:
|
|
|
112
113
|
assert body.startswith("# "), "body does not start with H1 heading"
|
|
113
114
|
|
|
114
115
|
|
|
116
|
+
class TestRuleFileSync:
|
|
117
|
+
"""CI sync tests: rules/modern-python.md body matches SKILL.md body."""
|
|
118
|
+
|
|
119
|
+
def _skill_body(self) -> str:
|
|
120
|
+
skill_md = (GUIDES_DIR.parent / "SKILL.md").read_text(encoding="utf-8")
|
|
121
|
+
parts = skill_md.split("---", 2)
|
|
122
|
+
return parts[2].lstrip("\n")
|
|
123
|
+
|
|
124
|
+
def _rule_path(self) -> Path:
|
|
125
|
+
return GUIDES_DIR.parent.parent.parent / "rules" / "modern-python.md"
|
|
126
|
+
|
|
127
|
+
def _rule_parts(self) -> tuple[str, str]:
|
|
128
|
+
text = self._rule_path().read_text(encoding="utf-8")
|
|
129
|
+
parts = text.split("---", 2)
|
|
130
|
+
return parts[1].strip(), parts[2].lstrip("\n")
|
|
131
|
+
|
|
132
|
+
def test_body_matches_skill(self):
|
|
133
|
+
"""rules/modern-python.md body == SKILL.md body (content sync)."""
|
|
134
|
+
skill_body = self._skill_body()
|
|
135
|
+
_, rule_body = self._rule_parts()
|
|
136
|
+
assert rule_body == skill_body
|
|
137
|
+
|
|
138
|
+
def test_matches_build_rule_text(self):
|
|
139
|
+
"""rules/modern-python.md == _build_rule_text() output (SoT enforcement)."""
|
|
140
|
+
actual = self._rule_path().read_text(encoding="utf-8")
|
|
141
|
+
expected = _build_rule_text()
|
|
142
|
+
assert actual == expected
|
|
143
|
+
|
|
144
|
+
def test_frontmatter_has_paths(self):
|
|
145
|
+
"""rules/modern-python.md frontmatter contains expected paths patterns."""
|
|
146
|
+
fm, _ = self._rule_parts()
|
|
147
|
+
for pattern in [
|
|
148
|
+
"**/*.py",
|
|
149
|
+
"*.py",
|
|
150
|
+
"**/pyproject.toml",
|
|
151
|
+
"**/requirements*.txt",
|
|
152
|
+
"**/setup.py",
|
|
153
|
+
"**/setup.cfg",
|
|
154
|
+
"**/.python-version",
|
|
155
|
+
"**/Pipfile",
|
|
156
|
+
]:
|
|
157
|
+
assert pattern in fm, f"missing path pattern: {pattern}"
|
|
158
|
+
|
|
159
|
+
def test_frontmatter_no_name_or_description(self):
|
|
160
|
+
"""rules/modern-python.md frontmatter has NO name/description keys."""
|
|
161
|
+
fm, _ = self._rule_parts()
|
|
162
|
+
assert "name:" not in fm
|
|
163
|
+
assert "description:" not in fm
|
|
164
|
+
|
|
165
|
+
|
|
115
166
|
class TestGuideInventory:
|
|
116
167
|
def test_no_duplicate_ids(self):
|
|
117
168
|
seen: dict[str, Path] = {}
|