Initial commit
This commit is contained in:
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