From 35f1ce3ea2e7dd0b506f36785e2cc9c514b82dbc Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Wed, 22 Oct 2025 12:53:08 +0000 Subject: [PATCH 01/21] add copier template --- .github/workflows/deploy.yaml | 121 ++++++++ .gitignore | 174 ++---------- .pre-commit-config.yaml | 52 ---- Makefile | 67 ++--- README.md | 261 +++--------------- copier.yaml | 75 +++++ includes/slugify.jinja | 9 + template/.gitignore | 164 +++++++++++ template/.pre-commit-config.yaml.jinja | 110 ++++++++ .prettierrc.json => template/.prettierrc.json | 0 .../.secrets.baseline | 0 template/Makefile.jinja | 51 ++++ template/README.md.jinja | 99 +++++++ .../pyproject.toml.jinja | 14 +- {tests => template/tests}/__init__.py | 0 {tests => template/tests}/conftest.py | 0 {tests => template/tests}/package_test.py | 0 uv.lock => template/uv.lock | 0 .../Dockerfile | 0 .../devcontainer.json.jinja | 4 +- .../docker-compose.yaml.jinja | 12 +- .../postCreateCommand.sh | 0 .../postStartCommand.sh | 0 .../extensions.json | 0 .../launch.json | 2 +- .../settings.json | 0 .../workflows/ci.yaml | 0 .../{{ _copier_conf.answers_file }}.jinja | 2 + .../{{ package_name }}}/__init__.py | 0 .../{{ package_name }}}/__main__.py | 0 30 files changed, 733 insertions(+), 484 deletions(-) create mode 100644 .github/workflows/deploy.yaml create mode 100644 copier.yaml create mode 100644 includes/slugify.jinja create mode 100644 template/.gitignore create mode 100644 template/.pre-commit-config.yaml.jinja rename .prettierrc.json => template/.prettierrc.json (100%) rename .secrets.baseline => template/.secrets.baseline (100%) create mode 100644 template/Makefile.jinja create mode 100644 template/README.md.jinja rename pyproject.toml => template/pyproject.toml.jinja (92%) rename {tests => template/tests}/__init__.py (100%) rename {tests => template/tests}/conftest.py (100%) rename {tests => template/tests}/package_test.py (100%) rename uv.lock => template/uv.lock (100%) rename {.devcontainer => template/{% if use_devcontainer %}.devcontainer{% endif %}}/Dockerfile (100%) rename .devcontainer/devcontainer.json => template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja (93%) rename .devcontainer/docker-compose.yaml => template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja (60%) rename {.devcontainer => template/{% if use_devcontainer %}.devcontainer{% endif %}}/postCreateCommand.sh (100%) rename {.devcontainer => template/{% if use_devcontainer %}.devcontainer{% endif %}}/postStartCommand.sh (100%) rename {.vscode => template/{% if use_devcontainer %}.vscode{% endif %}}/extensions.json (100%) rename {.vscode => template/{% if use_devcontainer %}.vscode{% endif %}}/launch.json (90%) rename {.vscode => template/{% if use_devcontainer %}.vscode{% endif %}}/settings.json (100%) rename {.github => template/{% if use_github_actions %}.github{% endif %}}/workflows/ci.yaml (100%) create mode 100644 template/{{ _copier_conf.answers_file }}.jinja rename {python_package => template/{{ package_name }}}/__init__.py (100%) rename {python_package => template/{{ package_name }}}/__main__.py (100%) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..c46482c --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,121 @@ +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: 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: Clone repository for main branch + run: | + cd .. + git clone https://x-access-token:${{ secrets.GITHUB_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 ../python_template/build_output/. . + # Remove copier answers file (shouldn't be in generated project) + rm -f .copier-answers.yml + + - name: Commit and create PR + run: | + cd ../main-repo + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + + # Check if there are changes + if git diff --staged --quiet; then + echo "No changes to commit" + echo "has_changes=false" >> $GITHUB_ENV + else + # If main doesn't exist on remote, push it first + if ! git ls-remote --heads origin main | grep -q main; then + echo "Creating main branch" + git commit -m "Initial commit from template@${{ steps.template_sha.outputs.sha }}" + git push origin main + fi + + # Create and push PR branch + git checkout -b template-build + git commit -m "Build from template@${{ steps.template_sha.outputs.sha }}" --allow-empty + git push -f origin template-build + echo "has_changes=true" >> $GITHUB_ENV + fi + + - name: Create Pull Request + if: env.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Try to create PR, if it fails because PR exists, update it + if ! gh pr create \ + --base main \ + --head template-build \ + --title "Build from template@${{ steps.template_sha.outputs.sha }}" \ + --body "## πŸ€– 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 + - Template tests passed + - Pre-commit hooks validated" 2>&1 | tee /tmp/pr-output.txt; then + # Check if error is because PR already exists + if grep -q "already exists" /tmp/pr-output.txt; then + echo "PR already exists, updating it" + gh pr edit template-build \ + --title "Build from template@${{ steps.template_sha.outputs.sha }}" \ + --body "## πŸ€– 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 + - Template tests passed + - Pre-commit hooks validated" + else + echo "Failed to create PR" + cat /tmp/pr-output.txt + exit 1 + fi + else + echo "PR created successfully" + fi diff --git a/.gitignore b/.gitignore index 68bc17f..9ef41cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,160 +1,22 @@ -# 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__/ - -# 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/ +# Python cache from test runs +__pycache__/ +*.pyc -# pytype static type analyzer -.pytype/ +# OS files +.DS_Store +Thumbs.db -# Cython debug symbols -cython_debug/ +# Editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ -# 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 environment +.venv \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 434d221..b185544 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: @@ -100,11 +56,3 @@ repos: args: ["--baseline", ".secrets.baseline"] exclude: uv.lock stages: [pre-push] - - - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.8.22 - hooks: - - id: uv-lock - name: Validate pyproject / lock - stages: [pre-push] - pass_filenames: false diff --git a/Makefile b/Makefile index a94e5b0..598e6c7 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 . "$$tmpdir" --defaults --force --trust; \ + cd "$$tmpdir"; \ + echo "πŸŒ€ Initializing git repo..."; \ + git add -A >/dev/null; \ + uvx prek install >/dev/null; \ + echo "πŸš€ Running pre-commit hooks..."; \ + uvx prek run --all-files; \ + 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 . build_output --defaults --force --trust --data skip_git_init=true + @echo "πŸš€ Running pre-commit hooks on build output..." + @cd build_output && uvx prek install && uvx prek run --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..a933d2b 100644 --- a/README.md +++ b/README.md @@ -1,247 +1,68 @@ -# 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/) template for modern Python projects using [uv](https://docs.astral.sh/uv/), [Ruff](https://docs.astral.sh/ruff/), [pytest](https://docs.pytest.org/), 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`: +Generate a new project using Copier: ```bash -uv tool install prek # no-op if already installed via devcontainer -prek install # sets up .git/hooks/* +uvx copier copy gh:martgra/python_template --vcs-ref=template ``` -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. +Or for interactive mode: -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 +uvx copier copy gh:martgra/python_template --vcs-ref=template ``` -If you really need to check in some code and a hook (via `prek`) blocks you, you can bypass once (not recommended): +## Development -```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. - -```bash -detect-secrets scan > .secrets.baseline -``` - -If the ```.secrets.baseline``` file needs updating you can do +This repository contains the Copier template itself. To test the template: ```bash -detect-secrets scan --baseline .secrets.baseline > .secrets.baseline +make test ``` -## 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```). - -### 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 +This will: +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 -Tests can always be run in the terminal by typing: +## Template Structure -```bash -uv run pytest tests ``` - -### 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 - -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! - -This means generating a coverage report using [coverage](https://coverage.readthedocs.io/en/7.0.1/). - -#### 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 -``` - -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) +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 + +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..ea28c66 --- /dev/null +++ b/copier.yaml @@ -0,0 +1,75 @@ +# 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" + +# 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" + +# 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 + +skip_git_init: + type: bool + when: false # Hidden parameter for testing + default: no + +_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 }}" \ No newline at end of file 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..775c194 --- /dev/null +++ b/template/.gitignore @@ -0,0 +1,164 @@ +# 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/ diff --git a/template/.pre-commit-config.yaml.jinja b/template/.pre-commit-config.yaml.jinja new file mode 100644 index 0000000..19accb2 --- /dev/null +++ b/template/.pre-commit-config.yaml.jinja @@ -0,0 +1,110 @@ +minimum_pre_commit_version: "3.5.0" +default_stages: [commit] + +############################################################ +# 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] + # 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 {{ package_name }} + language: system + pass_filenames: false + stages: [pre-push] + - id: vulture + name: Vulture (dead code) + entry: uv run vulture {{ package_name }} + language: system + pass_filenames: false + stages: [pre-push] + + - 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] + + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.8.22 + hooks: + - id: uv-lock + name: Validate pyproject / lock + stages: [pre-push] + pass_filenames: false diff --git a/.prettierrc.json b/template/.prettierrc.json similarity index 100% rename from .prettierrc.json rename to template/.prettierrc.json 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/Makefile.jinja b/template/Makefile.jinja new file mode 100644 index 0000000..f4abed3 --- /dev/null +++ b/template/Makefile.jinja @@ -0,0 +1,51 @@ +# 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 + +# 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" + +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/ diff --git a/template/README.md.jinja b/template/README.md.jinja new file mode 100644 index 0000000..695fb84 --- /dev/null +++ b/template/README.md.jinja @@ -0,0 +1,99 @@ +# Python Template + +![CI](https://github.com/martgra/python_template/actions/workflows/ci.yaml/badge.svg?branch=main) +![Python](https://img.shields.io/badge/python-3.10%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) + +A minimal Python project template designed for development in [Visual Studio Code](https://code.visualstudio.com/). +It uses modern tools like [uv](https://docs.astral.sh/uv/getting-started/installation/) for dependency management, +[Ruff](https://docs.astral.sh/ruff/) and [Pylint](https://pylint.pycqa.org/en/latest/) for linting, +[prek](https://github.com/j178/prek) for managing pre-commit hooks, and +[pytest](https://docs.pytest.org/) for testing. +An optional devcontainer allows you to spin up a reproducible environment using Docker. + +## Prerequisites + +* **VSCode** – install the editor and the Python extension. +* **uv** – used for installing, locking, and managing dependencies. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/). +* **Docker (optional)** – only required if you want to develop inside a VSCode devcontainer. + +## Genereate as a template + +The easiest way to get started is generating the project with [Copier](https://copier.readthedocs.io/en/stable/generating/)! + +```bash +uvx copier copy gh:martgra/python_template --vcs-ref=template --trust +``` + +## Project structure + +```text +β”œβ”€β”€ README.md +β”œβ”€β”€ .vscode/ # VSCode settings +β”œβ”€β”€ .devcontainer/ # Optional devcontainer definitions +β”œβ”€β”€ python_package/ # Your Python package +β”œβ”€β”€ tests/ # Unit tests +β”œβ”€β”€ pyproject.toml # Project metadata and dependencies +β”œβ”€β”€ uv.lock # Lock file generated by uv +β”œβ”€β”€ .pre-commit-config.yaml # Pre-commit/prek hooks +└── .gitignore +``` + +Python β‰₯ 3.10 is expected locally; the devcontainer pins Python 3.13 for parity. + +## Getting started + +1. **Install dependencies** + + ```bash + uv sync + ``` + + Creates a virtual environment, installs dependencies and dev tools, and adds the source folder to your `PYTHONPATH`. + +2. **Initialize git hooks** + + ```bash + uv run prek install + ``` + +## Linting and hooks + +Linting and code quality checks are configured in `pyproject.toml` and enforced via `prek` hooks. +Main tools: + +* **Ruff** – fast linting and formatting +* **Pylint** – static analysis +* **Deptry** – unused/missing dependencies +* **Vulture** – dead code detection +* **detect-secrets** – secret scanning + +See the [prek docs](https://github.com/j178/prek) for details. + +## Testing + +Uses [pytest](https://docs.pytest.org/) with optional [pytest-cov](https://pytest-cov.readthedocs.io/): + +```bash +uv run pytest tests +``` + +To include coverage: + +```bash +uv run pytest --cov=python_package tests +``` + +## Devcontainer (optional) + +If you prefer a containerized setup: + +1. Install Docker and the [Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers) extension. +2. In VSCode, open **Dev Containers: Open Folder in Container**. +3. The container preinstalls `uv` and `prek` and runs a frozen sync. + +See [VSCode’s devcontainer docs](https://code.visualstudio.com/docs/devcontainers/containers) for more details. + +## License and acknowledgements + +Distributed under the **MIT License**. diff --git a/pyproject.toml b/template/pyproject.toml.jinja similarity index 92% rename from pyproject.toml rename to template/pyproject.toml.jinja index 9854ac9..434e103 100644 --- a/pyproject.toml +++ b/template/pyproject.toml.jinja @@ -1,5 +1,5 @@ [project] -name = "python-package" +name = "{{ project_name }}" version = "0.1.0" description = "" authors = [{ name = "Martin Gran", email = "martgra@gmail.com" }] @@ -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,18 @@ 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/.devcontainer/Dockerfile b/template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile similarity index 100% rename from .devcontainer/Dockerfile rename to template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile diff --git a/.devcontainer/devcontainer.json b/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja similarity index 93% rename from .devcontainer/devcontainer.json rename to template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja index dfbab92..1f62517 100644 --- a/.devcontainer/devcontainer.json +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja @@ -1,6 +1,6 @@ { - // Dev Container definition for python_package - "name": "python_package", + // Dev Container definition for {{ package_name }} + "name": "{{ package_name }}", "dockerComposeFile": ["docker-compose.yaml"], "service": "app", "workspaceFolder": "/workspace", diff --git a/.devcontainer/docker-compose.yaml b/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja similarity index 60% rename from .devcontainer/docker-compose.yaml rename to template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja index 47b8396..a9df6e2 100644 --- a/.devcontainer/docker-compose.yaml +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja @@ -10,9 +10,9 @@ services: VARIANT: 3.13-bookworm volumes: - ..:/workspace:cached # Shared workspace between host and devcontainer - - python_template_extensions:/home/vscode/.vscode-server/extensions # Storing extensions - - python_template_commandhistory:/home/vscode/commandhistory # Persistant command line history - - python_template_cache:/home/vscode/.cache # Caching poetry/pip wheels + - {{ package_name }}_extensions:/home/vscode/.vscode-server/extensions # Storing extensions + - {{ package_name }}_commandhistory:/home/vscode/commandhistory # Persistant command line history + - {{ package_name }}_cache:/home/vscode/.cache # Caching poetry/pip wheels - /workspace/.venv # Overrides default command so things don't shut down after the process ends. @@ -20,6 +20,6 @@ services: # Volumes that are not shared between Host and Devcontainer must be listed here. volumes: - python_template_extensions: - python_template_commandhistory: - python_template_cache: + {{ package_name }}_extensions: + {{ package_name }}_commandhistory: + {{ package_name }}_cache: diff --git a/.devcontainer/postCreateCommand.sh b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postCreateCommand.sh similarity index 100% rename from .devcontainer/postCreateCommand.sh rename to template/{% if use_devcontainer %}.devcontainer{% endif %}/postCreateCommand.sh diff --git a/.devcontainer/postStartCommand.sh b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postStartCommand.sh similarity index 100% rename from .devcontainer/postStartCommand.sh rename to template/{% if use_devcontainer %}.devcontainer{% endif %}/postStartCommand.sh diff --git a/.vscode/extensions.json b/template/{% if use_devcontainer %}.vscode{% endif %}/extensions.json similarity index 100% rename from .vscode/extensions.json rename to template/{% if use_devcontainer %}.vscode{% endif %}/extensions.json diff --git a/.vscode/launch.json b/template/{% if use_devcontainer %}.vscode{% endif %}/launch.json similarity index 90% rename from .vscode/launch.json rename to template/{% if use_devcontainer %}.vscode{% endif %}/launch.json index c2e586f..e217d30 100644 --- a/.vscode/launch.json +++ b/template/{% if use_devcontainer %}.vscode{% endif %}/launch.json @@ -8,7 +8,7 @@ "name": "Python: Module", "type": "debugpy", "request": "launch", - "module": "python_package", + "module": "{{ package_name }}", "justMyCode": false } ] diff --git a/.vscode/settings.json b/template/{% if use_devcontainer %}.vscode{% endif %}/settings.json similarity index 100% rename from .vscode/settings.json rename to template/{% if use_devcontainer %}.vscode{% endif %}/settings.json diff --git a/.github/workflows/ci.yaml b/template/{% if use_github_actions %}.github{% endif %}/workflows/ci.yaml similarity index 100% rename from .github/workflows/ci.yaml rename to template/{% if use_github_actions %}.github{% endif %}/workflows/ci.yaml diff --git a/template/{{ _copier_conf.answers_file }}.jinja b/template/{{ _copier_conf.answers_file }}.jinja new file mode 100644 index 0000000..88acac8 --- /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 -}} \ No newline at end of file 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 From 51a8f326a93251186fbb639551f5f0341501ced7 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Thu, 23 Oct 2025 15:38:06 +0200 Subject: [PATCH 02/21] ADD lisence file --- template/LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 template/LICENSE 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. From 062c53abc60bae86f608a94ca048456b3702bdf3 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Thu, 23 Oct 2025 15:39:29 +0200 Subject: [PATCH 03/21] update gitignore --- template/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/template/.gitignore b/template/.gitignore index 775c194..f243be4 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -162,3 +162,4 @@ cython_debug/ # Template test output temp_out/ test_output/ +build_output/ From a6fb7fb68f225c448bbf59aeb5e07e764b277796 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Thu, 23 Oct 2025 15:42:00 +0200 Subject: [PATCH 04/21] add devcontainer to template --- .devcontainer/Dockerfile | 22 ++++++++++++++++ .devcontainer/devcontainer.json | 42 ++++++++++++++++++++++++++++++ .devcontainer/docker-compose.yaml | 25 ++++++++++++++++++ .devcontainer/postCreateCommand.sh | 20 ++++++++++++++ .devcontainer/postStartCommand.sh | 9 +++++++ .vscode/extensions.json | 12 +++++++++ .vscode/launch.json | 15 +++++++++++ .vscode/settings.json | 38 +++++++++++++++++++++++++++ 8 files changed, 183 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yaml create mode 100644 .devcontainer/postCreateCommand.sh create mode 100644 .devcontainer/postStartCommand.sh create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..b977678 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +ARG VARIANT=3.13-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/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9b70211 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,42 @@ +{ + // Dev Container definition for python_template + "name": "python_template", + "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", + "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" + } +} diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml new file mode 100644 index 0000000..47b8396 --- /dev/null +++ b/.devcontainer/docker-compose.yaml @@ -0,0 +1,25 @@ +version: "3.8" +services: + app: + user: vscode + build: + # Use repository root as build context so we can pre-warm dependencies (pyproject/uv.lock) + context: .. + dockerfile: .devcontainer/Dockerfile + args: + VARIANT: 3.13-bookworm + volumes: + - ..:/workspace:cached # Shared workspace between host and devcontainer + - python_template_extensions:/home/vscode/.vscode-server/extensions # Storing extensions + - python_template_commandhistory:/home/vscode/commandhistory # Persistant command line history + - python_template_cache:/home/vscode/.cache # Caching poetry/pip wheels + - /workspace/.venv + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + +# Volumes that are not shared between Host and Devcontainer must be listed here. +volumes: + python_template_extensions: + python_template_commandhistory: + python_template_cache: diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh new file mode 100644 index 0000000..d25b97e --- /dev/null +++ b/.devcontainer/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 ] || [ 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/.devcontainer/postStartCommand.sh b/.devcontainer/postStartCommand.sh new file mode 100644 index 0000000..3336c29 --- /dev/null +++ b/.devcontainer/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/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..5bbcaf2 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "ms-python.python", + "charliermarsh.ruff", + "njpwerner.autodocstring", + "eamodio.gitlens", + "mhutchie.git-graph", + "Gruntfuggly.todo-tree", + "esbenp.prettier-vscode", + "ms-vscode-remote.remote-containers" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e217d30 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Module", + "type": "debugpy", + "request": "launch", + "module": "{{ package_name }}", + "justMyCode": false + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2ea1e60 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,38 @@ +{ + // Editor settings for Python + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.rulers": [100], + "files.trimTrailingWhitespace": true + }, + + // Editor settings for YAML + "[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" +} From 9c4e8988943c4eac76a3635a5ebcce845bf3e556 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Thu, 23 Oct 2025 13:44:37 +0000 Subject: [PATCH 05/21] add vscode and devcontainer settings --- .devcontainer/postStartCommand.sh | 2 -- .gitignore | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.devcontainer/postStartCommand.sh b/.devcontainer/postStartCommand.sh index 3336c29..24031c4 100644 --- a/.devcontainer/postStartCommand.sh +++ b/.devcontainer/postStartCommand.sh @@ -4,6 +4,4 @@ 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/.gitignore b/.gitignore index 9ef41cb..3544b09 100644 --- a/.gitignore +++ b/.gitignore @@ -12,11 +12,10 @@ __pycache__/ Thumbs.db # Editor files -.vscode/ .idea/ *.swp *.swo *~ # Template environment -.venv \ No newline at end of file +.venv From 53b7a57d0028236b2ae0b95188ad0f846939ed5e Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Sat, 25 Oct 2025 12:29:01 +0000 Subject: [PATCH 06/21] bump python version --- .github/workflows/deploy.yaml | 6 +++--- copier.yaml | 2 +- template/pyproject.toml.jinja | 3 +-- template/{{ _copier_conf.answers_file }}.jinja | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index c46482c..062b13f 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -49,14 +49,14 @@ jobs: cp -r ../python_template/build_output/. . # Remove copier answers file (shouldn't be in generated project) rm -f .copier-answers.yml - + - name: Commit and create PR run: | cd ../main-repo git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add -A - + # Check if there are changes if git diff --staged --quiet; then echo "No changes to commit" @@ -68,7 +68,7 @@ jobs: git commit -m "Initial commit from template@${{ steps.template_sha.outputs.sha }}" git push origin main fi - + # Create and push PR branch git checkout -b template-build git commit -m "Build from template@${{ steps.template_sha.outputs.sha }}" --allow-empty diff --git a/copier.yaml b/copier.yaml index ea28c66..e527fd3 100644 --- a/copier.yaml +++ b/copier.yaml @@ -72,4 +72,4 @@ _tasks: when: "{{ not skip_git_init }}" - command: "uvx prek install" description: "πŸ”§ Installing pre-commit hooks" - when: "{{ not skip_git_init }}" \ No newline at end of file + when: "{{ not skip_git_init }}" diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 434e103..82b0e3e 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -3,7 +3,7 @@ name = "{{ project_name }}" version = "0.1.0" description = "" authors = [{ name = "Martin Gran", email = "martgra@gmail.com" }] -requires-python = ">=3.10" +requires-python = ">=3.11" readme = "README.md" dependencies = [] @@ -73,4 +73,3 @@ make_whitelist = true min_confidence = 80 paths = ["{{ package_name }}"] sort_by_size = true -verbose = true diff --git a/template/{{ _copier_conf.answers_file }}.jinja b/template/{{ _copier_conf.answers_file }}.jinja index 88acac8..69141bb 100644 --- a/template/{{ _copier_conf.answers_file }}.jinja +++ b/template/{{ _copier_conf.answers_file }}.jinja @@ -1,2 +1,2 @@ # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY -{{ _copier_answers|to_nice_yaml -}} \ No newline at end of file +{{ _copier_answers|to_nice_yaml -}} From 4e349ffb739cb5656224861fc90a7d85f6a92e31 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Sat, 25 Oct 2025 12:52:55 +0000 Subject: [PATCH 07/21] update readme --- README.md | 9 ++------- template/README.md.jinja | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a933d2b..fa8f516 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,7 @@ A [Copier](https://copier.readthedocs.io/) template for modern Python projects u Generate a new project using Copier: ```bash -uvx copier copy gh:martgra/python_template --vcs-ref=template -``` - -Or for interactive mode: - -```bash -uvx copier copy gh:martgra/python_template --vcs-ref=template +uvx copier copy gh:martgra/python_template ``` ## Development @@ -37,6 +31,7 @@ make test ``` This will: + 1. Generate a project from the template in a temp directory 2. Initialize git and pre-commit hooks 3. Run all quality checks diff --git a/template/README.md.jinja b/template/README.md.jinja index 695fb84..cfa2bf7 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -22,7 +22,7 @@ An optional devcontainer allows you to spin up a reproducible environment using The easiest way to get started is generating the project with [Copier](https://copier.readthedocs.io/en/stable/generating/)! ```bash -uvx copier copy gh:martgra/python_template --vcs-ref=template --trust +uvx copier copy gh:martgra/python_template --trust ``` ## Project structure From 2935f9c39436c6e728e328db550dff65a6f65312 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Sat, 25 Oct 2025 13:04:21 +0000 Subject: [PATCH 08/21] fix build error --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 598e6c7..50959c2 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ test: build: @echo "πŸ”§ Generating template into: build_output/" @rm -rf build_output - @uvx copier copy . build_output --defaults --force --trust --data skip_git_init=true + @uvx copier copy --vcs-ref=HEAD . build_output --defaults --force --trust --data skip_git_init=true @echo "πŸš€ Running pre-commit hooks on build output..." @cd build_output && uvx prek install && uvx prek run --files $$(find . -type f -not -path '*/\.git/*') @echo "βœ… Template generated and validated successfully!" From ecff774e9fd166c3b5b154c3b3d1a95cdcdcc17b Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Sat, 25 Oct 2025 13:07:05 +0000 Subject: [PATCH 09/21] fix build error --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 50959c2..926ca3d 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ test: @set -euo pipefail; \ tmpdir=$$(mktemp -d); \ echo "πŸ”§ Generating template into: $$tmpdir"; \ - uvx copier copy . "$$tmpdir" --defaults --force --trust; \ + uvx copier copy --vcs-ref=HEAD . "$$tmpdir" --defaults --force --trust; \ cd "$$tmpdir"; \ echo "πŸŒ€ Initializing git repo..."; \ git add -A >/dev/null; \ From 32a0828d25b206c2f61f5404935c9e2ec5a0946e Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Sat, 25 Oct 2025 13:24:52 +0000 Subject: [PATCH 10/21] add lyche for markdown link check --- .devcontainer/Dockerfile | 5 ++ .pre-commit-config.yaml | 9 +++ template/README.md.jinja | 137 +++++++++++++++++++++------------------ 3 files changed, 89 insertions(+), 62 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b977678..a5fa564 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -6,6 +6,11 @@ ARG UV_VERSION=v0.4.30 USER ${USERNAME} +# Install Rust and Cargo (needed for lychee link checker) +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ + . $HOME/.cargo/env && \ + cargo install lychee-link-checker@0.21.0 + 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/.pre-commit-config.yaml b/.pre-commit-config.yaml index b185544..a3c8825 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,3 +56,12 @@ repos: args: ["--baseline", ".secrets.baseline"] exclude: uv.lock stages: [pre-push] + + - repo: https://github.com/lycheeverse/lychee + rev: lychee-v0.21.0 + hooks: + - id: lychee + name: Check Links + args: ["--no-progress", "--exclude", "file://"] + files: \.(md|jinja)$ + stages: [pre-push] diff --git a/template/README.md.jinja b/template/README.md.jinja index cfa2bf7..474f136 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -1,99 +1,112 @@ -# Python Template +# {{ project_name }} ![CI](https://github.com/martgra/python_template/actions/workflows/ci.yaml/badge.svg?branch=main) ![Python](https://img.shields.io/badge/python-3.10%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) -A minimal Python project template designed for development in [Visual Studio Code](https://code.visualstudio.com/). -It uses modern tools like [uv](https://docs.astral.sh/uv/getting-started/installation/) for dependency management, -[Ruff](https://docs.astral.sh/ruff/) and [Pylint](https://pylint.pycqa.org/en/latest/) for linting, -[prek](https://github.com/j178/prek) for managing pre-commit hooks, and -[pytest](https://docs.pytest.org/) for testing. -An optional devcontainer allows you to spin up a reproducible environment using Docker. +Modern Python project template for VS Code. -## Prerequisites +**Tools:** [uv](https://docs.astral.sh/uv/) Β· [Ruff](https://docs.astral.sh/ruff/) Β· [Pylint](https://pylint.readthedocs.io/en/stable/) Β· [prek](https://github.com/j178/prek) Β· [pytest](https://docs.pytest.org/en/stable/) -* **VSCode** – install the editor and the Python extension. -* **uv** – used for installing, locking, and managing dependencies. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/). -* **Docker (optional)** – only required if you want to develop inside a VSCode devcontainer. +## Quick Start -## Genereate as a template +**Prerequisites:** [uv](https://docs.astral.sh/uv/getting-started/installation/), [VS Code](https://code.visualstudio.com/), Docker (optional) -The easiest way to get started is generating the project with [Copier](https://copier.readthedocs.io/en/stable/generating/)! +```bash +# 1. Generate project (if not already done) +uvx copier copy gh:martgra/python_template --trust + +# 2. Install dependencies +cd +uv sync + +# 3. Verify setup +make test +``` + +You're ready to code. +## Daily Workflows + +**Development:** ```bash -uvx copier copy gh:martgra/python_template --trust +make run # Run your application +make test # Run test suite +make format # Auto-format code ``` -## Project structure +**Before commit:** +```bash +make lint # Check code quality +# Hooks run automatically on commit +``` -```text -β”œβ”€β”€ README.md -β”œβ”€β”€ .vscode/ # VSCode settings -β”œβ”€β”€ .devcontainer/ # Optional devcontainer definitions -β”œβ”€β”€ python_package/ # Your Python package -β”œβ”€β”€ tests/ # Unit tests -β”œβ”€β”€ pyproject.toml # Project metadata and dependencies -β”œβ”€β”€ uv.lock # Lock file generated by uv -β”œβ”€β”€ .pre-commit-config.yaml # Pre-commit/prek hooks -└── .gitignore +**Maintenance:** +```bash +make update-deps # Update dependencies +make help # See all commands ``` -Python β‰₯ 3.10 is expected locally; the devcontainer pins Python 3.13 for parity. +**Direct tool access:** +```bash +uv run pytest tests/ +uv run ruff check {{ package_name }} +uv run python -m {{ package_name }} +``` -## Getting started +## Project Layout -1. **Install dependencies** +```text +{{ package_name }}/ # Your package +tests/ # Test suite +pyproject.toml # Dependencies & config +uv.lock # Locked versions +.pre-commit-config.yaml # Code quality hooks +.vscode/ # Editor config +.devcontainer/ # Docker setup (optional) +``` - ```bash - uv sync - ``` +Python β‰₯ 3.10 required locally. Devcontainer uses Python 3.13. - Creates a virtual environment, installs dependencies and dev tools, and adds the source folder to your `PYTHONPATH`. +## Tool Stack -2. **Initialize git hooks** +Configuration in `pyproject.toml` and `.pre-commit-config.yaml`. - ```bash - uv run prek install - ``` +- **[uv](https://docs.astral.sh/uv/)** – dependency management +- **[Ruff](https://docs.astral.sh/ruff/)** – fast linting & formatting +- **[Pylint](https://pylint.readthedocs.io/en/stable/)** – deep static analysis +- **[prek](https://github.com/j178/prek)** – pre-commit hooks +- **[pytest](https://docs.pytest.org/en/stable/)** – testing framework +- **[Deptry](https://github.com/fpgmaas/deptry)** – dependency validation +- **[Vulture](https://github.com/jendrikseipp/vulture)** – dead code detection +- **[detect-secrets](https://github.com/Yelp/detect-secrets)** – secret scanning -## Linting and hooks +{% if use_github_actions %} +## CI Pipeline -Linting and code quality checks are configured in `pyproject.toml` and enforced via `prek` hooks. -Main tools: +GitHub Actions runs on PRs and main branch pushes. Workflow: install deps β†’ run all prek hooks (lint, format, test, security). -* **Ruff** – fast linting and formatting -* **Pylint** – static analysis -* **Deptry** – unused/missing dependencies -* **Vulture** – dead code detection -* **detect-secrets** – secret scanning +See [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml). +{% endif %} -See the [prek docs](https://github.com/j178/prek) for details. +{% if use_devcontainer %} +## Devcontainer -## Testing +For reproducible Docker-based development, reopen the project in a container (**Dev Containers: Reopen in Container** in VS Code). Pre-configures Python 3.13, uv, and all tools. -Uses [pytest](https://docs.pytest.org/) with optional [pytest-cov](https://pytest-cov.readthedocs.io/): +Docs: [VS Code Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers) +{% endif %} -```bash -uv run pytest tests -``` +## Template Updates -To include coverage: +Keep your project current with template improvements: ```bash -uv run pytest --cov=python_package tests +uvx copier update ``` -## Devcontainer (optional) - -If you prefer a containerized setup: - -1. Install Docker and the [Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers) extension. -2. In VSCode, open **Dev Containers: Open Folder in Container**. -3. The container preinstalls `uv` and `prek` and runs a frozen sync. - -See [VSCode’s devcontainer docs](https://code.visualstudio.com/docs/devcontainers/containers) for more details. +Docs: [Copier Updates](https://copier.readthedocs.io/en/stable/updating/) -## License and acknowledgements +## License Distributed under the **MIT License**. From 26538e4bb400cab860f722e99dd9a3ea18f669ce Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Sat, 25 Oct 2025 13:30:36 +0000 Subject: [PATCH 11/21] add lyche to deploy --- .github/workflows/deploy.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 062b13f..36747a2 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -22,6 +22,14 @@ jobs: - 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://' 'template/**/*.md' 'template/**/*.jinja' 'README.md' + fail: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Avoid rate limiting when checking GitHub URLs + - name: Run template tests run: make test From 2fb52aa8b46204d035b1169eb5607c11dadbcf44 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Sat, 25 Oct 2025 13:34:38 +0000 Subject: [PATCH 12/21] update deploy --- .github/workflows/deploy.yaml | 108 +++++++++++++++------------------- 1 file changed, 47 insertions(+), 61 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 36747a2..7886615 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -25,7 +25,7 @@ jobs: - name: Check markdown links uses: lycheeverse/lychee-action@v2 with: - args: --verbose --no-progress --exclude 'file://' 'template/**/*.md' 'template/**/*.jinja' 'README.md' + 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 @@ -40,6 +40,24 @@ jobs: 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 .. @@ -54,76 +72,44 @@ jobs: # Remove all files except .git find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} + # Copy build output - cp -r ../python_template/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: Commit and create PR + - 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" - git add -A - # Check if there are changes - if git diff --staged --quiet; then - echo "No changes to commit" - echo "has_changes=false" >> $GITHUB_ENV + # 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 - # If main doesn't exist on remote, push it first - if ! git ls-remote --heads origin main | grep -q main; then - echo "Creating main branch" - git commit -m "Initial commit from template@${{ steps.template_sha.outputs.sha }}" - git push origin main - fi - - # Create and push PR branch - git checkout -b template-build - git commit -m "Build from template@${{ steps.template_sha.outputs.sha }}" --allow-empty - git push -f origin template-build - echo "has_changes=true" >> $GITHUB_ENV + git checkout main 2>/dev/null || git checkout --orphan main fi - - name: Create Pull Request - if: env.has_changes == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Try to create PR, if it fails because PR exists, update it - if ! gh pr create \ - --base main \ - --head template-build \ - --title "Build from template@${{ steps.template_sha.outputs.sha }}" \ - --body "## πŸ€– 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 - - Template tests passed - - Pre-commit hooks validated" 2>&1 | tee /tmp/pr-output.txt; then - # Check if error is because PR already exists - if grep -q "already exists" /tmp/pr-output.txt; then - echo "PR already exists, updating it" - gh pr edit template-build \ - --title "Build from template@${{ steps.template_sha.outputs.sha }}" \ - --body "## πŸ€– 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 - - Template tests passed - - Pre-commit hooks validated" - else - echo "Failed to create PR" - cat /tmp/pr-output.txt - exit 1 - fi + git add -A + if git diff --staged --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT else - echo "PR created successfully" + 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.GITHUB_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] " From 8a9960ff2f9ce226a2efe8acf27262de90111359 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Thu, 30 Oct 2025 10:01:02 +0000 Subject: [PATCH 13/21] fix dockerfile --- .devcontainer/Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a5fa564..5ec94af 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -4,12 +4,16 @@ 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 Cargo (needed for lychee link checker) +# 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-link-checker@0.21.0 + cargo install lychee RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ mkdir -p /home/${USERNAME}/.cache && \ From 0bcf057e11c077d6c1854fdd645075466dbda52e Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Thu, 30 Oct 2025 14:46:43 +0000 Subject: [PATCH 14/21] fix vscode bug --- .../extensions.json | 0 .../launch.json | 0 .../settings.json | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename template/{{% if use_devcontainer %}.vscode{% endif %} => {% if use_vscode %}.vscode{% endif %}}/extensions.json (100%) rename template/{{% if use_devcontainer %}.vscode{% endif %} => {% if use_vscode %}.vscode{% endif %}}/launch.json (100%) rename template/{{% if use_devcontainer %}.vscode{% endif %} => {% if use_vscode %}.vscode{% endif %}}/settings.json (100%) diff --git a/template/{% if use_devcontainer %}.vscode{% endif %}/extensions.json b/template/{% if use_vscode %}.vscode{% endif %}/extensions.json similarity index 100% rename from template/{% if use_devcontainer %}.vscode{% endif %}/extensions.json rename to template/{% if use_vscode %}.vscode{% endif %}/extensions.json diff --git a/template/{% if use_devcontainer %}.vscode{% endif %}/launch.json b/template/{% if use_vscode %}.vscode{% endif %}/launch.json similarity index 100% rename from template/{% if use_devcontainer %}.vscode{% endif %}/launch.json rename to template/{% if use_vscode %}.vscode{% endif %}/launch.json diff --git a/template/{% if use_devcontainer %}.vscode{% endif %}/settings.json b/template/{% if use_vscode %}.vscode{% endif %}/settings.json similarity index 100% rename from template/{% if use_devcontainer %}.vscode{% endif %}/settings.json rename to template/{% if use_vscode %}.vscode{% endif %}/settings.json From 54d87c422903755c241fdcb72f9a9276d4d867aa Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Fri, 31 Oct 2025 18:19:08 +0000 Subject: [PATCH 15/21] add name and email setup --- .devcontainer/postStartCommand.sh | 3 + Makefile | 4 +- copier.yaml | 25 +++- template/Makefile.jinja | 73 ++++++++++- template/README.md.jinja | 120 ++++++++---------- template/pyproject.toml.jinja | 4 +- .../devcontainer.json.jinja | 4 +- 7 files changed, 160 insertions(+), 73 deletions(-) diff --git a/.devcontainer/postStartCommand.sh b/.devcontainer/postStartCommand.sh index 24031c4..57a777e 100644 --- a/.devcontainer/postStartCommand.sh +++ b/.devcontainer/postStartCommand.sh @@ -4,4 +4,7 @@ set -euo pipefail echo "Configuring git safe directory (idempotent)..." git config --global --add safe.directory /workspace 2>/dev/null || true +echo "Adding copier as tool" +uv tool install copier + echo "PostStart complete" diff --git a/Makefile b/Makefile index 926ca3d..7d40362 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ test: @set -euo pipefail; \ tmpdir=$$(mktemp -d); \ echo "πŸ”§ Generating template into: $$tmpdir"; \ - uvx copier copy --vcs-ref=HEAD . "$$tmpdir" --defaults --force --trust; \ + 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; \ @@ -19,7 +19,7 @@ test: 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 + @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 --files $$(find . -type f -not -path '*/\.git/*') @echo "βœ… Template generated and validated successfully!" diff --git a/copier.yaml b/copier.yaml index e527fd3..68d6724 100644 --- a/copier.yaml +++ b/copier.yaml @@ -37,9 +37,21 @@ _message_after_copy: | project_name: type: str help: What is your project name? - default: "python-template" + 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 @@ -61,6 +73,17 @@ use_github_actions: 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_proxy: + type: bool + help: Add proxy settings to Devcontainer. + default: no + skip_git_init: type: bool when: false # Hidden parameter for testing diff --git a/template/Makefile.jinja b/template/Makefile.jinja index f4abed3..395b374 100644 --- a/template/Makefile.jinja +++ b/template/Makefile.jinja @@ -1,7 +1,7 @@ # 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 +.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: @@ -14,6 +14,10 @@ help: @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 @@ -49,3 +53,70 @@ clean: 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 %} + +github-create: + @echo "πŸ”— Creating GitHub repository..." + @if ! command -v gh &> /dev/null; then \ + echo "❌ GitHub CLI (gh) not found. Install it from: https://cli.github.com/"; \ + echo " Or run: make check-tools"; \ + exit 1; \ + fi + @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: + @echo "πŸ“€ Pushing to GitHub..." + @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 index 474f136..eef762f 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -1,101 +1,91 @@ # {{ project_name }} -![CI](https://github.com/martgra/python_template/actions/workflows/ci.yaml/badge.svg?branch=main) -![Python](https://img.shields.io/badge/python-3.10%2B-blue?logo=python\&logoColor=white) +{% 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-3.11%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) -Modern Python project template for VS Code. +{% if project_description %}{{ project_description }}{% else %}A solid project template for Python.{% endif %} -**Tools:** [uv](https://docs.astral.sh/uv/) Β· [Ruff](https://docs.astral.sh/ruff/) Β· [Pylint](https://pylint.readthedocs.io/en/stable/) Β· [prek](https://github.com/j178/prek) Β· [pytest](https://docs.pytest.org/en/stable/) +## ✨ Features + +- **Modern Python** – Requires Python β‰₯ 3.11. +- **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 -**Prerequisites:** [uv](https://docs.astral.sh/uv/getting-started/installation/), [VS Code](https://code.visualstudio.com/), Docker (optional) +Get started in seconds: ```bash -# 1. Generate project (if not already done) uvx copier copy gh:martgra/python_template --trust - -# 2. Install dependencies -cd -uv sync - -# 3. Verify setup -make test ``` -You're ready to code. - -## Daily Workflows +## Project Layout -**Development:** -```bash -make run # Run your application -make test # Run test suite -make format # Auto-format code ``` - -**Before commit:** -```bash -make lint # Check code quality -# Hooks run automatically on commit +{{ 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 %} ``` -**Maintenance:** -```bash -make update-deps # Update dependencies -make help # See all commands -``` +Python β‰₯ 3.11 is required locally. The dev container uses Python 3.13. -**Direct tool access:** -```bash -uv run pytest tests/ -uv run ruff check {{ package_name }} -uv run python -m {{ package_name }} -``` +## Git Hooks (Prek) -## Project Layout +[Prek](https://github.com/j178/prek) is a fast Rust‑based replacement for pre‑commit that uses the same configuration format. Install hooks with: -```text -{{ package_name }}/ # Your package -tests/ # Test suite -pyproject.toml # Dependencies & config -uv.lock # Locked versions -.pre-commit-config.yaml # Code quality hooks -.vscode/ # Editor config -.devcontainer/ # Docker setup (optional) +```bash +uvx prek install ``` -Python β‰₯ 3.10 required locally. Devcontainer uses Python 3.13. +### 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 -## Tool Stack +### Slower Push Hooks (run on `git push`) -Configuration in `pyproject.toml` and `.pre-commit-config.yaml`. +- **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 -- **[uv](https://docs.astral.sh/uv/)** – dependency management -- **[Ruff](https://docs.astral.sh/ruff/)** – fast linting & formatting -- **[Pylint](https://pylint.readthedocs.io/en/stable/)** – deep static analysis -- **[prek](https://github.com/j178/prek)** – pre-commit hooks -- **[pytest](https://docs.pytest.org/en/stable/)** – testing framework -- **[Deptry](https://github.com/fpgmaas/deptry)** – dependency validation -- **[Vulture](https://github.com/jendrikseipp/vulture)** – dead code detection -- **[detect-secrets](https://github.com/Yelp/detect-secrets)** – secret scanning +This two‑tier approach keeps commits fast while ensuring comprehensive quality checks before pushing. -{% if use_github_actions %} ## CI Pipeline -GitHub Actions runs on PRs and main branch pushes. Workflow: install deps β†’ run all prek hooks (lint, format, test, security). +{% 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). -{% endif %} +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 %} -{% if use_devcontainer %} ## Devcontainer -For reproducible Docker-based development, reopen the project in a container (**Dev Containers: Reopen in Container** in VS Code). Pre-configures Python 3.13, uv, and all tools. +{% 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) -{% endif %} +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 diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 82b0e3e..ea29cb9 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -1,8 +1,8 @@ [project] name = "{{ project_name }}" version = "0.1.0" -description = "" -authors = [{ name = "Martin Gran", email = "martgra@gmail.com" }] +description = "{{ project_description }}" +authors = [{ name = "{{ user_name }}", email = "{{ user_email }}" }] requires-python = ">=3.11" readme = "README.md" dependencies = [] diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja b/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja index 1f62517..64de891 100644 --- a/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja @@ -33,10 +33,10 @@ "PATH": "${containerEnv:PATH}:/home/vscode/.local/bin" }, "postCreateCommand": "bash .devcontainer/postCreateCommand.sh", - "postStartCommand": "bash .devcontainer/postStartCommand.sh", + "postStartCommand": "bash .devcontainer/postStartCommand.sh"{% if use_proxy %}, "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 %} } From 4172d5fc56190de7274af030c60bcae69f905e0e Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Mon, 17 Nov 2025 20:31:59 +0000 Subject: [PATCH 16/21] fixup bugs --- .github/workflows/deploy.yaml | 4 +- Makefile | 4 +- README.md | 2 +- copier.yaml | 1 + template/.pre-commit-config.yaml.jinja | 14 ++--- ...prettierrc.json => .prettierrc.json.jinja} | 5 +- template/Makefile.jinja | 14 ++--- .../devcontainer.json.jinja | 16 +++--- .../docker-compose.yaml.jinja | 54 ++++++++++--------- .../postCreateCommand.sh | 2 +- .../workflows/ci.yaml | 15 ------ .../workflows/ci.yaml.jinja | 32 +++++++++++ ...{extensions.json => extensions.json.jinja} | 5 +- .../launch.json | 15 ------ .../launch.json.jinja | 13 +++++ .../{settings.json => settings.json.jinja} | 8 ++- 16 files changed, 112 insertions(+), 92 deletions(-) rename template/{.prettierrc.json => .prettierrc.json.jinja} (81%) delete mode 100644 template/{% if use_github_actions %}.github{% endif %}/workflows/ci.yaml create mode 100644 template/{% if use_github_actions %}.github{% endif %}/workflows/ci.yaml.jinja rename template/{% if use_vscode %}.vscode{% endif %}/{extensions.json => extensions.json.jinja} (74%) delete mode 100644 template/{% if use_vscode %}.vscode{% endif %}/launch.json create mode 100644 template/{% if use_vscode %}.vscode{% endif %}/launch.json.jinja rename template/{% if use_vscode %}.vscode{% endif %}/{settings.json => settings.json.jinja} (92%) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 7886615..53e78c6 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -61,7 +61,7 @@ jobs: - name: Clone repository for main branch run: | cd .. - git clone https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git main-repo + 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 @@ -104,7 +104,7 @@ jobs: if: steps.check_changes.outputs.has_changes == 'true' uses: peter-evans/create-pull-request@v7 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.PAT_TOKEN }} path: ../main-repo branch: template-build base: main diff --git a/Makefile b/Makefile index 7d40362..76bbf1b 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ test: git add -A >/dev/null; \ uvx prek install >/dev/null; \ echo "πŸš€ Running pre-commit hooks..."; \ - uvx prek run --all-files; \ + 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." @@ -21,7 +21,7 @@ build: @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 --files $$(find . -type f -not -path '*/\.git/*') + @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/" diff --git a/README.md b/README.md index fa8f516..22845b5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![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) -A [Copier](https://copier.readthedocs.io/) template for modern Python projects using [uv](https://docs.astral.sh/uv/), [Ruff](https://docs.astral.sh/ruff/), [pytest](https://docs.pytest.org/), and optional VSCode devcontainer support. +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. ## Features diff --git a/copier.yaml b/copier.yaml index 68d6724..526ae94 100644 --- a/copier.yaml +++ b/copier.yaml @@ -15,6 +15,7 @@ _exclude: - "*.pyc" - "__pycache__" - ".git" + - "uv.lock" # Welcome message before copying _message_before_copy: | diff --git a/template/.pre-commit-config.yaml.jinja b/template/.pre-commit-config.yaml.jinja index 19accb2..3fd4abe 100644 --- a/template/.pre-commit-config.yaml.jinja +++ b/template/.pre-commit-config.yaml.jinja @@ -1,5 +1,5 @@ minimum_pre_commit_version: "3.5.0" -default_stages: [commit] +default_stages: [commit, manual] ############################################################ # Global Exclusions @@ -70,7 +70,7 @@ repos: language: system pass_filenames: false always_run: true - stages: [pre-push] + stages: [pre-push, manual] # Optional faster feedback: args: ["-q", "--maxfail=1"] - id: pylint name: Pylint (design checks) @@ -78,19 +78,19 @@ repos: language: system types: [python] args: ["--rcfile=pyproject.toml"] - stages: [pre-push] + stages: [pre-push, manual] - id: deptry name: Deptry (dependency hygiene) entry: uv run deptry {{ package_name }} language: system pass_filenames: false - stages: [pre-push] + stages: [pre-push, manual] - id: vulture name: Vulture (dead code) entry: uv run vulture {{ package_name }} language: system pass_filenames: false - stages: [pre-push] + stages: [pre-push, manual] - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 @@ -99,12 +99,12 @@ repos: name: Detect Secrets (baseline) args: ["--baseline", ".secrets.baseline"] exclude: uv.lock - stages: [pre-push] + 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] + stages: [pre-push, manual] pass_filenames: false diff --git a/template/.prettierrc.json b/template/.prettierrc.json.jinja similarity index 81% rename from template/.prettierrc.json rename to template/.prettierrc.json.jinja index 1ffcf9a..d33a4b0 100644 --- a/template/.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/template/Makefile.jinja b/template/Makefile.jinja index 395b374..91d02eb 100644 --- a/template/Makefile.jinja +++ b/template/Makefile.jinja @@ -91,13 +91,10 @@ check-tools: @echo "πŸ’‘ Install missing tools using the links above" {%- if github_username %} -github-create: - @echo "πŸ”— Creating GitHub repository..." - @if ! command -v gh &> /dev/null; then \ - echo "❌ GitHub CLI (gh) not found. Install it from: https://cli.github.com/"; \ - echo " Or run: make check-tools"; \ - exit 1; \ - fi +.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; \ @@ -110,8 +107,7 @@ github-create: @echo "" @echo "Next: Run 'make github-push' to push your code" -github-push: - @echo "πŸ“€ Pushing to GitHub..." +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; \ diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja b/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja index 64de891..05816ab 100644 --- a/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja @@ -1,6 +1,5 @@ -{ - // Dev Container definition for {{ package_name }} - "name": "{{ package_name }}", +{%- set devcontainer_config = { + "name": package_name, "dockerComposeFile": ["docker-compose.yaml"], "service": "app", "workspaceFolder": "/workspace", @@ -33,10 +32,15 @@ "PATH": "${containerEnv:PATH}:/home/vscode/.local/bin" }, "postCreateCommand": "bash .devcontainer/postCreateCommand.sh", - "postStartCommand": "bash .devcontainer/postStartCommand.sh"{% if use_proxy %}, + "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 %} -} + } +}) -%} +{%- 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 index a9df6e2..1ecb7a1 100644 --- a/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja @@ -1,25 +1,29 @@ -version: "3.8" -services: - app: - user: vscode - build: - # Use repository root as build context so we can pre-warm dependencies (pyproject/uv.lock) - context: .. - dockerfile: .devcontainer/Dockerfile - args: - VARIANT: 3.13-bookworm - volumes: - - ..:/workspace:cached # Shared workspace between host and devcontainer - - {{ package_name }}_extensions:/home/vscode/.vscode-server/extensions # Storing extensions - - {{ package_name }}_commandhistory:/home/vscode/commandhistory # Persistant command line history - - {{ package_name }}_cache:/home/vscode/.cache # Caching poetry/pip wheels - - /workspace/.venv - - # Overrides default command so things don't shut down after the process ends. - command: sleep infinity - -# Volumes that are not shared between Host and Devcontainer must be listed here. -volumes: - {{ package_name }}_extensions: - {{ package_name }}_commandhistory: - {{ package_name }}_cache: +{%- set docker_compose = { + "version": "3.8", + "services": { + "app": { + "user": "vscode", + "build": { + "context": "..", + "dockerfile": ".devcontainer/Dockerfile", + "args": { + "VARIANT": "3.13-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 index d25b97e..a6904d0 100644 --- a/template/{% if use_devcontainer %}.devcontainer{% endif %}/postCreateCommand.sh +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postCreateCommand.sh @@ -10,7 +10,7 @@ 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 ] || [ pyproject.toml -nt .venv ] || [ uv.lock -nt .venv ]; then +if [ ! -d .venv ] || [ ! -f uv.lock ] || [ pyproject.toml -nt .venv ] || [ uv.lock -nt .venv ]; then echo "Performing uv sync (post-create)..." uv sync else diff --git a/template/{% if use_github_actions %}.github{% endif %}/workflows/ci.yaml b/template/{% if use_github_actions %}.github{% endif %}/workflows/ci.yaml deleted file mode 100644 index d1f65fb..0000000 --- a/template/{% if use_github_actions %}.github{% endif %}/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/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 b/template/{% if use_vscode %}.vscode{% endif %}/extensions.json.jinja similarity index 74% rename from template/{% if use_vscode %}.vscode{% endif %}/extensions.json rename to template/{% if use_vscode %}.vscode{% endif %}/extensions.json.jinja index 5bbcaf2..be9e7bd 100644 --- a/template/{% if use_vscode %}.vscode{% endif %}/extensions.json +++ b/template/{% if use_vscode %}.vscode{% endif %}/extensions.json.jinja @@ -1,4 +1,4 @@ -{ +{%- set extensions = { "recommendations": [ "ms-python.python", "charliermarsh.ruff", @@ -9,4 +9,5 @@ "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 b/template/{% if use_vscode %}.vscode{% endif %}/launch.json deleted file mode 100644 index e217d30..0000000 --- a/template/{% if use_vscode %}.vscode{% endif %}/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Module", - "type": "debugpy", - "request": "launch", - "module": "{{ package_name }}", - "justMyCode": 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 b/template/{% if use_vscode %}.vscode{% endif %}/settings.json.jinja similarity index 92% rename from template/{% if use_vscode %}.vscode{% endif %}/settings.json rename to template/{% if use_vscode %}.vscode{% endif %}/settings.json.jinja index 2ea1e60..0e303de 100644 --- a/template/{% if use_vscode %}.vscode{% endif %}/settings.json +++ b/template/{% if use_vscode %}.vscode{% endif %}/settings.json.jinja @@ -1,5 +1,4 @@ -{ - // Editor settings for Python +{%- set settings = { "[python]": { "editor.formatOnSave": true, "editor.defaultFormatter": "charliermarsh.ruff", @@ -10,8 +9,6 @@ "editor.rulers": [100], "files.trimTrailingWhitespace": true }, - - // Editor settings for YAML "[yaml]": { "editor.insertSpaces": true, "editor.formatOnSave": true, @@ -35,4 +32,5 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python" -} +} -%} +{{ settings | to_nice_json(indent=2, sort_keys=False) }} From 6594b0ea9ae393fd14f2ab044fc8973b7188f2fe Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Sat, 22 Nov 2025 10:44:36 +0000 Subject: [PATCH 17/21] fix pre-commit --- template/.pre-commit-config.yaml.jinja | 4 +- .../install-gh.sh | 66 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100755 template/{% if github_username %}scripts{% endif %}/install-gh.sh diff --git a/template/.pre-commit-config.yaml.jinja b/template/.pre-commit-config.yaml.jinja index 3fd4abe..68ae496 100644 --- a/template/.pre-commit-config.yaml.jinja +++ b/template/.pre-commit-config.yaml.jinja @@ -1,5 +1,7 @@ minimum_pre_commit_version: "3.5.0" -default_stages: [commit, manual] +default_stages: [pre-commit, manual] +default_install_hook_types: [pre-commit, pre-push] + ############################################################ # Global Exclusions 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 From 256e84991b44ca627f086032cacb9d7ec3d4ccca Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Thu, 27 Nov 2025 11:27:04 +0000 Subject: [PATCH 18/21] add AGENTS.md --- copier.yaml | 11 ++ template/.pre-commit-config.yaml.jinja | 6 + template/README.md.jinja | 6 +- template/pyproject.toml.jinja | 2 +- ...if use_agents %}AGENTS.md{% endif %}.jinja | 187 ++++++++++++++++++ .../{Dockerfile => Dockerfile.jinja} | 2 +- .../docker-compose.yaml.jinja | 2 +- 7 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 template/{% if use_agents %}AGENTS.md{% endif %}.jinja rename template/{% if use_devcontainer %}.devcontainer{% endif %}/{Dockerfile => Dockerfile.jinja} (95%) diff --git a/copier.yaml b/copier.yaml index 526ae94..1b8ff72 100644 --- a/copier.yaml +++ b/copier.yaml @@ -80,9 +80,15 @@ github_username: 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: @@ -90,6 +96,11 @@ skip_git_init: 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" diff --git a/template/.pre-commit-config.yaml.jinja b/template/.pre-commit-config.yaml.jinja index 68ae496..5dbe5dc 100644 --- a/template/.pre-commit-config.yaml.jinja +++ b/template/.pre-commit-config.yaml.jinja @@ -110,3 +110,9 @@ repos: name: Validate pyproject / lock stages: [pre-push, manual] pass_filenames: false + + - repo: https://github.com/pysentry/pysentry-pre-commit + rev: v0.3.12 + hooks: + - id: pysentry # default pysentry settings + stages: [pre-push, manual] diff --git a/template/README.md.jinja b/template/README.md.jinja index eef762f..0b928c5 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -1,14 +1,14 @@ # {{ 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-3.11%2B-blue?logo=python&logoColor=white) +{% 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 β‰₯ 3.11. +- **Modern Python** – Requires Python β‰₯ {{ python_version }}. - **Dependency management with uv** – Fast dependency installation and lock file management. - **Quality tools** - Ruff formats and lints code @@ -43,7 +43,7 @@ Makefile # Common tasks (test, lint, format, etc.) {% if use_github_actions %}.github/workflows/ # CI/CD workflows{% endif %} ``` -Python β‰₯ 3.11 is required locally. The dev container uses Python 3.13. +Python β‰₯ {{ python_version }} is required locally. The dev container uses Python {{ python_version }}. ## Git Hooks (Prek) diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index ea29cb9..093b491 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -3,7 +3,7 @@ name = "{{ project_name }}" version = "0.1.0" description = "{{ project_description }}" authors = [{ name = "{{ user_name }}", email = "{{ user_email }}" }] -requires-python = ">=3.11" +requires-python = ">={{ python_version }}" readme = "README.md" dependencies = [] 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..fc4c4af --- /dev/null +++ b/template/{% if use_agents %}AGENTS.md{% endif %}.jinja @@ -0,0 +1,187 @@ +--- +name: coding-agent +description: Writes Python code following clean architecture principles. +--- + +You are a Python developer who writes clean, decoupled code with clear separation of concerns. You raise questions about unclear requirements and validate assumptions before implementing. + +## Project Overview + +- **Python Version:** {{ python_version }} +- **Package Manager:** uv (ALWAYS use `uv run` prefix) +- **Linting:** prek, ruff +- **Testing:** pytest (functions, not classes) +- **Documentation:** Google-style docstrings + +## Project Structure +``` +{{ package_name }}/ # Application source code +tests/ + β”œβ”€β”€ unit/ # Fast, isolated unit tests + β”œβ”€β”€ integration/ # Tests with external dependencies + └── e2e/ # End-to-end tests +docs/ # Comprehensive documentation + └── wip/ # Working analysis and reports +pyproject.toml # Dependencies and tool config +README.md # Project summary +``` + +## Critical Commands + +### Always use `uv run` prefix +```bash +uv sync # Install/sync dependencies +uv add # Add new dependency +uv run pytest # Run all tests +uv run pytest -v -x # Verbose, stop on first failure +uv run pytest tests/unit/ # Run specific test directory +uv run prek run --all-files # Run all linting checks using prek (compatible with pre-commit) +uv run ruff check --fix . # Lint and auto-fix +uv run ruff format . # Format code +``` + +### Pre-commit checklist +```bash +uv run ruff check --fix . +uv run prek run --all-files +uv run pytest +``` + +## Code Conventions + +- **Line length:** 99 characters maximum +- **Type hints:** Required for all function signatures +- **Docstrings:** Google-style for public functions/classes +- **Naming:** `snake_case` for functions/variables, `PascalCase` for classes +- **Imports:** Standard library β†’ third-party β†’ local (separated by blank lines) +- **Cyclomatic complexity:** Maximum 8 per function +- **Function length:** Prefer <50 lines; split if longer + +### βœ… Good Example +```python +from pathlib import Path +import json + +def load_resource(resource_id: str, cache_dir: Path) -> dict[str, str]: + """ + Load a resource from cache or fetch from remote source. + + Args: + resource_id: Unique resource identifier + cache_dir: Directory for cached resources + + Returns: + Dictionary containing resource data + + Raises: + ValueError: If resource_id is empty or invalid + FileNotFoundError: If resource not found in cache or remote source + """ + if not resource_id: + raise ValueError("Resource ID cannot be empty") + + cached_path = cache_dir / f"{resource_id}.json" + if cached_path.exists(): + return json.loads(cached_path.read_text()) + + result = fetch_from_remote(resource_id) + if result is None: + raise FileNotFoundError(f"Resource not found: {resource_id}") + return result +``` + +### ❌ Bad Example +```python +def load(x): + # No type hints, vague name, silent failures + try: + return json.loads(Path(x).read_text()) + except: + return {} +``` + +## Testing Guidelines + +### Pytest-style functions (NOT unittest classes) +# βœ… Good - pytest function with fixtures +```python +def test_processor_handles_empty_input(sample_data, processor): + result = processor.process(sample_data, max_items=100) + assert all(len(item) <= 100 for item in result) +``` + +# ❌ Bad - unittest class style +```python +class TestProcessor(unittest.TestCase): + def test_empty_input(self): + self.assertTrue(...) +``` + +## Error Handling + +- Define custom exceptions in `{{ package_name }}/exceptions.py` +- Inherit from a base project exception for easy catching +- Include context in exception messages +```python +# βœ… Good +class DocumentNotFoundError(LovligError): + """Raised when a document cannot be found.""" + def __init__(self, doc_id: str): + super().__init__(f"Document not found: {doc_id}") + self.doc_id = doc_id + +# ❌ Bad +raise Exception("not found") +``` + +### Testing patterns +- Use descriptive names: `test___` +- Prefer fixtures over setup/teardown methods +- Use `assert` directly, no `self.assertEqual()` +- Put shared fixtures in `tests/conftest.py` +- Never create throwaway test scripts outside `tests/` + +### Test organization by speed +- `tests/unit/` - Fast (<100ms), no external dependencies +- `tests/integration/` - Slower, requires services (DB, API) +- `tests/e2e/` - Full workflow tests + +## Architecture Patterns + +- **Separation of concerns:** Keep data access, business logic, and presentation separate +- **Dependency injection:** Pass dependencies as parameters, avoid global state +- **Error handling:** Raise specific exceptions, don't catch and ignore +- **Async:** Use `async`/`await` for I/O-bound operations + +## Documentation Requirements + +- Update `docs/` when adding features or changing behavior +- Write analysis reports to `docs/wip/` after completing complex tasks +- Keep `README.md` concise - detailed docs go in `docs/` +- Include "why" in comments for non-obvious design decisions + +## Boundaries + +### βœ… Always +- Run `uv run pytest` before any commit +- Use `uv run` prefix for ALL Python commands +- Write tests for new functionality +- Run `uv run ruff check --fix` on changed files +- Use type hints on all function signatures +- Ask clarifying questions about unclear requirements + +### ⚠️ Ask First +- Adding new dependencies with `uv add` +- Database schema or migration changes +- Modifying CI/CD configuration +- Changing core architecture patterns +- Deleting existing tests or code + +### 🚫 Never +- Commit secrets, API keys, or `.env` files +- Use `python` or `pytest` directly (always `uv run`) +- Remove failing tests without explicit approval +- Create test files outside `tests/` directory +- Modify `pyproject.toml` or lockfiles manually +- Catch exceptions without handling them (`except: pass`) +- Use `pip` instead of `uv` diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile b/template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile.jinja similarity index 95% rename from template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile rename to template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile.jinja index b977678..7f80d14 100644 --- a/template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile.jinja @@ -1,4 +1,4 @@ -ARG VARIANT=3.13-bookworm +ARG VARIANT={{ python_version }}-bookworm FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT} ARG USERNAME=vscode diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja b/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja index 1ecb7a1..28101e8 100644 --- a/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja @@ -7,7 +7,7 @@ "context": "..", "dockerfile": ".devcontainer/Dockerfile", "args": { - "VARIANT": "3.13-bookworm" + "VARIANT": python_version ~ "-bookworm" } }, "volumes": [ From 7ff0c903b314e29ab889b593a4b58dd65b7f5a72 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Fri, 28 Nov 2025 14:21:18 +0000 Subject: [PATCH 19/21] fix vulture settings --- template/.pre-commit-config.yaml.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/.pre-commit-config.yaml.jinja b/template/.pre-commit-config.yaml.jinja index 5dbe5dc..e7a3952 100644 --- a/template/.pre-commit-config.yaml.jinja +++ b/template/.pre-commit-config.yaml.jinja @@ -89,7 +89,7 @@ repos: stages: [pre-push, manual] - id: vulture name: Vulture (dead code) - entry: uv run vulture {{ package_name }} + entry: uv run vulture language: system pass_filenames: false stages: [pre-push, manual] From 9f6a44a1c20d4dab2e166aba48eae04fffbb0ec6 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Wed, 3 Dec 2025 14:09:18 +0000 Subject: [PATCH 20/21] fix agents.md --- template/{% if use_agents %}AGENTS.md{% endif %}.jinja | 1 + 1 file changed, 1 insertion(+) diff --git a/template/{% if use_agents %}AGENTS.md{% endif %}.jinja b/template/{% if use_agents %}AGENTS.md{% endif %}.jinja index fc4c4af..356fd11 100644 --- a/template/{% if use_agents %}AGENTS.md{% endif %}.jinja +++ b/template/{% if use_agents %}AGENTS.md{% endif %}.jinja @@ -140,6 +140,7 @@ raise Exception("not found") - Use `assert` directly, no `self.assertEqual()` - Put shared fixtures in `tests/conftest.py` - Never create throwaway test scripts outside `tests/` +- Use naming convention `_test.py` where the test is _test suffix to the name of the test. ### Test organization by speed - `tests/unit/` - Fast (<100ms), no external dependencies From cce26c8d1575746559886a4357f4417d74685dc2 Mon Sep 17 00:00:00 2001 From: Martin Gran Date: Fri, 5 Dec 2025 09:54:18 +0000 Subject: [PATCH 21/21] make package_name configurable --- copier.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/copier.yaml b/copier.yaml index 1b8ff72..549775b 100644 --- a/copier.yaml +++ b/copier.yaml @@ -41,6 +41,11 @@ project_name: default: "python_template" placeholder: "my-awesome-project" +# Auto-generated from project_name - never asked to user +package_name: + type: str + default: "{% from 'includes/slugify.jinja' import slugify %}{{ slugify(project_name) }}" + user_name: type: str help: What is your name? @@ -53,12 +58,6 @@ user_email: 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)