From 2645ee33fe275615b5c31c5a748704bb08288c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Wed, 7 Aug 2024 09:51:06 +0200 Subject: [PATCH 01/14] Add a _DummyNetwork class to designate "uninitialized" networks. This class can replace the dummy "None" value for attribute initializations, which can then be properly typed as Network to avoid static type checking errors. Switch to that new initialization in the NmtBase class as an example. 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 could be modified to provide a better exception message if desired, but this is just a minimal proof of concept so far. --- canopen/network.py | 7 +++++++ canopen/nmt.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/canopen/network.py b/canopen/network.py index 967d804a..dd2a9bcf 100644 --- a/canopen/network.py +++ b/canopen/network.py @@ -279,6 +279,13 @@ def __len__(self) -> int: return len(self.nodes) +class _DummyNetwork(Network): + """Empty network implementation as a placeholder before actual initialization.""" + + def __init__(self, bus: Optional[can.BusABC] = None): + """Do not initialize attributes, by skipping the parent constructor.""" + + class PeriodicMessageTask: """ Task object to transmit a message periodically using python-can's diff --git a/canopen/nmt.py b/canopen/nmt.py index 8ce737ea..4ec7dcca 100644 --- a/canopen/nmt.py +++ b/canopen/nmt.py @@ -2,7 +2,13 @@ import logging import struct import time -from typing import Callable, Optional +from typing import Callable, Optional, TYPE_CHECKING + +from canopen.network import _DummyNetwork + +if TYPE_CHECKING: + from canopen.network import Network + logger = logging.getLogger(__name__) @@ -45,7 +51,7 @@ class NmtBase: def __init__(self, node_id: int): self.id = node_id - self.network = None + self.network: Network = _DummyNetwork() self._state = 0 def on_command(self, can_id, data, timestamp): From 85375573d52b31b09fea297b854c7a598c54780e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Wed, 7 Aug 2024 10:14:49 +0200 Subject: [PATCH 02/14] Break import cycle. --- canopen/nmt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/canopen/nmt.py b/canopen/nmt.py index 4ec7dcca..e3e478f6 100644 --- a/canopen/nmt.py +++ b/canopen/nmt.py @@ -4,10 +4,10 @@ import time from typing import Callable, Optional, TYPE_CHECKING -from canopen.network import _DummyNetwork - if TYPE_CHECKING: - from canopen.network import Network + from canopen.network import Network, PeriodicMessageTask, _DummyNetwork +else: + def _DummyNetwork(): pass logger = logging.getLogger(__name__) From 0a2f169726146387b4ffa315e97fc57b39e71ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Wed, 7 Aug 2024 10:19:53 +0200 Subject: [PATCH 03/14] Even easier fix, move _DummyNetwork import to constructor. --- canopen/nmt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/canopen/nmt.py b/canopen/nmt.py index e3e478f6..cdc51fe2 100644 --- a/canopen/nmt.py +++ b/canopen/nmt.py @@ -5,9 +5,7 @@ from typing import Callable, Optional, TYPE_CHECKING if TYPE_CHECKING: - from canopen.network import Network, PeriodicMessageTask, _DummyNetwork -else: - def _DummyNetwork(): pass + from canopen.network import Network, PeriodicMessageTask logger = logging.getLogger(__name__) @@ -51,6 +49,7 @@ class NmtBase: def __init__(self, node_id: int): self.id = node_id + from canopen.network import _DummyNetwork self.network: Network = _DummyNetwork() self._state = 0 From 2f3d76308caa67976c49dc6bb0387a9d17d90506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Wed, 7 Aug 2024 10:40:05 +0200 Subject: [PATCH 04/14] Even simpler import cycle avoidance. This works because `from foo import bar` requires full module initialization at import time, while simply importing the module name does not. --- canopen/nmt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/canopen/nmt.py b/canopen/nmt.py index cdc51fe2..482d091d 100644 --- a/canopen/nmt.py +++ b/canopen/nmt.py @@ -4,6 +4,8 @@ import time from typing import Callable, Optional, TYPE_CHECKING +import canopen.network + if TYPE_CHECKING: from canopen.network import Network, PeriodicMessageTask @@ -49,8 +51,7 @@ class NmtBase: def __init__(self, node_id: int): self.id = node_id - from canopen.network import _DummyNetwork - self.network: Network = _DummyNetwork() + self.network: Network = canopen.network._DummyNetwork() self._state = 0 def on_command(self, can_id, data, timestamp): From 47f572c6d1c1c062dd56cb9bc98dafef54b67982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Wed, 7 Aug 2024 10:41:39 +0200 Subject: [PATCH 05/14] Provide a more useful exception message in _DummyNetwork. --- canopen/network.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/canopen/network.py b/canopen/network.py index dd2a9bcf..5f891aff 100644 --- a/canopen/network.py +++ b/canopen/network.py @@ -285,6 +285,10 @@ class _DummyNetwork(Network): def __init__(self, bus: Optional[can.BusABC] = None): """Do not initialize attributes, by skipping the parent constructor.""" + def __getattribute__(self, name): + raise RuntimeError("No actual Network object was assigned, " + "try associating to a real network first.") + class PeriodicMessageTask: """ From a318e0bbeea5687394f50cd515fd361a2825196f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Sun, 11 Aug 2024 23:03:08 +0200 Subject: [PATCH 06/14] Annotate node.BaseNode.network and avoid None dummy value. Use a _DummyNetwork object instead. --- canopen/node/base.py | 4 +++- canopen/node/local.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/canopen/node/base.py b/canopen/node/base.py index bf72d959..6efd867a 100644 --- a/canopen/node/base.py +++ b/canopen/node/base.py @@ -1,4 +1,6 @@ from typing import TextIO, Union + +import canopen.network from canopen.objectdictionary import ObjectDictionary, import_od @@ -17,7 +19,7 @@ def __init__( node_id: int, object_dictionary: Union[ObjectDictionary, str, TextIO], ): - self.network = None + self.network: canopen.network.Network = canopen.network._DummyNetwork() if not isinstance(object_dictionary, ObjectDictionary): object_dictionary = import_od(object_dictionary, node_id) diff --git a/canopen/node/local.py b/canopen/node/local.py index eb74b98d..a9914d94 100644 --- a/canopen/node/local.py +++ b/canopen/node/local.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import logging from typing import Dict, Union +import canopen.network from canopen.node.base import BaseNode from canopen.sdo import SdoServer, SdoAbortedError from canopen.pdo import PDO, TPDO, RPDO @@ -34,7 +37,7 @@ def __init__( self.add_write_callback(self.nmt.on_write) self.emcy = EmcyProducer(0x80 + self.id) - def associate_network(self, network): + def associate_network(self, network: canopen.network.Network): self.network = network self.sdo.network = network self.tpdo.network = network @@ -44,15 +47,15 @@ def associate_network(self, network): network.subscribe(self.sdo.rx_cobid, self.sdo.on_request) network.subscribe(0, self.nmt.on_command) - def remove_network(self): + def remove_network(self) -> None: self.network.unsubscribe(self.sdo.rx_cobid, self.sdo.on_request) self.network.unsubscribe(0, self.nmt.on_command) - self.network = None - self.sdo.network = None - self.tpdo.network = None - self.rpdo.network = None - self.nmt.network = None - self.emcy.network = None + self.network = canopen.network._DummyNetwork() + self.sdo.network = self.network + self.tpdo.network = self.network + self.rpdo.network = self.network + self.nmt.network = self.network + self.emcy.network = self.network def add_read_callback(self, callback): self._read_callbacks.append(callback) From 2e01485ef2abdba3906b2fffc4c0a9c871efc1c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Sun, 11 Aug 2024 23:06:57 +0200 Subject: [PATCH 07/14] Avoid None dummy value in node.RemoteNode.network. Use a _DummyNetwork object instead. Fix up check for "network is None" by providing a convenience has_network() function in the base class. --- canopen/node/base.py | 4 ++++ canopen/node/remote.py | 21 ++++++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/canopen/node/base.py b/canopen/node/base.py index 6efd867a..99cdc30d 100644 --- a/canopen/node/base.py +++ b/canopen/node/base.py @@ -26,3 +26,7 @@ def __init__( self.object_dictionary = object_dictionary self.id = node_id or self.object_dictionary.node_id + + def has_network(self) -> bool: + """Check whether the node has been associated to a network.""" + return not isinstance(self.network, canopen.network._DummyNetwork) diff --git a/canopen/node/remote.py b/canopen/node/remote.py index 4f3281db..0017c2f0 100644 --- a/canopen/node/remote.py +++ b/canopen/node/remote.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import logging from typing import Union, TextIO +import canopen.network from canopen.sdo import SdoClient, SdoCommunicationError, SdoAbortedError from canopen.nmt import NmtMaster from canopen.emcy import EmcyConsumer @@ -46,7 +49,7 @@ def __init__( if load_od: self.load_configuration() - def associate_network(self, network): + def associate_network(self, network: canopen.network.Network): self.network = network self.sdo.network = network self.pdo.network = network @@ -59,18 +62,18 @@ def associate_network(self, network): network.subscribe(0x80 + self.id, self.emcy.on_emcy) network.subscribe(0, self.nmt.on_command) - def remove_network(self): + def remove_network(self) -> None: for sdo in self.sdo_channels: self.network.unsubscribe(sdo.tx_cobid, sdo.on_response) self.network.unsubscribe(0x700 + self.id, self.nmt.on_heartbeat) self.network.unsubscribe(0x80 + self.id, self.emcy.on_emcy) self.network.unsubscribe(0, self.nmt.on_command) - self.network = None - self.sdo.network = None - self.pdo.network = None - self.tpdo.network = None - self.rpdo.network = None - self.nmt.network = None + self.network = canopen.network._DummyNetwork() + self.sdo.network = self.network + self.pdo.network = self.network + self.tpdo.network = self.network + self.rpdo.network = self.network + self.nmt.network = self.network def add_sdo(self, rx_cobid, tx_cobid): """Add an additional SDO channel. @@ -87,7 +90,7 @@ def add_sdo(self, rx_cobid, tx_cobid): """ client = SdoClient(rx_cobid, tx_cobid, self.object_dictionary) self.sdo_channels.append(client) - if self.network is not None: + if self.has_network(): self.network.subscribe(client.tx_cobid, client.on_response) return client From 1c56ec14ae77e47305bca6afdbf32ba396321548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Sun, 11 Aug 2024 23:15:26 +0200 Subject: [PATCH 08/14] Annotate emcy.EmcyProducer.network and avoid None dummy value. Use a _DummyNetwork object instead. --- canopen/emcy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/canopen/emcy.py b/canopen/emcy.py index 1bbdeb75..eb0338ee 100644 --- a/canopen/emcy.py +++ b/canopen/emcy.py @@ -4,6 +4,9 @@ import time from typing import Callable, List, Optional +import canopen.network + + # Error code, error register, vendor specific data EMCY_STRUCT = struct.Struct(" Date: Sun, 11 Aug 2024 23:16:06 +0200 Subject: [PATCH 09/14] Annotate sdo.SdoBase.network and avoid None dummy value. Use a _DummyNetwork object instead. --- canopen/sdo/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/canopen/sdo/base.py b/canopen/sdo/base.py index 0bb068b4..baca5de2 100644 --- a/canopen/sdo/base.py +++ b/canopen/sdo/base.py @@ -4,6 +4,7 @@ from typing import Iterator, Optional, Union from collections.abc import Mapping +import canopen.network from canopen import objectdictionary from canopen import variable from canopen.utils import pretty_index @@ -43,7 +44,7 @@ def __init__( """ self.rx_cobid = rx_cobid self.tx_cobid = tx_cobid - self.network = None + self.network: canopen.network.Network = canopen.network._DummyNetwork() self.od = od def __getitem__( From d2952f52e97f1e676802dcd7a88cd51ae0ff2890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Sun, 11 Aug 2024 23:21:20 +0200 Subject: [PATCH 10/14] Annotate lss.LssMaster.network and avoid None dummy value. Use a _DummyNetwork object instead. --- canopen/lss.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/canopen/lss.py b/canopen/lss.py index 93b7d4ec..bc05b158 100644 --- a/canopen/lss.py +++ b/canopen/lss.py @@ -3,6 +3,9 @@ import struct import queue +import canopen.network + + logger = logging.getLogger(__name__) # Command Specifier (CS) @@ -78,8 +81,8 @@ class LssMaster: #: Max time in seconds to wait for response from server RESPONSE_TIMEOUT = 0.5 - def __init__(self): - self.network = None + def __init__(self) -> None: + self.network: canopen.network.Network = canopen.network._DummyNetwork() self._node_id = 0 self._data = None self.responses = queue.Queue() From ab516b897c2d5ed165afcc6ef17e13dbf664a8dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Sun, 11 Aug 2024 23:29:20 +0200 Subject: [PATCH 11/14] Harmonize import of network.Network in NmtBase. --- canopen/nmt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/canopen/nmt.py b/canopen/nmt.py index 482d091d..bc2e13a4 100644 --- a/canopen/nmt.py +++ b/canopen/nmt.py @@ -7,7 +7,7 @@ import canopen.network if TYPE_CHECKING: - from canopen.network import Network, PeriodicMessageTask + from canopen.network import PeriodicMessageTask logger = logging.getLogger(__name__) @@ -51,7 +51,7 @@ class NmtBase: def __init__(self, node_id: int): self.id = node_id - self.network: Network = canopen.network._DummyNetwork() + self.network: canopen.network.Network = canopen.network._DummyNetwork() self._state = 0 def on_command(self, can_id, data, timestamp): From 378835ee7f5cbfea845c491b8d3db859366c2624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Sun, 11 Aug 2024 23:37:28 +0200 Subject: [PATCH 12/14] Avoid None dummy value in pdo.PdoBase.network. Use a _DummyNetwork object instead. --- canopen/pdo/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/canopen/pdo/base.py b/canopen/pdo/base.py index f2a7d205..3b0a316d 100644 --- a/canopen/pdo/base.py +++ b/canopen/pdo/base.py @@ -6,12 +6,12 @@ import logging import binascii +import canopen.network from canopen.sdo import SdoAbortedError from canopen import objectdictionary from canopen import variable if TYPE_CHECKING: - from canopen.network import Network from canopen import LocalNode, RemoteNode from canopen.pdo import RPDO, TPDO from canopen.sdo import SdoRecord @@ -30,7 +30,7 @@ class PdoBase(Mapping): """ def __init__(self, node: Union[LocalNode, RemoteNode]): - self.network: Optional[Network] = None + self.network: canopen.network.Network = canopen.network._DummyNetwork() self.map: Optional[PdoMaps] = None self.node: Union[LocalNode, RemoteNode] = node From 24560d0aaed934220473dabc4b891f50d3397d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Mon, 12 Aug 2024 09:43:58 +0200 Subject: [PATCH 13/14] Rename to _UninitializedNetwork, use singleton instance. --- canopen/emcy.py | 2 +- canopen/lss.py | 2 +- canopen/network.py | 6 +++++- canopen/nmt.py | 2 +- canopen/node/base.py | 4 ++-- canopen/node/local.py | 12 ++++++------ canopen/node/remote.py | 12 ++++++------ canopen/pdo/base.py | 2 +- canopen/sdo/base.py | 2 +- 9 files changed, 24 insertions(+), 20 deletions(-) diff --git a/canopen/emcy.py b/canopen/emcy.py index eb0338ee..520cfdc0 100644 --- a/canopen/emcy.py +++ b/canopen/emcy.py @@ -85,7 +85,7 @@ def wait( class EmcyProducer: def __init__(self, cob_id: int): - self.network: canopen.network.Network = canopen.network._DummyNetwork() + self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK self.cob_id = cob_id def send(self, code: int, register: int = 0, data: bytes = b""): diff --git a/canopen/lss.py b/canopen/lss.py index bc05b158..5ffcc11b 100644 --- a/canopen/lss.py +++ b/canopen/lss.py @@ -82,7 +82,7 @@ class LssMaster: RESPONSE_TIMEOUT = 0.5 def __init__(self) -> None: - self.network: canopen.network.Network = canopen.network._DummyNetwork() + self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK self._node_id = 0 self._data = None self.responses = queue.Queue() diff --git a/canopen/network.py b/canopen/network.py index af1b3137..9b3a919c 100644 --- a/canopen/network.py +++ b/canopen/network.py @@ -279,7 +279,7 @@ def __len__(self) -> int: return len(self.nodes) -class _DummyNetwork(Network): +class _UninitializedNetwork(Network): """Empty network implementation as a placeholder before actual initialization.""" def __init__(self, bus: Optional[can.BusABC] = None): @@ -290,6 +290,10 @@ def __getattribute__(self, name): "try associating to a real network first.") +#: Singleton instance +_UNINITIALIZED_NETWORK = _UninitializedNetwork() + + class PeriodicMessageTask: """ Task object to transmit a message periodically using python-can's diff --git a/canopen/nmt.py b/canopen/nmt.py index bc2e13a4..76f039a9 100644 --- a/canopen/nmt.py +++ b/canopen/nmt.py @@ -51,7 +51,7 @@ class NmtBase: def __init__(self, node_id: int): self.id = node_id - self.network: canopen.network.Network = canopen.network._DummyNetwork() + self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK self._state = 0 def on_command(self, can_id, data, timestamp): diff --git a/canopen/node/base.py b/canopen/node/base.py index 99cdc30d..45ad35b4 100644 --- a/canopen/node/base.py +++ b/canopen/node/base.py @@ -19,7 +19,7 @@ def __init__( node_id: int, object_dictionary: Union[ObjectDictionary, str, TextIO], ): - self.network: canopen.network.Network = canopen.network._DummyNetwork() + self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK if not isinstance(object_dictionary, ObjectDictionary): object_dictionary = import_od(object_dictionary, node_id) @@ -29,4 +29,4 @@ def __init__( def has_network(self) -> bool: """Check whether the node has been associated to a network.""" - return not isinstance(self.network, canopen.network._DummyNetwork) + return not isinstance(self.network, canopen.network._UninitializedNetwork) diff --git a/canopen/node/local.py b/canopen/node/local.py index a9914d94..eb614601 100644 --- a/canopen/node/local.py +++ b/canopen/node/local.py @@ -50,12 +50,12 @@ def associate_network(self, network: canopen.network.Network): def remove_network(self) -> None: self.network.unsubscribe(self.sdo.rx_cobid, self.sdo.on_request) self.network.unsubscribe(0, self.nmt.on_command) - self.network = canopen.network._DummyNetwork() - self.sdo.network = self.network - self.tpdo.network = self.network - self.rpdo.network = self.network - self.nmt.network = self.network - self.emcy.network = self.network + self.network = canopen.network._UNINITIALIZED_NETWORK + self.sdo.network = canopen.network._UNINITIALIZED_NETWORK + self.tpdo.network = canopen.network._UNINITIALIZED_NETWORK + self.rpdo.network = canopen.network._UNINITIALIZED_NETWORK + self.nmt.network = canopen.network._UNINITIALIZED_NETWORK + self.emcy.network = canopen.network._UNINITIALIZED_NETWORK def add_read_callback(self, callback): self._read_callbacks.append(callback) diff --git a/canopen/node/remote.py b/canopen/node/remote.py index 0017c2f0..b354b8f9 100644 --- a/canopen/node/remote.py +++ b/canopen/node/remote.py @@ -68,12 +68,12 @@ def remove_network(self) -> None: self.network.unsubscribe(0x700 + self.id, self.nmt.on_heartbeat) self.network.unsubscribe(0x80 + self.id, self.emcy.on_emcy) self.network.unsubscribe(0, self.nmt.on_command) - self.network = canopen.network._DummyNetwork() - self.sdo.network = self.network - self.pdo.network = self.network - self.tpdo.network = self.network - self.rpdo.network = self.network - self.nmt.network = self.network + self.network = canopen.network._UNINITIALIZED_NETWORK + self.sdo.network = canopen.network._UNINITIALIZED_NETWORK + self.pdo.network = canopen.network._UNINITIALIZED_NETWORK + self.tpdo.network = canopen.network._UNINITIALIZED_NETWORK + self.rpdo.network = canopen.network._UNINITIALIZED_NETWORK + self.nmt.network = canopen.network._UNINITIALIZED_NETWORK def add_sdo(self, rx_cobid, tx_cobid): """Add an additional SDO channel. diff --git a/canopen/pdo/base.py b/canopen/pdo/base.py index 3b0a316d..dc81aa60 100644 --- a/canopen/pdo/base.py +++ b/canopen/pdo/base.py @@ -30,7 +30,7 @@ class PdoBase(Mapping): """ def __init__(self, node: Union[LocalNode, RemoteNode]): - self.network: canopen.network.Network = canopen.network._DummyNetwork() + self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK self.map: Optional[PdoMaps] = None self.node: Union[LocalNode, RemoteNode] = node diff --git a/canopen/sdo/base.py b/canopen/sdo/base.py index baca5de2..b4be9e67 100644 --- a/canopen/sdo/base.py +++ b/canopen/sdo/base.py @@ -44,7 +44,7 @@ def __init__( """ self.rx_cobid = rx_cobid self.tx_cobid = tx_cobid - self.network: canopen.network.Network = canopen.network._DummyNetwork() + self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK self.od = od def __getitem__( From b89270a1fc744deba6f7d94a9ef0a742a5903fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Mon, 12 Aug 2024 10:16:52 +0200 Subject: [PATCH 14/14] Final --- canopen/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/canopen/network.py b/canopen/network.py index 9b3a919c..8f162d10 100644 --- a/canopen/network.py +++ b/canopen/network.py @@ -3,7 +3,7 @@ from collections.abc import MutableMapping import logging import threading -from typing import Callable, Dict, Iterator, List, Optional, Union +from typing import Callable, Dict, Final, Iterator, List, Optional, Union import can from can import Listener @@ -291,7 +291,7 @@ def __getattribute__(self, name): #: Singleton instance -_UNINITIALIZED_NETWORK = _UninitializedNetwork() +_UNINITIALIZED_NETWORK: Final[Network] = _UninitializedNetwork() class PeriodicMessageTask: