Skip to content

Commit 521ac75

Browse files
authored
Merge pull request #8 from fkleon/type-fixes
Update to Pybricks 3.6.x
2 parents 15cfb72 + a071499 commit 521ac75

10 files changed

Lines changed: 107 additions & 31 deletions

File tree

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,7 @@ jobs:
1818
run: make .venv
1919
- name: Run lint
2020
run: make lint
21+
- name: Run typecheck
22+
run: make typecheck
2123
- name: Run tests
2224
run: make test

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ dependencies = [
2121
"bluetooth-adapters ~= 0.16",
2222
"cachetools ~= 5.3",
2323
"dbus-fast ~= 2.15",
24-
"pybricks ~= 3.5.0",
24+
"pybricks ~= 3.6.0",
2525
]
2626

2727
[dependency-groups]
2828
debug = ["bluetooth-data-tools ~= 1.15"]
2929
dev = [
3030
"async-timer ~= 1.1.6",
31-
"mypy ~= 1.15.0",
31+
"mypy ~= 1.16.0",
3232
"pytest ~= 8.3",
33-
"pytest-asyncio ~= 0.26.0",
33+
"pytest-asyncio ~= 1.0.0",
3434
"python-dbusmock ~= 0.34.3",
3535
"ruff ~= 0.11.8",
3636
"types-cachetools ~= 5.3",

src/pb_ble/bluezdbus/broadcaster.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ async def stop_broadcast(self, adv: str | BroadcastAdvertisement):
8383
pass
8484
finally:
8585
self.bus.unexport(path)
86-
del self.advertisements[path]
86+
if path in self.advertisements:
87+
del self.advertisements[path]
8788

8889
async def stop(self):
8990
"""
@@ -114,7 +115,8 @@ async def broadcast(self, adv: BroadcastAdvertisement):
114115
def release_advertisement(path):
115116
try:
116117
self.bus.unexport(path)
117-
del self.advertisements[path]
118+
if path in self.advertisements:
119+
del self.advertisements[path]
118120
finally:
119121
on_release(path)
120122

src/pb_ble/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"""Type of a value that can be broadcast."""
1919

2020
PybricksBroadcastData: TypeAlias = (
21-
PybricksBroadcastValue | tuple[PybricksBroadcastValue]
21+
PybricksBroadcastValue | tuple[PybricksBroadcastValue, ...]
2222
)
2323
"""Type of the broadcast data."""
2424

src/pb_ble/messages.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
1111
"""
1212

13+
import logging
1314
from enum import IntEnum
1415
from struct import pack, unpack, unpack_from
15-
from typing import Literal, Tuple
16+
from typing import Literal, Tuple, cast
1617

17-
from .constants import PybricksBroadcast, PybricksBroadcastValue
18+
from .constants import PybricksBroadcast, PybricksBroadcastData, PybricksBroadcastValue
19+
20+
logger = logging.getLogger(__name__)
1821

1922

2023
def decode_message(
@@ -30,24 +33,32 @@ def decode_message(
3033
or a tuple.
3134
"""
3235

36+
logger.debug(f"decoding[{len(data)}]: {data!r}")
37+
3338
# idx 0 is the channel
3439
channel: int = unpack_from("<B", data)[0] # uint8
40+
logger.debug(f"channel: {channel}")
3541
# idx 1 is the message start
3642
idx = 1
37-
decoded_data = []
43+
decoded_data: list[PybricksBroadcastValue] = []
3844
single_object = False
3945

4046
while idx < len(data):
4147
idx, val = _decode_next_value(idx, data)
4248
if val is None:
49+
logger.debug(f"data[{len(decoded_data)}]: SINGLE_OBJECT marker")
4350
single_object = True
4451
else:
52+
logger.debug(f"data[{len(decoded_data)}] of {type(val)!s:<15}: {val!r}")
4553
decoded_data.append(val)
4654

4755
if single_object:
48-
return PybricksBroadcast(channel, decoded_data[0])
56+
decoded_value: PybricksBroadcastValue = decoded_data[0]
57+
return PybricksBroadcast(channel, decoded_value)
4958
else:
50-
return PybricksBroadcast(channel, tuple(decoded_data)) # type: ignore # https://github.com/python/mypy/issues/7509
59+
return PybricksBroadcast(
60+
channel, cast(PybricksBroadcastData, tuple(decoded_data))
61+
)
5162

5263

5364
def encode_message(channel: int = 0, *values: PybricksBroadcastValue) -> bytes:
@@ -63,17 +74,22 @@ def encode_message(channel: int = 0, *values: PybricksBroadcastValue) -> bytes:
6374

6475
# idx 0 is the channel
6576
encoded_channel = pack("<B", channel)
77+
logger.debug(f"channel: {channel} -> {encoded_channel!r}")
6678

6779
# idx 1 is the message start
6880
encoded_data = bytearray(encoded_channel)
6981

7082
if len(values) == 1:
7183
# set SINGLE_OBJECT marker
7284
header = PybricksBleBroadcastDataType.SINGLE_OBJECT << 5
85+
logger.debug(f"data[{len(encoded_data)}]: SINGLE_OBJECT marker")
7386
encoded_data.append(header)
7487

7588
for val in values:
7689
header, encoded_val = _encode_value(val)
90+
logger.debug(
91+
f"data[{len(encoded_data)}] of {type(val)!s}: {val!r} -> ({header!r}, {encoded_val!r})"
92+
)
7793
encoded_data.append(header)
7894

7995
if encoded_val is not None:
@@ -85,7 +101,9 @@ def encode_message(channel: int = 0, *values: PybricksBroadcastValue) -> bytes:
85101
f"Payload too large: {len(encoded_data)} bytes (maximum is {OBSERVED_DATA_MAX_SIZE} bytes)"
86102
)
87103

88-
return bytes(encoded_data)
104+
message = bytes(encoded_data)
105+
logger.debug(f"encoded[{len(message)}]: {message!r}")
106+
return message
89107

90108

91109
def unpack_pnp_id(data: bytes) -> Tuple[Literal["BT", "USB"], int, int, int]:
@@ -99,9 +117,14 @@ def unpack_pnp_id(data: bytes) -> Tuple[Literal["BT", "USB"], int, int, int]:
99117
:return: Tuple containing the vendor ID type (`BT` or `USB`), the vendor
100118
ID, the product ID and the product revision.
101119
"""
120+
vid_type: int
121+
vid: int
122+
pid: int
123+
rev: int
124+
102125
vid_type, vid, pid, rev = unpack("<BHHH", data)
103-
vid_type = "BT" if vid_type else "USB"
104-
return vid_type, vid, pid, rev
126+
127+
return "BT" if vid_type else "USB", vid, pid, rev
105128

106129

107130
def pack_pnp_id(

src/pb_ble/py.typed

Whitespace-only changes.

src/pb_ble/vhub.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sys
22
from contextlib import AsyncExitStack
3-
from typing import ClassVar, Optional, Sequence, Tuple, Union
3+
from typing import ClassVar, Optional, Sequence, Tuple, Union, cast
44

55
from dbus_fast.aio import MessageBus, ProxyObject
66
from dbus_fast.constants import BusType
@@ -16,6 +16,8 @@
1616
from .constants import (
1717
PYBRICKS_MAX_CHANNEL,
1818
PYBRICKS_MIN_CHANNEL,
19+
PybricksBroadcastData,
20+
PybricksBroadcastValue,
1921
ScanningMode,
2022
)
2123

@@ -61,19 +63,32 @@ async def __aenter__(self):
6163
raise
6264
return self
6365

64-
async def broadcast(self, data: Union[bool, int, float, str, bytes]) -> None:
65-
if data is None:
66+
async def broadcast(self, *data: PybricksBroadcastValue | None) -> None: # type: ignore [override]
67+
if len(data) == 0:
68+
raise ValueError("Broadcast must be a value or tuple.")
69+
if None in data:
6670
await self._broadcaster.stop_broadcast(self._adv)
6771
else:
6872
if not self._broadcaster.is_broadcasting(self._adv):
6973
await self._broadcaster.broadcast(self._adv)
70-
self._adv.message = data
74+
self._adv.message = cast(PybricksBroadcastData, tuple(data))
7175

7276
def observe(
7377
self, channel: int
7478
) -> Optional[Tuple[Union[bool, int, float, str, bytes], ...]]:
7579
advertisement = self._observer.observe(channel)
76-
return advertisement.data if advertisement is not None else None
80+
81+
if advertisement is not None:
82+
if isinstance(advertisement.data, tuple):
83+
return advertisement.data
84+
else:
85+
# TODO: Pybricks does expose single-value broadcasts
86+
# in a single-object tuple. However, that doesn't match
87+
# the type signature of the observe() method. To adhere
88+
# to the type signature, we only return wrapped values.
89+
return (advertisement.data,)
90+
else:
91+
return None
7792

7893
def signal_strength(self, channel: int) -> int:
7994
advertisement = self._observer.observe(channel)

tests/fixtures/bluez5_mock.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import sys
22
import unittest.mock
33

4-
import dbus
54
import pytest
5+
from dbus.proxies import ProxyObject
66
from dbusmock import SpawnedMock
77
from dbusmock.testcase import PrivateDBus
88

@@ -19,7 +19,7 @@ def pytest_runtest_setup(item) -> None:
1919

2020

2121
@pytest.fixture
22-
def bluez_mock(dbusmock_system: PrivateDBus) -> YieldFixture[dbus.proxies.ProxyObject]:
22+
def bluez_mock(dbusmock_system: PrivateDBus) -> YieldFixture[ProxyObject]:
2323
template = "bluez5"
2424
parameters = {
2525
"advertise": True,
@@ -34,9 +34,7 @@ def bluez_mock(dbusmock_system: PrivateDBus) -> YieldFixture[dbus.proxies.ProxyO
3434

3535

3636
@pytest.fixture(autouse=True)
37-
def adapter_mock(
38-
bluez_mock: dbus.proxies.ProxyObject, adapter_name: str
39-
) -> YieldFixture[str]:
37+
def adapter_mock(bluez_mock: ProxyObject, adapter_name: str) -> YieldFixture[str]:
4038
device_name = adapter_name
4139

4240
# Mock out the DBus adapter

tests/test_messages.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66

77
class TestPybricksBleDecodeMessage:
8+
# h.ble.broadcast(5)
89
def test_decode_message_single_object(self):
910
# channel 200
1011
# single object marker
@@ -13,22 +14,35 @@ def test_decode_message_single_object(self):
1314
channel, data = decode_message(message)
1415

1516
assert channel == 200
16-
assert not isinstance(data, tuple)
17+
assert isinstance(data, int), type(data)
1718

1819
assert data == 5
1920

20-
# TODO: Check behaviour against reference implementation
21+
# h.ble.broadcast((5,))
2122
def test_decode_message_single_object_tuple(self):
2223
# channel 200
2324
# int8: 5
2425
message = b"\xc8\x61\x05"
2526
channel, data = decode_message(message)
2627

2728
assert channel == 200
29+
assert isinstance(data, tuple), type(data)
30+
assert len(data) == 1
31+
32+
assert data == (5,)
33+
34+
# h.ble.broadcast((5,))
35+
def test_decode_message_single_object_tuple_0(self):
36+
# channel 0
37+
# int8: 5
38+
message = b"\x00a\x05"
39+
channel, data = decode_message(message)
40+
41+
assert channel == 0
2842
assert isinstance(data, tuple)
2943
assert len(data) == 1
3044

31-
assert data[0] == 5
45+
assert data == (5,)
3246

3347
def test_decode_message_int8_int16_int32(self):
3448
# channel: 200
@@ -127,9 +141,9 @@ def test_encode_message_single_object(self):
127141
data = encode_message(200, 5)
128142
assert data == b"\xc8\x00\x61\x05"
129143

130-
@pytest.mark.skip("Check behaviour against reference implementation")
144+
@pytest.mark.skip("Encoding single-object tuples is not supported")
131145
def test_encode_message_single_object_tuple(self):
132-
data = encode_message(200, (1))
146+
data = encode_message(200, (1,)) # type: ignore[arg-type]
133147
assert data == b"\xc8\x61\x01"
134148

135149
def test_encode_message_int8_int16_int32(self):

tests/test_vhub.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,37 @@ class TestVirtualBLE:
1616
async def test_create_vble(self, adapter):
1717
ble = await get_virtual_ble(broadcast_channel=1, observe_channels=[2])
1818
assert ble is not None
19+
assert not ble._broadcaster.is_broadcasting()
1920

20-
async def test_observe(self):
21+
async def test_observe_none(self):
2122
ble = await get_virtual_ble(broadcast_channel=1, observe_channels=[2])
2223
data = ble.observe(2)
2324
assert data is None
25+
assert not ble._broadcaster.is_broadcasting()
2426

25-
async def test_broadcast(self):
27+
async def test_broadcast_single(self):
2628
ble = await get_virtual_ble(broadcast_channel=1, observe_channels=[2])
2729
await ble.broadcast(42)
30+
assert ble._broadcaster.is_broadcasting()
31+
assert ble._adv.message == 42
32+
33+
async def test_broadcast_multiple(self):
34+
ble = await get_virtual_ble(broadcast_channel=1, observe_channels=[2])
35+
await ble.broadcast(42, 24)
36+
assert ble._broadcaster.is_broadcasting()
37+
assert ble._adv.message == (42, 24)
38+
39+
async def test_broadcast_none(self):
40+
ble = await get_virtual_ble(broadcast_channel=1, observe_channels=[2])
41+
await ble.broadcast(None)
42+
assert not ble._broadcaster.is_broadcasting()
43+
44+
async def test_broadcast_start_stop(self):
45+
ble = await get_virtual_ble(broadcast_channel=1, observe_channels=[2])
46+
await ble.broadcast(42)
47+
assert ble._broadcaster.is_broadcasting()
48+
await ble.broadcast(None)
49+
assert not ble._broadcaster.is_broadcasting()
2850

2951
async def test_context(self):
3052
async with await get_virtual_ble(

0 commit comments

Comments
 (0)