Skip to content

Commit 4b45c19

Browse files
committed
Merge branch 'master' into feature-mypy-step1
2 parents 3b66ae5 + 48cca7c commit 4b45c19

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+975
-282
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ name: Python package
55

66
on:
77
push:
8-
branches: [ "master" ]
8+
branches:
9+
- 'master'
10+
paths-ignore:
11+
- 'README.rst'
12+
- 'LICENSE.txt'
913
pull_request:
10-
branches: [ "master" ]
14+
branches:
15+
- 'master'
16+
paths-ignore:
17+
- 'README.rst'
18+
- 'LICENSE.txt'
1119

1220
jobs:
1321
build:
@@ -20,19 +28,19 @@ jobs:
2028
features: ['', '[db_export]']
2129

2230
steps:
23-
- uses: actions/checkout@v3
31+
- uses: actions/checkout@v4
2432
- name: Set up Python ${{ matrix.python-version }}
25-
uses: actions/setup-python@v3
33+
uses: actions/setup-python@v5
2634
with:
2735
python-version: ${{ matrix.python-version }}
36+
cache: 'pip'
37+
cache-dependency-path: |
38+
'pyproject.toml'
39+
'requirements-dev.txt'
2840
- name: Install dependencies
29-
run: |
30-
python -m pip install --upgrade pip
31-
pip install pytest pytest-cov
32-
pip install -e '.${{ matrix.features }}'
41+
run: python3 -m pip install -e '.${{ matrix.features }}' -r requirements-dev.txt
3342
- name: Test with pytest
34-
run: |
35-
pytest -v --cov=canopen --cov-report=xml --cov-branch
43+
run: pytest -v --cov=canopen --cov-report=xml --cov-branch
3644
- name: Upload coverage reports to Codecov
3745
uses: codecov/codecov-action@v4
3846
with:

README.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,18 @@ Install from PyPI_ using :program:`pip`::
4242

4343
Install from latest ``master`` on GitHub::
4444

45-
$ pip install https://github.com/christiansandberg/canopen/archive/master.zip
45+
$ pip install https://github.com/canopen-python/canopen/archive/master.zip
4646

4747
If you want to be able to change the code while using it, clone it then install
4848
it in `develop mode`_::
4949

50-
$ git clone https://github.com/christiansandberg/canopen.git
50+
$ git clone https://github.com/canopen-python/canopen.git
5151
$ cd canopen
5252
$ pip install -e .
5353

5454
Unit tests can be run using the pytest_ framework::
5555

56-
$ pip install pytest
56+
$ pip install -r requirements-dev.txt
5757
$ pytest -v
5858

5959
You can also use :mod:`unittest` standard library module::

canopen/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
from canopen.network import Network, NodeScanner
2-
from canopen.node import RemoteNode, LocalNode
3-
from canopen.sdo import SdoCommunicationError, SdoAbortedError
4-
from canopen.objectdictionary import import_od, export_od, ObjectDictionary, ObjectDictionaryError
2+
from canopen.node import LocalNode, RemoteNode
3+
from canopen.objectdictionary import (
4+
ObjectDictionary,
5+
ObjectDictionaryError,
6+
export_od,
7+
import_od,
8+
)
59
from canopen.profiles.p402 import BaseNode402
10+
from canopen.sdo import SdoAbortedError, SdoCommunicationError
11+
612
try:
713
from canopen._version import version as __version__
814
except ImportError:

canopen/emcy.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from __future__ import annotations
2-
import struct
32
import logging
3+
import struct
44
import threading
55
import time
66
from typing import Callable, List, Optional
77

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

