Initial commit
This commit is contained in:
11
.claude-plugin/plugin.json
Normal file
11
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "sanhe-py-best-practices",
|
||||||
|
"description": "Sanhe's Python Best Practices",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Sanhe Hu"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# sanhe-py-best-practices
|
||||||
|
|
||||||
|
Sanhe's Python Best Practices
|
||||||
77
plugin.lock.json
Normal file
77
plugin.lock.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:MacHu-GWU/sanhe-claude-code-plugins:plugins/python/sanhe-py-best-practices",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "c45fc44a6d6645852e85269ce56783ad51d07d10",
|
||||||
|
"treeHash": "91c0058cc278c8fcb301a94ce9461e9b5f3354dd800c1cdf64f47e7ed23f606e",
|
||||||
|
"generatedAt": "2025-11-28T10:12:04.021324Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "sanhe-py-best-practices",
|
||||||
|
"description": "Sanhe's Python Best Practices",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "7d1a1ced39b3dd3198cd0c99342dfbc02b64fa974e02adf4da0f736e895c641b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "5d81d454e4ec803c26a646b5e9be6951f34a87b0610ab916454aad8746e1b4db"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/python-test-strategy/SKILL.md",
|
||||||
|
"sha256": "4417256dd35abdd146d28da02169f39cb8046284725794870d8f1f4451b30ad7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/python-test-strategy/scripts/locate_test_file.py",
|
||||||
|
"sha256": "0118e2d17e913818d3801406af8bb7c3e1b1fd955c31fb7a3c36e9f5b764c5e0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/python-test-strategy/reference/public-api.md",
|
||||||
|
"sha256": "e0d8f6ecf04954d9640e8820e17e96b0dc4158c2b40e6bba0b765f801d9d7dfb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/python-test-strategy/reference/coverage.md",
|
||||||
|
"sha256": "121ab23f27a32a56f455648b2cb8e69b9b123fa5d2a5e06e34ad4467ced1884d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/python-test-strategy/reference/naming.md",
|
||||||
|
"sha256": "2d98af51097dd5db8de408de507a10f103f21a22390514314a8fb400d4023950"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/detect-python-version/SKILL.md",
|
||||||
|
"sha256": "6b3b345bc437cb5989d46d589c8db8db6d337486409478d328c61ff3333ec1bf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/detect-python-version/scripts/detect_version.py",
|
||||||
|
"sha256": "375692c059d9bbe8599136b4f6797165acd514cddff776c01cf2147422495b57"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/python-project-code-structure/SKILL.md",
|
||||||
|
"sha256": "c4eb28bace456e06d8351e8615fe28fec96c50adea30fcfa45b8a08b3634426a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/python-project-code-structure/scripts/detect_python_project_metadata.py",
|
||||||
|
"sha256": "c3967f1f0dc8756d05bb93fafa2ae1fe232e22f5dc99e332c28e75f934db5cb8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "91c0058cc278c8fcb301a94ce9461e9b5f3354dd800c1cdf64f47e7ed23f606e"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
21
skills/detect-python-version/SKILL.md
Normal file
21
skills/detect-python-version/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
name: detect-python-version
|
||||||
|
description: Detects the Python major and minor version of the current repository
|
||||||
|
---
|
||||||
|
|
||||||
|
# detect-python-version
|
||||||
|
|
||||||
|
Runs a script that outputs the Python version in `major.minor` format (e.g., `3.11`, `3.13`).
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
Run `scripts/detect_version.py` to detect the Python version.
|
||||||
|
|
||||||
|
The script outputs just the version string in `major.minor` format, nothing else.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Output:** `3.11`
|
||||||
|
|
||||||
|
**Output:** `3.13`
|
||||||
|
|
||||||
67
skills/detect-python-version/scripts/detect_version.py
Executable file
67
skills/detect-python-version/scripts/detect_version.py
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def locate_git_repo(dir_cwd: Path) -> Path | None:
|
||||||
|
"""
|
||||||
|
Locate the git repository root directory by searching for the .git folder.
|
||||||
|
"""
|
||||||
|
for _ in range(10):
|
||||||
|
if dir_cwd.joinpath(".git").exists():
|
||||||
|
return dir_cwd
|
||||||
|
dir_cwd = dir_cwd.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def locate_pyproject_toml(dir_cwd: Path) -> Path | None:
|
||||||
|
"""
|
||||||
|
Locate the pyproject.toml file by searching upwards in the directory tree.
|
||||||
|
"""
|
||||||
|
for _ in range(10):
|
||||||
|
if dir_cwd.joinpath("pyproject.toml").exists():
|
||||||
|
return dir_cwd.joinpath("pyproject.toml")
|
||||||
|
dir_cwd = dir_cwd.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def locate_venv_bin_python(dir_cwd: Path) -> Path | None:
|
||||||
|
"""
|
||||||
|
Locate the virtual environment directory by searching for the .venv folder.
|
||||||
|
"""
|
||||||
|
for _ in range(10):
|
||||||
|
path_venv_bin_python = dir_cwd.joinpath(".venv", "bin", "python")
|
||||||
|
if path_venv_bin_python.exists():
|
||||||
|
return path_venv_bin_python
|
||||||
|
dir_cwd = dir_cwd.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_python_version(path_venv_bin_python: Path) -> tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Get the Python version of the specified python executable.
|
||||||
|
"""
|
||||||
|
args = [
|
||||||
|
f"{path_venv_bin_python}",
|
||||||
|
"--version",
|
||||||
|
]
|
||||||
|
res = subprocess.run(args, capture_output=True)
|
||||||
|
text = res.stdout.decode("utf-8").strip()
|
||||||
|
parts = text.split()[1].split(".")
|
||||||
|
return int(parts[0]), int(parts[1]), int(parts[2])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dir_here = Path.cwd()
|
||||||
|
path_venv_bin_python = locate_venv_bin_python(dir_here)
|
||||||
|
if path_venv_bin_python is not None:
|
||||||
|
major, minor, micro = get_python_version(path_venv_bin_python)
|
||||||
|
py_ver = f"{major}.{minor}"
|
||||||
|
print(py_ver)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
22
skills/python-project-code-structure/SKILL.md
Normal file
22
skills/python-project-code-structure/SKILL.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: python-project-code-structure
|
||||||
|
description: Detects and displays Python project structure and important paths
|
||||||
|
---
|
||||||
|
|
||||||
|
# python-project-code-structure
|
||||||
|
|
||||||
|
**Use this skill when you need to:**
|
||||||
|
|
||||||
|
- Understand the directory structure of a Python project
|
||||||
|
- Find important paths (package location, tests, docs, venv, tools)
|
||||||
|
- Get package name and version from pyproject.toml
|
||||||
|
- Locate project root and configuration files
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
|
||||||
|
Runs `scripts/detect_python_project_metadata.py` to automatically detect and display all critical paths in your Python project, including:
|
||||||
|
|
||||||
|
- Package name and version
|
||||||
|
- Virtual environment paths (Python, pip, pytest)
|
||||||
|
- Tests and documentation directories
|
||||||
|
- Configuration files (Sphinx, Makefile)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import json
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def locate_pyproject_toml(dir_cwd: Path) -> Path | None:
|
||||||
|
"""
|
||||||
|
Locate the pyproject.toml file by searching upwards in the directory tree.
|
||||||
|
"""
|
||||||
|
for _ in range(10):
|
||||||
|
if dir_cwd.joinpath("pyproject.toml").exists():
|
||||||
|
return dir_cwd.joinpath("pyproject.toml")
|
||||||
|
dir_cwd = dir_cwd.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dir_here = Path.cwd()
|
||||||
|
path_pyproject_toml = locate_pyproject_toml(dir_here)
|
||||||
|
data = tomllib.loads(path_pyproject_toml.read_text(encoding="utf-8"))
|
||||||
|
package_name = data["project"]["name"]
|
||||||
|
package_version = data["project"]["version"]
|
||||||
|
dir_project_root = path_pyproject_toml.parent
|
||||||
|
dir_package = dir_project_root / package_name
|
||||||
|
dir_venv = dir_project_root / ".venv"
|
||||||
|
path_venv_bin_python = dir_venv / "bin" / "python"
|
||||||
|
path_venv_bin_pip = dir_venv / "bin" / "pip"
|
||||||
|
path_venv_bin_pytest = dir_venv / "bin" / "pytest"
|
||||||
|
dir_unit_tests = dir_project_root / "tests"
|
||||||
|
dir_docs_source = dir_project_root / "docs" / "source"
|
||||||
|
path_sphinx_conf_py = dir_docs_source / "conf.py"
|
||||||
|
path_makefile = dir_project_root / "Makefile"
|
||||||
|
python_project_metadata = {
|
||||||
|
"package_name": (package_name, "Package name from pyproject.toml"),
|
||||||
|
"package_version": (package_version, "Package version"),
|
||||||
|
"dir_package": (str(dir_package), "Main package directory"),
|
||||||
|
"path_venv_bin_python": (str(path_venv_bin_python), "virtualenv Python interpreter"),
|
||||||
|
"path_venv_bin_pip": (str(path_venv_bin_pip), "virtualenv Pip package manager"),
|
||||||
|
"path_venv_bin_pytest": (str(path_venv_bin_pytest), "virtualenv Pytest test runner"),
|
||||||
|
"dir_unit_tests": (str(dir_unit_tests), "Unit tests directory"),
|
||||||
|
"dir_docs_source": (str(dir_docs_source), "Documentation source"),
|
||||||
|
"path_sphinx_conf_py": (str(path_sphinx_conf_py), "Sphinx config file"),
|
||||||
|
"path_makefile": (str(path_makefile), "Commands for environment, testing, docs, and releases"),
|
||||||
|
}
|
||||||
|
print("Python Project Structure:")
|
||||||
|
for key, (path, description) in python_project_metadata.items():
|
||||||
|
print(f"{key}")
|
||||||
|
print(f" Path: {path}")
|
||||||
|
print(f" Desc: {description}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
32
skills/python-test-strategy/SKILL.md
Normal file
32
skills/python-test-strategy/SKILL.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: python-test-strategy
|
||||||
|
description: Python testing patterns with organized test file structure, coverage goals, and public API testing
|
||||||
|
---
|
||||||
|
|
||||||
|
# python-test-strategy
|
||||||
|
|
||||||
|
Organized testing strategy for Python projects: test file naming, coverage goals (95%+), and public API testing.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
**Find test location for any source file:**
|
||||||
|
```bash
|
||||||
|
python scripts/locate_test_file.py /path/to/source/file.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run tests:**
|
||||||
|
- Individual file: `.venv/bin/python tests/subpackage/test_*.py`
|
||||||
|
- Package: `.venv/bin/python tests/subpackage/all.py`
|
||||||
|
- All: `.venv/bin/python tests/all.py`
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
- **Test files mirror source**: `source/<pkg>/<module>.py` → `tests/<pkg>/test_<pkg>_<module>.py`
|
||||||
|
- **Coverage goal**: 95%+ for all implementation files
|
||||||
|
- **Public API**: Export all public interfaces in `api.py`, test in `tests/test_api.py`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- 🎯 [Naming & File Location](./reference/naming.md)
|
||||||
|
- 📊 [Coverage Setup](./reference/coverage.md)
|
||||||
|
- 🔌 [Public API Testing](./reference/public-api.md)
|
||||||
54
skills/python-test-strategy/reference/coverage.md
Normal file
54
skills/python-test-strategy/reference/coverage.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Code Coverage Configuration
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- **Target**: 95%+ coverage for all implementation files
|
||||||
|
- Prevents untested code paths
|
||||||
|
- Detects breaking changes early
|
||||||
|
|
||||||
|
## Configuration File
|
||||||
|
|
||||||
|
`.coveragerc` at project root controls coverage:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[report]
|
||||||
|
exclude_lines =
|
||||||
|
pragma: no cover
|
||||||
|
if __name__ == .__main__.:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
omit =
|
||||||
|
*/tests/*
|
||||||
|
*/venv/*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Coverage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Individual module with coverage report
|
||||||
|
.venv/bin/python tests/subpackage/test_module.py
|
||||||
|
|
||||||
|
# Entire package
|
||||||
|
.venv/bin/python tests/subpackage/all.py
|
||||||
|
|
||||||
|
# All tests
|
||||||
|
.venv/bin/python tests/all.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: HTML report in `htmlcov/` showing covered/uncovered lines.
|
||||||
|
|
||||||
|
## Marking Code as Non-Testable
|
||||||
|
|
||||||
|
Use `# pragma: no cover` for untestable code:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if sys.platform == "win32": # pragma: no cover
|
||||||
|
return "windows"
|
||||||
|
```
|
||||||
|
|
||||||
|
Use for:
|
||||||
|
- Platform-specific code
|
||||||
|
- Emergency fallbacks
|
||||||
|
- Code that shouldn't occur in normal operation
|
||||||
|
|
||||||
|
Don't use for regular logic or error handling.
|
||||||
45
skills/python-test-strategy/reference/naming.md
Normal file
45
skills/python-test-strategy/reference/naming.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Test File Naming & Organization
|
||||||
|
|
||||||
|
## Convention
|
||||||
|
|
||||||
|
Test files mirror source directory structure with consistent naming:
|
||||||
|
|
||||||
|
```
|
||||||
|
Source: <project>/<subpackage>/<module>.py
|
||||||
|
Test: tests/<subpackage>/test_<subpackage>_<module>.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
| Source | Test |
|
||||||
|
|--------|------|
|
||||||
|
| `learn_haystack/math/calculator.py` | `tests/math/test_math_calculator.py` |
|
||||||
|
| `learn_haystack/math/ops/add.py` | `tests/math/ops/test_math_ops_add.py` |
|
||||||
|
| `learn_haystack/utils.py` | `tests/test_utils.py` |
|
||||||
|
|
||||||
|
## Finding Test Location
|
||||||
|
|
||||||
|
Use the script to determine correct test path:
|
||||||
|
```bash
|
||||||
|
python scripts/locate_test_file.py /absolute/path/to/source.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
Test directories mirror source packages:
|
||||||
|
```
|
||||||
|
learn_haystack/ tests/
|
||||||
|
├── math/ → ├── math/
|
||||||
|
│ └── ops/ → │ └── ops/
|
||||||
|
└── utils.py → └── test_utils.py
|
||||||
|
|
||||||
|
tests/math/all.py # Run all tests in math package
|
||||||
|
tests/math/ops/all.py # Run all tests in ops subpackage
|
||||||
|
tests/all.py # Run all project tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why This Pattern
|
||||||
|
|
||||||
|
- Unique names prevent collisions across packages
|
||||||
|
- Directory mirroring makes tests easy to find
|
||||||
|
- Full path in filename supports tooling (IDE jumps, auto-generation)
|
||||||
57
skills/python-test-strategy/reference/public-api.md
Normal file
57
skills/python-test-strategy/reference/public-api.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Public API Testing
|
||||||
|
|
||||||
|
## API Module
|
||||||
|
|
||||||
|
Create `api.py` that exports all public interfaces (one import per line):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# project/api.py
|
||||||
|
from .math.operations import add_numbers
|
||||||
|
from .math.operations import subtract_numbers
|
||||||
|
from .utils.helpers import format_output
|
||||||
|
from .core.engine import create_engine
|
||||||
|
```
|
||||||
|
|
||||||
|
One import per line makes changes clear in diffs and easier to maintain.
|
||||||
|
|
||||||
|
## Test Public API
|
||||||
|
|
||||||
|
Test file at `tests/test_api.py` verifies all exports are accessible:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from project import api
|
||||||
|
|
||||||
|
def test_api():
|
||||||
|
"""Verify all public APIs are importable."""
|
||||||
|
_ = api.add_numbers
|
||||||
|
_ = api.subtract_numbers
|
||||||
|
_ = api.format_output
|
||||||
|
_ = api.create_engine
|
||||||
|
```
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
- Catch accidental removal of public exports
|
||||||
|
- Prevent breaking changes to API
|
||||||
|
- Document what users can import
|
||||||
|
- Fail tests if API changes unexpectedly
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Export only stable APIs** - Functions and classes that are unlikely to change
|
||||||
|
2. **Keep minimal** - Don't export internal utilities
|
||||||
|
3. **Document** - Add docstring to `api.py` explaining what's exported
|
||||||
|
4. **Group logically** - Organize imports by functional area
|
||||||
|
|
||||||
|
## Deprecation
|
||||||
|
|
||||||
|
Mark deprecated APIs before removal:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
def old_function():
|
||||||
|
"""Deprecated: Use new_function() instead."""
|
||||||
|
warnings.warn("Use new_function instead", DeprecationWarning, stacklevel=2)
|
||||||
|
return new_function()
|
||||||
|
```
|
||||||
196
skills/python-test-strategy/scripts/locate_test_file.py
Executable file
196
skills/python-test-strategy/scripts/locate_test_file.py
Executable file
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Locate the test file path for a given Python source file.
|
||||||
|
|
||||||
|
This utility calculates where a test file should be placed for a given Python
|
||||||
|
source file, following the Python test strategy naming convention:
|
||||||
|
|
||||||
|
Source: git-repo/<package_name>/<subpackage>/<module>.py
|
||||||
|
Test: tests/<subpackage>/test_<subpackage>_<module>.py
|
||||||
|
|
||||||
|
Given an absolute path to a source file, this script:
|
||||||
|
|
||||||
|
1. Finds the project root (by locating pyproject.toml)
|
||||||
|
2. Determines the relative path from project root to source file
|
||||||
|
3. Calculates the correct test file path using naming convention
|
||||||
|
4. Prints the absolute test file path
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
|
||||||
|
- IDE integrations that need to jump from source to test file
|
||||||
|
- Build tools that generate test files in the correct location
|
||||||
|
- Pre-commit hooks that validate tests exist for changed source files
|
||||||
|
- Development workflows that automate test file creation
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
Given source file:
|
||||||
|
/Users/dev/project/learn_claude_code/math/operations/calculator.py
|
||||||
|
|
||||||
|
The script outputs:
|
||||||
|
/Users/dev/project/tests/math/operations/test_math_operations_calculator.py
|
||||||
|
|
||||||
|
Another example:
|
||||||
|
Given source file:
|
||||||
|
/Users/dev/project/learn_claude_code/utils/helpers.py
|
||||||
|
|
||||||
|
The script outputs:
|
||||||
|
/Users/dev/project/tests/utils/test_utils_helpers.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def locate_pyproject_toml(dir_cwd: Path) -> Path | None:
|
||||||
|
"""
|
||||||
|
Locate the pyproject.toml file by searching upwards in the directory tree.
|
||||||
|
|
||||||
|
Searches up to 10 levels up from the starting directory to find the
|
||||||
|
pyproject.toml file, which indicates the project root.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dir_cwd: Starting directory for search
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to pyproject.toml if found, None otherwise
|
||||||
|
"""
|
||||||
|
for _ in range(10):
|
||||||
|
pyproject = dir_cwd / "pyproject.toml"
|
||||||
|
if pyproject.exists():
|
||||||
|
return pyproject
|
||||||
|
dir_cwd = dir_cwd.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_test_file_path(
|
||||||
|
source_file_path: Path,
|
||||||
|
project_root: Path,
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Calculate the test file path for a given source file.
|
||||||
|
|
||||||
|
Applies the naming convention:
|
||||||
|
tests/<subpackage>/test_<subpackage>_<module>.py
|
||||||
|
|
||||||
|
For example:
|
||||||
|
Source: learn_claude_code/math/operations/calculator.py
|
||||||
|
Test: tests/math/operations/test_math_operations_calculator.py
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_file_path: Absolute path to the source file
|
||||||
|
project_root: Absolute path to the project root
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path where the test file should be located
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If source file is not within the project
|
||||||
|
"""
|
||||||
|
# Get relative path from project root
|
||||||
|
try:
|
||||||
|
relative_source = source_file_path.relative_to(project_root)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
f"Source file {source_file_path} is not within project root {project_root}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all path parts: ['package_name', 'subpackage', 'module.py']
|
||||||
|
parts = relative_source.parts
|
||||||
|
skip_first = 1 # Skip the project/package name (first directory)
|
||||||
|
|
||||||
|
# Extract subdirectory and module name
|
||||||
|
# parts[1:-1] are subdirectories, parts[-1] is the module filename
|
||||||
|
subdirs = parts[skip_first:-1]
|
||||||
|
module_filename = parts[-1].replace(".py", "")
|
||||||
|
|
||||||
|
# Build test filename
|
||||||
|
if subdirs:
|
||||||
|
# Has subdirectories: test_<subdir1>_<subdir2>_<module>.py
|
||||||
|
test_filename = f"test_{'_'.join(subdirs)}_{module_filename}.py"
|
||||||
|
test_dir = project_root / "tests" / Path(*subdirs)
|
||||||
|
else:
|
||||||
|
# Root level: test_<module>.py
|
||||||
|
test_filename = f"test_{module_filename}.py"
|
||||||
|
test_dir = project_root / "tests"
|
||||||
|
|
||||||
|
return test_dir / test_filename
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Calculate test file path for a given Python source file",
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
%(prog)s /path/to/project/learn_claude_code/math/calculator.py
|
||||||
|
→ /path/to/project/tests/math/test_math_calculator.py
|
||||||
|
|
||||||
|
%(prog)s /path/to/project/learn_claude_code/math/operations/calculator.py
|
||||||
|
→ /path/to/project/tests/math/operations/test_math_operations_calculator.py
|
||||||
|
|
||||||
|
This script is useful for:
|
||||||
|
• IDE integrations (jump from source to test)
|
||||||
|
• Pre-commit hooks (verify tests exist)
|
||||||
|
• Build tools (generate test files)
|
||||||
|
• Development workflows
|
||||||
|
""",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"source_file",
|
||||||
|
type=str,
|
||||||
|
help="Absolute path to the Python source file",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--verbose",
|
||||||
|
"-v",
|
||||||
|
action="store_true",
|
||||||
|
help="Show detailed information about the calculation",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Convert to Path
|
||||||
|
source_path = Path(args.source_file)
|
||||||
|
|
||||||
|
# Don't check existence of source file for flexibility, this function
|
||||||
|
# is just to calculate the test path.
|
||||||
|
|
||||||
|
# Find project root
|
||||||
|
pyproject = locate_pyproject_toml(source_path.parent)
|
||||||
|
if not pyproject:
|
||||||
|
print(
|
||||||
|
"Error: Could not locate pyproject.toml (project root not found)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
project_root = pyproject.parent
|
||||||
|
|
||||||
|
# Calculate test file path
|
||||||
|
try:
|
||||||
|
test_path = calculate_test_file_path(source_path, project_root)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Output results
|
||||||
|
if args.verbose:
|
||||||
|
print(f"Project root: {project_root}")
|
||||||
|
print(f"Source file: {source_path}")
|
||||||
|
rel_source = source_path.relative_to(project_root)
|
||||||
|
print(f"Relative source: {rel_source}")
|
||||||
|
print(f"Test file: {test_path}")
|
||||||
|
else:
|
||||||
|
# Just print the test path
|
||||||
|
print(test_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user