Skip to content

Commit b43cf9f

Browse files
committed
Fix arg-type, return-value, and assignment mypy errors
- nmt.py: Use bytes() instead of list[int] for send_message/send_periodic - emcy.py: Fix wait() return type to Optional[EmcyError] - network.py: Fix add_node/create_node return types and type narrowing - variable.py: Fix missing return in read(), annotate Bits.raw, add type: ignore for inherently dynamic type operations - sdo/base.py: Fix missing return in get_variable(), add type: ignore for SdoArray.__len__ - objectdictionary/__init__.py: Fix __getitem__ indexability, add missing return in get_variable(), annotate ODVariable.parent, fix decode_phys/encode_phys types, add type: ignore for dynamic encode_raw operations
1 parent 4e789fe commit b43cf9f

8 files changed

Lines changed: 112 additions & 32 deletions

File tree

canopen/emcy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def reset(self):
5757

5858
def wait(
5959
self, emcy_code: Optional[int] = None, timeout: float = 10
60-
) -> EmcyError:
60+
) -> Optional[EmcyError]:
6161
"""Wait for a new EMCY to arrive.
6262
6363
:param emcy_code: EMCY code to wait for

canopen/network.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def add_node(
136136
node: Union[int, RemoteNode, LocalNode],
137137
object_dictionary: Union[str, ObjectDictionary, None] = None,
138138
upload_eds: bool = False,
139-
) -> RemoteNode:
139+
) -> Union[RemoteNode, LocalNode]:
140140
"""Add a remote node to the network.
141141
142142
:param node:
@@ -156,13 +156,14 @@ def add_node(
156156
if upload_eds:
157157
logger.info("Trying to read EDS from node %d", node)
158158
object_dictionary = import_from_node(node, self)
159-
node = RemoteNode(node, object_dictionary)
159+
node = RemoteNode(node, object_dictionary) # type: ignore[arg-type]
160+
assert node.id is not None
160161
self[node.id] = node
161162
return node
162163

163164
def create_node(
164165
self,
165-
node: int,
166+
node: Union[int, LocalNode],
166167
object_dictionary: Union[str, ObjectDictionary, None] = None,
167168
) -> LocalNode:
168169
"""Create a local node in the network.
@@ -178,7 +179,8 @@ def create_node(
178179
The Node object that was added.
179180
"""
180181
if isinstance(node, int):
181-
node = LocalNode(node, object_dictionary)
182+
node = LocalNode(node, object_dictionary) # type: ignore[arg-type]
183+
assert node.id is not None
182184
self[node.id] = node
183185
return node
184186

canopen/nmt.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def send_command(self, code: int):
147147
super(NmtMaster, self).send_command(code)
148148
logger.info(
149149
"Sending NMT command 0x%X to node %d", code, self.id)
150-
self.network.send_message(0, [code, self.id])
150+
self.network.send_message(0, bytes([code, self.id]))
151151

152152
def wait_for_heartbeat(self, timeout: float = 10):
153153
"""Wait until a heartbeat message is received."""
@@ -190,7 +190,7 @@ def start_node_guarding(self, period: float):
190190
"""
191191
if self._node_guarding_producer:
192192
self.stop_node_guarding()
193-
self._node_guarding_producer = self.network.send_periodic(0x700 + self.id, None, period, True)
193+
self._node_guarding_producer = self.network.send_periodic(0x700 + self.id, b'', period, True)
194194

195195
def stop_node_guarding(self):
196196
"""Stops the node guarding mechanism."""
@@ -225,7 +225,7 @@ def send_command(self, code: int) -> None:
225225

226226
if self._state == 0:
227227
logger.info("Sending boot-up message")
228-
self.network.send_message(0x700 + self.id, [0])
228+
self.network.send_message(0x700 + self.id, b'\x00')
229229

230230
# The heartbeat service should start on the transition
231231
# between INITIALIZING and PRE-OPERATIONAL state
@@ -256,7 +256,7 @@ def start_heartbeat(self, heartbeat_time_ms: int):
256256
if heartbeat_time_ms > 0:
257257
logger.info("Start the heartbeat timer, interval is %d ms", self._heartbeat_time_ms)
258258
self._send_task = self.network.send_periodic(
259-
0x700 + self.id, [self._state], heartbeat_time_ms / 1000.0)
259+
0x700 + self.id, bytes([self._state]), heartbeat_time_ms / 1000.0)
260260

261261
def stop_heartbeat(self):
262262
"""Stop the heartbeat service."""

canopen/objectdictionary/__init__.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def export_od(
6969
finally:
7070
# If dest is opened in this fn, it should be closed
7171
if opened_here:
72-
dest.close()
72+
dest.close() # type: ignore[union-attr]
7373

7474

7575
def import_od(
@@ -92,7 +92,7 @@ def import_od(
9292
return ObjectDictionary()
9393
if hasattr(source, "read"):
9494
# File like object
95-
filename = source.name
95+
filename = source.name # type: ignore[union-attr]
9696
elif hasattr(source, "tag"):
9797
# XML tree, probably from an EPF file
9898
filename = "od.epf"
@@ -139,7 +139,10 @@ def __getitem__(
139139
if item is None:
140140
if isinstance(index, str) and '.' in index:
141141
idx, sub = index.split('.', maxsplit=1)
142-
return self[idx][sub]
142+
parent = self[idx]
143+
if not isinstance(parent, (ODRecord, ODArray)):
144+
raise KeyError(f"{pretty_index(index)} was not found in Object Dictionary")
145+
return parent[sub]
143146
raise KeyError(f"{pretty_index(index)} was not found in Object Dictionary")
144147
return item
145148

@@ -188,6 +191,7 @@ def get_variable(
188191
return obj
189192
elif isinstance(obj, (ODRecord, ODArray)):
190193
return obj.get(subindex)
194+
return None
191195

192196

193197
class ODRecord(MutableMapping):
@@ -259,7 +263,7 @@ class ODArray(Mapping):
259263

260264
def __init__(self, name: str, index: int):
261265
#: The :class:`~canopen.ObjectDictionary` owning the record.
262-
self.parent = None
266+
self.parent: Optional[ObjectDictionary] = None
263267
#: 16-bit address of the array
264268
self.index = index
265269
#: Name of array
@@ -339,7 +343,7 @@ def __init__(self, name: str, index: int, subindex: int = 0):
339343
#: The :class:`~canopen.ObjectDictionary`,
340344
#: :class:`~canopen.objectdictionary.ODRecord` or
341345
#: :class:`~canopen.objectdictionary.ODArray` owning the variable
342-
self.parent = None
346+
self.parent: Union[ObjectDictionary, ODRecord, ODArray, None] = None
343347
#: 16-bit address of the object in the dictionary
344348
self.index = index
345349
#: 8-bit sub-index of the object in the dictionary
@@ -451,19 +455,19 @@ def encode_raw(self, value: Union[int, float, str, bytes, bytearray]) -> bytes:
451455
if isinstance(value, (bytes, bytearray)):
452456
return value
453457
elif self.data_type == VISIBLE_STRING:
454-
return value.encode("ascii")
458+
return value.encode("ascii") # type: ignore[union-attr]
455459
elif self.data_type == UNICODE_STRING:
456-
return value.encode("utf_16_le")
460+
return value.encode("utf_16_le") # type: ignore[union-attr]
457461
elif self.data_type in (DOMAIN, OCTET_STRING):
458-
return bytes(value)
462+
return bytes(value) # type: ignore[arg-type]
459463
elif self.data_type in self.STRUCT_TYPES:
460464
if self.data_type in INTEGER_TYPES:
461465
value = int(value)
462466
if self.data_type in NUMBER_TYPES:
463-
if self.min is not None and value < self.min:
467+
if self.min is not None and value < self.min: # type: ignore[operator]
464468
logger.warning(
465469
"Value %d is less than min value %d", value, self.min)
466-
if self.max is not None and value > self.max:
470+
if self.max is not None and value > self.max: # type: ignore[operator]
467471
logger.warning(
468472
"Value %d is greater than max value %d",
469473
value, self.max)
@@ -477,16 +481,15 @@ def encode_raw(self, value: Union[int, float, str, bytes, bytearray]) -> bytes:
477481
raise TypeError(
478482
f"Do not know how to encode {value!r} to data type 0x{self.data_type:X}")
479483

480-
def decode_phys(self, value: int) -> Union[int, bool, float, str, bytes]:
484+
def decode_phys(self, value: int) -> Union[int, float]:
481485
if self.data_type in INTEGER_TYPES:
482-
value *= self.factor
486+
return value * self.factor
483487
return value
484488

485489
def encode_phys(self, value: Union[int, bool, float, str, bytes]) -> int:
486490
if self.data_type in INTEGER_TYPES:
487-
value /= self.factor
488-
value = int(round(value))
489-
return value
491+
value = int(round(value / self.factor)) # type: ignore[operator]
492+
return value # type: ignore[return-value]
490493

491494
def decode_desc(self, value: int) -> str:
492495
if not self.value_descriptions:

canopen/sdo/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def get_variable(
7979
return obj
8080
elif isinstance(obj, (SdoRecord, SdoArray)):
8181
return obj.get(subindex)
82+
return None
8283

8384
def upload(self, index: int, subindex: int) -> bytes:
8485
raise NotImplementedError()
@@ -134,7 +135,7 @@ def __iter__(self) -> Iterator[int]:
134135
return iter(range(1, len(self) + 1))
135136

136137
def __len__(self) -> int:
137-
return self[0].raw
138+
return self[0].raw # type: ignore[return-value]
138139

139140
def __contains__(self, subindex: int) -> bool:
140141
return 0 <= subindex <= len(self)

canopen/variable.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def raw(self) -> Union[int, bool, float, str, bytes]:
7777
"""
7878
value = self.od.decode_raw(self.data)
7979
text = f"Value of {self.name!r} ({pretty_index(self.index, self.subindex)}) is {value!r}"
80-
if value in self.od.value_descriptions:
80+
if isinstance(value, int) and value in self.od.value_descriptions:
8181
text += f" ({self.od.value_descriptions[value]})"
8282
logger.debug(text)
8383
return value
@@ -97,7 +97,7 @@ def phys(self) -> Union[int, bool, float, str, bytes]:
9797
either a :class:`float` or an :class:`int`.
9898
Non integers will be passed as is.
9999
"""
100-
value = self.od.decode_phys(self.raw)
100+
value = self.od.decode_phys(self.raw) # type: ignore[arg-type]
101101
if self.od.unit:
102102
logger.debug("Physical value is %s %s", value, self.od.unit)
103103
return value
@@ -109,13 +109,13 @@ def phys(self, value: Union[int, bool, float, str, bytes]):
109109
@property
110110
def desc(self) -> str:
111111
"""Converts to and from a description of the value as a string."""
112-
value = self.od.decode_desc(self.raw)
112+
value = self.od.decode_desc(self.raw) # type: ignore[arg-type]
113113
logger.debug("Description is '%s'", value)
114114
return value
115115

116116
@desc.setter
117117
def desc(self, desc: str):
118-
self.raw = self.od.encode_desc(desc)
118+
self.raw = self.od.encode_desc(desc) # type: ignore[assignment]
119119

120120
@property
121121
def bits(self) -> "Bits":
@@ -142,6 +142,7 @@ def read(self, fmt: str = "raw") -> Union[int, bool, float, str, bytes]:
142142
return self.phys
143143
elif fmt == "desc":
144144
return self.desc
145+
raise ValueError(f"Invalid format '{fmt}'")
145146

146147
def write(
147148
self, value: Union[int, bool, float, str, bytes], fmt: str = "raw"
@@ -161,13 +162,14 @@ def write(
161162
elif fmt == "phys":
162163
self.phys = value
163164
elif fmt == "desc":
164-
self.desc = value
165+
self.desc = value # type: ignore[assignment]
165166

166167

167168
class Bits(Mapping):
168169

169170
def __init__(self, variable: Variable):
170171
self.variable = variable
172+
self.raw: Union[int, bool, float, str, bytes] = 0
171173
self.read()
172174

173175
@staticmethod
@@ -181,11 +183,11 @@ def _get_bits(key):
181183
return bits
182184

183185
def __getitem__(self, key) -> int:
184-
return self.variable.od.decode_bits(self.raw, self._get_bits(key))
186+
return self.variable.od.decode_bits(self.raw, self._get_bits(key)) # type: ignore[arg-type]
185187

186188
def __setitem__(self, key, value: int):
187189
self.raw = self.variable.od.encode_bits(
188-
self.raw, self._get_bits(key), value)
190+
self.raw, self._get_bits(key), value) # type: ignore[arg-type]
189191
self.write()
190192

191193
def __iter__(self):

test/test_od.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import unittest
22

33
from canopen import objectdictionary as od
4+
from canopen.variable import Variable
45

56

67
class TestDataConversions(unittest.TestCase):
@@ -260,6 +261,17 @@ def test_get_item_index(self):
260261
self.assertIsInstance(item, od.ODArray)
261262
self.assertIs(item, array)
262263

264+
def test_get_item_dot_on_variable(self):
265+
test_od = od.ObjectDictionary()
266+
var = od.ODVariable("Test Variable", 0x1000)
267+
test_od.add_object(var)
268+
with self.assertRaises(KeyError):
269+
test_od["Test Variable.sub"]
270+
271+
def test_get_variable_not_found(self):
272+
test_od = od.ObjectDictionary()
273+
self.assertIsNone(test_od.get_variable(0x9999))
274+
263275

264276
class TestArray(unittest.TestCase):
265277

@@ -276,5 +288,62 @@ def test_subindexes(self):
276288
self.assertEqual(array[3].name, "Test Variable_3")
277289

278290

291+
class _StubVariable(Variable):
292+
"""Minimal concrete Variable for testing read/write/bits."""
293+
294+
def __init__(self, od_var):
295+
super().__init__(od_var)
296+
self._data = od_var.encode_raw(od_var.default)
297+
298+
def get_data(self):
299+
return self._data
300+
301+
def set_data(self, data):
302+
self._data = data
303+
304+
305+
class TestVariable(unittest.TestCase):
306+
307+
def test_read_invalid_format(self):
308+
var = od.ODVariable("Test UNSIGNED8", 0x1000)
309+
var.data_type = od.UNSIGNED8
310+
var.default = 0
311+
v = _StubVariable(var)
312+
with self.assertRaises(ValueError):
313+
v.read(fmt="invalid")
314+
315+
def test_write_desc(self):
316+
var = od.ODVariable("Test UNSIGNED8", 0x1000)
317+
var.data_type = od.UNSIGNED8
318+
var.default = 0
319+
var.add_value_description(0, "Off")
320+
var.add_value_description(1, "On")
321+
v = _StubVariable(var)
322+
v.write("On", fmt="desc")
323+
self.assertEqual(v.raw, 1)
324+
325+
def test_raw_with_string_value(self):
326+
var = od.ODVariable("Test VISIBLE_STRING", 0x1000)
327+
var.data_type = od.VISIBLE_STRING
328+
var.default = "hello"
329+
var.add_value_description(0, "Off")
330+
v = _StubVariable(var)
331+
# String value must not be looked up in value_descriptions
332+
self.assertEqual(v.raw, "hello")
333+
334+
def test_bits(self):
335+
var = od.ODVariable("Test UNSIGNED8", 0x1000)
336+
var.data_type = od.UNSIGNED8
337+
var.default = 0
338+
var.add_bit_definition("BIT 0", [0])
339+
var.add_bit_definition("BIT 2 and 3", [2, 3])
340+
v = _StubVariable(var)
341+
v.raw = 5
342+
bits = v.bits
343+
self.assertEqual(bits[0], 1)
344+
bits[0] = 0
345+
self.assertEqual(v.raw, 4)
346+
347+
279348
if __name__ == "__main__":
280349
unittest.main()

test/test_sdo.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ def test_array_members_dynamic(self):
4848
for var in array.values():
4949
self.assertIsInstance(var, canopen.sdo.SdoVariable)
5050

51+
def test_get_variable_not_found(self):
52+
self.assertIsNone(self.sdo_node.get_variable(0x9999))
53+
5154

5255
class TestSDO(unittest.TestCase):
5356
"""

0 commit comments

Comments
 (0)