Skip to content

Commit 9536244

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: - @functools.total_ordering is not handled correctly, see astral-sh/ty#1202 - Some imports from fake-winreg are not handled correctly, but this could also be a problem with fake-winreg.
1 parent 28b6748 commit 9536244

File tree

10 files changed

+80
-29
lines changed

10 files changed

+80
-29
lines changed

.github/workflows/ci.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88
env:
99
REQUIRED_PACKAGES: make
1010
POETRY_SPEC: poetry >=2, <3
11+
TY_SPEC: ty ==0.0.1a28
1112

1213
jobs:
1314
format-code:
@@ -70,6 +71,23 @@ jobs:
7071
run: make install
7172
- name: Check code static typing
7273
run: make check-typing
74+
lint-typing-ty:
75+
name: Check static typing with ty
76+
runs-on: ubuntu-latest
77+
container: python:3.10-slim
78+
steps:
79+
- name: Checkout repository
80+
uses: actions/checkout@v4
81+
- name: Install required packages
82+
run: apt update && apt install -y ${REQUIRED_PACKAGES}
83+
- name: Install Poetry
84+
run: pip install "${POETRY_SPEC}"
85+
- name: Create virtual environment
86+
run: make install
87+
- name: Install ty
88+
run: poetry add "${TY_SPEC}"
89+
- name: Check code static typing
90+
run: poetry run ty check
7391
lint-poetry:
7492
name: Check poetry configuration
7593
runs-on: ubuntu-latest

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,6 @@ strict = true
9090
[tool.rstcheck]
9191
ignore_directives = ["autoclass", "autofunction", "automodule"]
9292
ignore_substitutions = ["nitrokey_sdk_version"]
93+
94+
[tool.ty.environment]
95+
extra-paths = ["stubs"]

