diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3537c50 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[Makefile] +indent_style = tab + +[*.{py,cfg,ini,toml,yaml,yml,md,sh}] +indent_style = space +indent_size = 4 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4f8a87a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "pre-commit" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5365571..b187c34 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,13 +6,16 @@ on: branches: - main +permissions: + contents: read + jobs: test: name: Build (Python ${{ matrix.python }}) runs-on: ubuntu-latest strategy: matrix: - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] env: TERM: xterm-256color @@ -21,19 +24,18 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v4 + with: + fetch-depth: 0 - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 with: + enable-cache: true python-version: ${{ matrix.python }} - - name: Install Dependencies - run: | - make dev-deps - - name: Build Packages run: | - make build + uv run make build - name: Upload Packages uses: actions/upload-artifact@v4 diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index f3c1a2d..026c94b 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -6,6 +6,9 @@ on: branches: - main +permissions: + contents: read + jobs: test: name: Install (Python ${{ matrix.python }}) @@ -14,11 +17,13 @@ jobs: TERM: xterm-256color strategy: matrix: - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - name: Checkout Code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..fb4bd8a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,42 @@ +name: Publish + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + name: Build and publish to PyPI + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + + - name: Build Packages + run: uv build + + - name: Check version matches tag + run: | + TAG="${GITHUB_REF_NAME#v}" + BUILT="$(ls dist/*.whl | head -1 | cut -d- -f2)" + echo "tag=$TAG built=$BUILT" + if [ "$GITHUB_REF_NAME" != "$TAG" ] && [ "$TAG" != "$BUILT" ]; then + echo "::error::built $BUILT from tag $TAG" + exit 1 + fi + + - name: Publish + run: uv publish --trusted-publishing always diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 2e166c0..e2c04bf 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -6,6 +6,9 @@ on: branches: - main +permissions: + contents: read + jobs: test: name: Linting & Spelling @@ -16,24 +19,23 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v4 + with: + fetch-depth: 0 - - name: Set up Python '3,11' - uses: actions/setup-python@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 with: + enable-cache: true python-version: '3.11' - - name: Install Dependencies - run: | - make dev-deps - - name: Run Quality Assurance run: | - make qa + uv run make qa - name: Run Code Checks run: | - make check + uv run make check - - name: Run Bash Code Checks + - name: Run Pre-Commit run: | - make shellcheck + uv run pre-commit run --all-files diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e29cb9..8e4fb06 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,9 @@ on: branches: - main +permissions: + contents: read + jobs: test: name: Test (Python ${{ matrix.python }}) @@ -14,30 +17,25 @@ jobs: TERM: xterm-256color strategy: matrix: - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - name: Checkout Code uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 with: + enable-cache: true python-version: ${{ matrix.python }} - - name: Install Dependencies - run: | - make dev-deps - - name: Run Tests run: | - make pytest + uv run make pytest - name: Coverage if: ${{ matrix.python == '3.9' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - python -m pip install coveralls - coveralls --service=github - + uvx coveralls --service=github diff --git a/.gitignore b/.gitignore index fa45562..c673bb6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ library/debian/ .coverage .pytest_cache .tox +.venv +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..53210a8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + args: ['--fix=lf'] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.20 + hooks: + - id: ruff-check + args: ['--fix'] + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + additional_dependencies: ['tomli'] + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.10.0.1 + hooks: + - id: shellcheck diff --git a/.stickler.yml b/.stickler.yml deleted file mode 100644 index 2466815..0000000 --- a/.stickler.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -linters: - flake8: - python: 3 - max-line-length: 160 diff --git a/Makefile b/Makefile index 56cf0df..d82edcf 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,13 @@ LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) -LIBRARY_VERSION := $(shell hatch version 2> /dev/null) +LIBRARY_VERSION := $(shell grep -m 1 '^[0-9]' CHANGELOG.md) +BUILD_VERSION := $(shell hatch version 2> /dev/null) -.PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy +.PHONY: usage version install uninstall dev-deps check pre-commit qa pytest nopost tag build clean testdeploy deploy usage: ifdef LIBRARY_NAME @echo "Library: ${LIBRARY_NAME}" - @echo "Version: ${LIBRARY_VERSION}\n" + @echo "Next version: ${LIBRARY_VERSION} (from CHANGELOG.md)" + @echo "Build version: ${BUILD_VERSION} (from the git tag)\n" else @echo "WARNING: You should 'make dev-deps'\n" endif @@ -13,17 +15,19 @@ endif @echo "install: install the library locally from source" @echo "uninstall: uninstall the local library" @echo "dev-deps: install Python dev dependencies" - @echo "check: perform basic integrity checks on the codebase" - @echo "qa: run linting and package QA" + @echo "check: verify CHANGELOG.md has an entry for an untagged version" + @echo "qa: run package QA (check-manifest, build, twine)" + @echo "pre-commit: run pre-commit hooks (lint, whitespace) on all files" @echo "pytest: run Python test fixtures" @echo "clean: clean Python build and dist directories" @echo "build: build Python distribution files" @echo "testdeploy: build and upload to test PyPi" @echo "deploy: build and upload to PyPi" - @echo "tag: tag the repository with the current version\n" + @echo "tag: tag the repository with the version from CHANGELOG.md\n" version: - @hatch version + @echo "${LIBRARY_VERSION} (next, from CHANGELOG.md)" + @echo "${BUILD_VERSION} (build, from the git tag)" install: ./install.sh --unstable @@ -32,14 +36,22 @@ uninstall: ./uninstall.sh dev-deps: - python3 -m pip install -r requirements-dev.txt - sudo apt install dos2unix shellcheck + python3 -m pip install --group dev + pre-commit install check: - @bash check.sh + @if [ -z "${LIBRARY_VERSION}" ]; then \ + echo "No version heading at the top of CHANGELOG.md."; \ + exit 1; \ + fi + @if git rev-parse -q --verify "refs/tags/v${LIBRARY_VERSION}" > /dev/null; then \ + echo "v${LIBRARY_VERSION} is already tagged. Add a CHANGELOG.md entry for the new version."; \ + exit 1; \ + fi + @echo "CHANGELOG.md is ready for v${LIBRARY_VERSION}." -shellcheck: - shellcheck *.sh +pre-commit: + pre-commit run --all-files qa: tox -e qa @@ -48,13 +60,17 @@ pytest: tox -e py nopost: - @bash check.sh --nopost + @POST_VERSION=`echo "${BUILD_VERSION}" | awk -F '.' '{print $$4}'`; \ + if [ -n "$$POST_VERSION" ]; then \ + echo "Found .$$POST_VERSION on the build version; tag a release first."; \ + exit 1; \ + fi -tag: version +tag: check git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" -build: check - @hatch build +build: + uv build clean: -rm -r dist diff --git a/PROJECT_NAME/__init__.py b/PROJECT_NAME/__init__.py index b41daff..e3ec3f7 100644 --- a/PROJECT_NAME/__init__.py +++ b/PROJECT_NAME/__init__.py @@ -1,4 +1,9 @@ -__version__ = '0.0.1' +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("PROJECT_NAME") +except PackageNotFoundError: + __version__ = "0.0.0" def test(): diff --git a/README.md b/README.md index 4652f1b..b457c12 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ product description goes here # Installing -We'd recommend using this library with Raspberry Pi OS Bookworm or later. It requires Python ≥3.7. +We'd recommend using this library with Raspberry Pi OS Bookworm or later. It requires Python ≥3.9. ## Full install (recommended): diff --git a/check.sh b/check.sh deleted file mode 100755 index 3dfe6f1..0000000 --- a/check.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash - -# This script handles some basic QA checks on the source - -NOPOST=$1 -LIBRARY_NAME=$(hatch project metadata name) -LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') -POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') -TERM=${TERM:="xterm-256color"} - -success() { - echo -e "$(tput setaf 2)$1$(tput sgr0)" -} - -inform() { - echo -e "$(tput setaf 6)$1$(tput sgr0)" -} - -warning() { - echo -e "$(tput setaf 1)$1$(tput sgr0)" -} - -while [[ $# -gt 0 ]]; do - K="$1" - case $K in - -p|--nopost) - NOPOST=true - shift - ;; - *) - if [[ $1 == -* ]]; then - printf "Unrecognised option: %s\n" "$1"; - exit 1 - fi - POSITIONAL_ARGS+=("$1") - shift - esac -done - -inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" - -inform "Checking for trailing whitespace..." -if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist -exclude-dir=.venv --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then - warning "Trailing whitespace found!" - exit 1 -else - success "No trailing whitespace found." -fi -printf "\n" - -inform "Checking for DOS line-endings..." -if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox -exclude-dir=.venv --exclude-dir=.git --exclude=Makefile; then - warning "DOS line-endings found!" - exit 1 -else - success "No DOS line-endings found." -fi -printf "\n" - -inform "Checking CHANGELOG.md..." -if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then - warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." - exit 1 -else - success "Changes found for version ${LIBRARY_VERSION}." -fi -printf "\n" - -inform "Checking for git tag ${LIBRARY_VERSION}..." -if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then - warning "Missing git tag for version ${LIBRARY_VERSION}" -fi -printf "\n" - -if [[ $NOPOST ]]; then - inform "Checking for .postN on library version..." - if [[ "$POST_VERSION" != "" ]]; then - warning "Found .$POST_VERSION on library version." - inform "Please only use these for testpypi releases." - exit 1 - else - success "OK" - fi -fi diff --git a/pyproject.toml b/pyproject.toml index b477b15..91c8b72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling", "hatch-fancy-pypi-readme", "hatch-requirements-txt"] +requires = ["hatchling", "hatch-fancy-pypi-readme", "hatch-requirements-txt", "uv-dynamic-versioning"] build-backend = "hatchling.build" [project] @@ -7,7 +7,7 @@ name = "PROJECT_NAME" dynamic = ["version", "readme", "optional-dependencies"] description = "__DESCRIPTION__" license = {file = "LICENSE"} -requires-python = ">= 3.7" +requires-python = ">= 3.9" authors = [ { name = "Philip Howard", email = "phil@pimoroni.com" }, ] @@ -24,11 +24,11 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development", "Topic :: Software Development :: Libraries", @@ -43,15 +43,30 @@ example-depends = ["requirements-examples.txt"] GitHub = "https://www.github.com/pimoroni/PROJECT_NAME-python" Homepage = "https://www.pimoroni.com" +[dependency-groups] +dev = [ + "check-manifest", + "ruff==0.15.20", + "codespell", + "pre-commit", + "twine", + "hatch", + "hatch-fancy-pypi-readme", + "hatch-requirements-txt", + "uv-dynamic-versioning", + "tox", + "pdoc", +] + [tool.hatch.version] -path = "PROJECT_NAME/__init__.py" +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" [tool.hatch.build] include = [ "PROJECT_NAME", - "README.md", - "CHANGELOG.md", - "LICENSE", "requirements-examples.txt" ] @@ -61,7 +76,8 @@ include = [ ] exclude = [ ".*", - "dist" + "dist", + "uv.lock" ] [tool.hatch.metadata.hooks.fancy-pypi-readme] @@ -83,24 +99,23 @@ exclude = [ ] line-length = 200 +[tool.ruff.lint] +extend-select = ["I", "UP", "SIM", "PIE", "C4", "B", "RUF", "PERF", "FURB", "PLE"] + [tool.codespell] skip = """ ./.tox,\ +./.venv,\ ./.egg,\ ./.git,\ ./__pycache__,\ ./build,\ -./dist.\ +./dist\ """ -[tool.isort] -line_length = 200 - [tool.check-manifest] ignore = [ - '.stickler.yml', 'boilerplate.md', - 'check.sh', 'install.sh', 'uninstall.sh', 'Makefile', @@ -108,7 +123,8 @@ ignore = [ 'tests/*', 'examples/*', '.coveragerc', - 'requirements-dev.txt' + '.editorconfig', + '.pre-commit-config.yaml' ] [tool.pimoroni] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index d392e8f..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,10 +0,0 @@ -check-manifest -ruff -codespell -isort -twine -hatch -hatch-fancy-pypi-readme -hatch-requirements-txt -tox -pdoc diff --git a/tox.ini b/tox.ini index 2b6d87b..e9042d6 100644 --- a/tox.ini +++ b/tox.ini @@ -19,9 +19,5 @@ commands = check-manifest python -m build --no-isolation python -m twine check dist/* - isort --check . - ruff check . - codespell . -deps = - -r{toxinidir}/requirements-dev.txt - +dependency_groups = + dev