Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,26 @@ on:
- main
- feature/*

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# https://www.python.org/downloads/
python-version: [
"3.10", # EOL: 2026-10
"3.11", # EOL: 2027-10
"3.12", # EOL: 2028-10
"3.13", # EOL: 2029-10
"3.14", # EOL: 2030-10
]
name: ${{ matrix.python-version }}
steps:
Expand All @@ -32,3 +41,6 @@ jobs:

- name: CI checks
run: make

- name: Dependency vulnerability scan
run: make audit
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ dist/

# Unit test / coverage reports
.pytest_cache/
.coverage
htmlcov/

# pyenv
.python-version
Expand Down
22 changes: 22 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-merge-conflict
- id: check-added-large-files

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.13
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format

- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
6 changes: 3 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.analysis.extraPaths": [
"my_module",
"tests",
".venv/bin/python"
"src",
"tests"
],
"editor.tabSize": 4,
"editor.insertSpaces": true,
Expand Down
35 changes: 19 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

Python boilerplate/template project using `uv` for dependency management. The project name `my_module` is meant to be renamed via `./rename_template.sh` when used as a template.
Python boilerplate/template project using `uv` for dependency management. To use as a template, run `./rename_template.sh <NEW_NAME>` — this renames the module, resets the version to `0.0.1`, rewrites `README.md`, and deletes itself and `CHANGELOG.md`.

## Development Commands

All operations are managed through the Makefile (facade for `uv` commands):

```bash
make # Full setup: clean, create venv, sync deps, run build
make test # Run pytest test suite
make # Full pipeline: clean + venv + sync + build (destructive — removes .venv first)
make test # Run pytest test suite (with coverage)
make build # Run test, mypy, lint, format, then package
make audit # Scan dependencies for known CVEs (pip-audit)
make pre-commit # Run all pre-commit hooks on all files
make mypy # Type checking
make lint # Ruff linting
make lint-fix # Auto-fix lint issues
make format # Format code with ruff
make run-venv # Run module: uv run python -m my_module
make clean # Remove .venv, caches, build artifacts
make venv # Recreate virtual environment
make update # Update dependencies
```

### Running Single Tests
Expand All @@ -28,24 +33,22 @@ uv run py.test tests/test_code.py::TestCode # Single class
uv run py.test tests/test_code.py -k "test_code" # By name pattern
```

### Environment Management
```bash
make clean # Remove .venv, caches, build artifacts
make venv # Recreate virtual environment
make update # Update dependencies
```

## Architecture

- `my_module/` - Main package with `__main__.py` entry point
- `tests/` - pytest test suite (class-based structure)
- CLI entry point: `my_module_cli` (defined in pyproject.toml)
The public API lives in `my_module/__init__.py` (exports via `__all__`). `__main__.py` is the CLI entry point that imports from `__init__.py` — new functionality goes in `__init__.py` (or submodules imported there), not in `__main__.py`.

- `my_module/py.typed` — PEP 561 marker; this package ships type information for downstream consumers
- `tests/` — pytest test suite, class-based structure mirroring module structure
- CLI entry point: `my_module_cli` → `my_module.__main__:main`

## Code Quality

- **Ruff**: Linting + formatting (line-length 88, quote-style preserve)
- Includes security scanning via flake8-bandit (S) rules
- **Mypy**: Strict mode (`disallow_untyped_defs = true`)
- **Python**: 3.10, 3.11, 3.12, 3.13
- Rules: pycodestyle, pyflakes, isort, pyupgrade (UP), flake8-bugbear (B), flake8-comprehensions (C4), flake8-use-pathlib (PTH), Ruff-native (RUF), flake8-bandit (S); `assert` allowed in tests
- **Mypy**: Strict mode (`disallow_untyped_defs`, `warn_return_any`, `no_implicit_reexport`, `strict_equality`)
- **pytest**: Coverage enforced at ≥80% (`pytest-cov`), randomized order (`pytest-randomly`), `__main__.py` excluded from coverage
- **pip-audit**: CVE scanning for all dependencies
- **pre-commit**: Ruff auto-fix + format, YAML/TOML/merge-conflict hygiene
- **Python**: 3.10–3.13

All config is centralized in `pyproject.toml`.
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ endif

export LC_ALL = C
export LANG = C.UTF-8
PY_FILES := my_module tests
PY_FILES := src tests

.PHONY: help all clean clear-cache venv build test mypy lint lint-fix format format-check outdated update run-venv install-run
.PHONY: help all clean clear-cache venv build test mypy lint lint-fix format format-check outdated update run-venv install-run audit pre-commit
.DEFAULT_GOAL := all

help: ## Show this help message
Expand Down Expand Up @@ -75,7 +75,14 @@ update: ## Update all dependencies
run-venv: ## Run module directly in venv
uv run python -m my_module


install-run: ## Install package and run CLI
python -m pip install --upgrade .
uv pip install --upgrade .
@echo --- Note: The next command might fail before you reload your shell
my_module_cli

audit: ## Scan dependencies for known CVEs
uv run pip-audit

pre-commit: ## Run all pre-commit hooks on all files
uv run pre-commit run --all-files
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ This boilerplate gives you a production-ready Python project structure with mode
- [uv](https://github.com/astral-sh/uv) with virtual environments and project builds
- [Ruff](https://github.com/astral-sh/ruff) for linting and code formatting (replaces black, flake8, isort)
- Security scanning via Ruff's [flake8-bandit](https://docs.astral.sh/ruff/rules/#flake8-bandit-s) rules
- Unit testing with [pytest](https://docs.pytest.org/en/latest/)
- Dependency vulnerability scanning with [pip-audit](https://github.com/pypa/pip-audit) (`make audit`)
- Unit testing with [pytest](https://docs.pytest.org/en/latest/) including coverage enforcement (`pytest-cov`) and randomized test order (`pytest-randomly`)
- [mypy](https://pypi.org/project/mypy/) for static type checking
- PEP 561 compliant (`py.typed` marker included)
- [pre-commit](https://pre-commit.com/) hooks for Ruff and file hygiene (`make pre-commit`)
- Publishing to PyPi.org
- [vscode](https://code.visualstudio.com/) editor configuration including plugin recommendations, debugging support, unit test discovery and on-save formatting
- [Github actions](https://github.com/BastiTee/python-boilerplate/actions) continuous integration with multi-python testing
- [Github actions](https://github.com/BastiTee/python-boilerplate/actions) continuous integration with multi-python testing and dependency scanning
- [Dependabot](https://docs.github.com/en/code-security/dependabot) for automated dependency updates
- Executable script so after package installation you can run from the CLI using `my_module_cli`

Expand Down
60 changes: 49 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["uv_build"]
requires = ["uv_build>=0.8,<0.9"]
build-backend = "uv_build"

[project]
Expand All @@ -26,22 +26,27 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13"
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14"
]
# Python version: https://www.python.org/downloads/
requires-python = ">=3.10,<3.14"
requires-python = ">=3.10,<3.15"
dependencies = []

[project.scripts]
my_module_cli = 'my_module.__main__:main'

[tool.uv.build-backend]
module-root = ""
module-root = "src"

[dependency-groups]
dev = [
"mypy>=2.1.0",
"mypy>=1.19.0",
"pip-audit>=2.9.0",
"pre-commit>=4.2.0",
"pytest>=9.0.3",
"pytest-cov>=6.0.0",
"pytest-randomly>=3.16.0",
"ruff>=0.15.12",
"typing-extensions>=4.15.0",
]
Expand All @@ -56,17 +61,31 @@ check_untyped_defs = true
# Import discovery
follow_imports = "normal"
ignore_missing_imports = true
mypy_path = "src"
# Warning configuration
warn_unused_ignores = true
warn_unreachable = true
warn_return_any = true
# Stricter checks
no_implicit_reexport = true
strict_equality = true
# Error message config
# pretty = true

[tool.pytest]
[tool.pytest.ini_options]
addopts = [
"-p",
"no:warnings"
"-p", "no:warnings",
"--import-mode=importlib",
"--cov=my_module",
"--cov-report=term-missing",
"--cov-fail-under=95",
]
testpaths = ["tests"]
pythonpath = ["src"]
xfail_strict = true

[tool.coverage.run]
omit = ["*/__main__.py"]

