From 52eb8701eaf453f70ed3bf56a53bfb99775df256 Mon Sep 17 00:00:00 2001 From: JP Hutchins Date: Fri, 21 Aug 2026 16:30:52 -0700 Subject: [PATCH 1/2] build: lock pyright alongside mypy; camas 0.1.18 -> 0.1.29 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add pyright as a second, gating type checker so `typecheck` is `Parallel(mypy, pyright)`. Two checkers disagree in useful ways: everything below was found by pyright on code mypy already passed, and `camas matrix` is green on Python 3.10-3.14 with both. Mirrors the configuration smp adopted in JPHutchins/smp#67, so the editor (Pylance) and the camas CLI share one `[tool.pyright]` (standard mode over `src`, `tests`, `examples`). Groundwork for the `screaming-goblin` port to smp's Frame[T]/msgspec API (intercreate/smpmgr#103): that port rewrites the request path wholesale, and it is far cheaper to have both checkers gating before it starts than to retrofit one after. ## @override must be inside @property 7 properties across 5 transports were decorated @override @property def mtu(self) -> int: ... which applies `override` to the `property` object rather than to the function. mypy accepts it; pyright rejects it (`reportArgumentType`). Swapped to `@property` outermost, verified accepted by both checkers and correct at runtime. ## Explicit `...` bodies on Protocol stubs `SMPTransport` and `SerialFraming` methods had docstring-only bodies. For a stub whose declared return type is not None-compatible, pyright reports `reportReturnType` — "must return value on all code paths". Added the explicit `...` body already used by `smpclient.generics`, and applied it uniformly rather than only where the return type happened to trip the check. ## bytes/bytearray boundaries are now honest mypy implicitly promotes `bytearray`/`memoryview` to `bytes`; pyright does not, and the typing spec has deprecated the promotion. Enabling `disableBytesTypePromotions = false` to paper over the difference was tried and rejected: it makes `case bytes()` look exhaustive when at runtime it does not match a `bytearray`, and it duly broke the `assert_never` in `SMPBumbleTransport._next_chunk`. So the promotion stays off and the four boundaries are stated accurately instead: - `SMPBLETransport._notify_callback` takes `bytearray` — that is what bleak passes it. - `Header.loads()` gets `bytes(...)` at the two callers that sliced a `bytearray` receive buffer (BLE, Bumble); an 8-byte copy. - `SMPUDPTransport.receive()` returns `bytes(message)` rather than returning its `bytearray` accumulator from a `-> bytes` signature. ## bleak 3 specifics - `BleakGATTCharacteristic` is no longer re-exported from the `bleak` top level (`reportPrivateImportUsage`); import it from `bleak.backends.characteristic`, in the transport and in its test. - `_bluez_backend`/`_winrt_backend` took `BaseBleakClient`, but off-platform one of `BleakClientBlueZDBus`/`BleakClientWinRT` is a local `Protocol` stub, and the `and`-guarded branch above the `elif` leaves pyright holding `BaseBleakClient | BleakClientWinRT` — correctly, since a false `if` can mean "was WinRT, but MTU != 20". Both predicates only read `__class__.__name__`, so they now take a `_ClientBackend` alias covering the real backend and both stubs. - The Windows MTU workaround assigns an int to `BleakGATTCharacteristic._max_write_without_response_size`, which bleak declares `Callable[[], int]`. The assignment is deliberate and supported: bleak's own getter does `if isinstance(self._max_write_without_response_size, int): return ...` "for backwards compatibility". Behavior unchanged; the type error is suppressed at that line. `generics.py` still emits 8 `reportInvalidTypeVarUse` warnings (Protocol TypeVars that should be covariant). Warnings do not gate, and that file is reshaped by the smp port — smp's `SMPRequest` already has the correct covariance — so they are left for the port rather than fixed and discarded. Co-Authored-By: Claude Opus 5 (1M context) --- pyproject.toml | 7 +- src/smpclient/transport/__init__.py | 6 ++ src/smpclient/transport/ble.py | 26 ++++-- src/smpclient/transport/bumble/__init__.py | 4 +- src/smpclient/transport/serial/encoded.py | 4 +- .../transport/serial/framing/__init__.py | 4 + src/smpclient/transport/serial/unencoded.py | 2 +- src/smpclient/transport/udp.py | 6 +- tasks.py | 22 +++-- tests/test_smp_ble_transport.py | 3 +- uv.lock | 86 ++++++++++++------- 11 files changed, 112 insertions(+), 58 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f7c0fb..b3700be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,8 @@ dev = [ "pytest-cov>=7.0.0", "pytest-asyncio>=0.23.2", "ruff>=0.12.0", -"camas[mcp,check]==0.1.14", +"camas[mcp]==0.1.29", +"pyright>=1.1,<2", ] doc = [ "mkdocstrings[python]>=0.26.1", @@ -123,3 +124,7 @@ exclude_lines = [ ':\s*\.\.\.\s*$', '^\s*\.\.\.\s*$', ] + +[tool.pyright] +include = ["src", "tests", "examples"] +typeCheckingMode = "standard" diff --git a/src/smpclient/transport/__init__.py b/src/smpclient/transport/__init__.py index 5fa3028..100004c 100644 --- a/src/smpclient/transport/__init__.py +++ b/src/smpclient/transport/__init__.py @@ -29,9 +29,11 @@ async def connect(self, address: str, timeout_s: float) -> None: # pragma: no c address: The SMP server address. timeout_s: The connection timeout in seconds. """ + ... async def disconnect(self) -> None: # pragma: no cover """Disconnect the `SMPTransport`.""" + ... async def send(self, data: bytes) -> None: # pragma: no cover """Send the encoded `SMPRequest` `data`. @@ -39,6 +41,7 @@ async def send(self, data: bytes) -> None: # pragma: no cover Args: data: The encoded `SMPRequest`. """ + ... async def receive(self) -> bytes: # pragma: no cover """Receive the decoded `SMPResponse` data. @@ -46,6 +49,7 @@ async def receive(self) -> bytes: # pragma: no cover Returns: The `SMPResponse` bytes. """ + ... async def send_and_receive(self, data: bytes) -> bytes: # pragma: no cover """Send the encoded `SMPRequest` `data` and receive the decoded `SMPResponse`. @@ -56,6 +60,7 @@ async def send_and_receive(self, data: bytes) -> bytes: # pragma: no cover Returns: The `SMPResponse` bytes. """ + ... def initialize(self, smp_server_transport_buffer_size: int) -> None: # pragma: no cover """Initialize the `SMPTransport` with the server transport buffer size. @@ -68,6 +73,7 @@ def initialize(self, smp_server_transport_buffer_size: int) -> None: # pragma: @property def mtu(self) -> int: # pragma: no cover """The Maximum Transmission Unit (MTU) in 8-bit bytes.""" + ... @property def max_unencoded_size(self) -> int: # pragma: no cover diff --git a/src/smpclient/transport/ble.py b/src/smpclient/transport/ble.py index 17e6826..54b6107 100644 --- a/src/smpclient/transport/ble.py +++ b/src/smpclient/transport/ble.py @@ -5,12 +5,13 @@ import re import sys from collections.abc import Coroutine -from typing import Any, Final, Protocol, TypeGuard, TypeVar +from typing import Any, Final, Protocol, TypeAlias, TypeGuard, TypeVar from uuid import UUID try: - from bleak import BleakClient, BleakGATTCharacteristic, BleakScanner + from bleak import BleakClient, BleakScanner from bleak.args.winrt import WinRTClientArgs + from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient from bleak.backends.device import BLEDevice except ModuleNotFoundError as e: @@ -48,6 +49,15 @@ class BleakClientWinRT(Protocol): def _session(self) -> GattSession: ... +_ClientBackend: TypeAlias = BaseBleakClient | BleakClientBlueZDBus | BleakClientWinRT +"""Any `BleakClient._backend`: the platform's real backend, plus the off-platform stubs. + +On each platform one of `BleakClientBlueZDBus`/`BleakClientWinRT` is bleak's real +`BaseBleakClient` subclass and the other is the local `Protocol` stub, so the backend +predicates below must accept the union to narrow either one. +""" + + MAC_ADDRESS_PATTERN: Final = re.compile(r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE) UUID_PATTERN: Final = re.compile( r"^[a-f0-9]{8}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{12}\Z", @@ -135,7 +145,7 @@ async def _connect(self, address: str, timeout_s: float) -> None: "The SMP characteristic MTU is 20 bytes, possibly a Windows bug, checking again" ) await asyncio.sleep(2) - smp_characteristic._max_write_without_response_size = ( + smp_characteristic._max_write_without_response_size = ( # pyright: ignore[reportAttributeAccessIssue] self._client._backend._session.max_pdu_size - 3 # type: ignore ) self._max_write_without_response_size = ( @@ -182,7 +192,7 @@ async def receive(self) -> bytes: logger.debug(f"Waiting for notify on {SMP_CHARACTERISTIC_UUID=}") await self._notify_or_disconnect() - header: Final = smphdr.Header.loads(self._buffer[: smphdr.Header.SIZE]) + header: Final = smphdr.Header.loads(bytes(self._buffer[: smphdr.Header.SIZE])) logger.debug(f"Received {header=}") message_length: Final = header.length + header.SIZE @@ -199,7 +209,7 @@ async def receive(self) -> bytes: raise SMPBLETransportException("Length of buffer passed expected message size.") await self._notify_or_disconnect() - async def _notify_callback(self, sender: BleakGATTCharacteristic, data: bytes) -> None: + async def _notify_callback(self, sender: BleakGATTCharacteristic, data: bytearray) -> None: if sender.uuid != str(SMP_CHARACTERISTIC_UUID): # pragma: no cover raise SMPBLETransportException(f"Unexpected notify from {sender}; {data=}") async with self._notify_condition: @@ -211,8 +221,8 @@ async def send_and_receive(self, data: bytes) -> bytes: await self.send(data) return await self.receive() - @override @property + @override def mtu(self) -> int: return self._max_write_without_response_size @@ -230,11 +240,11 @@ async def scan(timeout: int = 5) -> list[BLEDevice]: return smp_servers @staticmethod - def _bluez_backend(client_backend: BaseBleakClient) -> TypeGuard[BleakClientBlueZDBus]: + def _bluez_backend(client_backend: _ClientBackend) -> TypeGuard[BleakClientBlueZDBus]: return client_backend.__class__.__name__ == "BleakClientBlueZDBus" @staticmethod - def _winrt_backend(client_backend: BaseBleakClient) -> TypeGuard[BleakClientWinRT]: + def _winrt_backend(client_backend: _ClientBackend) -> TypeGuard[BleakClientWinRT]: return client_backend.__class__.__name__ == "BleakClientWinRT" def _set_disconnected_event(self, client: BleakClient) -> None: diff --git a/src/smpclient/transport/bumble/__init__.py b/src/smpclient/transport/bumble/__init__.py index 79da46e..9e64ac5 100644 --- a/src/smpclient/transport/bumble/__init__.py +++ b/src/smpclient/transport/bumble/__init__.py @@ -298,7 +298,7 @@ async def receive(self) -> bytes: while len(buffer) < smphdr.Header.SIZE: buffer.extend(await self._next_chunk()) - header: Final = smphdr.Header.loads(buffer[: smphdr.Header.SIZE]) + header: Final = smphdr.Header.loads(bytes(buffer[: smphdr.Header.SIZE])) logger.debug(f"Received {header=}") message_length: Final = header.length + smphdr.Header.SIZE @@ -428,8 +428,8 @@ async def pair( case _: assert_never(self._state) - @override @property + @override def mtu(self) -> int: return self._require_connected("mtu").max_write diff --git a/src/smpclient/transport/serial/encoded.py b/src/smpclient/transport/serial/encoded.py index a2d478e..adbe673 100644 --- a/src/smpclient/transport/serial/encoded.py +++ b/src/smpclient/transport/serial/encoded.py @@ -709,13 +709,13 @@ def _could_be_smp_packet_start(self, byte: int) -> bool: """Return True if the given byte value matches the start of any SMP packet delimiter.""" return byte == smppacket.START_DELIMITER[0] or byte == smppacket.CONTINUE_DELIMITER[0] - @override @property + @override def mtu(self) -> int: return self._max_smp_encoded_frame_size - @override @property + @override def max_unencoded_size(self) -> int: """The maximum unencoded SMP message size, in bytes. diff --git a/src/smpclient/transport/serial/framing/__init__.py b/src/smpclient/transport/serial/framing/__init__.py index 470994e..8d7cfc2 100644 --- a/src/smpclient/transport/serial/framing/__init__.py +++ b/src/smpclient/transport/serial/framing/__init__.py @@ -13,9 +13,11 @@ class SerialFraming(Protocol): def encode(self, data: bytes) -> Iterator[bytes]: # pragma: no cover """Yield the wire bytes framing the SMP message `data`.""" + ... def feed(self, data: bytes) -> None: # pragma: no cover """Buffer received bytes for decoding.""" + ... def take(self) -> bytes | None: # pragma: no cover """Return the next decoded SMP message, or `None` if no complete frame is buffered. @@ -23,6 +25,8 @@ def take(self) -> bytes | None: # pragma: no cover Unconsumed bytes persist for the next call (a read may span frame boundaries), and a framing that can detect corruption drops the damaged frame and resynchronises. """ + ... def reset(self) -> None: # pragma: no cover """Discard buffered bytes so a new connection starts clean.""" + ... diff --git a/src/smpclient/transport/serial/unencoded.py b/src/smpclient/transport/serial/unencoded.py index 0c1a281..6cd488b 100644 --- a/src/smpclient/transport/serial/unencoded.py +++ b/src/smpclient/transport/serial/unencoded.py @@ -173,7 +173,7 @@ async def _poll_read_into(self, buf: bytearray) -> None: else: await asyncio.sleep(self._POLLING_INTERVAL_S) - @override @property + @override def mtu(self) -> int: return self._mtu diff --git a/src/smpclient/transport/udp.py b/src/smpclient/transport/udp.py index c06cb6c..9e4e568 100644 --- a/src/smpclient/transport/udp.py +++ b/src/smpclient/transport/udp.py @@ -119,20 +119,20 @@ async def receive(self) -> bytes: raise SMPClientException(error) logger.debug(f"Finished receiving message of length {message_length} B") - return message + return bytes(message) @override async def send_and_receive(self, data: bytes) -> bytes: await self.send(data) return await self.receive() - @override @property + @override def mtu(self) -> int: return self._mtu - @override @property + @override def max_unencoded_size(self) -> int: """Maximum UDP payload size (MSS) to avoid fragmentation. diff --git a/tasks.py b/tasks.py index d845ae1..7f0bdb8 100644 --- a/tasks.py +++ b/tasks.py @@ -2,21 +2,23 @@ from pathlib import Path -from camas import Config, Parallel, Sequential, Task +from camas import Claude, Config, Parallel, Sequential, Task -format = Task("ruff format .", mutates=True) +format = Task("ruff format {paths}", mutates=True, paths=".") lint = Parallel( - Task("ruff check ."), - Task("pydoclint src/smpclient"), + Task("ruff check {paths}", paths="."), + Task("pydoclint {paths}", paths="src/smpclient"), ) fix = Sequential( - Task("ruff check --fix .", mutates=True), - Task("ruff format .", mutates=True), + Task("ruff check --fix {paths}", mutates=True, paths="."), + Task("ruff format {paths}", mutates=True, paths="."), ) -typecheck = Task("mypy .") +mypy = Task("mypy .") +pyright = Task("pyright") +typecheck = Parallel(mypy, pyright) test = Task("pytest -v --ignore=tests/integration") @@ -40,4 +42,8 @@ matrix={"PY": tuple(_PYTHONS)}, ) -_ = Config(default_task=all, github_task=check) +_ = Config( + default_task=all, + github_task=check, + agent=Claude(fix=fix, check=Parallel(lint, typecheck)), +) diff --git a/tests/test_smp_ble_transport.py b/tests/test_smp_ble_transport.py index 2250cdf..5e927ca 100644 --- a/tests/test_smp_ble_transport.py +++ b/tests/test_smp_ble_transport.py @@ -6,7 +6,8 @@ from uuid import UUID import pytest -from bleak import BleakClient, BleakGATTCharacteristic +from bleak import BleakClient +from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.device import BLEDevice from smpclient.requests.os_management import EchoWrite diff --git a/uv.lock b/uv.lock index a5e59c1..1d2ffea 100644 --- a/uv.lock +++ b/uv.lock @@ -401,7 +401,7 @@ wheels = [ [[package]] name = "camas" -version = "0.1.14" +version = "0.1.29" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, @@ -410,40 +410,38 @@ dependencies = [ { name = "tomli", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform == 'emscripten'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/58/e864eb4bbfc33bf2722134e468a815e2ed34b4c330a88cd8372287ba3a75/camas-0.1.14.tar.gz", hash = "sha256:46f6ca636a80c2a993e8b99a1daad5860d75e6baaa429b8a2df87744a023a33b", size = 105557, upload-time = "2026-06-22T23:45:29.907Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/25/243573da57a1080a907ce4dcdd1aeb879c87a71fdd54cb15eb07c8237f6a/camas-0.1.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:248400441ec6c00a3b83aa360b76e8eab992c93e997a97c4538f9c38ef88326a", size = 585390, upload-time = "2026-06-22T23:44:50.437Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/90035b667557a402b0a9edf075ed40479372e7243edebfeea5c05dc7e46a/camas-0.1.14-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:528cc6a0375f612b0a051af5ac3cae07db7345dd590c79b5c24207161796243e", size = 830287, upload-time = "2026-06-22T23:44:52.409Z" }, - { url = "https://files.pythonhosted.org/packages/3f/a8/f834f90509536148539fe1633e8c21d6ba96350d5b76a466ba9760924109/camas-0.1.14-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0849279cc1a26ceff82b4951742c57ff8bdb2e493a591cd5685bb9b47637a7ff", size = 859392, upload-time = "2026-06-22T23:44:54.106Z" }, - { url = "https://files.pythonhosted.org/packages/5a/8f/e00430d9175d6020d2a3e958bf74465918e7087cbce0fb65bb89226d127a/camas-0.1.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd012ec38822d856168b9f3b6a757c436068fb2bedb99101b955c93bc4604834", size = 843681, upload-time = "2026-06-22T23:44:55.55Z" }, - { url = "https://files.pythonhosted.org/packages/72/96/f5c5358be5c6c5f9686b2fe44dd33e3d187c3d7d5b4a26cc8bac34c0b660/camas-0.1.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d2dff9e21fee0ff625e79190ba9f9e08c952baba0402e539b3b024f347412253", size = 852459, upload-time = "2026-06-22T23:44:57.119Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e4/601dc86c54183bd90bb8534b0655ebe37856fc87f288891e08851015a865/camas-0.1.14-cp311-cp311-win_amd64.whl", hash = "sha256:6afdb3943ad3e08d943cb05b012891a7bf2a382e200f24bf5c48c49de369467f", size = 469876, upload-time = "2026-06-22T23:44:58.883Z" }, - { url = "https://files.pythonhosted.org/packages/c2/40/eb55fda60938b1d727791cceae8ef375900d0f0b7957d9d80100416b020a/camas-0.1.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ccb7e0cde2a66d14b4c96cd1f450f2b84d81600178dc5990adb037290d3b52a", size = 588858, upload-time = "2026-06-22T23:45:00.726Z" }, - { url = "https://files.pythonhosted.org/packages/c6/9f/32f69e5c7a9a98fd318d18a6bdc62c4af980cd1c5f3b66bd9a3dd013d80d/camas-0.1.14-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5289b531ebffa036addb4d8db9293193ba6f0bec78a028bac9d63eb453402a25", size = 869033, upload-time = "2026-06-22T23:45:02.159Z" }, - { url = "https://files.pythonhosted.org/packages/85/0c/19f45c889015030c4400226d24a051d8c798669077fad2adc9eea0ccfdb6/camas-0.1.14-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:593134bae953bbf9183860b905eb2b185e00abac24b3b7a3c3fead81779da447", size = 903084, upload-time = "2026-06-22T23:45:03.927Z" }, - { url = "https://files.pythonhosted.org/packages/ad/54/77d15cc1849a946145874d3b352be122aeb306983b040e81880f1a886bfa/camas-0.1.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:db0dd441c62c04ca28aac69c69adca1d8e0b1867dfaa9283919c01b9aefef96c", size = 879939, upload-time = "2026-06-22T23:45:05.393Z" }, - { url = "https://files.pythonhosted.org/packages/8e/2f/a4a48e89bfd360c45089a5952a4f28ca1353cde0b2c3999b92b4642d6c6e/camas-0.1.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:26f2819fc0d99ce082d356bf0b438a54f3d825ba798bc4e54852ee1845e43e39", size = 897585, upload-time = "2026-06-22T23:45:06.903Z" }, - { url = "https://files.pythonhosted.org/packages/59/c4/0300042073863bcdfc5a93ea95b56e8e19f3952867d83a81d381ed5d471b/camas-0.1.14-cp312-cp312-win_amd64.whl", hash = "sha256:45b627280940c60bccd9b935bf2d74cc3455f66120f81e3d3e9264f0c63ffbd8", size = 472034, upload-time = "2026-06-22T23:45:08.459Z" }, - { url = "https://files.pythonhosted.org/packages/5c/48/f1804574bc8e5398079d441cb4633f85087e54cefb67ed9b1d7c662a1af6/camas-0.1.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c34e9a350cc2b9c3b1e9d61db617cd919501b8196f522d7a7b5b1aa67430b11", size = 587239, upload-time = "2026-06-22T23:45:10.295Z" }, - { url = "https://files.pythonhosted.org/packages/f2/31/bcd4a78b21b7a34412d3f11a44168b38447e926337d0495c2171b03d0a96/camas-0.1.14-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acb5a10be5732832ac8025862a31d8d407041b32e19bb6e4be2247f83ae82149", size = 863341, upload-time = "2026-06-22T23:45:12.034Z" }, - { url = "https://files.pythonhosted.org/packages/03/90/2827497de0644a2be9a15d6815decf99598918be56f6e4bffab5e5152be8/camas-0.1.14-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e01d2e04b73280a5baf257c6290d6f0995b37275664bc98bb4cc31ef2394a49", size = 897187, upload-time = "2026-06-22T23:45:13.871Z" }, - { url = "https://files.pythonhosted.org/packages/af/e6/2484df5d8741d283b92eab111d563c83a09298b653df8e1bfbbedcf044ab/camas-0.1.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15208184de3fcf0a4fdc659ed4f16fb75e0eb97504a617fde91630a362e70fad", size = 874294, upload-time = "2026-06-22T23:45:15.564Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a8/c928e47bfbf3679c1a9a1d0b606bb4e62d2d14b5013ff33e5e4138d918ff/camas-0.1.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f726c28f0e64c262e35945d605b309a964738674e73b4457cf9009ab0caa2a94", size = 891703, upload-time = "2026-06-22T23:45:17.094Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e4/9e4c1cb89889cc88d6c4b0bb3dc0e6613aaa4202d52431de4d52e4dca4a3/camas-0.1.14-cp313-cp313-win_amd64.whl", hash = "sha256:893b9791ee18959237c2e76f4c5d37b64d6a630e18c4d0fe2ee761fbe2886823", size = 471982, upload-time = "2026-06-22T23:45:18.763Z" }, - { url = "https://files.pythonhosted.org/packages/dc/0e/f153c685ff51cbd17b252efd7f6021153f7139404a1b25c16dd55cd44223/camas-0.1.14-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c5637e4922d3011eccf44a968dc10d3cc43c58131b980b180e69de81d9e248f5", size = 588289, upload-time = "2026-06-22T23:45:20.223Z" }, - { url = "https://files.pythonhosted.org/packages/74/b1/b811798e525acb1cc2c9585efce8086442031b2be3042156840dc5f4b003/camas-0.1.14-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2941d1f07db4d4e8842e105c834b04ef312488c6bf60fc5891365c246f8ddeeb", size = 871196, upload-time = "2026-06-22T23:45:21.909Z" }, - { url = "https://files.pythonhosted.org/packages/18/a5/ef289003382ad2a019a2dc456903199db4d0aaa57e1884fe209aef0e3783/camas-0.1.14-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:029a6f078c9f33da1b87654f218fec680ee753990a83b1af61d0106203898e66", size = 902410, upload-time = "2026-06-22T23:45:23.556Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f4/f630a916a423468727033b891a133535efeb1dcd81b95310bc8f899b27fd/camas-0.1.14-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f6886fa909c4b542c0f9e8677104e23a0955ebde2e500da08933269840972c2", size = 882215, upload-time = "2026-06-22T23:45:25.249Z" }, - { url = "https://files.pythonhosted.org/packages/ff/bd/d71b0fbbb5239af892c1cb2bf1fc42d41c8aa793446df1a4374a9031bff1/camas-0.1.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f6990f47d68622b81cef71b9ae0467c877ee97f818f84f599e4077fecb948860", size = 897431, upload-time = "2026-06-22T23:45:27.108Z" }, - { url = "https://files.pythonhosted.org/packages/74/cc/a2f235affe65018e81fe6de5ddb2b07e83b16fd0a7394e7402e0c5b67fd6/camas-0.1.14-cp314-cp314-win_amd64.whl", hash = "sha256:609b5a736fabecd950e14aebc6d215b41d506519bc83967979612da88f7c5e2f", size = 478946, upload-time = "2026-06-22T23:45:28.562Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/69/06/478d9a0038bccb9ba04ead373be0f5a797af59f020522850f658112cb071/camas-0.1.29.tar.gz", hash = "sha256:8ed98007cb2f66dc3e20e9d5603cc38546bbb9f11c53104e69633a1f8f43a91c", size = 203898, upload-time = "2026-07-25T01:03:47.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/65/9bca36eafdce947fd3cdede7f18ca90870777a82e2c9f1873cec272c7243/camas-0.1.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34110d8e99a243b47adf8d69ceb5800fde2764fc012ac73182c0eda50d433de7", size = 795507, upload-time = "2026-07-25T01:03:10.789Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/5b42ebdccf99669ba6fcba5a4ca4722b6b36b1fce1746d53b13a7554f787/camas-0.1.29-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0bb62b5e84dd4d1b1ed97f87ca209ec3c6e4db2ad9153b0501bc664bcc8d03e", size = 1075190, upload-time = "2026-07-25T01:03:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/30/a9/70515ee00465992a363bbc06e3239a508babef7826e8746c003717096641/camas-0.1.29-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef870f56899d306c4b7c1dcc581ff35ccf96dbec881d75dc78008b28eeb04344", size = 1115643, upload-time = "2026-07-25T01:03:14.15Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2e/84077a0764af92fc58fc7b5cdb722129451afcb21760f04a4b96bc96de43/camas-0.1.29-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad5193b602622b78af54c9c58e735cc12fefa0c1d5279ce296781c582fb48ecc", size = 1090177, upload-time = "2026-07-25T01:03:15.614Z" }, + { url = "https://files.pythonhosted.org/packages/6e/da/d8489ea89f700c43e8b2e93e47cf2e66a6b8bf9ad071e837e7489d91af9e/camas-0.1.29-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82013ae8268e52ce7828fc8dba29634903cbe95abb5fddcf1613d6591d213824", size = 1105715, upload-time = "2026-07-25T01:03:17.402Z" }, + { url = "https://files.pythonhosted.org/packages/1e/76/02158d9e54d5e45a85f0f8f3ad5d8775180f57a664398f7b4413e7317d9c/camas-0.1.29-cp311-cp311-win_amd64.whl", hash = "sha256:3e0aeac1c4adaec2d386e7ba05be74a4a1c20b4158d2b86b3f5f8e3b1fddd129", size = 638578, upload-time = "2026-07-25T01:03:18.941Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a4/26b9119ddc8eb7255c23d7ee1c55f5f15d7bac610a40702240eebfaaff2a/camas-0.1.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:add1f707ceac5ed49cbf6e80dfd98bc8366f410d272097f5967343fb7fd5d5b0", size = 800063, upload-time = "2026-07-25T01:03:20.573Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3c/c2cec614203bd5fa4c9e52ad8b592ea74fab37f7b8150d81447cad7b0d75/camas-0.1.29-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5644ccd3535b15801b1bcfb6d17b72bb12d27cfbddb68c1a05b3477823c79c37", size = 1123124, upload-time = "2026-07-25T01:03:22.037Z" }, + { url = "https://files.pythonhosted.org/packages/68/54/77c71003e18c53c9de3b5b5ac26206117e8a26f9c963c400d98550f863c6/camas-0.1.29-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04daf15d05f5c62d2284746851f5f426f6301737a5484df86267f14d9a165c66", size = 1168476, upload-time = "2026-07-25T01:03:23.672Z" }, + { url = "https://files.pythonhosted.org/packages/96/e1/4d4c3e1c8fa5e659e94067ef67c03ecbd6e6553b1378bbc95d18d6e009d2/camas-0.1.29-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3c4f3a038feddc9a87b5b47d9016e88de9b92e1c89ab35d05e9bce567d36162", size = 1139385, upload-time = "2026-07-25T01:03:25.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/19/921d2fd08554a7b7bd422b2f0673f475a484ad0bd2de694e5b1b49e422a6/camas-0.1.29-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef6d7ab33139d91a93311f92e19e8b57c2c7eeb5be0aadb7f7523697a7f5b90d", size = 1161448, upload-time = "2026-07-25T01:03:27.007Z" }, + { url = "https://files.pythonhosted.org/packages/6f/65/e49518044baea4d3315a2cfe3f84bd12c4d131498ad12ec7809490fbdfc0/camas-0.1.29-cp312-cp312-win_amd64.whl", hash = "sha256:b500f544801badd0f41ab7eba4827b6578b255682ba045f6e7c95285ec0213f3", size = 640713, upload-time = "2026-07-25T01:03:28.668Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/fe11694f4710b423f7d6006393c51cad4c408f385f5307f86a12254c396d/camas-0.1.29-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52d13d2474524a43660fdfadc121b194e17199dfbd6d8b3cf3cc66f037f01917", size = 787822, upload-time = "2026-07-25T01:03:30.07Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/d666cdb5ac6b75c11fe61869d7bacd19891c23c7743254e7e249fada2e52/camas-0.1.29-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392dafc58df0c5a03fc1b25e6792f7735cc46e7443b7a5006d258f263a819622", size = 1115489, upload-time = "2026-07-25T01:03:31.514Z" }, + { url = "https://files.pythonhosted.org/packages/98/ea/16d764903cce4e76b5e32292d37c0795899ccc06dd614309d43fa0c75e22/camas-0.1.29-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73218e0d4caae670476448aa941bc90f4a63b6231393aa704cd580d887d825da", size = 1162147, upload-time = "2026-07-25T01:03:33.049Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5c/f1aa9b52a112cb65db3d1802d8262af3fa5ca0540a99e03d7196eeab33d8/camas-0.1.29-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7636a0df2df4cb1e8ff7f00dc30d010b5780aac70008c37dc19e41a468145437", size = 1133006, upload-time = "2026-07-25T01:03:34.549Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/00f1b550a4bc30d2b23974da785773955a586cd5d81eb79104e692209460/camas-0.1.29-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e87e1c606edf75939f31b06f09c6856e95576e0f1f85a547bf9bd9ce987800f", size = 1155163, upload-time = "2026-07-25T01:03:35.941Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/0d47a657c273be95579db234f7d9016fb044469009f5800b0b058832374b/camas-0.1.29-cp313-cp313-win_amd64.whl", hash = "sha256:9842e33cf4be81cf869c8d5938eb7b58a6f274df55eb1c52a2d33831ca2c858d", size = 643923, upload-time = "2026-07-25T01:03:37.26Z" }, + { url = "https://files.pythonhosted.org/packages/14/91/c5df906987d3cc8d7ca84a23280ab0c08eaf6818cad464d7974c7d535025/camas-0.1.29-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2b02eeffde6afec72fce736a9827d3044f96c379cf1d8d3c2e1b5fcd1ff290f0", size = 788514, upload-time = "2026-07-25T01:03:38.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/8a/07e0c748dff64dc292ab5e27ca8d0c239aaca1ece69f95478d2774f39e48/camas-0.1.29-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:540b1d0ec48f481ebfdb51cf40760aa39e18dbfd760cea734f966870d755cbb6", size = 1127274, upload-time = "2026-07-25T01:03:39.982Z" }, + { url = "https://files.pythonhosted.org/packages/52/26/fdaf5c62231b80ea5a6e9ad6cbf1a8f6ad746e522f843d472404a664f784/camas-0.1.29-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cffb1a49be03a55cbe689b75d887e5c591bda082b32dee38fe50039241e49f36", size = 1169740, upload-time = "2026-07-25T01:03:41.797Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/576373d05d284cdf56bfc202b757186bff930cb3923215de22a07d07a654/camas-0.1.29-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e1755d53549c7419e24417796524170efd8eeb93e44780648072de80d6132d6", size = 1142613, upload-time = "2026-07-25T01:03:43.352Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/ade1ab5bcb51b0c356c99ab40abaa47eb281fbef150696d24d62b69338d3/camas-0.1.29-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b890b6d0bfa2b78e22f2766e6cb79dde9aa67432eab67b2ab1497870b0065ba", size = 1163695, upload-time = "2026-07-25T01:03:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/2d/af/308bc3a442401135a9b2e3d0369b6ad7748bb6b7155f704272c6ee9fc49c/camas-0.1.29-cp314-cp314-win_amd64.whl", hash = "sha256:f93b90c0321712c023d0047ffefa084c6968cc1a569e6e5edaecdbdf747739d8", size = 654438, upload-time = "2026-07-25T01:03:46.176Z" }, ] [package.optional-dependencies] -check = [ - { name = "ty" }, -] mcp = [ { name = "mcp" }, + { name = "ty" }, ] [[package]] @@ -2068,6 +2066,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -2612,6 +2619,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] +[[package]] +name = "pyright" +version = "1.1.411" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, +] + [[package]] name = "pyserial" version = "3.5" @@ -3265,11 +3285,12 @@ serial = [ [package.dev-dependencies] dev = [ - { name = "camas", extra = ["check", "mcp"] }, + { name = "camas", extra = ["mcp"] }, { name = "hatch" }, { name = "mypy" }, { name = "mypy-extensions" }, { name = "pydoclint" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -3308,11 +3329,12 @@ provides-extras = ["all", "ble", "bumble", "hci-firmware", "serial", "udp"] [package.metadata.requires-dev] dev = [ - { name = "camas", extras = ["mcp", "check"], specifier = "==0.1.14" }, + { name = "camas", extras = ["mcp"], specifier = "==0.1.29" }, { name = "hatch", specifier = ">=1.14.1" }, { name = "mypy", specifier = ">=1.7.0" }, { name = "mypy-extensions", specifier = ">=1.0.0" }, { name = "pydoclint", specifier = ">=0.5.8" }, + { name = "pyright", specifier = ">=1.1,<2" }, { name = "pytest", specifier = ">=8.4.0" }, { name = "pytest-asyncio", specifier = ">=0.23.2" }, { name = "pytest-cov", specifier = ">=7.0.0" }, From a43f5e66408dbbe33815a58c40ee84e416c960a2 Mon Sep 17 00:00:00 2001 From: JP Hutchins Date: Fri, 21 Aug 2026 17:46:48 -0700 Subject: [PATCH 2/2] build: regenerate the camas MCP/Claude wiring for 0.1.29 `camas mcp init --claude` on 0.1.29. The only tracked change is `.mcp.json` dropping `--rich`, which 0.1.29 still accepts but now only for back-compat (rich output is the default; `--plain` is the opt-out). The rest of what init wrote is untracked -- `.claude/` is gitignored -- but is the reason to run it, and one of those changes fixes a hazard this session hit for real: - The `PostToolBatch` autofix hook becomes `uv run camas mcp fix || exit 0`. Without the `|| exit 0`, any non-zero exit from the hook stops the agent's tool continuation. That is not hypothetical: checking out a branch based on `main` reverted `pyproject.toml` to the old `camas==0.1.14` pin, `uv run` re-synced the venv down to it, and 0.1.14 has no `camas mcp fix` subcommand -- so the hook failed to parse its arguments and blocked the session until the venv was resynced. The guard makes the hook advisory instead of fatal. - Adds the `Stop` hooks 0.1.29 expects: a second autofix pass, plus an async `camas mcp gate --under 5s --nudge` rewake. - Adds the tiered `camas-lint-fixer-haiku` / `camas-lint-fixer-sonnet` / `camas-test-fixer` agents and the `gate` skill. Verified: `camas mcp fix --paths ...` and `camas mcp gate --under 5s --paths ...` both exit 0, and `camas_check` reports the 12-task definition valid. Co-Authored-By: Claude Opus 5 (1M context) --- .mcp.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.mcp.json b/.mcp.json index edad6fc..ef75f90 100644 --- a/.mcp.json +++ b/.mcp.json @@ -3,12 +3,7 @@ "camas": { "type": "stdio", "command": "uv", - "args": [ - "run", - "camas", - "mcp", - "--rich" - ] + "args": ["run", "camas", "mcp"] } } }