diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b977678..5ec94af 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -4,8 +4,17 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT} ARG USERNAME=vscode ARG UV_VERSION=v0.4.30 +# Install dependencies for lychee (link checker) +USER root +RUN apt-get update && apt-get install -y gcc pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* + USER ${USERNAME} +# Install Rust and lychee link checker +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ + . $HOME/.cargo/env && \ + cargo install lychee + RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ mkdir -p /home/${USERNAME}/.cache && \ chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.cache diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dfbab92..9b70211 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { - // Dev Container definition for python_package - "name": "python_package", + // Dev Container definition for python_template + "name": "python_template", "dockerComposeFile": ["docker-compose.yaml"], "service": "app", "workspaceFolder": "/workspace", diff --git a/.devcontainer/postStartCommand.sh b/.devcontainer/postStartCommand.sh index 3336c29..57a777e 100644 --- a/.devcontainer/postStartCommand.sh +++ b/.devcontainer/postStartCommand.sh @@ -4,6 +4,7 @@ set -euo pipefail echo "Configuring git safe directory (idempotent)..." git config --global --add safe.directory /workspace 2>/dev/null || true -uv sync --frozen +echo "Adding copier as tool" +uv tool install copier echo "PostStart complete" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index d1f65fb..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: ci - -on: - pull_request: - push: - branches: [main] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Install uv based on the version defined in pyproject.toml - uses: astral-sh/setup-uv@v6 - - uses: actions/checkout@v3 - - uses: j178/prek-action@v1 diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..53e78c6 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,115 @@ +name: Build and Deploy Template + +on: + push: + branches: + - template + +jobs: + test-and-deploy: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout template branch + uses: actions/checkout@v4 + with: + ref: template + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Check markdown links + uses: lycheeverse/lychee-action@v2 + with: + args: --verbose --no-progress --exclude 'file://' --exclude 'host\.docker\.internal' '**/*.md' '**/*.md.jinja' + fail: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Avoid rate limiting when checking GitHub URLs + + - name: Run template tests + run: make test + + - name: Build template output + run: make build + + - name: Get template commit SHA + id: template_sha + run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Prepare PR body + id: pr_body + run: | + cat << 'EOF' > /tmp/pr_body.md + ## πŸ€– Automated Template Build + + This PR contains the rendered template from the latest changes on the `template` branch. + + **Source commit:** `${{ steps.template_sha.outputs.sha }}` + **Build timestamp:** ${{ github.event.head_commit.timestamp }} + + ### βœ… Validation + - Link checking passed + - Template tests passed + - Pre-commit hooks validated + EOF + echo "body_file=/tmp/pr_body.md" >> $GITHUB_OUTPUT + + - name: Clone repository for main branch + run: | + cd .. + git clone https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/${{ github.repository }}.git main-repo + cd main-repo + # Try to checkout main, or create orphan if it doesn't exist + git checkout main 2>/dev/null || git checkout --orphan main + + - name: Apply build output to main branch + run: | + cd ../main-repo + # Remove all files except .git + find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} + + # Copy build output + cp -r ../${{ github.event.repository.name }}/build_output/. . + # Remove copier answers file (shouldn't be in generated project) + rm -f .copier-answers.yml + + - name: Check for changes + id: check_changes + run: | + cd ../main-repo + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Ensure main branch exists + if ! git ls-remote --heads origin main | grep -q main; then + git checkout --orphan main + git add -A + git commit -m "Initial commit from template@${{ steps.template_sha.outputs.sha }}" + git push origin main + else + git checkout main 2>/dev/null || git checkout --orphan main + fi + + git add -A + if git diff --staged --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.check_changes.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.PAT_TOKEN }} + path: ../main-repo + branch: template-build + base: main + title: "Build from template@${{ steps.template_sha.outputs.sha }}" + body-path: ${{ steps.pr_body.outputs.body_file }} + commit-message: "Build from template@${{ steps.template_sha.outputs.sha }}" + committer: "github-actions[bot] " + author: "github-actions[bot] " diff --git a/.gitignore b/.gitignore index 68bc17f..3544b09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,160 +1,21 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py +# Copier template testing outputs +temp_out/ +test_output/ +build_output/ -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ +# Python cache from test runs +__pycache__/ +*.pyc -# Celery stuff -celerybeat-schedule -celerybeat.pid +# OS files +.DS_Store +Thumbs.db -# SageMath parsed files -*.sage.py +# Editor files +.idea/ +*.swp +*.swo +*~ -# Environments -.env +# Template environment .venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 434d221..a3c8825 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,50 +48,6 @@ repos: - id: check-added-large-files args: ["--maxkb=500"] # Policy: reject very large blobs - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.2 - hooks: - - id: ruff - name: Ruff Linter (autofix) - args: [--fix, "--config=pyproject.toml"] - - id: ruff-format - name: Ruff Formatter - args: ["--config=pyproject.toml"] - - ############################################################ - # PUSH-TIME (SLOWER / ANALYSIS) HOOKS - # These run less frequently to keep commit loop fast. - ############################################################ - - repo: local - hooks: - - id: pytest-check - name: Run Test Suite - entry: uv run pytest - language: system - pass_filenames: false - always_run: true - stages: [pre-push] - # Optional faster feedback: args: ["-q", "--maxfail=1"] - - id: pylint - name: Pylint (design checks) - entry: uv run pylint - language: system - types: [python] - args: ["--rcfile=pyproject.toml"] - stages: [pre-push] - - id: deptry - name: Deptry (dependency hygiene) - entry: uv run deptry python_package - language: system - pass_filenames: false - stages: [pre-push] - - id: vulture - name: Vulture (dead code) - entry: uv run vulture python_package - language: system - pass_filenames: false - stages: [pre-push] - - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 hooks: @@ -101,10 +57,11 @@ repos: exclude: uv.lock stages: [pre-push] - - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.8.22 + - repo: https://github.com/lycheeverse/lychee + rev: lychee-v0.21.0 hooks: - - id: uv-lock - name: Validate pyproject / lock + - id: lychee + name: Check Links + args: ["--no-progress", "--exclude", "file://"] + files: \.(md|jinja)$ stages: [pre-push] - pass_filenames: false diff --git a/.vscode/launch.json b/.vscode/launch.json index c2e586f..e217d30 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "name": "Python: Module", "type": "debugpy", "request": "launch", - "module": "python_package", + "module": "{{ package_name }}", "justMyCode": false } ] diff --git a/Makefile b/Makefile index a94e5b0..76bbf1b 100644 --- a/Makefile +++ b/Makefile @@ -1,44 +1,31 @@ -# Makefile for uv with smart install + explicit updates - -.DEFAULT_GOAL := install -.PHONY: install update-deps test lint format clean run help check all - -# Help target -help: - @echo "Available targets:" - @echo " install - Install dependencies (frozen)" - @echo " update-deps - Update and sync dependencies" - @echo " test - Run tests with pytest" - @echo " lint - Check code with ruff" - @echo " format - Format code with ruff" - @echo " run - Run the main application" - @echo " clean - Remove cache and temporary files" - -install: uv.lock - uv sync --frozen - -uv.lock: pyproject.toml - uv sync - -update-deps: - uv sync +SHELL := /bin/bash +.PHONY: test build clean test: - uv run pytest tests/ - -lint: - uv run ruff check python_package tests - -format: - uv run ruff format python_package tests - -run: - uv run python python_package/__main__.py + @set -euo pipefail; \ + tmpdir=$$(mktemp -d); \ + echo "πŸ”§ Generating template into: $$tmpdir"; \ + uvx copier copy --vcs-ref=HEAD . "$$tmpdir" --defaults --force --trust --data use_proxy=true --data github_username=martgra; \ + cd "$$tmpdir"; \ + echo "πŸŒ€ Initializing git repo..."; \ + git add -A >/dev/null; \ + uvx prek install >/dev/null; \ + echo "πŸš€ Running pre-commit hooks..."; \ + uvx prek install && uvx prek run --hook-stage manual --files $$(find . -type f -not -path '*/\.git/*'); \ + cd - >/dev/null; \ + rm -rf "$$tmpdir"; \ + echo "βœ… All checks passed and temp folder cleaned up." + +build: + @echo "πŸ”§ Generating template into: build_output/" + @rm -rf build_output + @uvx copier copy --vcs-ref=HEAD . build_output --defaults --force --trust --data skip_git_init=true --data use_proxy=false --data github_username=martgra + @echo "πŸš€ Running pre-commit hooks on build output..." + @cd build_output && uvx prek install && uvx prek run --hook-stage manual --files $$(find . -type f -not -path '*/\.git/*') + @echo "βœ… Template generated and validated successfully!" + @echo "πŸ“ Check the output in: build_output/" clean: - find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - find . -type f -name "*.pyc" -delete - find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true - rm -rf .coverage htmlcov/ dist/ build/ + @echo "🧹 Cleaning build output..." + @rm -rf build_output + @echo "βœ… Cleaned!" diff --git a/README.md b/README.md index 97f4bc7..22845b5 100644 --- a/README.md +++ b/README.md @@ -1,247 +1,63 @@ -# Setting up Python in VSCode +# Python Template -## Rationale ✏️ +![CI](https://github.com/martgra/python_template/actions/workflows/ci.yaml/badge.svg?branch=main) +[![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier) -This sets up a simple python repository with a few handy tools to make Python development (a little) smoother. +A [Copier](https://copier.readthedocs.io/en/stable/) template for modern Python projects using [uv](https://docs.astral.sh/uv/), [Ruff](https://docs.astral.sh/ruff/), [pytest](https://docs.pytest.org/en/stable/), and optional VSCode devcontainer support. -## Getting started πŸš€ +## Features -### Prerequisites 🧱 +- πŸš€ **Modern tooling**: uv for dependency management, Ruff for linting/formatting +- πŸ§ͺ **Testing ready**: pytest with coverage support +- πŸ”’ **Quality checks**: Pylint, Deptry, Vulture, detect-secrets via prek hooks +- 🐳 **Optional devcontainer**: reproducible development environment with Docker +- βš™οΈ **VSCode integration**: pre-configured settings and extensions +- πŸ€– **GitHub Actions**: CI/CD workflow included -To get the most out of this setup you need to install +## Usage -* [VSCode editor](https://code.visualstudio.com/Download) -* [uv: Python packaging](https://docs.astral.sh/uv/getting-started/installation/) -* [Docker (Optional)](https://docs.docker.com/engine/install/) - ->_Docker_ with _docker-compose_ is only necessary if one plans to utilize VSCode _devcontainers_ - -## Project structure 🧭 - -The structure is fairly simple. In this section we focus on the files directly related to setting up/configuring the development environment. - -1. Editor settings are in ```.vscode/settings.json``` referencing ```pyproject.toml``` as the ground truth. -2. Dependencies are defined in ```pyproject.toml``` and locked with ```uv.lock``` which replaces ```requirements.txt``` AND ```setup.py```. -3. Git hooks are configured in ```.pre-commit-config.yaml``` (executed via `prek`) to keep garbage out of the git tree. -4. The content of ```.devcontainer/``` is auto detected by VSCode and can spin up a containerized environment. - -> Supported Python: `>=3.10` (Docker devcontainer currently pins 3.13). Use a matching interpreter locally for parity. - -```bash -β”œβ”€β”€ README.md -β”œβ”€β”€ .vscode -| └── settings.json (1) -β”œβ”€β”€ .devcontainer (4) -β”œβ”€β”€ python_package -β”œβ”€β”€ tests -β”œβ”€β”€ pyproject.toml (2) -β”œβ”€β”€ uv.lock (2) -β”œβ”€β”€ .gitignore -└── .pre-commit-config.yaml (3) -``` - -## Dependencies and building πŸ•ΈοΈ - -Instead of installing packages with ```pip```, keeping track of them with ```requirements.txt``` and building with ```setup.py```, this project uses ```uv``` bundled with ```pyproject.toml``` and ```uv.lock```. UV is both a build and dependency tool which in many ways is comparable with ```npm```. - ->If you haven't already - here is the [guide to install UV](https://docs.astral.sh/uv/getting-started/installation/) - -### Installing the project - -When the project is defined by a ```pyproject.toml``` it can easily be installed and synced with the ```uv``` CLI. This will create a virtual environment, install all dependencies and dev-tools to this environment and add the source folder to the ```PYTHONPATH```. - -```bash -uv sync -``` - -```pyproject.toml``` is compatible with ```pip``` and can still be installed by running: - -```bash -pip install . -``` - -### Locking down dependencies - -The true power of ```uv``` lies in resolving dependency conflicts and locking these down. This way an identical working version of the project can be installed across environments. - -When adding or removing dependencies from the project a ```uv.lock``` file is generated/altered. This file **should** be checked into the repository. - ->The ```pyproject.toml``` file can be edited directly or altered with the ```uv``` CLI. [The CLI documentation can be found here](https://docs.astral.sh/uv/reference/cli) - -### Building - -Since ```pyproject.toml``` replaces ```setup.py```, the project can easily be built by running: - -```bash -uv build -``` - -UV will then build wheels and by default add them to a ```dist``` folder in the project root. - -## Linting πŸ”Ž - -### Autoformatting in VSCode - -### Linting in VSCode - -VSCode supports the Python language with really good intellisense support through the Python extension(s). Lately there have also been created extensions for the most popular linters. These can also be used by installing them in the Python environment (we will in fact do both). - -For an enhanced coding experience: - -* [Ruff](https://docs.astral.sh/ruff/) - linting and formatting implemented in blistering fast Rust (replacing flake8, isort, black and pydocstyle). -* [Pylint](https://pylint.pycqa.org/en/latest/) - deeper design/static analysis (runs pre-push via hook) -* [Autodocstring](https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring) Helps us create docstring templates (and have type hint support) - -The configuration of the linters are set in ```pyproject.toml```. The linters are also managed by ```.vscode/settings.json```. - ->For VSCode these linters are actual extensions with bundled executables and _should_ be faster than invoking linters installed with the Python interpreter. To use the linters installed with the interpreter, set -```importStrategy``` to ```fromEnvironment```. - -### Linting with prek - -_Migration note:_ We replaced the `pre-commit` CLI with the faster drop‑in alternative `prek`; the config file (`.pre-commit-config.yaml`) remains the same. - -Linting (and other code quality measures) are enforced by [prek](https://github.com/j178/prek) β€” a faster drop‑in replacement for [pre-commit](https://pre-commit.com/#intro). It uses the same ```.pre-commit-config.yaml``` format. Hooks run either before `commit` or on `pre-push` depending on their configuration. - ->Think of these git hooks (managed by `prek`) as a lightweight local CI pipeline enforcing agreed coding styles & safety checks. Configuration lives in `.pre-commit-config.yaml` (same schema as pre-commit). - -To install / refresh the git hooks defined in `.pre-commit-config.yaml`: - -```bash -uv tool install prek # no-op if already installed via devcontainer -prek install # sets up .git/hooks/* -``` - -The hooks are now installed and most of them will be run every time you try to ```commit``` to your local branch. A few are ```on push``` only. - -Useful `prek` commands: -```bash -prek run # run all hooks on staged files -prek run --all-files # run all hooks on the whole repo -prek run ruff ruff-format # run a subset -``` - -If you really need to check in some code and a hook (via `prek`) blocks you, you can bypass once (not recommended): - -```bash -git commit -m "commit message" --no-verify -``` - -#### Hooks (prek consuming `.pre-commit-config.yaml`) - -| Name | Explanation | -|----------------|-----------------------------------------------------------------------------| -| ruff | Lints Python code and applies automatic fixes using Ruff, driven by your `pyproject.toml` config. | -| ruff-format | Formats Python code according to Ruff’s formatting rules, using your project config. | -| pylint | Runs Pylint static analysis on your Python files (using `pyproject.toml` for settings). | -| pytest-check | Executes your test suite with pytest to ensure no tests are broken before pushing. | -| uv-lock | Validates the integrity and correctness of your `pyproject.toml` lock file. | -| deptry | Detects unused or missing dependencies in your Python project. | -| vulture | Detects unused (dead) code | -| detect-secrets | Scans your codebase for potential secret tokens or credentials before pushing. | - - -#### Some extra words about Detect-secrets -To prevent accidental commits of sensitive information, we use [detect-secrets](https://github.com/Yelp/detect-secrets) with `prek` (pre-commit compatible hook). These are usually passwords, tokens or other credentials that you don't want public. - -Detect-Secrets will prevent pushes to remote repository if a secret has been checked in. Detect-secrets checks the content of the repository towards -```.secrets.baseline```. - -If a new potential secret is added we can rescan the repositroy to add lines of code we want to allow to push. - -Create the baseline file the first time - audit this carefully. +Generate a new project using Copier: ```bash -detect-secrets scan > .secrets.baseline +uvx copier copy gh:martgra/python_template ``` -If the ```.secrets.baseline``` file needs updating you can do - -```bash -detect-secrets scan --baseline .secrets.baseline > .secrets.baseline -``` - -## Testing πŸ‘· - -The Python extension in VSCode comes with support of several testing frameworks. Here the choice fell on ```pytest``` and ```pytest-cov``` (the coverage plugin for ```pytest```). +## Development -### Configuring pytest - -Pytest is configured via `[tool.pytest]` in `pyproject.toml` (tests path). `.vscode/settings.json` enables auto discovery on save. - -### Running tests - -To run the tests in VSCode you can either: - -#### Run tests from the GUI - -...by going to the ```tests``` pane or opening an actual test file like ```package_test.py``` and going to a specific test. Next to the test an icon should appear that one can click to run the specific test! This requires that the tests are discovered. - ->Tests should have been auto discovered by VSCode - but if they're not showing - try running -> ```Python: Configure Tests``` in the VSCode command palette 🎨. - -#### Run tests from the command line - -Tests can always be run in the terminal by typing: +This repository contains the Copier template itself. To test the template: ```bash -uv run pytest tests +make test ``` -### Debugging tests - -One of the nicest features with VSCode and testing is the ability to easily debug tests. In the world of test driven development (TDD) this is really useful! - -To debug a specific test - right-click the "Run" icon next to the test and select ```debug test``` from the drop-down. Add a breakpoint in the test and step through your code! - -### Coverage +This will: -Test coverage is currently not well supported in VSCode. Although there are a few extensions available (a bit less than 1m downloads). In this setup we choose to do coverage the old-fashioned way! +1. Generate a project from the template in a temp directory +2. Initialize git and pre-commit hooks +3. Run all quality checks +4. Clean up the temp directory -This means generating a coverage report using [coverage](https://coverage.readthedocs.io/en/7.0.1/). +## Template Structure -#### Generate a coverage report - -To generate a coverage report run the following command in the terminal: - -```bash -uv run pytest --cov-report term-missing --cov=python_package tests ``` +template/ # Template files (what gets copied) +β”œβ”€β”€ README.md.jinja # Generated project README +β”œβ”€β”€ pyproject.toml.jinja # Project configuration +β”œβ”€β”€ uv.lock # Dependency lock file +β”œβ”€β”€ {{ package_name }}/ # Python package +β”œβ”€β”€ tests/ # Test files +β”œβ”€β”€ includes/ # Jinja macros and utilities +└── {% if ... %}/ # Conditional directories -To generate an interactive coverage report in html simply run: - -```bash -uv run pytest --cov-report html --cov=python_package tests -``` -Open `htmlcov/index.html` in a browser. - -## Devcontainer πŸ›Έ - -Devcontainers are one of the most powerful features of VSCode. Instead of going around "helping" everyone with environment-related issues we can package everything into a docker-container and spin it up! -Did an intentional or unintentional change happen to your environment? No worries - check in intentional changes and distribute faster than I can say "git commit" or rebuild the entire environment with a click of a button! - -The devcontainer files sit in ```.devcontainer``` folder and consist of - -```bash -β”œβ”€β”€ Dockerfile (1) -β”œβ”€β”€ devcontainer.json (2) -β”œβ”€β”€ docker-compose.yaml (3) -β”œβ”€β”€ postCreateCommand.sh (4) -└── postStartCommand.sh (5) +copier.yaml # Template configuration +Makefile # Template testing ``` -1. The Dockerfile that defines the environment. Build your own or there is [a variety to choose from here](https://hub.docker.com/_/microsoft-vscode-devcontainers) -2. ```devcontainer.json``` - The file that configures the environment! -3. ```docker-compose.yaml``` - I have chosen to extend ```.devcontainer.json``` with a docker-compose file. This allows easily extending the environment with supporting services such as a database or a S3 mock. -4. ```postCreateCommand.sh``` is a script that is run after the environment is built the first time (installs `prek` & hooks, syncs deps). -5. ```postStartCommand.sh``` runs each container start (sync / idempotent setup tasks). -> The devcontainer pre-installs uv + prek and performs a frozen sync on start; local hosts only need `uv sync` + `prek install` once. +## Requirements -[Follow this excellent guide to get going!](https://code.visualstudio.com/docs/devcontainers/containers) +- [Copier](https://copier.readthedocs.io/) 9.0.0+ +- [uv](https://docs.astral.sh/uv/getting-started/installation/) -There is not much more to add other than: +## License -1. Open the repository root! -2. Windows users - you **must** use this in context of WSL2. It's both faster, simpler and less error-prone! ([and strongly recommended!](https://code.visualstudio.com/docs/devcontainers/containers#_open-a-wsl-2-folder-in-a-container-on-windows)) -3. Press ```ctrl/command+shift+p``` and type ```"open folder in container"``` -4. Sit back and relax while the environment is spun up πŸš€πŸš€πŸš€! +MIT License diff --git a/copier.yaml b/copier.yaml new file mode 100644 index 0000000..1b8ff72 --- /dev/null +++ b/copier.yaml @@ -0,0 +1,110 @@ +# Minimum Copier version required +_min_copier_version: "9.0.0" + +# Template files are in the template/ subdirectory +_subdirectory: template + +_skip_if_exists: + - ".env" + - "{{ package_name }}/" + - ".secrets.baseline" + +# Exclude helper files from being copied +_exclude: + - includes + - "*.pyc" + - "__pycache__" + - ".git" + - "uv.lock" + +# Welcome message before copying +_message_before_copy: | + πŸš€ Welcome to the Python Template! + + Let's set up your new Python project. + +# Success message after copying +_message_after_copy: | + βœ… Project "{{ project_name }}" created successfully! + + Next steps: + cd {{ _copier_conf.dst_path }} + uv sync + make test + + Happy coding! πŸŽ‰ + +# Questions for the user +project_name: + type: str + help: What is your project name? + default: "python_template" + placeholder: "my-awesome-project" + +user_name: + type: str + help: What is your name? + placeholder: "Your Name" + default: "User Name" + +user_email: + type: str + help: What is your email address? + placeholder: "you@example.com" + default: "you@example.com" + +# Auto-generated from project_name - never asked to user +package_name: + type: str + when: false # This makes it hidden - user is never prompted + default: "{% from 'includes/slugify.jinja' import slugify %}{{ slugify(project_name) }}" + +use_devcontainer: + type: bool + help: Include VSCode devcontainer configuration? (Docker required) + default: yes + +use_vscode: + type: bool + help: Include VSCode configuration? + default: yes + +use_github_actions: + type: bool + help: Include GitHub Actions CI configuration? + default: yes + +github_username: + type: str + help: GitHub username (leave empty to skip GitHub integration in Makefile) + placeholder: "your-github-username" + default: "" + +use_agents: + type: bool + help: Include boilerplate AGENTS.md for coding agents? + default: yes + +use_proxy: + type: bool + help: Add proxy settings to Devcontainer. + when: "{{ use_devcontainer }}" + default: no + +skip_git_init: + type: bool + when: false # Hidden parameter for testing + default: no + +python_version: + type: str + default: "3.13" + when: false + +_tasks: + - command: "git init" + description: "πŸŒ€ Initializing git repository" + when: "{{ not skip_git_init }}" + - command: "uvx prek install" + description: "πŸ”§ Installing pre-commit hooks" + when: "{{ not skip_git_init }}" diff --git a/includes/slugify.jinja b/includes/slugify.jinja new file mode 100644 index 0000000..25b8c51 --- /dev/null +++ b/includes/slugify.jinja @@ -0,0 +1,9 @@ +{% macro slugify(value) -%} +{{ value + |lower + |replace(' ', '_') + |replace('-', '_') + |replace('.', '') + |replace(',', '') +}} +{%- endmacro %} diff --git a/template/.gitignore b/template/.gitignore new file mode 100644 index 0000000..f243be4 --- /dev/null +++ b/template/.gitignore @@ -0,0 +1,165 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Template test output +temp_out/ +test_output/ +build_output/ diff --git a/template/.pre-commit-config.yaml.jinja b/template/.pre-commit-config.yaml.jinja new file mode 100644 index 0000000..68ae496 --- /dev/null +++ b/template/.pre-commit-config.yaml.jinja @@ -0,0 +1,112 @@ +minimum_pre_commit_version: "3.5.0" +default_stages: [pre-commit, manual] +default_install_hook_types: [pre-commit, pre-push] + + +############################################################ +# Global Exclusions +# These directories rarely contain source worth linting. +############################################################ +exclude: | + (?x)( + ^data/.*| + ^\.vscode/.*| + ^\.devcontainer/.* + ) + +############################################################ +# FAST COMMIT HOOKS (keep quick & mostly autofix) +############################################################ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 # Latest stable as of 2025-09 + hooks: + - id: trailing-whitespace + name: Trim Trailing Whitespace + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + name: Ensure Final Newline + - id: check-json + name: Validate JSON (excluding tool configs) + exclude: | + (?x)^( + \.vscode/.*| + \.devcontainer/devcontainer.json| + )$ + - id: pretty-format-json + name: Format JSON + args: ["--autofix", "--no-ensure-ascii", "--no-sort-keys"] + exclude: | + (?x)^( + \.vscode/.*| + \.devcontainer/devcontainer.json| + .*\.ipynb + )$ + - id: check-toml + - id: check-yaml + - id: check-ast + - id: name-tests-test + - id: detect-private-key # Early lightweight key pattern scan + - id: check-added-large-files + args: ["--maxkb=500"] # Policy: reject very large blobs + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.13.2 + hooks: + - id: ruff + name: Ruff Linter (autofix) + args: [--fix, "--config=pyproject.toml"] + - id: ruff-format + name: Ruff Formatter + args: ["--config=pyproject.toml"] + + ############################################################ + # PUSH-TIME (SLOWER / ANALYSIS) HOOKS + # These run less frequently to keep commit loop fast. + ############################################################ + - repo: local + hooks: + - id: pytest-check + name: Run Test Suite + entry: uv run pytest + language: system + pass_filenames: false + always_run: true + stages: [pre-push, manual] + # Optional faster feedback: args: ["-q", "--maxfail=1"] + - id: pylint + name: Pylint (design checks) + entry: uv run pylint + language: system + types: [python] + args: ["--rcfile=pyproject.toml"] + stages: [pre-push, manual] + - id: deptry + name: Deptry (dependency hygiene) + entry: uv run deptry {{ package_name }} + language: system + pass_filenames: false + stages: [pre-push, manual] + - id: vulture + name: Vulture (dead code) + entry: uv run vulture {{ package_name }} + language: system + pass_filenames: false + stages: [pre-push, manual] + + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + name: Detect Secrets (baseline) + args: ["--baseline", ".secrets.baseline"] + exclude: uv.lock + stages: [pre-push, manual] + + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.8.22 + hooks: + - id: uv-lock + name: Validate pyproject / lock + stages: [pre-push, manual] + pass_filenames: false diff --git a/.prettierrc.json b/template/.prettierrc.json.jinja similarity index 81% rename from .prettierrc.json rename to template/.prettierrc.json.jinja index 1ffcf9a..d33a4b0 100644 --- a/.prettierrc.json +++ b/template/.prettierrc.json.jinja @@ -1,4 +1,4 @@ -{ +{%- set prettier_config = { "printWidth": 120, "tabWidth": 2, "useTabs": false, @@ -15,4 +15,5 @@ "vueIndentScriptAndStyle": false, "endOfLine": "lf", "embeddedLanguageFormatting": "auto" -} +} -%} +{{ prettier_config | to_nice_json(indent=2, sort_keys=False) }} diff --git a/.secrets.baseline b/template/.secrets.baseline similarity index 100% rename from .secrets.baseline rename to template/.secrets.baseline diff --git a/template/LICENSE b/template/LICENSE new file mode 100644 index 0000000..b4d2e42 --- /dev/null +++ b/template/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) {% now 'utc', '%Y' %} {{ project_name }} contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/template/Makefile.jinja b/template/Makefile.jinja new file mode 100644 index 0000000..91d02eb --- /dev/null +++ b/template/Makefile.jinja @@ -0,0 +1,118 @@ +# Makefile for uv with smart install + explicit updates +SHELL := /bin/bash +.DEFAULT_GOAL := install +.PHONY: install update-deps test lint format clean run help check all secrets check-tools{% if github_username %} github-create github-push{% endif %} + +# Help target +help: + @echo "Available targets:" + @echo " install - Install dependencies (frozen)" + @echo " update-deps - Update and sync dependencies" + @echo " test - Run tests with pytest" + @echo " lint - Check code with ruff" + @echo " format - Format code with ruff" + @echo " run - Run the main application" + @echo " clean - Remove cache and temporary files" + @echo " secrets - Scan for secrets using detect-secrets" + @echo " check-tools - Check if required tools are installed" +{% if github_username %} @echo " github-create - Create GitHub repository (requires gh CLI)" + @echo " github-push - Push to GitHub (run after github-create)" +{% endif %} + +install: uv.lock + uv sync --frozen + +uv.lock: pyproject.toml + uv sync + +update-deps: + uv sync + +test: + uv run pytest tests/ + +lint: + uv run ruff check {{ package_name }} tests + +format: + uv run ruff format {{ package_name }} tests + +run: + uv run python {{ package_name }}/__main__.py + +secrets: .secrets.baseline + uv run detect-secrets scan --baseline .secrets.baseline + +.secrets.baseline: + uv run detect-secrets scan > .secrets.baseline + +clean: + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete + find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true + find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true + find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true + rm -rf .coverage htmlcov/ dist/ build/ + +check-tools: + @echo "πŸ” Checking required tools..." + @echo "" + @printf "%-20s" "uv:"; \ + if command -v uv &> /dev/null; then \ + echo "βœ… $(shell uv --version)"; \ + else \ + echo "❌ Not installed - https://docs.astral.sh/uv/getting-started/installation/"; \ + fi + @printf "%-20s" "git:"; \ + if command -v git &> /dev/null; then \ + echo "βœ… $(shell git --version)"; \ + else \ + echo "❌ Not installed"; \ + fi +{% if use_devcontainer %} @printf "%-20s" "docker:"; \ + if command -v docker &> /dev/null; then \ + echo "βœ… $(shell docker --version)"; \ + else \ + echo "❌ Not installed - https://docs.docker.com/get-docker/"; \ + fi +{% endif %}{% if github_username %} @printf "%-20s" "GitHub CLI:"; \ + if command -v gh &> /dev/null; then \ + echo "βœ… $(shell gh --version | head -n1)"; \ + else \ + echo "❌ Not installed - https://cli.github.com/"; \ + fi +{% endif %} @printf "%-20s" "prek:"; \ + if command -v prek &> /dev/null; then \ + echo "βœ… $(shell prek --version)"; \ + else \ + echo "⚠️ Not installed - Run: uvx prek install"; \ + fi + @echo "" + @echo "πŸ’‘ Install missing tools using the links above" +{%- if github_username %} + +.check-gh: + @command -v gh &> /dev/null || (echo "❌ GitHub CLI (gh) not found. Install it from: https://cli.github.com/" && exit 1) + +github-create: .check-gh + @if [ ! -d .git ]; then \ + echo "πŸ“ Initializing git repository..."; \ + git init; \ + git add .; \ + git commit -m "Initial commit from python_template"; \ + fi + @echo "πŸš€ Creating GitHub repository {{ github_username }}/{{ project_name }}..." + @gh repo create {{ github_username }}/{{ project_name }} --public --source=. --remote=origin + @echo "βœ… Repository created at: https://github.com/{{ github_username }}/{{ project_name }}" + @echo "" + @echo "Next: Run 'make github-push' to push your code" + +github-push: .check-gh + @if ! git remote get-url origin &> /dev/null; then \ + echo "❌ No remote 'origin' found. Run 'make github-create' first"; \ + exit 1; \ + fi + @git push -u origin main || git push -u origin master + @echo "βœ… Code pushed to GitHub" + @echo "πŸ”— View at: https://github.com/{{ github_username }}/{{ project_name }}" +{%- endif %} diff --git a/template/README.md.jinja b/template/README.md.jinja new file mode 100644 index 0000000..0b928c5 --- /dev/null +++ b/template/README.md.jinja @@ -0,0 +1,102 @@ +# {{ project_name }} + +{% if github_username %}![CI](https://github.com/{{ github_username }}/{{ project_name }}/actions/workflows/ci.yaml/badge.svg?branch=main) +{% endif %}![Python](https://img.shields.io/badge/python-{{ python_version }}%2B-blue?logo=python&logoColor=white) +[![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier) + +{% if project_description %}{{ project_description }}{% else %}A solid project template for Python.{% endif %} + +## ✨ Features + +- **Modern Python** – Requires Python β‰₯ {{ python_version }}. +- **Dependency management with uv** – Fast dependency installation and lock file management. +- **Quality tools** + - Ruff formats and lints code + - Pylint performs deeper static analysis + - Deptry detects unused, missing and transitive dependencies + - Vulture finds dead code. +- **Secret scanning with detect-secrets** - Prevent secrets getting commited and pushed. +- **Git hooks with Prek** – Automated quality checks on every commit and push. +- **Automated CI/CD** – GitHub Actions run all Prek hooks on pull requests and pushes to ensure code quality. +- **Dev Container** – Devcontainer provides a reproducible environment with Python 3.13, uv and all tools preconfigured. + +## Quick Start + +Get started in seconds: + +```bash +uvx copier copy gh:martgra/python_template --trust +``` + +## Project Layout + +``` +{{ package_name }}/ # Your package +tests/ # Test suite +pyproject.toml # Dependencies & configuration +uv.lock # Locked versions +.pre-commit-config.yaml # Git hook configuration (used by Prek) +.secrets.baseline # detect-secrets baseline +Makefile # Common tasks (test, lint, format, etc.) +{% if use_vscode %}.vscode/ # VSCode settings{% endif %} +{% if use_devcontainer %}.devcontainer/ # Dev container configuration{% endif %} +{% if use_github_actions %}.github/workflows/ # CI/CD workflows{% endif %} +``` + +Python β‰₯ {{ python_version }} is required locally. The dev container uses Python {{ python_version }}. + +## Git Hooks (Prek) + +[Prek](https://github.com/j178/prek) is a fast Rust‑based replacement for pre‑commit that uses the same configuration format. Install hooks with: + +```bash +uvx prek install +``` + +### Fast Commit Hooks (run on every commit) + +- **Ruff** – Lints and formats Python code (auto‑fix enabled) +- **File checks** – Trailing whitespace, end‑of‑file newlines, JSON/YAML/TOML validation +- **Security** – Detect private keys + +### Slower Push Hooks (run on `git push`) + +- **pytest** – Full test suite +- **Pylint** – Deep static analysis for code design issues +- **Deptry** – Checks for unused, missing, and transitive dependencies +- **Vulture** – Finds dead/unused code +- **detect‑secrets** – Scans for secrets against baseline +- **uv‑lock** – Validates `pyproject.toml` and lock file consistency + +This two‑tier approach keeps commits fast while ensuring comprehensive quality checks before pushing. + +## CI Pipeline + +{% if use_github_actions %}GitHub Actions run on pull requests and pushes to the main branch. The workflow uses the same Prek configuration, executing all hooks (both commit and push stages) to ensure code quality. + +See [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml).{% else %}This template can generate GitHub Actions CI/CD. Re-run `uvx copier update` to add it. + +For other CI platforms, run: +```bash +uvx prek run --all-files +```{% endif %} + +## Devcontainer + +{% if use_devcontainer %}For reproducible Docker‑based development, reopen the project in a container (**Dev Containers: Reopen in Container** in VS Code). The container pre‑configures Python 3.13, uv and all tools. + +Docs: [VS Code Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers){% else %}This template can generate a devcontainer. Re-run `uvx copier update` to add it.{% endif %} + +## Template Updates + +Keep your project current with template improvements: + +```bash +uvx copier update +``` + +Docs: [Copier Updates](https://copier.readthedocs.io/en/stable/updating/) + +## License + +Distributed under the **MIT License**. diff --git a/pyproject.toml b/template/pyproject.toml.jinja similarity index 87% rename from pyproject.toml rename to template/pyproject.toml.jinja index 9854ac9..093b491 100644 --- a/pyproject.toml +++ b/template/pyproject.toml.jinja @@ -1,9 +1,9 @@ [project] -name = "python-package" +name = "{{ project_name }}" version = "0.1.0" -description = "" -authors = [{ name = "Martin Gran", email = "martgra@gmail.com" }] -requires-python = ">=3.10" +description = "{{ project_description }}" +authors = [{ name = "{{ user_name }}", email = "{{ user_email }}" }] +requires-python = ">={{ python_version }}" readme = "README.md" dependencies = [] @@ -28,10 +28,10 @@ default-groups = [ ] [tool.hatch.build.targets.sdist] -include = ["python_package"] +include = ["{{ package_name }}"] [tool.hatch.build.targets.wheel] -include = ["python_package"] +include = ["{{ package_name }}"] [build-system] requires = ["hatchling"] @@ -49,7 +49,7 @@ select = ["E", "W", "D", "F", "UP", "B", "SIM","I"] "__init__.py" = ["D104"] [tool.ruff.lint.isort] -known-first-party = ["python_package"] +known-first-party = ["{{ package_name }}"] [tool.ruff.lint.pydocstyle] convention = "google" @@ -59,18 +59,17 @@ max-line-length = 100 disable = ["E0401", "C0114", "R0903", "E0237","E1142","E0014","E1300","E0013","E1310","E1307","E2502","E6005","E6004","E0116","E0108","E0241","E1303","E0102","E0100","E0605","E0604","E0304","E0308","E2510","E2513","E2514","E2512","E2515","E0309","E0305","E0303","E1206","E1205","E0704","E1304","E1302","E4703","E0213","E0107","E0115","E0117","E0103","E0711","E0643","E0402","E1132","E0106","E0101","E0104","E1519","E1520","E0001","E1306","E1305","E0112","E1301","E0603","E0602","E0302","E0118","E1700","E0105","W1401","W0129","W0199","W3201","W1302","W1300","W1501","W0211","W0702","W0711","W1502","W0718","W0719","W0640","W0160","W0102","W0705","W0109","W1308","W0130","W1641","W0123","W0122","W0106","W1309","W0511","W1515","W1305","W1310","W0604","W0603","W0602","W1404","W0406","W1405","W1508","W1113","W1202","W1203","W1201","W0150","W1518","W0410","W1303","W0131","W0177","W3301","W2402","W0133","W0104","W0212","W0707","W0622","W2901","W1406","W0404","W0127","W1509","W1510","W0245","W0706","W0012","W0108","W0107","W0301","W1514","W0613","W1304","W1301","W0611","W0612","W0120","W2101","W2601","W0401","C0202","C0198","C1901","C0201","C0501","C0206","C0199","C0112","C0415","C2701","C0103","C0301","C2201","C0115","C0304","C0116","C0114","C0410","C0321","C2403","C2401","C0205","C0121","C0303","C0131","C0105","C0132","C0412","C0123","C3002","C2801","C3001","C0113","C0208","C0414","C0411","C0413","R0133","R0124","R6003","R1701","R6002","R6104","R1717","R1728","R1715","R1714","R1730","R1731","R1718","R1722","R1706","R1732","R5501","R2044","R1710","R0123","R2004","R0202","R1723","R1724","R1720","R1705","R6301","R0203","R0206","R1704","R1719","R1703","R1725","R1260","R0913","R0916","R0912","R0914","R1702","R0904","R0911","R0915","R1707","R1721","R1733","R1736","R1729","R1735","R1734","R6201","R0205","R0022","R1711"] [tool.deptry] -known_first_party = ["python_package"] +known_first_party = ["{{ package_name }}"] [tool.pytest] testpaths = ["tests"] [tool.coverage.run] branch = true -source = ["python_package"] +source = ["{{ package_name }}"] [tool.vulture] make_whitelist = true min_confidence = 80 -paths = ["python_package"] +paths = ["{{ package_name }}"] sort_by_size = true -verbose = true diff --git a/tests/__init__.py b/template/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to template/tests/__init__.py diff --git a/tests/conftest.py b/template/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to template/tests/conftest.py diff --git a/tests/package_test.py b/template/tests/package_test.py similarity index 100% rename from tests/package_test.py rename to template/tests/package_test.py diff --git a/uv.lock b/template/uv.lock similarity index 100% rename from uv.lock rename to template/uv.lock diff --git a/template/{% if github_username %}scripts{% endif %}/install-gh.sh b/template/{% if github_username %}scripts{% endif %}/install-gh.sh new file mode 100755 index 0000000..54be5ba --- /dev/null +++ b/template/{% if github_username %}scripts{% endif %}/install-gh.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -e + +detect_os() { + case "$(uname -s)" in + Linux*) os="linux";; + Darwin*) os="macos";; + CYGWIN*|MINGW*|MSYS*) os="windows";; + *) os="unknown";; + esac + echo "$os" +} + +install_gh_linux() { + if command -v apt >/dev/null 2>&1; then + type -p curl >/dev/null || sudo apt install curl -y + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | + sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | + sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null + sudo apt update + sudo apt install gh -y + elif command -v dnf >/dev/null 2>&1; then + sudo dnf install 'dnf-command(config-manager)' -y + sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo + sudo dnf install gh -y + elif command -v yum >/dev/null 2>&1; then + sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo + sudo yum install gh -y + else + echo "Unsupported Linux distribution for automatic installation." + exit 1 + fi +} + +install_gh_macos() { + if command -v brew >/dev/null 2>&1; then + brew install gh + else + echo "Homebrew not installed. Installing via pkg..." + curl -fsSL https://github.com/cli/cli/releases/latest/download/gh_$(uname -m)_macOS.pkg -o gh.pkg + sudo installer -pkg gh.pkg -target / + rm gh.pkg + fi +} + +install_gh_windows() { + echo "Windows detected. Use winget or choco:" + echo " winget install GitHub.cli" + echo " choco install gh" +} + +main() { + os=$(detect_os) + echo "Detected OS: $os" + + case "$os" in + linux) install_gh_linux;; + macos) install_gh_macos;; + windows) install_gh_windows;; + *) echo "Unsupported OS"; exit 1;; + esac +} + +main diff --git a/template/{% if use_agents %}AGENTS.md{% endif %}.jinja b/template/{% if use_agents %}AGENTS.md{% endif %}.jinja new file mode 100644 index 0000000..a340814 --- /dev/null +++ b/template/{% if use_agents %}AGENTS.md{% endif %}.jinja @@ -0,0 +1,92 @@ +--- +name: coding-agent +description: Writes python code. +--- + +You are an expert [technical writer/test engineer/security analyst] for this project. + +## Persona + +- You specialize in writing clear decoupled python code with clear separation of concern +- You raise concerns about unclear instructions and ask for clarifications +- Your output: code changes that support functionallity + +## Project knowledge + +- **Tech Stack:** {{ python_version }} +- **File Structure:** + - `pyproject.toml` – project dependencies, development configuration + - `pyproject.toml` – common developer commands + - `{{ package_name }}/` – source code for the application + - `tests/unit` – unit tests + - `tests/integration` – integration tests + - `tests/e2e` – end-to-end tests + - `README.md` – Consise doc summarizing the project + - `docs/*` – Comprahensive docs + - `docs/wip` – Working reports, analysis or reports after finishing tasks + +## Tools you can use + +- **Lint:** `uv run prek run --all-files` Run linting. +- **Add dependencies:** `uv add` Add project dependency +- **Test:** `uv run pytest` (runs pytest, must pass before commits) +- **Fix formatting:** `uv run ruff check --fix` (auto-fixes ESLint errors) + +## Python Instructions + +- Write clear and concise comments for each function. +- Ensure functions have descriptive names and include type hints. +- Provide docstrings following PEP 257 conventions. +- Use the `typing` module for type annotations (e.g., `list[str]`, `dict[str, int]`). +- Break down complex functions into smaller, more manageable functions. + +## General Instructions + +- Always prioritize readability and clarity. +- For algorithm-related code, include explanations of the approach used. +- Write code with good maintainability practices, including comments on why certain design decisions were made. +- Handle edge cases and write clear exception handling. +- For libraries or external dependencies, mention their usage and purpose in comments. +- Use consistent naming conventions and follow language-specific best practices. +- Write concise, efficient, and idiomatic code that is also easily understandable. + +## Code Style and Formatting + +- Follow the **PEP 8** style guide for Python. +- Maintain proper indentation (use 4 spaces for each level of indentation). +- Ensure lines do not exceed 99 characters. +- Place function and class docstrings immediately after the `def` or `class` keyword. +- Use blank lines to separate functions, classes, and code blocks where appropriate. +- Use Google style doc strings + +## Edge Cases and Testing + +- Always include test cases for critical paths of the application. +- Account for common edge cases like empty inputs, invalid data types, and large datasets. +- Include comments for edge cases and the expected behavior in those cases. +- Write unit tests for functions and document them with docstrings explaining the test cases. + +## Example of Proper Documentation + +```python +import math + +def calculate_area(radius: float) -> float: + """ + Calculate the area of a circle given the radius. + + Parameters: + radius (float): The radius of the circle. + + Returns: + float: The area of the circle, calculated as Ο€ * radius^2. + """ + return math.pi * radius ** 2 +``` + +Boundaries + +- βœ… **Always:** Write to `{{ python_package }}/` and `tests/`, run tests before commits, follow naming conventions +- βœ… **Always:** Update docs after code changes +- ⚠️ **Ask first:** Database schema changes, adding dependencies, modifying CI/CD config +- 🚫 **Never:** Commit secrets or API keys diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile.jinja b/template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile.jinja new file mode 100644 index 0000000..7f80d14 --- /dev/null +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile.jinja @@ -0,0 +1,22 @@ +ARG VARIANT={{ python_version }}-bookworm +FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT} + +ARG USERNAME=vscode +ARG UV_VERSION=v0.4.30 + +USER ${USERNAME} + +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + mkdir -p /home/${USERNAME}/.cache && \ + chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.cache + + +# Ensure VS Code extension & history directories exist with correct ownership +RUN mkdir -p /home/$USERNAME/.vscode-server/extensions \ + && chown -R $USERNAME /home/$USERNAME/.vscode-server + +RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/home/$USERNAME/commandhistory/.zsh_history" \ + && mkdir -p /home/$USERNAME/commandhistory \ + && touch /home/$USERNAME/commandhistory/.zsh_history \ + && chown -R $USERNAME /home/$USERNAME/commandhistory \ + && grep -q "commandhistory/.zsh_history" /home/$USERNAME/.zshrc || echo "$SNIPPET" >> /home/$USERNAME/.zshrc diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja b/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja new file mode 100644 index 0000000..05816ab --- /dev/null +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja @@ -0,0 +1,46 @@ +{%- set devcontainer_config = { + "name": package_name, + "dockerComposeFile": ["docker-compose.yaml"], + "service": "app", + "workspaceFolder": "/workspace", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": true + }, + "ghcr.io/devcontainers/features/git:1": {} + }, + "customizations": { + "vscode": { + "settings": { + "python.defaultInterpreterPath": "${containerWorkspaceFolder}/.venv/bin/python", + "terminal.integrated.defaultProfile.linux": "zsh" + }, + "extensions": [ + "ms-python.python", + "charliermarsh.ruff", + "njpwerner.autodocstring", + "eamodio.gitlens", + "mhutchie.git-graph", + "Gruntfuggly.todo-tree", + "esbenp.prettier-vscode" + ] + } + }, + "forwardPorts": [8888], + "remoteUser": "vscode", + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/home/vscode/.local/bin" + }, + "postCreateCommand": "bash .devcontainer/postCreateCommand.sh", + "postStartCommand": "bash .devcontainer/postStartCommand.sh" +} -%} +{%- if use_proxy -%} +{%- set _ = devcontainer_config.update({ + "containerEnv": { + "HTTP_PROXY": "http://host.docker.internal:3128", + "HTTPS_PROXY": "http://host.docker.internal:3128", + "NO_PROXY": "localhost,127.0.0.1,::1,host.docker.internal,.local,.internal" + } +}) -%} +{%- endif -%} +{{ devcontainer_config | to_nice_json(indent=2, sort_keys=False) }} diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja b/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja new file mode 100644 index 0000000..f231176 --- /dev/null +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja @@ -0,0 +1,29 @@ +{%- set docker_compose = { + "version": "3.8", + "services": { + "app": { + "user": "vscode", + "build": { + "context": "..", + "dockerfile": ".devcontainer/Dockerfile", + "args": { + "VARIANT": "{{ python_version }}-bookworm" + } + }, + "volumes": [ + "../:/workspace:cached", + package_name ~ "_extensions:/home/vscode/.vscode-server/extensions", + package_name ~ "_commandhistory:/home/vscode/commandhistory", + package_name ~ "_cache:/home/vscode/.cache", + "/workspace/.venv" + ], + "command": "sleep infinity" + } + }, + "volumes": { + (package_name ~ "_extensions"): None, + (package_name ~ "_commandhistory"): None, + (package_name ~ "_cache"): None + } +} -%} +{{ docker_compose | to_nice_yaml(indent=2) }} \ No newline at end of file diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/postCreateCommand.sh b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postCreateCommand.sh new file mode 100644 index 0000000..a6904d0 --- /dev/null +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postCreateCommand.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -euo pipefail + +sudo chown -R vscode:vscode /workspace/.venv + +# Install/update PreK tool (idempotent) +echo "Ensuring prek installed (pinned via uv tool cache)..." +uv tool install prek >/dev/null 2>&1 || true +prek install + +# Sync dependencies only if environment missing or manifests changed (safety net; main pre-warm is in image layer) +if [ ! -d .venv ] || [ ! -f uv.lock ] || [ pyproject.toml -nt .venv ] || [ uv.lock -nt .venv ]; then + echo "Performing uv sync (post-create)..." + uv sync +else + echo "uv sync skipped (environment up-to-date)." +fi + +echo "PostCreate complete" diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/postStartCommand.sh b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postStartCommand.sh new file mode 100644 index 0000000..3336c29 --- /dev/null +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postStartCommand.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -euo pipefail + +echo "Configuring git safe directory (idempotent)..." +git config --global --add safe.directory /workspace 2>/dev/null || true + +uv sync --frozen + +echo "PostStart complete" diff --git a/template/{% if use_github_actions %}.github{% endif %}/workflows/ci.yaml.jinja b/template/{% if use_github_actions %}.github{% endif %}/workflows/ci.yaml.jinja new file mode 100644 index 0000000..15d2a13 --- /dev/null +++ b/template/{% if use_github_actions %}.github{% endif %}/workflows/ci.yaml.jinja @@ -0,0 +1,32 @@ +{%- set ci_workflow = { + "name": "ci", + "on": { + "pull_request": None, + "push": { + "branches": ["main"] + } + }, + "jobs": { + "lint": { + "runs-on": "ubuntu-latest", + "steps": [ + { + "name": "Checkout code", + "uses": "actions/checkout@v4" + }, + { + "name": "Install uv", + "uses": "astral-sh/setup-uv@v6" + }, + { + "name": "Run prek hooks", + "uses": "j178/prek-action@v1", + "with": { + "extra-args": "--hook-stage manual --all-files" + } + } + ] + } + } +} -%} +{{ ci_workflow | to_nice_yaml(indent=2) }} \ No newline at end of file diff --git a/template/{% if use_vscode %}.vscode{% endif %}/extensions.json.jinja b/template/{% if use_vscode %}.vscode{% endif %}/extensions.json.jinja new file mode 100644 index 0000000..be9e7bd --- /dev/null +++ b/template/{% if use_vscode %}.vscode{% endif %}/extensions.json.jinja @@ -0,0 +1,13 @@ +{%- set extensions = { + "recommendations": [ + "ms-python.python", + "charliermarsh.ruff", + "njpwerner.autodocstring", + "eamodio.gitlens", + "mhutchie.git-graph", + "Gruntfuggly.todo-tree", + "esbenp.prettier-vscode", + "ms-vscode-remote.remote-containers" + ] +} -%} +{{ extensions | to_nice_json(indent=2, sort_keys=False) }} diff --git a/template/{% if use_vscode %}.vscode{% endif %}/launch.json.jinja b/template/{% if use_vscode %}.vscode{% endif %}/launch.json.jinja new file mode 100644 index 0000000..528f300 --- /dev/null +++ b/template/{% if use_vscode %}.vscode{% endif %}/launch.json.jinja @@ -0,0 +1,13 @@ +{%- set launch = { + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Module", + "type": "debugpy", + "request": "launch", + "module": package_name, + "justMyCode": false + } + ] +} -%} +{{ launch | to_nice_json(indent=2, sort_keys=False) }} diff --git a/template/{% if use_vscode %}.vscode{% endif %}/settings.json.jinja b/template/{% if use_vscode %}.vscode{% endif %}/settings.json.jinja new file mode 100644 index 0000000..0e303de --- /dev/null +++ b/template/{% if use_vscode %}.vscode{% endif %}/settings.json.jinja @@ -0,0 +1,36 @@ +{%- set settings = { + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.rulers": [100], + "files.trimTrailingWhitespace": true + }, + "[yaml]": { + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2, + "editor.autoIndent": "keep", + "gitlens.codeLens.scopes": ["document"], + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": true + } + }, + "python.testing.pytestEnabled": true, + "python.testing.autoTestDiscoverOnSaveEnabled": true, + "python.testing.pytestArgs": ["tests"], + "ruff.enable": true, + "python.analysis.typeCheckingMode": "basic", + "python.analysis.autoImportCompletions": true, + "python.testing.unittestEnabled": false, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python" +} -%} +{{ settings | to_nice_json(indent=2, sort_keys=False) }} diff --git a/template/{{ _copier_conf.answers_file }}.jinja b/template/{{ _copier_conf.answers_file }}.jinja new file mode 100644 index 0000000..69141bb --- /dev/null +++ b/template/{{ _copier_conf.answers_file }}.jinja @@ -0,0 +1,2 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +{{ _copier_answers|to_nice_yaml -}} diff --git a/python_package/__init__.py b/template/{{ package_name }}/__init__.py similarity index 100% rename from python_package/__init__.py rename to template/{{ package_name }}/__init__.py diff --git a/python_package/__main__.py b/template/{{ package_name }}/__main__.py similarity index 100% rename from python_package/__main__.py rename to template/{{ package_name }}/__main__.py