[tool.ruff]
line-length = 88
Expand All @@ -79,17 +98,36 @@ select = [
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade (modern Python syntax)
"B", # flake8-bugbear (common bugs + opinionated style)
"C4", # flake8-comprehensions (idiomatic comprehensions)
"BLE", # flake8-blind-except
"A", # flake8-builtins
"PTH", # flake8-use-pathlib (prefer pathlib over os.path)
"RUF", # Ruff-native rules
"S", # flake8-bandit (security)
"PERF", # flake8-perflint (performance anti-patterns)
"SIM", # flake8-simplify (simplifiable code patterns)
"TCH", # flake8-type-checking (move imports to TYPE_CHECKING block)
"ERA", # eradicate (flag commented-out code)
"T20", # flake8-print (catch stray print() calls)
"PLC", # pylint conventions
"PLE", # pylint errors
"PLR", # pylint refactoring
"PLW", # pylint warnings
"G", # flake8-logging-format
"TRY", # tryceratops (try/except patterns)
]
ignore = [
"E203", # whitespace before colon (Black-compatible)
"S101", # allow assert in tests
"E203", # whitespace before colon (Black-compatible)
"S101", # allow assert in tests (also set per-file below)
"TRY003", # allow long exception messages
"PLR2004", # allow magic values in comparisons (too noisy)
]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"] # allow assert in test files
"tests/**/*.py" = ["S101"] # allow assert in test files
"**/__main__.py" = ["T201"] # allow print() in CLI entry points

[tool.ruff.format]
quote-style = "preserve"
2 changes: 1 addition & 1 deletion rename_template.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ if [ -z "$1" ]; then show_help; exit 1; fi
make clean

# Rename module
mv -v my_module "$1"
mv -v src/my_module "src/$1"

# Replace module references with new name
find . -type f -exec grep -l my_module {} + |\
Expand Down
1 change: 0 additions & 1 deletion my_module/__init__.py → src/my_module/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""my_module - A Python module template.

This module provides example functions demonstrating proper typing,
Expand Down
1 change: 0 additions & 1 deletion my_module/__main__.py → src/my_module/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Module main-file.

A module's __name__ is set equal to '__main__' when read from standard input,
Expand Down
File renamed without changes.
1 change: 0 additions & 1 deletion tests/test_code.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Test suite for my_module."""

from my_module import greet
Expand Down
Loading