Skip to content

Commit 9e07ce7

Browse files
committed
Merge remote-tracking branch 'erlend-aasland/typing/ci-and-config' into mypy
2 parents 295bc51 + cc4dc96 commit 9e07ce7

File tree

19 files changed

+539
-175
lines changed

19 files changed

+539
-175
lines changed

.github/workflows/pr-linters.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Run PR linters
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: read
9+
pull-requests: read
10+
11+
jobs:
12+
13+
mypy:
14+
name: Run mypy static type checker (optional)
15+
runs-on: ubuntu-latest
16+
continue-on-error: true
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: 3.12
22+
cache: pip
23+
cache-dependency-path: |
24+
'pyproject.toml'
25+
'requirements-dev.txt'
26+
- run: pip install -r requirements-dev.txt -e .
27+
- name: Run mypy and report
28+
run: mypy --config-file pyproject.toml .

.github/workflows/pythonpackage.yml

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@ name: Python package
55

66
on:
77
push:
8-
branches: [ "master", "mypy" ]
8+
branches:
9+
- 'master'
10+
- 'mypy'
11+
paths-ignore:
12+
- 'README.rst'
13+
- 'LICENSE.txt'
914
pull_request:
10-
branches: [ "master", "mypy" ]
15+
branches:
16+
- 'master'
17+
- 'mypy'
18+
paths-ignore:
19+
- 'README.rst'
20+
- 'LICENSE.txt'
1121

1222
jobs:
1323
build:
@@ -20,19 +30,19 @@ jobs:
2030
features: ['', '[db_export]']
2131

2232
steps:
23-
- uses: actions/checkout@v3
33+
- uses: actions/checkout@v4
2434
- name: Set up Python ${{ matrix.python-version }}
25-
uses: actions/setup-python@v3
35+
uses: actions/setup-python@v5
2636
with:
2737
python-version: ${{ matrix.python-version }}
38+
cache: 'pip'
39+
cache-dependency-path: |
40+
'pyproject.toml'
41+
'requirements-dev.txt'
2842
- name: Install dependencies
29-
run: |
30-
python -m pip install --upgrade pip
31-
pip install pytest pytest-cov
32-
pip install -e '.${{ matrix.features }}'
43+
run: python3 -m pip install -e '.${{ matrix.features }}' -r requirements-dev.txt
3344
- name: Test with pytest
34-
run: |
35-
pytest -v --cov=canopen --cov-report=xml --cov-branch
45+
run: pytest -v --cov=canopen --cov-report=xml --cov-branch
3646
- name: Upload coverage reports to Codecov
3747
uses: codecov/codecov-action@v4
3848
with:
@@ -53,18 +63,3 @@ jobs:
5363
run: python3 -m pip install -r doc/requirements.txt -e .
5464
- name: Build docs
5565
run: make -C doc html
56-
57-
mypy:
58-
continue-on-error: true
59-
runs-on: ubuntu-latest
60-
steps:
61-
- uses: actions/checkout@v4
62-
- uses: actions/setup-python@v5
63-
with:
64-
python-version: 3.12
65-
cache: pip
66-
cache-dependency-path: |
67-
'pyproject.toml'
68-
'requirements-dev.txt'
69-
- run: pip install -r requirements-dev.txt -e .
70-
- run: mypy --config-file pyproject.toml .

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ it in `develop mode`_::
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/network.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
class Network(MutableMapping):
2626
"""Representation of one CAN bus containing one or more nodes."""
2727

