Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:38:39 +08:00
commit 363bb25e08
12 changed files with 641 additions and 0 deletions

View 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
View File

@@ -0,0 +1,3 @@
# sanhe-py-best-practices
Sanhe's Python Best Practices

77
plugin.lock.json Normal file
View 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": []
}
}

View 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`

View 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()

View 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)

View File

@@ -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()

View 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)

View 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.

View 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)

View 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()
```

View 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()