Skip to content

Commit d5308f9

Browse files
Add experimental type checks with ty
This patch prepares the repository for type checks with ty and adds an experimental CI job that runs the checks. ty detected these problems in the current code: - TrussedBootloaderNrf52.update renamed an argument while overriding TrussedBootloader.update - Some ConfigFieldType functions did not handle unexpected variants. Ideally, we would use typing.assert_never here, but this has only been added in Python 3.11. - logger.warn is deprecated in favor of logger.warning. We still have to ignore these false-positives: - Some imports from fake-winreg are not handled correctly, but this could also be a problem with fake-winreg.
1 parent 183de1e commit d5308f9

File tree

9 files changed

+104
-26
lines changed

9 files changed

+104
-26
lines changed

.github/workflows/ci.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ jobs:
5555
run: make install
5656
- name: Check code static typing
5757
run: make check-typing
58+
lint-typing-ty:
59+
name: Check static typing with ty
60+
runs-on: ubuntu-latest
61+
container: python:3.10-slim
62+
steps:
63+
- name: Checkout repository
64+
uses: actions/checkout@v4
65+
- name: Install required packages
66+
run: apt update && apt install -y ${REQUIRED_PACKAGES}
67+
- name: Install Poetry
68+
run: pip install "${POETRY_SPEC}"
69+
- name: Create virtual environment
70+
run: make install
71+
- name: Check code static typing
72+
run: poetry run ty check
5873
lint-poetry:
5974
name: Check poetry configuration
6075
runs-on: ubuntu-latest

poetry.lock

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,33 @@ files = [
13201320
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
13211321
]
13221322