28+
NOTIFIER_CYCLE: float = 1.0 #: Maximum waiting time for one notifier iteration.
29+
NOTIFIER_SHUTDOWN_TIMEOUT: float = 5.0 #: Maximum waiting time to stop notifiers.
30+
2831
def __init__(self, bus: Optional[can.BusABC] = None):
2932
"""
3033
:param can.BusABC bus:
@@ -38,7 +41,7 @@ def __init__(self, bus: Optional[can.BusABC] = None):
3841
#: List of :class:`can.Listener` objects.
3942
#: Includes at least MessageListener.
4043
self.listeners = [MessageListener(self)]
41-
self.notifier = None
44+
self.notifier: Optional[can.Notifier] = None
4245
self.nodes: Dict[int, Union[RemoteNode, LocalNode]] = {}
4346
self.subscribers: Dict[int, List[Callback]] = {}
4447
self.send_lock = threading.Lock()
@@ -105,7 +108,7 @@ def connect(self, *args, **kwargs) -> Network:
105108
if self.bus is None:
106109
self.bus = can.Bus(*args, **kwargs)
107110
logger.info("Connected to '%s'", self.bus.channel_info)
108-
self.notifier = can.Notifier(self.bus, self.listeners, 1)
111+
self.notifier = can.Notifier(self.bus, self.listeners, self.NOTIFIER_CYCLE)
109112
return self
110113

111114
def disconnect(self) -> None:
@@ -117,7 +120,7 @@ def disconnect(self) -> None:
117120
if hasattr(node, "pdo"):
118121
node.pdo.stop()
119122
if self.notifier is not None:
120-
self.notifier.stop()
123+
self.notifier.stop(self.NOTIFIER_SHUTDOWN_TIMEOUT)
121124
if self.bus is not None:
122125
self.bus.shutdown()
123126
self.bus = None
@@ -308,7 +311,6 @@ def __init__(
308311
self.msg = can.Message(is_extended_id=can_id > 0x7FF,
309312
arbitration_id=can_id,
310313
data=data, is_remote_frame=remote)
311-
self._task = None
312314
self._start()
313315

314316
def _start(self):

canopen/nmt.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
import logging
33
import struct
44
import time
5-
from typing import Callable, Optional
5+
from typing import Callable, Optional, TYPE_CHECKING
6+
7+
if TYPE_CHECKING:
8+
from canopen.network import PeriodicMessageTask
9+
610

711
logger = logging.getLogger(__name__)
812

@@ -107,7 +111,7 @@ class NmtMaster(NmtBase):
107111
def __init__(self, node_id: int):
108112
super(NmtMaster, self).__init__(node_id)
109113
self._state_received = None
110-
self._node_guarding_producer = None
114+
self._node_guarding_producer: Optional[PeriodicMessageTask] = None
111115
#: Timestamp of last heartbeat message
112116
self.timestamp: Optional[float] = None
113117
self.state_update = threading.Condition()
@@ -197,7 +201,7 @@ class NmtSlave(NmtBase):
197201

198202
def __init__(self, node_id: int, local_node):
199203
super(NmtSlave, self).__init__(node_id)
200-
self._send_task = None
204+
self._send_task: Optional[PeriodicMessageTask] = None
201205
self._heartbeat_time_ms = 0
202206
self._local_node = local_node
203207

canopen/sdo/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,12 @@ def __getitem__(self, subindex: Union[int, str]) -> SdoVariable:
105105
return SdoVariable(self.sdo_node, self.od[subindex])
106106

107107
def __iter__(self) -> Iterator[int]:
108-
return iter(self.od)
108+
# Skip the "highest subindex" entry, which is not part of the data
109+
return filter(None, iter(self.od))
109110

110111
def __len__(self) -> int:
111-
return len(self.od)
112+
# Skip the "highest subindex" entry, which is not part of the data
113+
return len(self.od) - int(0 in self.od)
112114

113115
def __contains__(self, subindex: Union[int, str]) -> bool:
114116
return subindex in self.od
@@ -127,6 +129,7 @@ def __getitem__(self, subindex: Union[int, str]) -> SdoVariable:
127129
return SdoVariable(self.sdo_node, self.od[subindex])
128130

129131
def __iter__(self) -> Iterator[int]:
132+
# Skip the "highest subindex" entry, which is not part of the data
130133
return iter(range(1, len(self) + 1))
131134

132135
def __len__(self) -> int:

doc/conf.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
4545

4646
html_theme = 'furo'
47-
html_static_path = ['_static']
4847

4948
# -- Options for HTML help output --------------------------------------------
5049
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-help-output

doc/sdo.rst

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,10 @@ API
189189

190190
.. describe:: iter(record)
191191

192-
Return an iterator over the subindexes from the record.
192+
Return an iterator over the subindexes from the record. Only those with
193+
a matching object dictionary entry are considered. The "highest
194+
subindex" entry is officially not part of the data and thus skipped in
195+
the yielded values.
193196

194197
.. describe:: subindex in record
195198

@@ -198,7 +201,9 @@ API
198201

199202
.. describe:: len(record)
200203

201-
Return the number of subindexes in the record.
204+
Return the number of subindexes in the record, not counting the "highest
205+
subindex" entry itself. Only those with a matching object dictionary
206+
entry are considered.
202207

203208
.. method:: values()
204209

@@ -220,25 +225,27 @@ API
220225
.. describe:: iter(array)
221226

222227
Return an iterator over the subindexes from the array.
223-
This will make a SDO read operation on subindex 0 in order to get the
224-
actual length of the array.
228+
This will make an SDO read operation on subindex 0 in order to get the
229+
actual length of the array. This "highest subindex" entry is officially
230+
not part of the data and thus skipped in the yielded values.
225231

226232
.. describe:: subindex in array
227233

228234
Return ``True`` if the subindex (as int) or name (as string) exists in
229235
the array.
230-
This will make a SDO read operation on subindex 0 in order to get the
236+
This will make an SDO read operation on subindex 0 in order to get the
231237
actual length of the array.
232238

233239
.. describe:: len(array)
234240

235-
Return the length of the array.
236-
This will make a SDO read operation on subindex 0.
241+
Return the length of the array, not counting the "highest subindex" entry
242+
itself.
243+
This will make an SDO read operation on subindex 0.
237244

238245
.. method:: values()
239246

240247
Return a list of :class:`canopen.sdo.SdoVariable` in the array.
241-
This will make a SDO read operation on subindex 0 in order to get the
248+
This will make an SDO read operation on subindex 0 in order to get the
242249
actual length of the array.
243250

244251

requirements-dev.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
mypy~=1.10
2+
pytest~=8.3
3+
pytest-cov~=5.0

test/sample.eds

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,7 @@ DataType=0x0007
100100
AccessType=ro
101101
PDOMapping=0
102102

103-
[1018sub3]
104-
ParameterName=Revision number
105-
ObjectType=0x7
106-
DataType=0x0007
107-
AccessType=ro
108-
PDOMapping=0
103+
; [1018sub3] left out for testing
109104

110105
[1018sub4]
111106
ParameterName=Serial number
@@ -123,11 +118,62 @@ SupportedObjects=3
123118
[1003]
124119
ParameterName=Pre-defined error field
125120
ObjectType=0x8
126-
CompactSubObj=255
121+
SubNumber=9
122+
123+
[1003sub0]
124+
ParameterName=Number of errors
125+
ObjectType=0x7
126+
DataType=0x0005
127+
AccessType=rw
128+
DefaultValue=3
129+
PDOMapping=0
130+
131+
[1003sub1]
132+
ParameterName=Pre-defined error field_1
133+
ObjectType=0x7
134+
DataType=0x0007
135+
AccessType=ro
136+
DefaultValue=0
137+
PDOMapping=0
138+
139+
; [1003sub2] left out for testing
140+
141+
[1003sub3]
142+
ParameterName=Pre-defined error field_3
143+
ObjectType=0x7
144+
DataType=0x0007
145+
AccessType=ro
146+
DefaultValue=0
147+
PDOMapping=0
148+
149+
[1003sub4]
150+
ParameterName=Pre-defined error field_4
151+
ObjectType=0x7
152+
DataType=0x0007
153+
AccessType=ro
154+
DefaultValue=0
155+
PDOMapping=0
156+
157+
[1003sub5]
158+
ParameterName=Pre-defined error field_5
159+
ObjectType=0x7
160+
DataType=0x0007
161+
AccessType=ro
162+
DefaultValue=0
163+
PDOMapping=0
164+
165+
; [1003sub6] left out for testing
166+
167+
[1003sub7]
168+
ParameterName=Pre-defined error field_7
169+
ObjectType=0x7
127170
DataType=0x0007
128171
AccessType=ro
172+
DefaultValue=0
129173
PDOMapping=0
130174

175+
; [1003sub8] left out for testing
176+
131177
[1008]
132178
ParameterName=Manufacturer device name
133179
ObjectType=0x7

0 commit comments

Comments
 (0)