Skip to content

Commit c781a22

Browse files
authored
Add a dummy object to designate "uninitialized" networks (fixes #511) (#525)
Add an _UnitializedNetwork class and a singleton _UNINITIALIZED_NETWORK instance. It can replace the dummy "None" value for attribute initializations, which can then be properly typed as Network to avoid static type checking errors. This has the benefit of not needing `self.network is not None` checks at run-time wherever a method or attribute access is used, but still satisfies static type checking. When hitting such a code path at run-time, of course it will lead to an exception because the attributes required in the Network methods are not set. But that is a case of wrong API usage (accessing a network without associating it first), which a static checker cannot detect reliably. The dummy class provides a descriptive exception message when any attribute is accessed on it.
1 parent 6bc90a8 commit c781a22

File tree

9 files changed

+62
-26
lines changed

9 files changed

+62
-26
lines changed

canopen/emcy.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import time
55
from typing import Callable, List, Optional
66

7+
import canopen.network
8+
9+
710
# Error code, error register, vendor specific data
811
EMCY_STRUCT = struct.Struct("<HB5s")
912

@@ -82,7 +85,7 @@ def wait(
8285
class EmcyProducer:
8386

8487
def __init__(self, cob_id: int):
85-
self.network = None
88+
self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK
8689
self.cob_id = cob_id
8790

8891
def send(self, code: int, register: int = 0, data: bytes = b""):

canopen/lss.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import struct
44
import queue
55

6+
import canopen.network
7+
8+
69
logger = logging.getLogger(__name__)
710

811
# Command Specifier (CS)
@@ -78,8 +81,8 @@ class LssMaster:
7881
#: Max time in seconds to wait for response from server
7982
RESPONSE_TIMEOUT = 0.5
8083

81-
def __init__(self):
82-
self.network = None
84+
def __init__(self) -> None:
85+
self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK
8386
self._node_id = 0
8487
self._data = None
8588
self.responses = queue.Queue()

canopen/network.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from collections.abc import MutableMapping
44
import logging
55
import threading
6-
from typing import Callable, Dict, Iterator, List, Optional, Union
6+
from typing import Callable, Dict, Final, Iterator, List, Optional, Union
77

88
import can
99
from can import Listener
@@ -282,6 +282,21 @@ def __len__(self) -> int:
282282
return len(self.nodes)
283283

284284

285+
class _UninitializedNetwork(Network):
286+
"""Empty network implementation as a placeholder before actual initialization."""
287+
288+
def __init__(self, bus: Optional[can.BusABC] = None):
289+
"""Do not initialize attributes, by skipping the parent constructor."""
290+
291+
def __getattribute__(self, name):
292+
raise RuntimeError("No actual Network object was assigned, "
293+
"try associating to a real network first.")
294+
295+
296+
#: Singleton instance
297+
_UNINITIALIZED_NETWORK: Final[Network] = _UninitializedNetwork()
298+
299+
285300
class PeriodicMessageTask:
286301
"""
287302
Task object to transmit a message periodically using python-can's

canopen/nmt.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import time
55
from typing import Callable, Optional, TYPE_CHECKING
66

7+
import canopen.network
8+
79
if TYPE_CHECKING:
810
from canopen.network import PeriodicMessageTask
911

@@ -49,7 +51,7 @@ class NmtBase:
4951

5052
def __init__(self, node_id: int):
5153
self.id = node_id
52-
self.network = None
54+
self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK
5355
self._state = 0
5456

5557
def on_command(self, can_id, data, timestamp):

canopen/node/base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from typing import TextIO, Union
2+
3+
import canopen.network
24
from canopen.objectdictionary import ObjectDictionary, import_od
35

46

@@ -17,10 +19,14 @@ def __init__(
1719
node_id: int,
1820
object_dictionary: Union[ObjectDictionary, str, TextIO],
1921
):
20-
self.network = None
22+
self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK
2123

2224
if not isinstance(object_dictionary, ObjectDictionary):
2325
object_dictionary = import_od(object_dictionary, node_id)
2426
self.object_dictionary = object_dictionary
2527

2628
self.id = node_id or self.object_dictionary.node_id
29+
30+
def has_network(self) -> bool:
31+
"""Check whether the node has been associated to a network."""
32+
return not isinstance(self.network, canopen.network._UninitializedNetwork)

canopen/node/local.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from __future__ import annotations
2+
13
import logging
24
from typing import Dict, Union
35

6+
import canopen.network
47
from canopen.node.base import BaseNode
58
from canopen.sdo import SdoServer, SdoAbortedError
69
from canopen.pdo import PDO, TPDO, RPDO
@@ -34,7 +37,7 @@ def __init__(
3437
self.add_write_callback(self.nmt.on_write)
3538
self.emcy = EmcyProducer(0x80 + self.id)
3639

37-
def associate_network(self, network):
40+
def associate_network(self, network: canopen.network.Network):
3841
self.network = network
3942
self.sdo.network = network
4043
self.tpdo.network = network
@@ -44,15 +47,15 @@ def associate_network(self, network):
4447
network.subscribe(self.sdo.rx_cobid, self.sdo.on_request)
4548
network.subscribe(0, self.nmt.on_command)
4649

47-
def remove_network(self):
50+
def remove_network(self) -> None:
4851
self.network.unsubscribe(self.sdo.rx_cobid, self.sdo.on_request)
4952
self.network.unsubscribe(0, self.nmt.on_command)
50-
self.network = None
51-
self.sdo.network = None
52-
self.tpdo.network = None
53-
self.rpdo.network = None
54-
self.nmt.network = None
55-
self.emcy.network = None
53+
self.network = canopen.network._UNINITIALIZED_NETWORK
54+
self.sdo.network = canopen.network._UNINITIALIZED_NETWORK
55+
self.tpdo.network = canopen.network._UNINITIALIZED_NETWORK
56+
self.rpdo.network = canopen.network._UNINITIALIZED_NETWORK
57+
self.nmt.network = canopen.network._UNINITIALIZED_NETWORK
58+
self.emcy.network = canopen.network._UNINITIALIZED_NETWORK
5659

5760
def add_read_callback(self, callback):
5861
self._read_callbacks.append(callback)

canopen/node/remote.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from __future__ import annotations
2+
13
import logging
24
from typing import Union, TextIO
35

6+
import canopen.network
47
from canopen.sdo import SdoClient, SdoCommunicationError, SdoAbortedError
58
from canopen.nmt import NmtMaster
69
from canopen.emcy import EmcyConsumer
@@ -46,7 +49,7 @@ def __init__(
4649
if load_od:
4750
self.load_configuration()
4851

49-
def associate_network(self, network):
52+
def associate_network(self, network: canopen.network.Network):
5053
self.network = network
5154
self.sdo.network = network
5255
self.pdo.network = network
@@ -59,18 +62,18 @@ def associate_network(self, network):
5962
network.subscribe(0x80 + self.id, self.emcy.on_emcy)
6063
network.subscribe(0, self.nmt.on_command)
6164

62-
def remove_network(self):
65+
def remove_network(self) -> None:
6366
for sdo in self.sdo_channels:
6467
self.network.unsubscribe(sdo.tx_cobid, sdo.on_response)
6568
self.network.unsubscribe(0x700 + self.id, self.nmt.on_heartbeat)
6669
self.network.unsubscribe(0x80 + self.id, self.emcy.on_emcy)
6770
self.network.unsubscribe(0, self.nmt.on_command)
68-
self.network = None
69-
self.sdo.network = None
70-
self.pdo.network = None
71-
self.tpdo.network = None
72-
self.rpdo.network = None
73-
self.nmt.network = None
71+
self.network = canopen.network._UNINITIALIZED_NETWORK
72+
self.sdo.network = canopen.network._UNINITIALIZED_NETWORK
73+
self.pdo.network = canopen.network._UNINITIALIZED_NETWORK
74+
self.tpdo.network = canopen.network._UNINITIALIZED_NETWORK
75+
self.rpdo.network = canopen.network._UNINITIALIZED_NETWORK
76+
self.nmt.network = canopen.network._UNINITIALIZED_NETWORK
7477

7578
def add_sdo(self, rx_cobid, tx_cobid):
7679
"""Add an additional SDO channel.
@@ -87,7 +90,7 @@ def add_sdo(self, rx_cobid, tx_cobid):
8790
"""
8891
client = SdoClient(rx_cobid, tx_cobid, self.object_dictionary)
8992
self.sdo_channels.append(client)
90-
if self.network is not None:
93+
if self.has_network():
9194
self.network.subscribe(client.tx_cobid, client.on_response)
9295
return client
9396

canopen/pdo/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import logging
77
import binascii
88

9+
import canopen.network
910
from canopen.sdo import SdoAbortedError
1011
from canopen import objectdictionary
1112
from canopen import variable
1213

1314
if TYPE_CHECKING:
14-
from canopen.network import Network
1515
from canopen import LocalNode, RemoteNode
1616
from canopen.pdo import RPDO, TPDO
1717
from canopen.sdo import SdoRecord
@@ -30,7 +30,7 @@ class PdoBase(Mapping):
3030
"""
3131

3232
def __init__(self, node: Union[LocalNode, RemoteNode]):
33-
self.network: Optional[Network] = None
33+
self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK
3434
self.map: Optional[PdoMaps] = None
3535
self.node: Union[LocalNode, RemoteNode] = node
3636

canopen/sdo/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Iterator, Optional, Union
55
from collections.abc import Mapping
66

7+
import canopen.network
78
from canopen import objectdictionary
89
from canopen import variable
910
from canopen.utils import pretty_index
@@ -43,7 +44,7 @@ def __init__(
4344
"""
4445
self.rx_cobid = rx_cobid
4546
self.tx_cobid = tx_cobid
46-
self.network = None
47+
self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK
4748
self.od = od
4849

4950
def __getitem__(

0 commit comments

Comments
 (0)