src/nitrokey/trussed/_bootloader/lpc55.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def _list_vid_pid(cls: type[T], vid: int, pid: int) -> list[T]:
105105
try:
106106
devices.append(cls(device))
107107
except ValueError:
108-
logger.warn(
108+
logger.warning(
109109
f"Invalid Nitrokey 3 LPC55 bootloader returned by enumeration: {device}"
110110
)
111111
return devices
@@ -114,16 +114,16 @@ def _list_vid_pid(cls: type[T], vid: int, pid: int) -> list[T]:
114114
def _open(cls: type[T], path: str) -> Optional[T]:
115115
devices = UsbDevice.enumerate(path=path)
116116
if len(devices) == 0:
117-
logger.warn(f"No HID device at {path}")
117+
logger.warning(f"No HID device at {path}")
118118
return None
119119
if len(devices) > 1:
120-
logger.warn(f"Multiple HID devices at {path}: {devices}")
120+
logger.warning(f"Multiple HID devices at {path}: {devices}")
121121
return None
122122

123123
try:
124124
return cls(devices[0])
125125
except ValueError:
126-
logger.warn(
126+
logger.warning(
127127
f"No Nitrokey 3 bootloader at path {path}", exc_info=sys.exc_info()
128128
)
129129
return None

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

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

496496
@property
497-
def tags(self) -> List[str]:
497+
def tags(self) -> List[int]:
498498
"""List of tags representing Available commands."""
499499
return [
500-
cmd_tag.tag # type: ignore
500+
cmd_tag.tag
501501
for cmd_tag in CommandTag
502502
if cmd_tag.tag > 0 and (1 << cmd_tag.tag - 1) & self.value
503503
]
@@ -516,11 +516,13 @@ def __contains__(self, item: int) -> bool:
516516

517517
def to_str(self) -> str:
518518
"""Get stringified property representation."""
519-
return [
520-
cmd_tag.label # type: ignore
521-
for cmd_tag in CommandTag
522-
if cmd_tag.tag > 0 and (1 << cmd_tag.tag - 1) & self.value
523-
]
519+
return ", ".join(
520+
[
521+
cmd_tag.label
522+
for cmd_tag in CommandTag
523+
if cmd_tag.tag > 0 and (1 << cmd_tag.tag - 1) & self.value
524+
]
525+
)
524526

525527

526528
class IrqNotifierPinValue(PropertyValueBase):

src/nitrokey/trussed/_bootloader/nrf52.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,28 +151,28 @@ def reboot(self) -> bool:
151151
def uuid(self) -> Optional[Uuid]:
152152
return Uuid(self._uuid)
153153

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

159-
image = Image.parse(data, self._signature_keys)
159+
parsed_image = Image.parse(image, self._signature_keys)
160160

161161
time.sleep(3)
162162

163163
dfu = DfuTransportSerial(self.path)
164164

165165
if callback:
166-
total = len(image.firmware_bin)
166+
total = len(parsed_image.firmware_bin)
167167
callback(0, total)
168168
dfu.register_events_callback(
169169
DfuEvent.PROGRESS_EVENT,
170170
CallbackWrapper(callback, total),
171171
)
172172

173173
dfu.open()
174-
dfu.send_init_packet(image.firmware_dat)
175-
dfu.send_firmware(image.firmware_bin)
174+
dfu.send_init_packet(parsed_image.firmware_dat)
175+
dfu.send_firmware(parsed_image.firmware_bin)
176176
dfu.close()
177177

178178
@classmethod
@@ -205,7 +205,7 @@ def _list_ports(vid: int, pid: int) -> list[tuple[str, int]]:
205205
product_id = int(device.product_id, base=16)
206206
assert device.com_ports
207207
if len(device.com_ports) > 1:
208-
logger.warn(
208+
logger.warning(
209209
f"Nitrokey 3 NRF52 bootloader has multiple com ports: {device.com_ports}"
210210
)
211211
if vendor_id == vid and product_id == pid:

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
@@ -138,7 +138,10 @@ def get_serial_serial_no(
138138
vendor_id, product_id
139139
)
140140
try:
141-
vendor_product_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
141+
vendor_product_hkey = winreg.OpenKeyEx(
142+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
143+
hkey_path,
144+
)
142145
except EnvironmentError:
143146
return None
144147

@@ -155,7 +158,10 @@ def get_serial_serial_no(
155158
)
156159

157160
try:
158-
device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
161+
device_hkey = winreg.OpenKeyEx(
162+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
163+
hkey_path,
164+
)
159165
except EnvironmentError:
160166
continue
161167

@@ -187,7 +193,10 @@ def get_serial_serial_no(
187193
def com_port_is_open(port: str) -> bool:
188194
hkey_path = "HARDWARE\\DEVICEMAP\\SERIALCOMM"
189195
try:
190-
device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
196+
device_hkey = winreg.OpenKeyEx(
197+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
198+
hkey_path,
199+
)
191200
except EnvironmentError:
192201
# Unable to check enumerated serialports. Assume open.
193202
return True
@@ -215,7 +224,10 @@ def list_all_com_ports(
215224
)
216225

217226
try:
218-
device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
227+
device_hkey = winreg.OpenKeyEx(
228+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
229+
hkey_path,
230+
)
219231
except EnvironmentError:
220232
return ports
221233

@@ -231,7 +243,10 @@ def list_all_com_ports(
231243
vendor_id, product_id, serial_number
232244
)
233245
try:
234-
device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
246+
device_hkey = winreg.OpenKeyEx(
247+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
248+
hkey_path,
249+
)
235250
try:
236251
COM_port = winreg.QueryValueEx(device_hkey, "PortName")[0]
237252
ports.append(COM_port)
@@ -257,7 +272,10 @@ def list_all_com_ports(
257272
)
258273
iface_id += 1
259274
try:
260-
device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
275+
device_hkey = winreg.OpenKeyEx(
276+
winreg.HKEY_LOCAL_MACHINE, # ty: ignore[possibly-missing-attribute]
277+
hkey_path,
278+
)
261279
except EnvironmentError:
262280
break
263281

src/nitrokey/trussed/_device.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,14 @@ def open(cls: type[T], path: str) -> Optional[T]:
101101
else:
102102
device = open_device(path)
103103
except Exception:
104-
logger.warn(f"No CTAPHID device at path {path}", exc_info=sys.exc_info())
104+
logger.warning(f"No CTAPHID device at path {path}", exc_info=sys.exc_info())
105105
return None
106106
try:
107107
return cls.from_device(device)
108108
except ValueError:
109-
logger.warn(f"No Nitrokey device at path {path}", exc_info=sys.exc_info())
109+
logger.warning(
110+
f"No Nitrokey device at path {path}", exc_info=sys.exc_info()
111+
)
110112
return None
111113

112114
@classmethod

src/nitrokey/trussed/_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,9 @@ class Fido2Certs:
232232

233233
@staticmethod
234234
def get(certs: Sequence["Fido2Certs"], version: Version) -> Optional["Fido2Certs"]:
235-
matching_certs = [c for c in certs if version >= c.start]
235+
matching_certs = [
236+
c for c in certs if version >= c.start # ty: ignore[unsupported-operator]
237+
]
236238
if matching_certs:
237239
return max(matching_certs, key=lambda c: c.start)
238240
else:

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

src/nitrokey/trussed/updates.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,16 @@ def get(
130130
if variant == Variant.NRF52:
131131
if (
132132
current is None
133-
or current <= Version(1, 2, 2)
134-
and new >= Version(1, 3, 0)
133+
or current <= Version(1, 2, 2) # ty: ignore[unsupported-operator]
134+
and new >= Version(1, 3, 0) # ty: ignore[unsupported-operator]
135135
):
136136
migrations.add(cls.NRF_IFS_MIGRATION)
137137

138138
ifs_migration_v2 = Version(1, 8, 2)
139139
if (
140140
current is not None
141141
and current < ifs_migration_v2
142-
and new >= ifs_migration_v2
142+
and new >= ifs_migration_v2 # ty: ignore[unsupported-operator]
143143
):
144144
migrations.add(cls.IFS_MIGRATION_V2)
145145

0 commit comments

Comments
 (0)