From 4ebb900a12e412fca48e70e387d5f74e0e1c2110 Mon Sep 17 00:00:00 2001 From: Martin Holmberg Date: Thu, 4 Jun 2026 13:51:47 +0200 Subject: [PATCH 1/4] Add CI and Ruff linting --- .github/workflows/ci.yml | 36 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 3 +++ examples/qt_example/qt_example.py | 2 +- qtm_rt/__init__.py | 21 ++++++++---------- qtm_rt/control.py | 1 - qtm_rt/discovery.py | 5 ++--- qtm_rt/protocol.py | 3 +-- qtm_rt/qrt.py | 4 ++-- qtm_rt/receiver.py | 8 +++---- requirements-dev.txt | 1 + test/qrtconnection_test.py | 2 +- test/qtmprotocol_test.py | 2 +- 12 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a5c3014 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + +jobs: + test: + name: Lint and test Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-dev.txt + + - name: Lint + run: python -m ruff check . + + - name: Run tests + run: python -m pytest test/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f8ba66..c371c5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,9 @@ jobs: python -m pip install --upgrade pip python -m pip install -r requirements-dev.txt + - name: Lint + run: python -m ruff check . + - name: Run tests run: python -m pytest test/ diff --git a/examples/qt_example/qt_example.py b/examples/qt_example/qt_example.py index 834a6d0..70fee81 100644 --- a/examples/qt_example/qt_example.py +++ b/examples/qt_example/qt_example.py @@ -70,7 +70,7 @@ async def _discover_qtm(self, interface): async for qtm_instance in qtm_rt.Discover(interface): info = qtm_instance.info.decode("utf-8").split(",")[0] - if not info in self._found_qtms: + if info not in self._found_qtms: self.discoveredQTM.emit(info, qtm_instance.host) self._found_qtms[info] = True except Exception: diff --git a/qtm_rt/__init__.py b/qtm_rt/__init__.py index e934db9..cf16399 100644 --- a/qtm_rt/__init__.py +++ b/qtm_rt/__init__.py @@ -1,20 +1,17 @@ """ Python SDK for QTM """ import logging -import sys import os -PYTHON3 = sys.version_info.major == 3 - -if PYTHON3: - from .discovery import Discover - from .reboot import reboot - from .qrt import connect, QRTConnection - from .protocol import QRTCommandException - from .control import TakeControl - -from .packet import QRTPacket, QRTEvent -from .receiver import Receiver +from .control import TakeControl as TakeControl +from .discovery import Discover as Discover +from .packet import QRTEvent as QRTEvent +from .packet import QRTPacket as QRTPacket +from .protocol import QRTCommandException as QRTCommandException +from .qrt import QRTConnection as QRTConnection +from .qrt import connect as connect +from .reboot import reboot as reboot +from .receiver import Receiver as Receiver # pylint: disable=C0330 diff --git a/qtm_rt/control.py b/qtm_rt/control.py index 55c98d9..433d463 100644 --- a/qtm_rt/control.py +++ b/qtm_rt/control.py @@ -1,4 +1,3 @@ -import asyncio import logging from .qrt import QRTConnection diff --git a/qtm_rt/discovery.py b/qtm_rt/discovery.py index 936e170..7c9efa4 100644 --- a/qtm_rt/discovery.py +++ b/qtm_rt/discovery.py @@ -75,9 +75,8 @@ async def __anext__(self) -> QRTDiscoveryResponse: loop = asyncio.get_event_loop() if self.first: - protocol_factory = lambda: QRTDiscoveryProtocol( - receiver=self.queue.put_nowait - ) + def protocol_factory(): + return QRTDiscoveryProtocol(receiver=self.queue.put_nowait) _, protocol = await loop.create_datagram_endpoint( protocol_factory, diff --git a/qtm_rt/protocol.py b/qtm_rt/protocol.py index a6f248d..8ac0f0d 100644 --- a/qtm_rt/protocol.py +++ b/qtm_rt/protocol.py @@ -8,8 +8,7 @@ import logging from qtm_rt.packet import QRTPacketType -from qtm_rt.packet import QRTPacket, QRTEvent -from qtm_rt.packet import RTheader, RTEvent, RTCommand +from qtm_rt.packet import RTheader, RTCommand from qtm_rt.receiver import Receiver # pylint: disable=C0330 diff --git a/qtm_rt/qrt.py b/qtm_rt/qrt.py index ff4b014..2e0f449 100644 --- a/qtm_rt/qrt.py +++ b/qtm_rt/qrt.py @@ -104,7 +104,7 @@ async def get_parameters(self, parameters=None): parameters = ["all"] else: for parameter in parameters: - if not parameter in [ + if parameter not in [ "all", "general", "3d", @@ -374,7 +374,7 @@ async def connect( def _validate_components(components): for component in components: - if not component.lower() in [ + if component.lower() not in [ "2d", "2dlin", "3d", diff --git a/qtm_rt/receiver.py b/qtm_rt/receiver.py index 82b6fd3..cdf2e5b 100644 --- a/qtm_rt/receiver.py +++ b/qtm_rt/receiver.py @@ -2,7 +2,7 @@ from qtm_rt.packet import QRTPacketType from qtm_rt.packet import QRTPacket, QRTEvent -from qtm_rt.packet import RTheader, RTEvent, RTCommand +from qtm_rt.packet import RTheader, RTEvent LOG = logging.getLogger("qtm_rt") @@ -17,16 +17,16 @@ def data_received(self, data): self._received_data += data h_size = RTheader.size data = self._received_data - data_len = len(data); + data_len = len(data) while data_len >= h_size: size, type_ = RTheader.unpack_from(data, 0) if data_len >= size: self._parse_received(data[h_size:size], type_) data = data[size:] - data_len = len(data); + data_len = len(data) else: - break; + break self._received_data = data diff --git a/requirements-dev.txt b/requirements-dev.txt index 8555572..6bdf805 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,5 +2,6 @@ build==1.2.1 pytest-asyncio==0.23.7 pytest-mock==3.14.0 pytest==8.2.2 +ruff==0.15.15 sphinx==7.3.7 twine==6.2.0 diff --git a/test/qrtconnection_test.py b/test/qrtconnection_test.py index d1227bd..b132cda 100644 --- a/test/qrtconnection_test.py +++ b/test/qrtconnection_test.py @@ -316,7 +316,7 @@ async def xml(*_): a_qrt._protocol.receive_response.side_effect = xml with pytest.raises(QRTCommandException): - response = await a_qrt.calibrate() + await a_qrt.calibrate() a_qrt._protocol.send_command.assert_called_once_with("calibrate") diff --git a/test/qtmprotocol_test.py b/test/qtmprotocol_test.py index ca705cc..8ba60af 100644 --- a/test/qtmprotocol_test.py +++ b/test/qtmprotocol_test.py @@ -8,7 +8,7 @@ import pytest from qtm_rt.protocol import QTMProtocol, QRTCommandException -from qtm_rt.packet import QRTEvent, RTEvent +from qtm_rt.packet import QRTEvent # pylint: disable=W0621, C0111, W0212 From 5d520637778f31acff3f5aba832b821aa1faed2c Mon Sep 17 00:00:00 2001 From: Martin Holmberg Date: Thu, 4 Jun 2026 14:22:34 +0200 Subject: [PATCH 2/4] Make Ruff rule set explicit in pyproject.toml Pin Ruff's effective configuration so a future Ruff upgrade can't silently change what CI enforces: - line-length = 88 (current default) - target-version = py310 (matches requires-python) - lint.select = current default set (E4, E7, E9, F) No behaviour change: ruff check . still passes. Comment documents how to broaden (e.g. I for import sorting, UP for pyupgrade). Co-Authored-By: Claude Opus 4.8 --- pyproject.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9c48299..97d66ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,13 @@ Source = "https://github.com/qualisys/qualisys_python_sdk" packages = ["qtm_rt"] zip-safe = true include-package-data = false + +[tool.ruff] +line-length = 88 +target-version = "py310" + +[tool.ruff.lint] +# Ruff's default rule set (pyflakes + a subset of pycodestyle), pinned +# explicitly so a Ruff version bump can't silently change what CI enforces. +# Broaden deliberately, e.g. add "I" (import sorting) or "UP" (pyupgrade). +select = ["E4", "E7", "E9", "F"] From 9259493b00adfb758804f212c506d9e1f55fca73 Mon Sep 17 00:00:00 2001 From: Martin Holmberg Date: Thu, 4 Jun 2026 14:27:54 +0200 Subject: [PATCH 3/4] Bump GitHub Actions to Node 24 runtimes The Node 20 action runtime is deprecated: GitHub forces Node 24 from 2026-06-16 and removes Node 20 on 2026-09-16. Update to the latest majors, which run on Node 24: - actions/checkout v4 -> v6 - actions/setup-python v5 -> v6 - actions/upload-artifact v4 -> v7 (release.yml) - actions/download-artifact v4 -> v8 (release.yml) upload-artifact v7 and download-artifact v8 co-evolved and remain compatible; the default zip round-trip used here is unchanged. pypa/gh-action-pypi-publish is a container action and is unaffected. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5c3014..ade634e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c371c5d..c42e803 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,10 +11,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" @@ -36,7 +36,7 @@ jobs: run: python -m twine check dist/* - name: Upload release distributions - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: distributions path: dist/ @@ -51,7 +51,7 @@ jobs: steps: - name: Download release distributions - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: distributions path: dist/ @@ -68,7 +68,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Package examples and demo data run: | From ab5a511a427de76f76fab72bd80258d4ebf319e4 Mon Sep 17 00:00:00 2001 From: Martin Holmberg Date: Thu, 4 Jun 2026 14:49:18 +0200 Subject: [PATCH 4/4] CI: run push builds only on master to avoid duplicate PR runs push.branches "**" matched every branch, so each commit on a PR branch triggered both a push run and a pull_request run for the same SHA. Scope push to master; pull_request already covers all PRs (including forks, built against the merge result). Branches without an open PR no longer build until a PR is opened. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ade634e..4c08341 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: ["**"] + branches: [master] pull_request: jobs: