From b8a5d277d1e6fb53df7aef56e8fe4af72eaef960 Mon Sep 17 00:00:00 2001 From: JP Hutchins Date: Tue, 3 Mar 2026 11:10:44 -0800 Subject: [PATCH 1/2] BREAKING: ble and serial dependencies are optional Use [ble], [serial], or [all] to enable the transports that are required. Existing projects are encouraged to use [all] to minimize migration friction. --- .github/workflows/test.yaml | 50 +++++++++++++++++++++++++++++++ README.md | 22 ++++++++++++++ pyproject.toml | 9 ++++-- src/smpclient/__init__.py | 12 ++++++++ src/smpclient/transport/ble.py | 13 +++++--- src/smpclient/transport/serial.py | 7 ++++- uv.lock | 23 +++++++++++--- 7 files changed, 125 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 13d6e4b..dbb85f0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,6 +35,56 @@ jobs: dist/*.tar.gz dist/*.whl + transport-extras: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: no-extras + extras: "" + expect-serial: fail + expect-ble: fail + - name: serial-only + extras: "[serial]" + expect-serial: pass + expect-ble: fail + - name: ble-only + extras: "[ble]" + expect-serial: fail + expect-ble: pass + - name: all + extras: "[all]" + expect-serial: pass + expect-ble: pass + name: transport-extras (${{ matrix.name }}) + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: astral-sh/setup-uv@v6 + + - run: uv venv && uv pip install .${{ matrix.extras }} + + - name: serial transport should ${{ matrix.expect-serial }} + run: | + if [ "${{ matrix.expect-serial }}" = "pass" ]; then + .venv/bin/python -c "from smpclient.transport.serial import SMPSerialTransport" + else + ! .venv/bin/python -c "from smpclient.transport.serial import SMPSerialTransport" 2>/dev/null + fi + + - name: ble transport should ${{ matrix.expect-ble }} + run: | + if [ "${{ matrix.expect-ble }}" = "pass" ]; then + .venv/bin/python -c "from smpclient.transport.ble import SMPBLETransport" + else + ! .venv/bin/python -c "from smpclient.transport.ble import SMPBLETransport" 2>/dev/null + fi + + - name: udp transport should pass + run: .venv/bin/python -c "from smpclient.transport.udp import SMPUDPTransport" + coverage: runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 590b1cd..feca36d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,28 @@ If you'd like an SMP CLI application instead of a library, then you should try `smpclient` is [distributed by PyPI](https://pypi.org/project/smpclient/) and can be installed with `uv`, `pip`, and other dependency managers. +Build with all transports: + +``` +smpclient[all] +``` + +Or none (UDP transport only): + +``` +smpclient +``` + +Or build with only the transports you need: + +``` +smpclient[serial] # Serial (UART, USB, CAN) +smpclient[ble] # Bluetooth Low Energy +smpclient[serial,ble] # Serial + BLE +``` + +The UDP transport has no additional dependencies and is always available. + ## User Documentation Documentation is in the source code so that it is available to your editor. diff --git a/pyproject.toml b/pyproject.toml index c0d3f95..484b5a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,13 +21,17 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", ] dependencies = [ -"pyserial>=3.5", "smp>=4.0.2", "intelhex>=2.3.0", -"bleak>=2.0.0", "async-timeout>=4.0.3; python_version < '3.11'", ] +[project.optional-dependencies] +serial = ["pyserial>=3.5"] +ble = ["bleak>=2.0.0"] +udp = [] +all = ["smpclient[serial,ble,udp]"] + [project.urls] Homepage = "https://www.intercreate.io" Documentation = "https://intercreate.github.io/smpclient" @@ -46,6 +50,7 @@ source = "vcs" [dependency-groups] dev = [ +"smpclient[all]", "hatch>=1.14.1", "mypy>=1.7.0", "mypy-extensions>=1.0.0", diff --git a/src/smpclient/__init__.py b/src/smpclient/__init__.py index 332851d..de7ec99 100644 --- a/src/smpclient/__init__.py +++ b/src/smpclient/__init__.py @@ -7,6 +7,18 @@ Additionally, SMP is extensible, allowing for custom commands to be defined to meet the specific needs of the product. +### Transports + +Transports are optional extras. Install only the transports you need, or all: + +``` +smpclient[serial] +smpclient[ble] +smpclient[all] +``` + +The UDP transport has no additional dependencies and is always available. + ### Operating Systems | | Windows 11 (x86) | Ubuntu (Arm/x86) | macOS (Arm/x86) | diff --git a/src/smpclient/transport/ble.py b/src/smpclient/transport/ble.py index 85acbc8..a896305 100644 --- a/src/smpclient/transport/ble.py +++ b/src/smpclient/transport/ble.py @@ -7,10 +7,15 @@ from typing import Final, Protocol, TypeGuard from uuid import UUID -from bleak import BleakClient, BleakGATTCharacteristic, BleakScanner -from bleak.args.winrt import WinRTClientArgs -from bleak.backends.client import BaseBleakClient -from bleak.backends.device import BLEDevice +try: + from bleak import BleakClient, BleakGATTCharacteristic, BleakScanner + from bleak.args.winrt import WinRTClientArgs + from bleak.backends.client import BaseBleakClient + from bleak.backends.device import BLEDevice +except ImportError as _e: + raise ImportError( + "BLE transport requires the 'ble' extra. Install with: pip install smpclient[ble]" + ) from _e from smp import header as smphdr from typing_extensions import override diff --git a/src/smpclient/transport/serial.py b/src/smpclient/transport/serial.py index 702e78b..a93a936 100644 --- a/src/smpclient/transport/serial.py +++ b/src/smpclient/transport/serial.py @@ -11,7 +11,12 @@ from functools import cached_property from typing import Final -from serial import Serial, SerialException +try: + from serial import Serial, SerialException +except ImportError as _e: + raise ImportError( + "Serial transport requires the 'serial' extra. Install with: pip install smpclient[serial]" + ) from _e from smp import packet as smppacket from typing_extensions import override diff --git a/uv.lock b/uv.lock index ed71f2d..4badf63 100644 --- a/uv.lock +++ b/uv.lock @@ -1977,12 +1977,22 @@ name = "smpclient" source = { editable = "." } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11'" }, - { name = "bleak" }, { name = "intelhex" }, - { name = "pyserial" }, { name = "smp" }, ] +[package.optional-dependencies] +all = [ + { name = "bleak" }, + { name = "pyserial" }, +] +ble = [ + { name = "bleak" }, +] +serial = [ + { name = "pyserial" }, +] + [package.dev-dependencies] dev = [ { name = "hatch" }, @@ -1993,6 +2003,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "ruff" }, + { name = "smpclient", extra = ["all"] }, { name = "taskipy" }, { name = "types-pyserial" }, ] @@ -2008,11 +2019,14 @@ doc = [ [package.metadata] requires-dist = [ { name = "async-timeout", marker = "python_full_version < '3.11'", specifier = ">=4.0.3" }, - { name = "bleak", specifier = ">=2.0.0" }, + { name = "bleak", marker = "extra == 'all'", specifier = ">=2.0.0" }, + { name = "bleak", marker = "extra == 'ble'", specifier = ">=2.0.0" }, { name = "intelhex", specifier = ">=2.3.0" }, - { name = "pyserial", specifier = ">=3.5" }, + { name = "pyserial", marker = "extra == 'all'", specifier = ">=3.5" }, + { name = "pyserial", marker = "extra == 'serial'", specifier = ">=3.5" }, { name = "smp", specifier = ">=4.0.2" }, ] +provides-extras = ["all", "ble", "serial", "udp"] [package.metadata.requires-dev] dev = [ @@ -2024,6 +2038,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=0.23.2" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "ruff", specifier = ">=0.12.0" }, + { name = "smpclient", extras = ["all"] }, { name = "taskipy", specifier = ">=1" }, { name = "types-pyserial", specifier = ">=3.5.0.11" }, ] From 75e4c716eb7c83bf2bd32e26afc946ba610c8266 Mon Sep 17 00:00:00 2001 From: JP Hutchins Date: Wed, 4 Mar 2026 12:17:58 -0800 Subject: [PATCH 2/2] fix: narrow import check for optional dependencies --- src/smpclient/transport/ble.py | 8 ++++---- src/smpclient/transport/serial.py | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/smpclient/transport/ble.py b/src/smpclient/transport/ble.py index a896305..0cc2c3f 100644 --- a/src/smpclient/transport/ble.py +++ b/src/smpclient/transport/ble.py @@ -12,10 +12,10 @@ from bleak.args.winrt import WinRTClientArgs from bleak.backends.client import BaseBleakClient from bleak.backends.device import BLEDevice -except ImportError as _e: - raise ImportError( - "BLE transport requires the 'ble' extra. Install with: pip install smpclient[ble]" - ) from _e +except ModuleNotFoundError as e: + if e.name == "bleak": + raise ImportError("BLE transport requires the 'ble' extra. Use smpclient[ble]") from e + raise from smp import header as smphdr from typing_extensions import override diff --git a/src/smpclient/transport/serial.py b/src/smpclient/transport/serial.py index a93a936..45e6924 100644 --- a/src/smpclient/transport/serial.py +++ b/src/smpclient/transport/serial.py @@ -13,10 +13,12 @@ try: from serial import Serial, SerialException -except ImportError as _e: - raise ImportError( - "Serial transport requires the 'serial' extra. Install with: pip install smpclient[serial]" - ) from _e +except ModuleNotFoundError as e: + if e.name == "serial": + raise ImportError( + "Serial transport requires the 'serial' extra. Use smpclient[serial]" + ) from e + raise from smp import packet as smppacket from typing_extensions import override