From 84059c3cb5be12fcd1fd0c693a30758ea1267995 Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 14:21:59 +0100 Subject: [PATCH 01/13] Local docker based test env --- Dockerfile.testing | 27 ++++++ requirements-dev.lock | 197 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 Dockerfile.testing create mode 100644 requirements-dev.lock diff --git a/Dockerfile.testing b/Dockerfile.testing new file mode 100644 index 0000000..6b0cdf3 --- /dev/null +++ b/Dockerfile.testing @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:1 +# Stage 1: UV binary source +FROM ghcr.io/astral-sh/uv:0.7.13 AS uv-base + +# Stage 2: Testing environment +FROM python:3.11-slim-bookworm AS testing + +# Copy UV binary from uv-base stage +COPY --from=uv-base /uv /uvx /usr/local/bin/ + +# Install system dependencies +# hadolint ignore=DL3008 +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + make \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy dependency lock file first for layer caching +COPY requirements-dev.lock ./ + +# Install dev dependencies from pinned lock file +RUN uv pip install --system --no-cache -r requirements-dev.lock + +# Copy project source +COPY . . diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..de6e548 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,197 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements-dev.txt --output-file requirements-dev.lock --python-version 3.11 +anyio==4.13.0 + # via httpx +backports-tarfile==1.2.0 + # via jaraco-context +backports-zstd==1.5.0 + # via hatch +build==1.5.0 + # via check-manifest +cachetools==7.1.4 + # via tox +certifi==2026.5.20 + # via + # httpcore + # httpx + # requests +cffi==2.0.0 + # via cryptography +charset-normalizer==3.4.7 + # via requests +check-manifest==0.51 + # via -r requirements-dev.txt +click==8.4.1 + # via + # hatch + # userpath +codespell==2.4.2 + # via -r requirements-dev.txt +colorama==0.4.6 + # via tox +cryptography==48.0.0 + # via secretstorage +distlib==0.4.0 + # via virtualenv +docutils==0.23 + # via readme-renderer +filelock==3.29.0 + # via + # python-discovery + # tox + # virtualenv +h11==0.16.0 + # via httpcore +hatch==1.16.5 + # via -r requirements-dev.txt +hatch-fancy-pypi-readme==25.1.0 + # via -r requirements-dev.txt +hatchling==1.29.0 + # via + # hatch + # hatch-fancy-pypi-readme +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via hatch +hyperlink==21.0.0 + # via hatch +id==1.6.1 + # via twine +idna==3.17 + # via + # anyio + # httpx + # hyperlink + # requests +importlib-metadata==9.0.0 + # via keyring +isort==8.0.1 + # via -r requirements-dev.txt +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.1.2 + # via keyring +jaraco-functools==4.5.0 + # via keyring +jeepney==0.9.0 + # via + # keyring + # secretstorage +jinja2==3.1.6 + # via pdoc +keyring==25.7.0 + # via + # hatch + # twine +markdown-it-py==4.2.0 + # via rich +markdown2==2.5.5 + # via pdoc +markupsafe==3.0.3 + # via + # jinja2 + # pdoc +mdurl==0.1.2 + # via markdown-it-py +more-itertools==11.1.0 + # via + # jaraco-classes + # jaraco-functools +nh3==0.3.5 + # via readme-renderer +packaging==26.2 + # via + # build + # hatch + # hatchling + # pyproject-api + # tox + # twine +pathspec==1.1.1 + # via hatchling +pdoc==16.0.0 + # via -r requirements-dev.txt +pexpect==4.9.0 + # via hatch +platformdirs==4.10.0 + # via + # hatch + # python-discovery + # tox + # virtualenv +pluggy==1.6.0 + # via + # hatchling + # tox +ptyprocess==0.7.0 + # via pexpect +pycparser==3.0 + # via cffi +pygments==2.20.0 + # via + # pdoc + # readme-renderer + # rich +pyproject-api==1.10.1 + # via tox +pyproject-hooks==1.2.0 + # via + # build + # hatch +python-discovery==1.4.0 + # via + # hatch + # tox + # virtualenv +readme-renderer==44.0 + # via twine +requests==2.34.2 + # via + # requests-toolbelt + # twine +requests-toolbelt==1.0.0 + # via twine +rfc3986==2.0.0 + # via twine +rich==15.0.0 + # via + # hatch + # twine +ruff==0.15.15 + # via -r requirements-dev.txt +secretstorage==3.5.0 + # via keyring +setuptools==82.0.1 + # via check-manifest +shellingham==1.5.4 + # via hatch +tomli-w==1.2.0 + # via + # hatch + # tox +tomlkit==0.15.0 + # via hatch +tox==4.55.0 + # via -r requirements-dev.txt +trove-classifiers==2026.5.22.10 + # via hatchling +twine==6.2.0 + # via -r requirements-dev.txt +typing-extensions==4.15.0 + # via anyio +urllib3==2.7.0 + # via + # id + # requests + # twine +userpath==1.9.2 + # via hatch +uv==0.11.17 + # via hatch +virtualenv==21.4.1 + # via + # hatch + # tox +zipp==4.1.0 + # via importlib-metadata From da86e2dc94bb9cfb367875504853691a2088cd07 Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 14:27:34 +0100 Subject: [PATCH 02/13] More tools, and build docs --- Dockerfile.testing | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Dockerfile.testing b/Dockerfile.testing index 6b0cdf3..098d0a5 100644 --- a/Dockerfile.testing +++ b/Dockerfile.testing @@ -1,4 +1,13 @@ # syntax=docker/dockerfile:1 +# +# Tagging convention: inventorhatmini-dev:python-v +# python-ver — Python minor version in the base image (e.g. 3.11) +# testing-ver — SemVer, always at least one patch ahead of the released +# inventorhatmini library version (e.g. library 1.0.0 → testing v1.0.1) +# +# Build example: +# docker build -f Dockerfile.testing -t inventorhatmini-dev:python3.11-v1.0.1 . +# # Stage 1: UV binary source FROM ghcr.io/astral-sh/uv:0.7.13 AS uv-base @@ -13,6 +22,8 @@ COPY --from=uv-base /uv /uvx /usr/local/bin/ RUN apt-get update \ && apt-get install -y --no-install-recommends \ make \ + dos2unix \ + shellcheck \ && rm -rf /var/lib/apt/lists/* WORKDIR /app From 46556c40bee96ea656fdef0f7695f276ddc91491 Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 14:35:41 +0100 Subject: [PATCH 03/13] Real linter issue --- firmware/set_i2c_address.py | 38 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/firmware/set_i2c_address.py b/firmware/set_i2c_address.py index d99ec36..347a4d9 100644 --- a/firmware/set_i2c_address.py +++ b/firmware/set_i2c_address.py @@ -9,18 +9,18 @@ # flash write ongoing # flash write ongoing # flash write finished -# pi@raspberrypi:~ $ -# pi@raspberrypi:~ $ +# pi@raspberrypi:~ $ +# pi@raspberrypi:~ $ # pi@raspberrypi:~ $ i2cdetect -y 1 # 0 1 2 3 4 5 6 7 8 9 a b c d e f -# 00: -- -- -- -- -- -- -- -- -- -- -- -- -- -# 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 20: -- -- 22 -- -- -- -- -- -- -- -- -- -- -- -- -- -# 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 70: -- -- -- -- -- -- -- -- +# 00: -- -- -- -- -- -- -- -- -- -- -- -- -- +# 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 20: -- -- 22 -- -- -- -- -- -- -- -- -- -- -- -- -- +# 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 70: -- -- -- -- -- -- -- -- # pi@raspberrypi:~ $ python set_i2c_address.py 0x22 0x23 # Waiting for flash writing to start.. # flash write ongoing @@ -33,14 +33,14 @@ # flash write finished # pi@raspberrypi:~ $ i2cdetect -y 1 # 0 1 2 3 4 5 6 7 8 9 a b c d e f -# 00: -- -- -- -- -- -- -- -- -- -- -- -- -- -# 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 20: -- -- -- 23 -- -- -- -- -- -- -- -- -- -- -- -- -# 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 70: -- -- -- -- -- -- -- -- +# 00: -- -- -- -- -- -- -- -- -- -- -- -- -- +# 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 20: -- -- -- 23 -- -- -- -- -- -- -- -- -- -- -- -- +# 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 70: -- -- -- -- -- -- -- -- import time import os @@ -52,7 +52,7 @@ GPIO.setmode(GPIO.BCM) CHIP_ID = 0xBA11 -VERSION = 1 +VERSION = 1 # Registers specific to the trackball From c88049a8e2a2db5097c2efb114f21f7726ac12d9 Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 14:36:17 +0100 Subject: [PATCH 04/13] Not all child processes were getting this --- check.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/check.sh b/check.sh index 38dfc3a..177d3b8 100755 --- a/check.sh +++ b/check.sh @@ -7,6 +7,7 @@ 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"} +export TERM success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" From 7f89512f3bb446f4ad6c2bbdb364d3e5ba25ac5e Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 14:41:24 +0100 Subject: [PATCH 05/13] Add git - so make check can pass when upstream tags present. --- Dockerfile.testing | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.testing b/Dockerfile.testing index 098d0a5..a59666c 100644 --- a/Dockerfile.testing +++ b/Dockerfile.testing @@ -24,6 +24,7 @@ RUN apt-get update \ make \ dos2unix \ shellcheck \ + git \ && rm -rf /var/lib/apt/lists/* WORKDIR /app From c1444869419bebeff98db788e21ce122e5c0421d Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 14:45:36 +0100 Subject: [PATCH 06/13] Make QA isort issues --- firmware/firmware_update.py | 3 --- firmware/set_i2c_address.py | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/firmware/firmware_update.py b/firmware/firmware_update.py index 90fd589..4f810da 100644 --- a/firmware/firmware_update.py +++ b/firmware/firmware_update.py @@ -5,14 +5,11 @@ # Usage: python firmware_update.py filename.hex import binascii - import sys import time - from smbus2 import SMBus, i2c_msg - # Small nuvoton: bootloader_id = 0xB001 bootloader_version = 172 diff --git a/firmware/set_i2c_address.py b/firmware/set_i2c_address.py index 347a4d9..cc44938 100644 --- a/firmware/set_i2c_address.py +++ b/firmware/set_i2c_address.py @@ -42,13 +42,13 @@ # 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 70: -- -- -- -- -- -- -- -- -import time import os import sys +import time +import RPi.GPIO as GPIO from smbus2 import SMBus, i2c_msg -import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) CHIP_ID = 0xBA11 From 791d6ef45f6220cab9c9be0915687ffe98b2d3b4 Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 14:45:59 +0100 Subject: [PATCH 07/13] qa should not try to install - default github will be x86 not pi/arm --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 4726cef..5bc5040 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ deps = build [testenv:qa] +skip_install = true commands = check-manifest python -m build --no-isolation From f4750ad8c56a215f361d2d154890445309b9a62d Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 15:06:35 +0100 Subject: [PATCH 08/13] Let's not write any linter fixes (ruff?) as root. --- Dockerfile.testing | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Dockerfile.testing b/Dockerfile.testing index a59666c..2193f31 100644 --- a/Dockerfile.testing +++ b/Dockerfile.testing @@ -27,6 +27,13 @@ RUN apt-get update \ git \ && rm -rf /var/lib/apt/lists/* +# Create a non-root user matching typical host UID/GID (1000:1000). +# Override at runtime with: --user $(id -u):$(id -g) +ARG UID=1000 +ARG GID=1000 +RUN groupadd -g "${GID}" appuser \ + && useradd -l -u "${UID}" -g "${GID}" -m appuser + WORKDIR /app # Copy dependency lock file first for layer caching @@ -37,3 +44,5 @@ RUN uv pip install --system --no-cache -r requirements-dev.lock # Copy project source COPY . . + +USER appuser From c7a880a1fe3a501f7e74e37e0d47810fa9c6d1a4 Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 15:06:59 +0100 Subject: [PATCH 09/13] Fixes for a green QA stage --- firmware/firmware_update.py | 6 +++--- firmware/set_i2c_address.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/firmware/firmware_update.py b/firmware/firmware_update.py index 4f810da..669e17d 100644 --- a/firmware/firmware_update.py +++ b/firmware/firmware_update.py @@ -105,7 +105,7 @@ def write_page_to_aprom(): try: i2c_read8(bootloader_i2c_addr, 0x00) break - except: + except Exception: print("waiting for page write to finish...") def read_page_from_aprom(): @@ -123,7 +123,7 @@ def firmware_update(bin_data, i2c_address, chip_id): try: i2c_read8(bootloader_i2c_addr, 0x00) - except: + except Exception: # We're not in bootloader yet, let's enter first confirm_id(i2c_address, chip_id) enter_bootloader(i2c_address) @@ -184,7 +184,7 @@ def firmware_update(bin_data, i2c_address, chip_id): if __name__ == "__main__": - filename = sys.argv[1]; + filename = sys.argv[1] # if bin_filename.endswith(".bin"): # bin_data = open(bin_filename, "rb").read() # bin_data = [ord(x) for x in bin_data] diff --git a/firmware/set_i2c_address.py b/firmware/set_i2c_address.py index cc44938..98f045e 100644 --- a/firmware/set_i2c_address.py +++ b/firmware/set_i2c_address.py @@ -42,7 +42,6 @@ # 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 70: -- -- -- -- -- -- -- -- -import os import sys import time From fd1bf3ce5776c28aaf61eb4816da72d4758c86e7 Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 15:22:30 +0100 Subject: [PATCH 10/13] Initial contributing guide --- CONTRIBUTING.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..61f8497 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing + +## Development environment + +A Docker-based testing image is provided so checks can be run against multiple Python versions +without installing tooling locally. Python 3.11 is the first version set up; others will follow. + +### Build the image + +Pass your host UID and GID so that files written inside the container are owned by your user, +not root: + +```bash +docker build -f Dockerfile.testing \ + --build-arg UID=$(id -u) \ + --build-arg GID=$(id -g) \ + -t inventorhatmini-dev:python3.11-v1.0.1 . +``` + +> **Image tag convention:** `inventorhatmini-dev:python-v` +> The testing version is always at least one patch ahead of the released library version +> (e.g. library `1.0.0` → testing image `v1.0.1`). + +### Run checks + +All commands below mount the repository into the container so changes are picked up without +a rebuild. Run them from the repository root. + +The `git config` prefix used in some commands below is required because Git's ownership +safety check fires on volume-mounted directories. + +**Integrity checks** (trailing whitespace, DOS line-endings, CHANGELOG entry, git tag): + +```bash +docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 \ + bash -c 'git config --global --add safe.directory /app && make check' +``` + +**Shell script linting:** + +```bash +docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 make shellcheck +``` + +**QA** (ruff, isort, codespell, check-manifest, build, twine check): + +```bash +docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 \ + bash -c 'git config --global --add safe.directory /app && make qa' +``` + +### Dependency lock file + +The image installs from `requirements-dev.lock`. Regenerate it when `requirements-dev.txt` changes: + +```bash +uv pip compile requirements-dev.txt --output-file requirements-dev.lock --python-version 3.11 +``` + +Then rebuild the image. From ee4c73af75eabf24f5df28ebcd3ec7caafaa8af4 Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 16:45:32 +0100 Subject: [PATCH 11/13] Fix around the git config. --- CONTRIBUTING.md | 9 ++------- Dockerfile.testing | 4 ++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61f8497..0577f98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,14 +26,10 @@ docker build -f Dockerfile.testing \ All commands below mount the repository into the container so changes are picked up without a rebuild. Run them from the repository root. -The `git config` prefix used in some commands below is required because Git's ownership -safety check fires on volume-mounted directories. - **Integrity checks** (trailing whitespace, DOS line-endings, CHANGELOG entry, git tag): ```bash -docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 \ - bash -c 'git config --global --add safe.directory /app && make check' +docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 make check ``` **Shell script linting:** @@ -45,8 +41,7 @@ docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 make shel **QA** (ruff, isort, codespell, check-manifest, build, twine check): ```bash -docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 \ - bash -c 'git config --global --add safe.directory /app && make qa' +docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 make qa ``` ### Dependency lock file diff --git a/Dockerfile.testing b/Dockerfile.testing index 2193f31..f650c0e 100644 --- a/Dockerfile.testing +++ b/Dockerfile.testing @@ -46,3 +46,7 @@ RUN uv pip install --system --no-cache -r requirements-dev.lock COPY . . USER appuser + +# Allow git to read the volume-mounted repo (ownership matches host UID but +# git's safe.directory check still fires on mounted directories). +RUN git config --global --add safe.directory /app From ef8458155d2ba2c34ab1f691bdb9491b747f25b2 Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 16:50:31 +0100 Subject: [PATCH 12/13] Fix test env --- tox.ini | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 5bc5040..99963b2 100644 --- a/tox.ini +++ b/tox.ini @@ -5,14 +5,23 @@ isolated_build = true minversion = 4.0.0 [testenv] -commands = - coverage run -m pytest -v -r wsx - coverage report +# rpi_ws281x is a Raspberry Pi hardware library with a C extension that cannot +# be compiled on x86. All hardware deps are mocked in the test suite, so we +# skip the package install and install it manually with --no-deps. +skip_install = true deps = mock pytest>=3.1 pytest-cov build + pimoroni-ioexpander>=1.0.1 + gpiodevice + smbus2 +commands_pre = + pip install --no-deps {toxinidir} +commands = + coverage run -m pytest -v -r wsx + coverage report [testenv:qa] skip_install = true From ffbcfbe1c9851229503ca78e9aa3e56ab5365405 Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Sat, 30 May 2026 17:12:10 +0100 Subject: [PATCH 13/13] Pick up the current upload-artifact --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ddee38e..703f122 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: make dist - name: Upload Packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v7 with: name: ${{ env.RELEASE_FILE }} path: dist/