@@ -83,7 +86,7 @@ def wait(
8386
class EmcyProducer:
8487

8588
def __init__(self, cob_id: int):
86-
self.network = None
89+
self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK
8790
self.cob_id = cob_id
8891

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

canopen/lss.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import logging
2-
import time
3-
import struct
42
import queue
3+
import struct
4+
import time
5+
6+
import canopen.network
7+
58

69
logger = logging.getLogger(__name__)
710

@@ -78,11 +81,11 @@ 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
85-
self.responses = queue.Queue()
88+
self.responses: queue.Queue[bytes] = queue.Queue()
8689

8790
def send_switch_state_global(self, mode):
8891
"""switch mode to CONFIGURATION_STATE or WAITING_STATE

canopen/network.py

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,21 @@
11
from __future__ import annotations
22

3-
from collections.abc import MutableMapping
43
import logging
54
import threading
5+
from collections.abc import MutableMapping
6+
from typing import Callable, Dict, Final, Iterator, List, Optional, Union
67
from typing import Callable, Dict, Iterator, List, Optional, Union, TYPE_CHECKING, TextIO
78

8-
try:
9-
import can
10-
from can import Listener
11-
from can import CanError
12-
except ImportError:
13-
# Type checkers don't like this conditional logic, so it is only run when
14-
# not type checking
15-
if not TYPE_CHECKING:
16-
# Do not fail if python-can is not installed
17-
can = None
18-
CanError = Exception
19-
class Listener:
20-
""" Dummy listener """
21-
22-
from canopen.node import RemoteNode, LocalNode
23-
from canopen.sync import SyncProducer
24-
from canopen.timestamp import TimeProducer
25-
from canopen.nmt import NmtMaster
9+
import can
10+
from can import Listener
11+
2612
from canopen.lss import LssMaster
27-
from canopen.objectdictionary.eds import import_from_node
13+
from canopen.nmt import NmtMaster
14+
from canopen.node import LocalNode, RemoteNode
2815
from canopen.objectdictionary import ObjectDictionary
16+
from canopen.objectdictionary.eds import import_from_node
17+
from canopen.sync import SyncProducer
18+
from canopen.timestamp import TimeProducer
2919

3020
if TYPE_CHECKING:
3121
from can.typechecking import CanData
@@ -38,6 +28,9 @@ class Listener:
3828
class Network(MutableMapping):
3929
"""Representation of one CAN bus containing one or more nodes."""
4030

31+
NOTIFIER_CYCLE: float = 1.0 #: Maximum waiting time for one notifier iteration.
32+
NOTIFIER_SHUTDOWN_TIMEOUT: float = 5.0 #: Maximum waiting time to stop notifiers.
33+
4134
def __init__(self, bus: Optional[can.BusABC] = None):
4235
"""
4336
:param can.BusABC bus:
@@ -118,7 +111,7 @@ def connect(self, *args, **kwargs) -> Network:
118111
if self.bus is None:
119112
self.bus = can.Bus(*args, **kwargs)
120113
logger.info("Connected to '%s'", self.bus.channel_info)
121-
self.notifier = can.Notifier(self.bus, self.listeners, 1)
114+
self.notifier = can.Notifier(self.bus, self.listeners, self.NOTIFIER_CYCLE)
122115
return self
123116

124117
def disconnect(self) -> None:
@@ -130,7 +123,7 @@ def disconnect(self) -> None:
130123
if hasattr(node, "pdo"):
131124
node.pdo.stop()
132125
if self.notifier is not None:
133-
self.notifier.stop()
126+
self.notifier.stop(self.NOTIFIER_SHUTDOWN_TIMEOUT)
134127
if self.bus is not None:
135128
self.bus.shutdown()
136129
self.bus = None
@@ -296,6 +289,21 @@ def __len__(self) -> int:
296289
return len(self.nodes)
297290

298291

292+
class _UninitializedNetwork(Network):
293+
"""Empty network implementation as a placeholder before actual initialization."""
294+
295+
def __init__(self, bus: Optional[can.BusABC] = None):
296+
"""Do not initialize attributes, by skipping the parent constructor."""
297+
298+
def __getattribute__(self, name):
299+
raise RuntimeError("No actual Network object was assigned, "
300+
"try associating to a real network first.")
301+
302+
303+
#: Singleton instance
304+
_UNINITIALIZED_NETWORK: Final[Network] = _UninitializedNetwork()
305+
306+
299307
class PeriodicMessageTask:
300308
"""
301309
Task object to transmit a message periodically using python-can's
@@ -325,7 +333,6 @@ def __init__(
325333
self.msg = can.Message(is_extended_id=can_id > 0x7FF,
326334
arbitration_id=can_id,
327335
data=data, is_remote_frame=remote)
328-
self._task = None
329336
self._start()
330337

331338
def _start(self):
@@ -391,9 +398,6 @@ class NodeScanner:
391398
The network to use when doing active searching.
392399
"""
393400

394-
#: Activate or deactivate scanning
395-
active = True
396-
397401
SERVICES = (0x700, 0x580, 0x180, 0x280, 0x380, 0x480, 0x80)
398402

399403
def __init__(self, network: Optional[Network] = None):

canopen/nmt.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import threading
21
import logging
32
import struct
3+
import threading
44
import time
5-
from typing import Callable, Optional, List, TYPE_CHECKING
5+
from typing import Callable, Optional, TYPE_CHECKING, List
6+
7+
import canopen.network
68

79
if TYPE_CHECKING:
8-
from canopen.network import Network, PeriodicMessageTask
10+
from canopen.network import PeriodicMessageTask
11+
912

1013
logger = logging.getLogger(__name__)
1114

@@ -48,7 +51,7 @@ class NmtBase:
4851

4952
def __init__(self, node_id: int):
5053
self.id = node_id
51-
self.network: Optional[Network] = None
54+
self.network: canopen.network.Network = canopen.network._UNINITIALIZED_NETWORK
5255
self._state = 0
5356

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

canopen/node/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from canopen.node.remote import RemoteNode
21
from canopen.node.local import LocalNode
2+
from canopen.node.remote import RemoteNode

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, Optional
2+
3+
import canopen.network
24
from canopen.objectdictionary import ObjectDictionary, import_od
35

46

@@ -17,7 +19,7 @@ def __init__(
1719
node_id: Optional[int],
1820
object_dictionary: Union[ObjectDictionary, str, TextIO, None],
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)
@@ -27,3 +29,7 @@ def __init__(
2729
if node_id is None:
2830
raise ValueError("Node ID must be specified")
2931
self.id: int = node_id
32+
33+
def has_network(self) -> bool:
34+
"""Check whether the node has been associated to a network."""
35+
return not isinstance(self.network, canopen.network._UninitializedNetwork)

canopen/node/local.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
from __future__ import annotations
2+
13
import logging
24
from typing import Dict, Union, List, Protocol, TextIO, Optional
35

4-
from canopen.node.base import BaseNode
5-
from canopen.sdo import SdoServer, SdoAbortedError
6-
from canopen.pdo import PDO, TPDO, RPDO
7-
from canopen.nmt import NmtSlave
6+
import canopen.network
7+
from canopen import objectdictionary
88
from canopen.emcy import EmcyProducer
9+
from canopen.nmt import NmtSlave
10+
from canopen.node.base import BaseNode
911
from canopen.objectdictionary import ObjectDictionary, ODVariable
10-
from canopen import objectdictionary
12+
from canopen.pdo import PDO, RPDO, TPDO
13+
from canopen.sdo import SdoAbortedError, SdoServer
14+
1115

1216
logger = logging.getLogger(__name__)
1317

@@ -50,7 +54,7 @@ def __init__(
5054
self.add_write_callback(self.nmt.on_write)
5155
self.emcy = EmcyProducer(0x80 + self.id)
5256

53-
def associate_network(self, network):
57+
def associate_network(self, network: canopen.network.Network):
5458
self.network = network
5559
self.sdo.network = network
5660
self.tpdo.network = network
@@ -60,15 +64,15 @@ def associate_network(self, network):
6064
network.subscribe(self.sdo.rx_cobid, self.sdo.on_request)
6165
network.subscribe(0, self.nmt.on_command)
6266

63-
def remove_network(self):
67+
def remove_network(self) -> None:
6468
self.network.unsubscribe(self.sdo.rx_cobid, self.sdo.on_request)
6569
self.network.unsubscribe(0, self.nmt.on_command)
66-
self.network = None
67-
self.sdo.network = None
68-
self.tpdo.network = None
69-
self.rpdo.network = None
70-
self.nmt.network = None
71-
self.emcy.network = None
70+
self.network = canopen.network._UNINITIALIZED_NETWORK
71+
self.sdo.network = canopen.network._UNINITIALIZED_NETWORK
72+
self.tpdo.network = canopen.network._UNINITIALIZED_NETWORK
73+
self.rpdo.network = canopen.network._UNINITIALIZED_NETWORK
74+
self.nmt.network = canopen.network._UNINITIALIZED_NETWORK
75+
self.emcy.network = canopen.network._UNINITIALIZED_NETWORK
7276

7377
def add_read_callback(self, callback):
7478
self._read_callbacks.append(callback)

0 commit comments

Comments
 (0)