Skip to content

Commit 0f78db5

Browse files
authored
Merge branch 'pybricks:master' into feature/catch-syntax-error
2 parents 11c7fdb + 2930231 commit 0f78db5

6 files changed

Lines changed: 83 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [2.3.2] - 2026-01-23
10+
11+
### Fixed
12+
- Removed debug print statements in `pybricksdev.compile.compile_multi_file()`.
13+
- Fixed `__init__.py` files in packages not being included when compiling multi-file projects ([pybricksdev#131]).
14+
15+
[pybricksdev#131]: https://github.com/pybricks/pybricksdev/issues/131
16+
17+
## [2.3.1] - 2026-01-18
18+
919
### Changed
1020
- The `run` command now catches syntax errors in the input file. ([pybricksdev#126])
1121
- Changed `pybricksdev.compile.compile_multi_file()` to use `mpy-tool` to find imports
1222
instead of Python's `ModuleFinder`.
1323

1424
### Fixed
1525
- Fixed compiling multi-file projects with implicit namespace packages.
26+
- Fixed EV3 firmware upload on Windows ([pybricksdev#128]).
27+
28+
[pybricksdev#128]: https://github.com/pybricks/pybricksdev/pull/128
1629

1730
[pybricksdev#126]: https://github.com/pybricks/pybricksdev/pull/126
1831

@@ -540,7 +553,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
540553
- Typo in `pip` arguments `README.md`.
541554

542555

543-
[Unreleased]: https://github.com/pybricks/pybricksdev/compare/v2.2.0...HEAD
556+
[Unreleased]: https://github.com/pybricks/pybricksdev/compare/v2.3.2...HEAD
557+
[2.3.2]: https://github.com/pybricks/pybricksdev/compare/v2.3.1...v2.3.2
558+
[2.3.1]: https://github.com/pybricks/pybricksdev/compare/v2.3.0...v2.3.1
559+
[2.3.0]: https://github.com/pybricks/pybricksdev/compare/v2.2.0...v2.3.0
544560
[2.2.0]: https://github.com/pybricks/pybricksdev/compare/v2.1.1...v2.2.0
545561
[2.1.1]: https://github.com/pybricks/pybricksdev/compare/v2.1.0...v2.1.1
546562
[2.1.0]: https://github.com/pybricks/pybricksdev/compare/v2.0.1...v2.1.0

pybricksdev/compile.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,20 @@ async def _compile_module_and_get_imports(
180180
"""
181181
with TemporaryDirectory() as temp_dir:
182182
module_path = os.path.join(*module_name.split(".")) + ".py"
183+
package_path = os.path.join(*module_name.split("."), "__init__.py")
183184

184185
# TODO: check for pre-compiled .mpy file first?
185186

186-
mpy = await compile_file(proj_dir, module_path, abi)
187+
if os.path.exists(os.path.join(proj_dir, module_path)):
188+
src_path = module_path
189+
elif os.path.exists(os.path.join(proj_dir, package_path)):
190+
src_path = package_path
191+
else:
192+
raise FileNotFoundError(
193+
f"Module {module_name} not found. Searched for {module_path} and {package_path}."
194+
)
195+
196+
mpy = await compile_file(proj_dir, src_path, abi)
187197

188198
mpy_path = os.path.join(temp_dir, TMP_MPY_SCRIPT)
189199

@@ -251,9 +261,6 @@ async def _compile_multi_file_with_mpy_tool(
251261
parts.append(name.encode() + b"\x00")
252262
parts.append(mpy)
253263

254-
print(imported_modules)
255-
print(not_found_modules)
256-
257264
return b"".join(parts)
258265

259266

pybricksdev/connections/ev3.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,17 @@ def _send_command(self, command: Command, payload: bytes | None = None) -> int:
8484

8585
length += len(payload)
8686

87+
# report_id is not used by the EV3 but is required by HIDAPI on Windows.
88+
# It does no harm on Linux, so we include it unconditionally. Note that
89+
# the report ID is automatically stripped from incoming messages by
90+
# HIDAPI on all platforms.
91+
report_id = 0
92+
8793
message_number = next(self._msg_count)
8894

8995
message = struct.pack(
90-
"<HHBB",
96+
"<BHHBB",
97+
report_id,
9198
length,
9299
message_number,
93100
MessageType.SYSTEM_COMMAND_REPLY,
@@ -126,7 +133,7 @@ def _receive_reply(
126133

127134
if reply_number != message_number:
128135
raise RuntimeError(
129-
"message sequence number mismatch: {reply_number} != {message_number}"
136+
f"message sequence number mismatch: {reply_number} != {message_number}"
130137
)
131138

132139
if message_type == MessageType.SYSTEM_REPLY_ERROR:

pybricksdev/connections/pybricks.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,8 +946,6 @@ def __init__(self, device: USBDevice):
946946
self._response_queue = asyncio.Queue[bytes]()
947947

948948
async def _client_connect(self) -> bool:
949-
# Reset is essential to ensure endpoints are in a good state.
950-
self._device.reset()
951949
self._device.set_configuration()
952950

953951
# Save input and output endpoints
@@ -964,6 +962,12 @@ async def _client_connect(self) -> bool:
964962
== ENDPOINT_OUT,
965963
)
966964

965+
# Clearing halt (even if not stalled) resets the even/odd state on the
966+
# endpoints in case it was left in an invalid state by a previous
967+
# connection.
968+
self._ep_in.clear_halt()
969+
self._ep_out.clear_halt()
970+
967971
# There is 1 byte overhead for PybricksUsbMessageType
968972
self._max_write_size = self._ep_out.wMaxPacketSize - 1
969973

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pybricksdev"
3-
version = "2.3.0"
3+
version = "2.3.2"
44
description = "Pybricks developer tools"
55
authors = [{ name = "The Pybricks Authors", email = "dev@pybricks.com" }]
66
maintainers = [

tests/test_compile.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# SPDX-License-Identifier: MIT
2-
# Copyright (c) 2022 The Pybricks Authors
2+
# Copyright (c) 2022-2026 The Pybricks Authors
33

44

55
import contextlib
66
import os
77
import struct
88
import sys
99
from tempfile import TemporaryDirectory
10+
from typing import Any
1011

1112
import pytest
1213

@@ -16,18 +17,18 @@
1617
if sys.version_info < (3, 11):
1718
from contextlib import AbstractContextManager
1819

19-
class chdir(AbstractContextManager):
20+
class chdir(AbstractContextManager[None]):
2021
"""Non thread-safe context manager to change the current working directory."""
2122

22-
def __init__(self, path):
23+
def __init__(self, path: str) -> None:
2324
self.path = path
24-
self._old_cwd = []
25+
self._old_cwd: list[str] = []
2526

26-
def __enter__(self):
27+
def __enter__(self) -> None:
2728
self._old_cwd.append(os.getcwd())
2829
os.chdir(self.path)
2930

30-
def __exit__(self, *excinfo):
31+
def __exit__(self, *excinfo: Any) -> None:
3132
os.chdir(self._old_cwd.pop())
3233

3334
setattr(contextlib, "chdir", chdir)
@@ -73,6 +74,8 @@ async def test_compile_multi_file(abi: int):
7374
"import test1\n",
7475
"from test2 import thing2\n",
7576
"from nested.test3 import thing3\n",
77+
"from test4 import thing4\n",
78+
"from nested.test5 import thing5\n",
7679
]
7780
)
7881

@@ -100,6 +103,24 @@ async def test_compile_multi_file(abi: int):
100103
) as f3:
101104
f3.write("thing3 = 'thing3'\n")
102105

106+
# test4 and test5 are to test package modules with non-empty __init__.py
107+
108+
os.mkdir("test4")
109+
110+
with open(
111+
os.path.join(temp_dir, "test4", "__init__.py"), "w", encoding="utf-8"
112+
) as f4:
113+
f4.write("thing4 = 'thing4'\n")
114+
115+
os.mkdir(os.path.join("nested", "test5"))
116+
117+
with open(
118+
os.path.join(temp_dir, "nested", "test5", "__init__.py"),
119+
"w",
120+
encoding="utf-8",
121+
) as f5:
122+
f5.write("thing5 = 'thing5'\n")
123+
103124
multi_mpy = await compile_multi_file("test.py", abi)
104125
pos = 0
105126

@@ -130,13 +151,21 @@ def unpack_mpy(data: bytes) -> tuple[bytes, bytes]:
130151
names.add(name2.decode())
131152
name3, mpy3 = unpack_mpy(multi_mpy)
132153
names.add(name3.decode())
154+
133155
if uses_module_finder:
134156
# ModuleFinder requires __init__.py.
135157
name4, mpy4 = unpack_mpy(multi_mpy)
136158
names.add(name4.decode())
159+
137160
name5, mpy5 = unpack_mpy(multi_mpy)
138161
names.add(name5.decode())
139162

163+
name6, mpy6 = unpack_mpy(multi_mpy)
164+
names.add(name6.decode())
165+
166+
name7, mpy7 = unpack_mpy(multi_mpy)
167+
names.add(name7.decode())
168+
140169
assert pos == len(multi_mpy)
141170

142171
# It is important that the main module is first.
@@ -150,9 +179,9 @@ def unpack_mpy(data: bytes) -> tuple[bytes, bytes]:
150179
assert "nested.test3" in names
151180

152181
if uses_module_finder:
153-
assert len(names) == 4
182+
assert len(names) == 6
154183
else:
155-
assert len(names) == 3
184+
assert len(names) == 5
156185

157186
def check_mpy(mpy: bytes) -> None:
158187
magic, abi_ver, flags, int_bits = struct.unpack_from("<BBBB", mpy)
@@ -168,3 +197,5 @@ def check_mpy(mpy: bytes) -> None:
168197
if uses_module_finder:
169198
check_mpy(mpy4) # pyright: ignore[reportPossiblyUnboundVariable]
170199
check_mpy(mpy5)
200+
check_mpy(mpy6)
201+
check_mpy(mpy7)

0 commit comments

Comments
 (0)