diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dfbab92..9b70211 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { - // Dev Container definition for python_package - "name": "python_package", + // Dev Container definition for python_template + "name": "python_template", "dockerComposeFile": ["docker-compose.yaml"], "service": "app", "workspaceFolder": "/workspace", diff --git a/.devcontainer/postStartCommand.sh b/.devcontainer/postStartCommand.sh index 3336c29..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/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..062b13f --- /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..3544b09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,160 +1,21 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py +# Copier template testing outputs +temp_out/ +test_output/ +build_output/ -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ +# Python cache from test runs +__pycache__/ +*.pyc -# Celery stuff -celerybeat-schedule -celerybeat.pid +# OS files +.DS_Store +Thumbs.db -# SageMath parsed files -*.sage.py +# Editor files +.idea/ +*.swp +*.swo +*~ -# Environments -.env +# Template environment .venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 434d221..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/.vscode/launch.json b/.vscode/launch.json index c2e586f..e217d30 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "name": "Python: Module", "type": "debugpy", "request": "launch", - "module": "python_package", + "module": "{{ package_name }}", "justMyCode": false } ] diff --git a/Makefile b/Makefile index a94e5b0..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..fa8f516 100644 --- a/README.md +++ b/README.md @@ -1,247 +1,63 @@ -# Setting up Python in VSCode +# Python Template -## Rationale ✏️ +![CI](https://github.com/martgra/python_template/actions/workflows/ci.yaml/badge.svg?branch=main) +[![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier) -This sets up a simple python repository with a few handy tools to make Python development (a little) smoother. +A [Copier](https://copier.readthedocs.io/) 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`: - -```bash -uv tool install prek # no-op if already installed via devcontainer -prek install # sets up .git/hooks/* -``` - -The hooks are now installed and most of them will be run every time you try to ```commit``` to your local branch. A few are ```on push``` only. - -Useful `prek` commands: -```bash -prek run # run all hooks on staged files -prek run --all-files # run all hooks on the whole repo -prek run ruff ruff-format # run a subset -``` - -If you really need to check in some code and a hook (via `prek`) blocks you, you can bypass once (not recommended): - -```bash -git commit -m "commit message" --no-verify -``` - -#### Hooks (prek consuming `.pre-commit-config.yaml`) - -| Name | Explanation | -|----------------|-----------------------------------------------------------------------------| -| ruff | Lints Python code and applies automatic fixes using Ruff, driven by your `pyproject.toml` config. | -| ruff-format | Formats Python code according to Ruff’s formatting rules, using your project config. | -| pylint | Runs Pylint static analysis on your Python files (using `pyproject.toml` for settings). | -| pytest-check | Executes your test suite with pytest to ensure no tests are broken before pushing. | -| uv-lock | Validates the integrity and correctness of your `pyproject.toml` lock file. | -| deptry | Detects unused or missing dependencies in your Python project. | -| vulture | Detects unused (dead) code | -| detect-secrets | Scans your codebase for potential secret tokens or credentials before pushing. | - - -#### Some extra words about Detect-secrets -To prevent accidental commits of sensitive information, we use [detect-secrets](https://github.com/Yelp/detect-secrets) with `prek` (pre-commit compatible hook). These are usually passwords, tokens or other credentials that you don't want public. - -Detect-Secrets will prevent pushes to remote repository if a secret has been checked in. Detect-secrets checks the content of the repository towards -```.secrets.baseline```. - -If a new potential secret is added we can rescan the repositroy to add lines of code we want to allow to push. - -Create the baseline file the first time - audit this carefully. +Generate a new project using Copier: ```bash -detect-secrets scan > .secrets.baseline +uvx copier copy gh:martgra/python_template ``` -If the ```.secrets.baseline``` file needs updating you can do - -```bash -detect-secrets scan --baseline .secrets.baseline > .secrets.baseline -``` - -## Testing πŸ‘· - -The Python extension in VSCode comes with support of several testing frameworks. Here the choice fell on ```pytest``` and ```pytest-cov``` (the coverage plugin for ```pytest```). +## Development -### Configuring pytest - -Pytest is configured via `[tool.pytest]` in `pyproject.toml` (tests path). `.vscode/settings.json` enables auto discovery on save. - -### Running tests - -To run the tests in VSCode you can either: - -#### Run tests from the GUI - -...by going to the ```tests``` pane or opening an actual test file like ```package_test.py``` and going to a specific test. Next to the test an icon should appear that one can click to run the specific test! This requires that the tests are discovered. - ->Tests should have been auto discovered by VSCode - but if they're not showing - try running -> ```Python: Configure Tests``` in the VSCode command palette 🎨. - -#### Run tests from the command line - -Tests can always be run in the terminal by typing: +This repository contains the Copier template itself. To test the template: ```bash -uv run pytest tests +make test ``` -### Debugging tests - -One of the nicest features with VSCode and testing is the ability to easily debug tests. In the world of test driven development (TDD) this is really useful! - -To debug a specific test - right-click the "Run" icon next to the test and select ```debug test``` from the drop-down. Add a breakpoint in the test and step through your code! - -### Coverage +This will: -Test coverage is currently not well supported in VSCode. Although there are a few extensions available (a bit less than 1m downloads). In this setup we choose to do coverage the old-fashioned way! +1. Generate a project from the template in a temp directory +2. Initialize git and pre-commit hooks +3. Run all quality checks +4. Clean up the temp directory -This means generating a coverage report using [coverage](https://coverage.readthedocs.io/en/7.0.1/). +## Template Structure -#### Generate a coverage report - -To generate a coverage report run the following command in the terminal: - -```bash -uv run pytest --cov-report term-missing --cov=python_package tests ``` +template/ # Template files (what gets copied) +β”œβ”€β”€ README.md.jinja # Generated project README +β”œβ”€β”€ pyproject.toml.jinja # Project configuration +β”œβ”€β”€ uv.lock # Dependency lock file +β”œβ”€β”€ {{ package_name }}/ # Python package +β”œβ”€β”€ tests/ # Test files +β”œβ”€β”€ includes/ # Jinja macros and utilities +└── {% if ... %}/ # Conditional directories -To generate an interactive coverage report in html simply run: - -```bash -uv run pytest --cov-report html --cov=python_package tests -``` -Open `htmlcov/index.html` in a browser. - -## Devcontainer πŸ›Έ - -Devcontainers are one of the most powerful features of VSCode. Instead of going around "helping" everyone with environment-related issues we can package everything into a docker-container and spin it up! -Did an intentional or unintentional change happen to your environment? No worries - check in intentional changes and distribute faster than I can say "git commit" or rebuild the entire environment with a click of a button! - -The devcontainer files sit in ```.devcontainer``` folder and consist of - -```bash -β”œβ”€β”€ Dockerfile (1) -β”œβ”€β”€ devcontainer.json (2) -β”œβ”€β”€ docker-compose.yaml (3) -β”œβ”€β”€ postCreateCommand.sh (4) -└── postStartCommand.sh (5) +copier.yaml # Template configuration +Makefile # Template testing ``` -1. The Dockerfile that defines the environment. Build your own or there is [a variety to choose from here](https://hub.docker.com/_/microsoft-vscode-devcontainers) -2. ```devcontainer.json``` - The file that configures the environment! -3. ```docker-compose.yaml``` - I have chosen to extend ```.devcontainer.json``` with a docker-compose file. This allows easily extending the environment with supporting services such as a database or a S3 mock. -4. ```postCreateCommand.sh``` is a script that is run after the environment is built the first time (installs `prek` & hooks, syncs deps). -5. ```postStartCommand.sh``` runs each container start (sync / idempotent setup tasks). -> The devcontainer pre-installs uv + prek and performs a frozen sync on start; local hosts only need `uv sync` + `prek install` once. +## Requirements -[Follow this excellent guide to get going!](https://code.visualstudio.com/docs/devcontainers/containers) +- [Copier](https://copier.readthedocs.io/) 9.0.0+ +- [uv](https://docs.astral.sh/uv/getting-started/installation/) -There is not much more to add other than: +## License -1. Open the repository root! -2. Windows users - you **must** use this in context of WSL2. It's both faster, simpler and less error-prone! ([and strongly recommended!](https://code.visualstudio.com/docs/devcontainers/containers#_open-a-wsl-2-folder-in-a-container-on-windows)) -3. Press ```ctrl/command+shift+p``` and type ```"open folder in container"``` -4. Sit back and relax while the environment is spun up πŸš€πŸš€πŸš€! +MIT License diff --git a/copier.yaml b/copier.yaml new file mode 100644 index 0000000..e527fd3 --- /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 }}" diff --git a/includes/slugify.jinja b/includes/slugify.jinja new file mode 100644 index 0000000..25b8c51 --- /dev/null +++ b/includes/slugify.jinja @@ -0,0 +1,9 @@ +{% macro slugify(value) -%} +{{ value + |lower + |replace(' ', '_') + |replace('-', '_') + |replace('.', '') + |replace(',', '') +}} +{%- endmacro %} diff --git a/template/.gitignore b/template/.gitignore new file mode 100644 index 0000000..f243be4 --- /dev/null +++ b/template/.gitignore @@ -0,0 +1,165 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Template test output +temp_out/ +test_output/ +build_output/ diff --git a/template/.pre-commit-config.yaml.jinja b/template/.pre-commit-config.yaml.jinja new file mode 100644 index 0000000..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/LICENSE b/template/LICENSE new file mode 100644 index 0000000..b4d2e42 --- /dev/null +++ b/template/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) {% now 'utc', '%Y' %} {{ project_name }} contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/template/Makefile.jinja b/template/Makefile.jinja new file mode 100644 index 0000000..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..cfa2bf7 --- /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 --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 91% rename from pyproject.toml rename to template/pyproject.toml.jinja index 9854ac9..82b0e3e 100644 --- a/pyproject.toml +++ b/template/pyproject.toml.jinja @@ -1,9 +1,9 @@ [project] -name = "python-package" +name = "{{ project_name }}" version = "0.1.0" description = "" authors = [{ name = "Martin Gran", email = "martgra@gmail.com" }] -requires-python = ">=3.10" +requires-python = ">=3.11" readme = "README.md" dependencies = [] @@ -28,10 +28,10 @@ default-groups = [ ] [tool.hatch.build.targets.sdist] -include = ["python_package"] +include = ["{{ package_name }}"] [tool.hatch.build.targets.wheel] -include = ["python_package"] +include = ["{{ package_name }}"] [build-system] requires = ["hatchling"] @@ -49,7 +49,7 @@ select = ["E", "W", "D", "F", "UP", "B", "SIM","I"] "__init__.py" = ["D104"] [tool.ruff.lint.isort] -known-first-party = ["python_package"] +known-first-party = ["{{ package_name }}"] [tool.ruff.lint.pydocstyle] convention = "google" @@ -59,18 +59,17 @@ max-line-length = 100 disable = ["E0401", "C0114", "R0903", "E0237","E1142","E0014","E1300","E0013","E1310","E1307","E2502","E6005","E6004","E0116","E0108","E0241","E1303","E0102","E0100","E0605","E0604","E0304","E0308","E2510","E2513","E2514","E2512","E2515","E0309","E0305","E0303","E1206","E1205","E0704","E1304","E1302","E4703","E0213","E0107","E0115","E0117","E0103","E0711","E0643","E0402","E1132","E0106","E0101","E0104","E1519","E1520","E0001","E1306","E1305","E0112","E1301","E0603","E0602","E0302","E0118","E1700","E0105","W1401","W0129","W0199","W3201","W1302","W1300","W1501","W0211","W0702","W0711","W1502","W0718","W0719","W0640","W0160","W0102","W0705","W0109","W1308","W0130","W1641","W0123","W0122","W0106","W1309","W0511","W1515","W1305","W1310","W0604","W0603","W0602","W1404","W0406","W1405","W1508","W1113","W1202","W1203","W1201","W0150","W1518","W0410","W1303","W0131","W0177","W3301","W2402","W0133","W0104","W0212","W0707","W0622","W2901","W1406","W0404","W0127","W1509","W1510","W0245","W0706","W0012","W0108","W0107","W0301","W1514","W0613","W1304","W1301","W0611","W0612","W0120","W2101","W2601","W0401","C0202","C0198","C1901","C0201","C0501","C0206","C0199","C0112","C0415","C2701","C0103","C0301","C2201","C0115","C0304","C0116","C0114","C0410","C0321","C2403","C2401","C0205","C0121","C0303","C0131","C0105","C0132","C0412","C0123","C3002","C2801","C3001","C0113","C0208","C0414","C0411","C0413","R0133","R0124","R6003","R1701","R6002","R6104","R1717","R1728","R1715","R1714","R1730","R1731","R1718","R1722","R1706","R1732","R5501","R2044","R1710","R0123","R2004","R0202","R1723","R1724","R1720","R1705","R6301","R0203","R0206","R1704","R1719","R1703","R1725","R1260","R0913","R0916","R0912","R0914","R1702","R0904","R0911","R0915","R1707","R1721","R1733","R1736","R1729","R1735","R1734","R6201","R0205","R0022","R1711"] [tool.deptry] -known_first_party = ["python_package"] +known_first_party = ["{{ package_name }}"] [tool.pytest] testpaths = ["tests"] [tool.coverage.run] branch = true -source = ["python_package"] +source = ["{{ package_name }}"] [tool.vulture] make_whitelist = true min_confidence = 80 -paths = ["python_package"] +paths = ["{{ package_name }}"] sort_by_size = true -verbose = true diff --git a/tests/__init__.py b/template/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to template/tests/__init__.py diff --git a/tests/conftest.py b/template/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to template/tests/conftest.py diff --git a/tests/package_test.py b/template/tests/package_test.py similarity index 100% rename from tests/package_test.py rename to template/tests/package_test.py diff --git a/uv.lock b/template/uv.lock similarity index 100% rename from uv.lock rename to template/uv.lock diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile b/template/{% if use_devcontainer %}.devcontainer{% endif %}/Dockerfile new file mode 100644 index 0000000..b977678 --- /dev/null +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/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/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja b/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja new file mode 100644 index 0000000..1f62517 --- /dev/null +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/devcontainer.json.jinja @@ -0,0 +1,42 @@ +{ + // Dev Container definition for {{ package_name }} + "name": "{{ package_name }}", + "dockerComposeFile": ["docker-compose.yaml"], + "service": "app", + "workspaceFolder": "/workspace", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": true + }, + "ghcr.io/devcontainers/features/git:1": {} + }, + "customizations": { + "vscode": { + "settings": { + "python.defaultInterpreterPath": "${containerWorkspaceFolder}/.venv/bin/python", + "terminal.integrated.defaultProfile.linux": "zsh" + }, + "extensions": [ + "ms-python.python", + "charliermarsh.ruff", + "njpwerner.autodocstring", + "eamodio.gitlens", + "mhutchie.git-graph", + "Gruntfuggly.todo-tree", + "esbenp.prettier-vscode" + ] + } + }, + "forwardPorts": [8888], + "remoteUser": "vscode", + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/home/vscode/.local/bin" + }, + "postCreateCommand": "bash .devcontainer/postCreateCommand.sh", + "postStartCommand": "bash .devcontainer/postStartCommand.sh", + "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/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja b/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja new file mode 100644 index 0000000..a9df6e2 --- /dev/null +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/docker-compose.yaml.jinja @@ -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 + - {{ 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: diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/postCreateCommand.sh b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postCreateCommand.sh new file mode 100644 index 0000000..d25b97e --- /dev/null +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postCreateCommand.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -euo pipefail + +sudo chown -R vscode:vscode /workspace/.venv + +# Install/update PreK tool (idempotent) +echo "Ensuring prek installed (pinned via uv tool cache)..." +uv tool install prek >/dev/null 2>&1 || true +prek install + +# Sync dependencies only if environment missing or manifests changed (safety net; main pre-warm is in image layer) +if [ ! -d .venv ] || [ pyproject.toml -nt .venv ] || [ uv.lock -nt .venv ]; then + echo "Performing uv sync (post-create)..." + uv sync +else + echo "uv sync skipped (environment up-to-date)." +fi + +echo "PostCreate complete" diff --git a/template/{% if use_devcontainer %}.devcontainer{% endif %}/postStartCommand.sh b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postStartCommand.sh new file mode 100644 index 0000000..3336c29 --- /dev/null +++ b/template/{% if use_devcontainer %}.devcontainer{% endif %}/postStartCommand.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -euo pipefail + +echo "Configuring git safe directory (idempotent)..." +git config --global --add safe.directory /workspace 2>/dev/null || true + +uv sync --frozen + +echo "PostStart complete" diff --git a/template/{% if use_devcontainer %}.vscode{% endif %}/extensions.json b/template/{% if use_devcontainer %}.vscode{% endif %}/extensions.json new file mode 100644 index 0000000..5bbcaf2 --- /dev/null +++ b/template/{% if use_devcontainer %}.vscode{% endif %}/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/template/{% if use_devcontainer %}.vscode{% endif %}/launch.json b/template/{% if use_devcontainer %}.vscode{% endif %}/launch.json new file mode 100644 index 0000000..e217d30 --- /dev/null +++ b/template/{% if use_devcontainer %}.vscode{% endif %}/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/template/{% if use_devcontainer %}.vscode{% endif %}/settings.json b/template/{% if use_devcontainer %}.vscode{% endif %}/settings.json new file mode 100644 index 0000000..2ea1e60 --- /dev/null +++ b/template/{% if use_devcontainer %}.vscode{% endif %}/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" +} 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..69141bb --- /dev/null +++ b/template/{{ _copier_conf.answers_file }}.jinja @@ -0,0 +1,2 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +{{ _copier_answers|to_nice_yaml -}} diff --git a/python_package/__init__.py b/template/{{ package_name }}/__init__.py similarity index 100% rename from python_package/__init__.py rename to template/{{ package_name }}/__init__.py diff --git a/python_package/__main__.py b/template/{{ package_name }}/__main__.py similarity index 100% rename from python_package/__main__.py rename to template/{{ package_name }}/__main__.py