1323+
[[package]]
1324+
name = "ty"
1325+
version = "0.0.16"
1326+
description = "An extremely fast Python type checker, written in Rust."
1327+
optional = false
1328+
python-versions = ">=3.8"
1329+
groups = ["dev"]
1330+
files = [
1331+
{file = "ty-0.0.16-py3-none-linux_armv6l.whl", hash = "sha256:6d8833b86396ed742f2b34028f51c0e98dbf010b13ae4b79d1126749dc9dab15"},
1332+
{file = "ty-0.0.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934c0055d3b7f1cf3c8eab78c6c127ef7f347ff00443cef69614bda6f1502377"},
1333+
{file = "ty-0.0.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b55e8e8733b416d914003cd22e831e139f034681b05afed7e951cc1a5ea1b8d4"},
1334+
{file = "ty-0.0.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feccae8f4abd6657de111353bd604f36e164844466346eb81ffee2c2b06ea0f0"},
1335+
{file = "ty-0.0.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1cad5e29d8765b92db5fa284940ac57149561f3f89470b363b9aab8a6ce553b0"},
1336+
{file = "ty-0.0.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86f28797c7dc06f081238270b533bf4fc8e93852f34df49fb660e0b58a5cda9a"},
1337+
{file = "ty-0.0.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be971a3b42bcae44d0e5787f88156ed2102ad07558c05a5ae4bfd32a99118e66"},
1338+
{file = "ty-0.0.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c9f982b7c4250eb91af66933f436b3a2363c24b6353e94992eab6551166c8b7"},
1339+
{file = "ty-0.0.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d122edf85ce7bdf6f85d19158c991d858fc835677bd31ca46319c4913043dc84"},
1340+
{file = "ty-0.0.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:497ebdddbb0e35c7758ded5aa4c6245e8696a69d531d5c9b0c1a28a075374241"},
1341+
{file = "ty-0.0.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e1e0ac0837bde634b030243aeba8499383c0487e08f22e80f5abdacb5b0bd8ce"},
1342+
{file = "ty-0.0.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1216c9bcca551d9f89f47a817ebc80e88ac37683d71504e5509a6445f24fd024"},
1343+
{file = "ty-0.0.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:221bbdd2c6ee558452c96916ab67fcc465b86967cf0482e19571d18f9c831828"},
1344+
{file = "ty-0.0.16-py3-none-win32.whl", hash = "sha256:d52c4eb786be878e7514cab637200af607216fcc5539a06d26573ea496b26512"},
1345+
{file = "ty-0.0.16-py3-none-win_amd64.whl", hash = "sha256:f572c216aa8ecf79e86589c6e6d4bebc01f1f3cb3be765c0febd942013e1e73a"},
1346+
{file = "ty-0.0.16-py3-none-win_arm64.whl", hash = "sha256:430eadeb1c0de0c31ef7bef9d002bdbb5f25a31e3aad546f1714d76cd8da0a87"},
1347+
{file = "ty-0.0.16.tar.gz", hash = "sha256:a999b0db6aed7d6294d036ebe43301105681e0c821a19989be7c145805d7351c"},
1348+
]
1349+
13231350
[[package]]
13241351
name = "typer"
13251352
version = "0.16.0"
@@ -1502,4 +1529,4 @@ files = [
15021529
[metadata]
15031530
lock-version = "2.1"
15041531
python-versions = ">=3.10, <4"
1505-
content-hash = "1a20a18c696c423e7b3333b9218c5ca13d6f16ac69168fed5c72a6e0fc362674"
1532+
content-hash = "446cf7daba9ad1c2a5e272db9ece7d87dd7bf4af460144b1fce8f530cdd529d8"

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ mypy = "^1.4"
5050
rstcheck = { version = "^6", extras = ["sphinx"] }
5151
ruff = "^0.14"
5252
sphinx = "^7"
53+
ty = "^0.0.16"
5354
types-protobuf = ">=5.26, <7"
5455
types-requests = "^2.16"
5556
typing-extensions = "^4.1"
@@ -97,3 +98,10 @@ split-on-trailing-comma = false
9798

9899
[tool.ruff.lint.mccabe]
99100
max-complexity = 30
101+
102+
[tool.ty.environment]
103+
extra-paths = ["stubs"]
104+
105+
[tool.ty.rules]
106+
# some type: ignore comments are only needed for mypy so ty should not report them as unused
107+
unused-type-ignore-comment = "ignore"

src/nitrokey/trussed/_bootloader/lpc55.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def _list_vid_pid(cls: type[T], vid: int, pid: int) -> list[T]:
100100
try:
101101
devices.append(cls(device))
102102
except ValueError:
103-
logger.warn(
103+
logger.warning(
104104
f"Invalid Nitrokey 3 LPC55 bootloader returned by enumeration: {device}"
105105
)
106106
return devices
@@ -109,16 +109,16 @@ def _list_vid_pid(cls: type[T], vid: int, pid: int) -> list[T]:
109109
def _open(cls: type[T], path: str) -> Optional[T]:
110110
devices = UsbDevice.enumerate(path=path)
111111
if len(devices) == 0:
112-
logger.warn(f"No HID device at {path}")
112+
logger.warning(f"No HID device at {path}")
113113
return None
114114
if len(devices) > 1:
115-
logger.warn(f"Multiple HID devices at {path}: {devices}")
115+
logger.warning(f"Multiple HID devices at {path}: {devices}")
116116
return None
117117

118118
try:
119119
return cls(devices[0])
120120
except ValueError:
121-
logger.warn(f"No Nitrokey 3 bootloader at path {path}", exc_info=sys.exc_info())
121+
logger.warning(f"No Nitrokey 3 bootloader at path {path}", exc_info=sys.exc_info())
122122
return None
123123

124124

src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/properties.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -470,10 +470,10 @@ class AvailableCommandsValue(PropertyValueBase):
470470
__slots__ = ("value",)
471471

472472
@property
473-
def tags(self) -> List[str]:
473+
def tags(self) -> List[int]:
474474
"""List of tags representing Available commands."""
475475
return [
476-
cmd_tag.tag # type: ignore
476+
cmd_tag.tag
477477
for cmd_tag in CommandTag
478478
if cmd_tag.tag > 0 and (1 << cmd_tag.tag - 1) & self.value
479479
]
@@ -492,11 +492,13 @@ def __contains__(self, item: int) -> bool:
492492

493493
def to_str(self) -> str:
494494
"""Get stringified property representation."""
495-
return [
496-
cmd_tag.label # type: ignore
497-
for cmd_tag in CommandTag
498-
if cmd_tag.tag > 0 and (1 << cmd_tag.tag - 1) & self.value
499-
]
495+
return ", ".join(
496+
[
497+
cmd_tag.label
498+
for cmd_tag in CommandTag
499+
if cmd_tag.tag > 0 and (1 << cmd_tag.tag - 1) & self.value
500+
]
501+
)
500502

501503

502504
class IrqNotifierPinValue(PropertyValueBase):

src/nitrokey/trussed/_bootloader/nrf52.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,25 +141,25 @@ def reboot(self) -> bool:
141141
def uuid(self) -> Optional[Uuid]:
142142
return Uuid(self._uuid)
143143

144-
def update(self, data: bytes, callback: Optional[ProgressCallback] = None) -> None:
144+
def update(self, image: bytes, callback: Optional[ProgressCallback] = None) -> None:
145145
# based on https://github.com/NordicSemiconductor/pc-nrfutil/blob/1caa347b1cca3896f4695823f48abba15fbef76b/nordicsemi/dfu/dfu.py
146146
# we have to implement this ourselves because we want to read the files
147147
# from memory, not from the filesystem
148148

149-
image = Image.parse(data, self._signature_keys)
149+
parsed_image = Image.parse(image, self._signature_keys)
150150

151151
time.sleep(3)
152152

153153
dfu = DfuTransportSerial(self.path)
154154

155155
if callback:
156-
total = len(image.firmware_bin)
156+
total = len(parsed_image.firmware_bin)
157157
callback(0, total)
158158
dfu.register_events_callback(DfuEvent.PROGRESS_EVENT, CallbackWrapper(callback, total))
159159

160160
dfu.open()
161-
dfu.send_init_packet(image.firmware_dat)
162-
dfu.send_firmware(image.firmware_bin)
161+
dfu.send_init_packet(parsed_image.firmware_dat)
162+
dfu.send_firmware(parsed_image.firmware_bin)
163163
dfu.close()
164164

165165
@classmethod
@@ -192,7 +192,9 @@ def _list_ports(vid: int, pid: int) -> list[tuple[str, int]]:
192192
product_id = int(device.product_id, base=16)
193193
assert device.com_ports
194194
if len(device.com_ports) > 1:
195-
logger.warn(f"Nitrokey 3 NRF52 bootloader has multiple com ports: {device.com_ports}")
195+
logger.warning(
196+
f"Nitrokey 3 NRF52 bootloader has multiple com ports: {device.com_ports}"
197+
)
196198
if vendor_id == vid and product_id == pid:
197199
port = device.com_ports[0]
198200
serial = int(device.serial_number, base=16)

src/nitrokey/trussed/_bootloader/nrf52_upload/lister/windows/lister_win32.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@ def get_serial_serial_no(
136136

137137
hkey_path = "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{}&PID_{}".format(vendor_id, product_id)
138138
try:
139-
vendor_product_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
139+
vendor_product_hkey = winreg.OpenKeyEx(
140+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
141+
hkey_path,
142+
)
140143
except EnvironmentError:
141144
return None
142145

@@ -153,7 +156,10 @@ def get_serial_serial_no(
153156
)
154157

155158
try:
156-
device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
159+
device_hkey = winreg.OpenKeyEx(
160+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
161+
hkey_path,
162+
)
157163
except EnvironmentError:
158164
continue
159165

@@ -185,7 +191,10 @@ def get_serial_serial_no(
185191
def com_port_is_open(port: str) -> bool:
186192
hkey_path = "HARDWARE\\DEVICEMAP\\SERIALCOMM"
187193
try:
188-
device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
194+
device_hkey = winreg.OpenKeyEx(
195+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
196+
hkey_path,
197+
)
189198
except EnvironmentError:
190199
# Unable to check enumerated serialports. Assume open.
191200
return True
@@ -211,7 +220,10 @@ def list_all_com_ports(vendor_id: str, product_id: str, serial_number: str) -> l
211220
)
212221

213222
try:
214-
device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
223+
device_hkey = winreg.OpenKeyEx(
224+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
225+
hkey_path,
226+
)
215227
except EnvironmentError:
216228
return ports
217229

@@ -227,7 +239,10 @@ def list_all_com_ports(vendor_id: str, product_id: str, serial_number: str) -> l
227239
vendor_id, product_id, serial_number
228240
)
229241
try:
230-
device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
242+
device_hkey = winreg.OpenKeyEx(
243+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
244+
hkey_path,
245+
)
231246
try:
232247
COM_port = winreg.QueryValueEx(device_hkey, "PortName")[0]
233248
ports.append(COM_port)
@@ -253,7 +268,10 @@ def list_all_com_ports(vendor_id: str, product_id: str, serial_number: str) -> l
253268
)
254269
iface_id += 1
255270
try:
256-
device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
271+
device_hkey = winreg.OpenKeyEx(
272+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
273+
hkey_path,
274+
)
257275
except EnvironmentError:
258276
break
259277

src/nitrokey/trussed/_device.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,12 @@ def open(cls: type[T], path: str) -> Optional[T]:
9090
else:
9191
device = open_device(path)
9292
except Exception:
93-
logger.warn(f"No CTAPHID device at path {path}", exc_info=sys.exc_info())
93+
logger.warning(f"No CTAPHID device at path {path}", exc_info=sys.exc_info())
9494
return None
9595
try:
9696
return cls.from_device(device)
9797
except ValueError:
98-
logger.warn(f"No Nitrokey device at path {path}", exc_info=sys.exc_info())
98+
logger.warning(f"No Nitrokey device at path {path}", exc_info=sys.exc_info())
9999
return None
100100

101101
@classmethod

src/nitrokey/trussed/admin_app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,18 @@ def is_valid(self, value: str) -> bool:
176176
return False
177177
except ValueError:
178178
return False
179+
else:
180+
# TODO: use typing.assert_never from Python 3.11
181+
raise ValueError(self)
179182

180183
def __str__(self) -> str:
181184
if self == ConfigFieldType.BOOL:
182185
return "Bool"
183186
elif self == ConfigFieldType.U8:
184187
return "u8"
188+
else:
189+
# TODO: use typing.assert_never from Python 3.11
190+
raise ValueError(self)
185191

186192

187193
@dataclass

0 commit comments

Comments
 (0)