commit 43a36cc73344fdcc6d1f87243199063411201412 Author: Jev Date: Mon Aug 11 11:36:10 2025 +0200 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..a51ec57 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# Python Library Template + +A [Cookiecutter](https://github.com/cookiecutter/cookiecutter) template for creating modern Python libraries with uv, ruff, mypy, and pytest. + +## Features + +- **Modern Python setup** with Python 3.12+ support +- **uv** for fast dependency management +- **Ruff** for linting and formatting +- **MyPy** for type checking +- **Pytest** with coverage support +- **bump-my-version** for automated version management +- **Invoke** tasks for common operations +- **Pre-configured** development workflow +- **Type hints** support with py.typed marker +- **CLAUDE.md** guidance for Claude Code integration + +## Quick Start + +1. Install cookiecutter: + ```bash + pipx install cookiecutter + ``` + +2. Generate a new project: + ```bash + cookiecutter https://github.com/your-username/python-lib-template + ``` + + Or locally: + ```bash + cookiecutter /path/to/python-lib-template + ``` + +3. The template will prompt for project details and automatically: + - Initialize git repository + - Set up uv environment + - Run initial linting and formatting + - Execute tests to verify setup + +## Template Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `project_name` | Human-readable project name | "My Python Library" | +| `project_slug` | Repository/directory name | "my-python-library" | +| `package_name` | Python package name | "my_python_library" | +| `description` | Short project description | "A modern Python library" | +| `author_name` | Author's full name | "Your Name" | +| `author_email` | Author's email | "your.email@example.com" | +| `version` | Initial version | "0.1.0" | +| `python_version` | Minimum Python version | "3.12" | + +## Generated Project Structure + +``` +your-project/ +├── CLAUDE.md # Claude Code guidance +├── README.md # Project documentation +├── pyproject.toml # Project configuration +├── tasks.py # Invoke tasks +├── src/ +│ └── your_package/ +│ ├── __init__.py +│ ├── core.py +│ └── py.typed +├── tests/ +│ └── test_your_package.py +└── examples/ + └── basic_usage.py +``` + +## Development Workflow + +The generated project includes these common tasks: + +### Setup +```bash +uv sync +``` + +### Code Quality +```bash +uv run ruff check --fix # Linting +uv run ruff format # Formatting +uv run mypy . # Type checking +uv run invoke lint # All quality checks +``` + +### Testing +```bash +uv run pytest # Run tests +uv run invoke test # Run tests with coverage +``` + +### Version Management +```bash +uv run bump-my-version bump patch # 0.1.0 → 0.1.1 +uv run bump-my-version bump minor # 0.1.0 → 0.2.0 +uv run bump-my-version bump major # 0.1.0 → 1.0.0 +``` + +## Template Development + +To modify this template: + +1. Edit files in `{{cookiecutter.project_slug}}/` +2. Update variables in `cookiecutter.json` +3. Modify the post-generation hook in `hooks/post_gen_project.py` +4. Test changes: + ```bash + cookiecutter . --output-dir /tmp + ``` + +## Requirements + +- Python 3.12+ +- uv (automatically installed during generation) +- git (for version control) + +## License + +MIT License - see LICENSE file for details. diff --git a/cookiecutter.json b/cookiecutter.json new file mode 100644 index 0000000..dccf19f --- /dev/null +++ b/cookiecutter.json @@ -0,0 +1,11 @@ +{ + "project_name": "My Python Library", + "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '-').replace('_', '-') }}", + "package_name": "{{ cookiecutter.project_slug.replace('-', '_') }}", + "description": "A modern Python library", + "author_name": "Your Name", + "author_email": "your.email@example.com", + "version": "0.1.0", + "python_version": "3.12", + "year": "{% now 'utc', '%Y' %}" +} \ No newline at end of file diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py new file mode 100755 index 0000000..2fd0fd8 --- /dev/null +++ b/hooks/post_gen_project.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Post-generation hook for the Python library template.""" + +import subprocess +import sys +from pathlib import Path + + +def run_command(cmd: list[str], description: str) -> None: + """Run a command and handle errors.""" + print(f"Running: {description}") + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + if result.stdout: + print(result.stdout) + except subprocess.CalledProcessError as e: + print(f"Error running {description}: {e}") + if e.stderr: + print(f"Error output: {e.stderr}") + sys.exit(1) + + +def main(): + """Initialize the generated project.""" + project_dir = Path.cwd() + print(f"Setting up project in: {project_dir}") + + # Initialize git repository + run_command(["git", "init"], "git init") + + # Sync dependencies with uv + run_command(["uv", "sync"], "uv sync") + + # Run initial formatting and linting + run_command(["uv", "run", "ruff", "format", "src"], "ruff format") + run_command(["uv", "run", "ruff", "check", "--fix", "src"], "ruff check --fix") + + # Run type checking + try: + run_command(["uv", "run", "mypy", "src"], "mypy type checking") + except SystemExit: + # mypy might fail on initial template, continue anyway + print("MyPy check failed - this is normal for initial template") + + # Run tests to ensure everything works + try: + run_command(["uv", "run", "pytest"], "pytest") + except SystemExit: + print("Tests failed - you may need to adjust the generated code") + + print("\n✅ Project setup complete!") + print("Next steps:") + print("1. Review and customize the generated files") + print("2. Add your actual dependencies to pyproject.toml") + print("3. Implement your library functionality in src/") + print("4. Update tests as needed") + print("5. Update README.md with proper documentation") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/CLAUDE.md b/{{cookiecutter.project_slug}}/CLAUDE.md new file mode 100644 index 0000000..8e82838 --- /dev/null +++ b/{{cookiecutter.project_slug}}/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +`{{ cookiecutter.project_slug }}` is a Python library for {{ cookiecutter.description.lower() }}. + +## Development Commands + +### Setup +```bash +uv sync +``` + +### Code Quality +- **Linting and formatting**: `uv run ruff check --fix` and `uv run ruff format` +- **Type checking**: `uv run mypy .` +- **Combined linting**: `uv run invoke lint` (runs ruff check, ruff format --check, and mypy) + +### Testing +- **Run tests**: `uv run pytest` +- **Run tests with coverage**: `uv run invoke test` + +### Maintenance +- **Clean untracked files**: `uv run invoke clean` (interactive) +- **Version bumping**: `uv run bump-my-version bump [patch|minor|major]` + +## Code Architecture + +### Core Components (`src/{{ cookiecutter.package_name }}/core.py`) + +The library implements the main functionality in the core module. Update this section with: + +1. **Key Classes**: Describe main classes and their responsibilities +2. **Core Functions**: Document important functions and their purpose +3. **Data Flow**: Explain how data flows through the system +4. **Dependencies**: List and explain key dependencies + +### Project Structure + +``` +src/{{ cookiecutter.package_name }}/ +├── __init__.py # Package initialization +├── core.py # Main functionality +└── py.typed # Type hints marker + +examples/ +└── basic_usage.py # Usage examples + +tests/ +└── test_{{ cookiecutter.package_name }}.py # Test suite +``` + +## Development Notes + +- Uses Python {{ cookiecutter.python_version }}+ with modern type hints (PEP 604) +- Configured with ruff for linting/formatting and mypy for type checking +- Built with uv for dependency management +- Includes invoke tasks for common operations +- Version {{ cookiecutter.version }} ({{ cookiecutter.year }}) + +## Implementation Guidelines + +- Follow test-driven development practices +- Use descriptive function names and one-liner docstrings for non-trivial functions +- Keep files between 300–500 lines where possible +- Don't duplicate code; build upon existing implementations +- Always use type hints as supported by Python {{ cookiecutter.python_version }}+ \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md new file mode 100644 index 0000000..7d4dfd5 --- /dev/null +++ b/{{cookiecutter.project_slug}}/README.md @@ -0,0 +1,55 @@ +# {{ cookiecutter.project_name }} + +{{ cookiecutter.description }} + +## Installation + +```bash +pip install {{ cookiecutter.project_slug }} +``` + +## Development + +This project uses `uv` for dependency management and the following tools: + +### Setup +```bash +uv sync +``` + +### Code Quality +- **Ruff**: Linting and formatting + ```bash + uv run ruff check --fix + uv run ruff format + ``` + +- **MyPy**: Type checking + ```bash + uv run mypy . + ``` + +### Testing +```bash +uv run pytest +``` + +### Version Management +- **bump-my-version**: Automated version bumping with git tags + ```bash + uv run bump-my-version bump patch # {{ cookiecutter.version }} → 0.1.1 + uv run bump-my-version bump minor # {{ cookiecutter.version }} → 0.2.0 + uv run bump-my-version bump major # {{ cookiecutter.version }} → 1.0.0 + ``` + +## Usage + +```python +from {{ cookiecutter.package_name }} import hello + +print(hello()) +``` + +## License + +MIT License - see LICENSE file for details. \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/examples/basic_usage.py b/{{cookiecutter.project_slug}}/examples/basic_usage.py new file mode 100644 index 0000000..d111476 --- /dev/null +++ b/{{cookiecutter.project_slug}}/examples/basic_usage.py @@ -0,0 +1,22 @@ +"""Basic usage example for {{ cookiecutter.project_name }}.""" + +from {{ cookiecutter.package_name }} import hello +from {{ cookiecutter.package_name }}.core import process_data, {{ cookiecutter.project_name.replace(' ', '').replace('-', '') }} + + +def main(): + """Demonstrate basic usage.""" + # Basic hello + print(hello()) + + # Process some data + result = process_data("example data") + print(result) + + # Use main class + instance = {{ cookiecutter.project_name.replace(' ', '').replace('-', '') }}("example") + print(instance.run()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml new file mode 100644 index 0000000..1d4f7ef --- /dev/null +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -0,0 +1,51 @@ +[project] +name = "{{ cookiecutter.project_slug }}" +version = "{{ cookiecutter.version }}" +description = "{{ cookiecutter.description }}" +readme = "README.md" +authors = [ + { name = "{{ cookiecutter.author_name }}", email = "{{ cookiecutter.author_email }}" } +] +requires-python = ">={{ cookiecutter.python_version }}" +dependencies = [ + # Add your project dependencies here +] + +[build-system] +requires = ["uv_build>=0.8.8,<0.9.0"] +build-backend = "uv_build" + +[dependency-groups] +dev = [ + "bump-my-version>=1.2.1", + "invoke>=2.2.0", + "mypy>=1.17.1", + "pytest>=8.4.1", + "pytest-cov>=6.1.0", + "ruff>=0.12.8", +] + +[tool.bumpversion] +current_version = "{{ cookiecutter.version }}" +commit = true +tag = true +tag_name = "v{new_version}" + +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = "version = \"{current_version}\"" +replace = "version = \"{new_version}\"" + +#------------------ruff configuration---------------- +[tool.ruff.lint] +extend-select = ["B", "I", "C4", "TID", "SIM", "PLE", "RUF"] +ignore = [ + "D100", "D101", "D102", "D103", # Missing docstrings + "N806", "N803", # Invalid name patterns + "G201", # Logging f-string interpolation + "ARG001", # Unused function argument + "BLE001", # Blind except +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["ARG001"] # Allow unused arguments in tests \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__init__.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__init__.py new file mode 100644 index 0000000..edc0ab2 --- /dev/null +++ b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__init__.py @@ -0,0 +1,8 @@ +"""{{ cookiecutter.project_name }} - {{ cookiecutter.description }}""" + +__version__ = "{{ cookiecutter.version }}" + + +def hello() -> str: + """Return a hello message.""" + return "Hello from {{ cookiecutter.project_name }}!" \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/core.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/core.py new file mode 100644 index 0000000..4b2aeea --- /dev/null +++ b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/core.py @@ -0,0 +1,17 @@ +"""Core functionality for {{ cookiecutter.project_name }}.""" + + +def process_data(data: str) -> str: + """Process input data and return result.""" + return f"Processed: {data}" + + +class {{ cookiecutter.project_name.replace(' ', '').replace('-', '') }}: + """Main class for {{ cookiecutter.project_name }}.""" + + def __init__(self, name: str = "default") -> None: + self.name = name + + def run(self) -> str: + """Run the main functionality.""" + return f"Running {{ cookiecutter.project_name }} with {self.name}" \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/py.typed b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_slug}}/tasks.py b/{{cookiecutter.project_slug}}/tasks.py new file mode 100644 index 0000000..999f586 --- /dev/null +++ b/{{cookiecutter.project_slug}}/tasks.py @@ -0,0 +1,39 @@ +# type: ignore +from invoke import task + + +@task +def clean(ctx): + """ + Remove all files and directories that are not under version control to ensure a pristine working environment. + Use caution as this operation cannot be undone and might remove untracked files. + + """ + + ctx.run("git clean -nfdx") + + response = ( + input("Are you sure you want to remove all untracked files? (y/n) [n]: ") + .strip() + .lower() + ) + if response == "y": + ctx.run("git clean -fdx") + + +@task +def lint(ctx): + """ + Perform static analysis on the source code to check for syntax errors and enforce style consistency. + """ + ctx.run("ruff check src", pty=True) + ctx.run("ruff format --check src", pty=True) + ctx.run("mypy src", pty=True) + + +@task +def test(ctx): + """ + Run tests with coverage information. + """ + ctx.run("pytest --cov=src --cov-report=term-missing", pty=True) \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/tests/test_{{cookiecutter.package_name}}.py b/{{cookiecutter.project_slug}}/tests/test_{{cookiecutter.package_name}}.py new file mode 100644 index 0000000..0d302e5 --- /dev/null +++ b/{{cookiecutter.project_slug}}/tests/test_{{cookiecutter.package_name}}.py @@ -0,0 +1,25 @@ +"""Tests for {{ cookiecutter.package_name }}.""" + +import pytest + +from {{ cookiecutter.package_name }} import hello +from {{ cookiecutter.package_name }}.core import process_data, {{ cookiecutter.project_name.replace(' ', '').replace('-', '') }} + + +def test_hello(): + """Test hello function.""" + result = hello() + assert "Hello from {{ cookiecutter.project_name }}" in result + + +def test_process_data(): + """Test process_data function.""" + result = process_data("test") + assert result == "Processed: test" + + +def test_main_class(): + """Test main class.""" + instance = {{ cookiecutter.project_name.replace(' ', '').replace('-', '') }}("test") + result = instance.run() + assert "Running {{ cookiecutter.project_name }} with test" in result \ No newline at end of file