Skip to content

Commit 7855da1

Browse files
Tbruno25TJ Brunofelixdivozariiii9003
authored
Add __del__ method to BusABC (hardbyte#1489)
* Add del method * Add unittest * Satisfy black formatter * Satisfy pylint linter * PR feedback Co-authored-by: Felix Divo <[email protected]> * PR feedback Co-authored-by: Felix Divo <[email protected]> * Move to cls attribute * Add unittest * Call parent shutdown from socketcand * Wrap del in try except * Call parent shutdown from ixxat * Black & pylint * PR feedback Co-authored-by: Felix Divo <[email protected]> * Remove try/except & fix ordering * Fix unittest * Call parent shutdown from etas * Add warning filter * Make multicast_udp back2back test more specific * clean up test_interface_canalystii.py * carry over from hardbyte#1519 * fix AttributeError --------- Co-authored-by: TJ Bruno <[email protected]> Co-authored-by: Felix Divo <[email protected]> Co-authored-by: zariiii9003 <[email protected]>
1 parent 740c50c commit 7855da1

File tree

9 files changed

+94
-23
lines changed

9 files changed

+94
-23
lines changed

can/bus.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Contains the ABC bus implementation and its documentation.
33
"""
4-
4+
import contextlib
55
from typing import cast, Any, Iterator, List, Optional, Sequence, Tuple, Union
66

77
import can.typechecking
@@ -44,12 +44,14 @@ class BusABC(metaclass=ABCMeta):
4444
#: Log level for received messages
4545
RECV_LOGGING_LEVEL = 9
4646

47+
_is_shutdown: bool = False
48+
4749
@abstractmethod
4850
def __init__(
4951
self,
5052
channel: Any,
5153
can_filters: Optional[can.typechecking.CanFilters] = None,
52-
**kwargs: object
54+
**kwargs: object,
5355
):
5456
"""Construct and open a CAN bus instance of the specified type.
5557
@@ -301,6 +303,10 @@ def stop_all_periodic_tasks(self, remove_tasks: bool = True) -> None:
301303
:param remove_tasks:
302304
Stop tracking the stopped tasks.
303305
"""
306+
if not hasattr(self, "_periodic_tasks"):
307+
# avoid AttributeError for partially initialized BusABC instance
308+
return
309+
304310
for task in self._periodic_tasks:
305311
# we cannot let `task.stop()` modify `self._periodic_tasks` while we are
306312
# iterating over it (#634)
@@ -415,9 +421,15 @@ def flush_tx_buffer(self) -> None:
415421

416422
def shutdown(self) -> None:
417423
"""
418-
Called to carry out any interface specific cleanup required
419-
in shutting down a bus.
424+
Called to carry out any interface specific cleanup required in shutting down a bus.
425+
426+
This method can be safely called multiple times.
420427
"""
428+
if self._is_shutdown:
429+
LOG.debug("%s is already shut down", self.__class__)
430+
return
431+
432+
self._is_shutdown = True
421433
self.stop_all_periodic_tasks()
422434

423435
def __enter__(self):
@@ -426,6 +438,14 @@ def __enter__(self):
426438
def __exit__(self, exc_type, exc_val, exc_tb):
427439
self.shutdown()
428440

441+
def __del__(self) -> None:
442+
if not self._is_shutdown:
443+
LOG.warning("%s was not properly shut down", self.__class__)
444+
# We do some best-effort cleanup if the user
445+
# forgot to properly close the bus instance
446+
with contextlib.suppress(AttributeError):
447+
self.shutdown()
448+
429449
@property
430450
def state(self) -> BusState:
431451
"""

can/interfaces/canalystii.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import collections
1+
from collections import deque
22
from ctypes import c_ubyte
33
import logging
44
import time
55
from typing import Any, Dict, Optional, Deque, Sequence, Tuple, Union
66

77
from can import BitTiming, BusABC, Message, BitTimingFd
8-
from can.exceptions import CanTimeoutError, CanInitializationError
8+
from can.exceptions import CanTimeoutError
99
from can.typechecking import CanFilters
1010
from can.util import deprecated_args_alias, check_or_adjust_timing_clock
1111

@@ -50,11 +50,12 @@ def __init__(
5050
If set, software received message queue can only grow to this many
5151
messages (for all channels) before older messages are dropped
5252
"""
53-
super().__init__(channel=channel, can_filters=can_filters, **kwargs)
54-
5553
if not (bitrate or timing):
5654
raise ValueError("Either bitrate or timing argument is required")
5755

56+
# Do this after the error handling
57+
super().__init__(channel=channel, can_filters=can_filters, **kwargs)
58+
5859
if isinstance(channel, str):
5960
# Assume comma separated string of channels
6061
self.channels = [int(ch.strip()) for ch in channel.split(",")]
@@ -63,23 +64,23 @@ def __init__(
6364
else: # Sequence[int]
6465
self.channels = list(channel)
6566

66-
self.rx_queue = collections.deque(
67-
maxlen=rx_queue_size
68-
) # type: Deque[Tuple[int, driver.Message]]
67+
self.rx_queue: Deque[Tuple[int, driver.Message]] = deque(maxlen=rx_queue_size)
6968

7069
self.channel_info = f"CANalyst-II: device {device}, channels {self.channels}"
7170

7271
self.device = driver.CanalystDevice(device_index=device)
73-
for channel in self.channels:
72+
for single_channel in self.channels:
7473
if isinstance(timing, BitTiming):
7574
timing = check_or_adjust_timing_clock(timing, valid_clocks=[8_000_000])
76-
self.device.init(channel, timing0=timing.btr0, timing1=timing.btr1)
75+
self.device.init(
76+
single_channel, timing0=timing.btr0, timing1=timing.btr1
77+
)
7778
elif isinstance(timing, BitTimingFd):
7879
raise NotImplementedError(
7980
f"CAN FD is not supported by {self.__class__.__name__}."
8081
)
8182
else:
82-
self.device.init(channel, bitrate=bitrate)
83+
self.device.init(single_channel, bitrate=bitrate)
8384

8485
# Delay to use between each poll for new messages
8586
#

can/interfaces/etas/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ def flush_tx_buffer(self) -> None:
248248
OCI_ResetQueue(self.txQueue)
249249

250250
def shutdown(self) -> None:
251+
super().shutdown()
251252
# Cleanup TX
252253
if self.txQueue:
253254
OCI_DestroyCANTxQueue(self.txQueue)

can/interfaces/ixxat/canlib.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def _send_periodic_internal(self, msgs, period, duration=None):
147147
return self.bus._send_periodic_internal(msgs, period, duration)
148148

149149
def shutdown(self) -> None:
150+
super().shutdown()
150151
self.bus.shutdown()
151152

152153
@property

can/interfaces/socketcand/socketcand.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,5 +183,5 @@ def send(self, msg, timeout=None):
183183
self._tcp_send(ascii_msg)
184184

185185
def shutdown(self):
186-
self.stop_all_periodic_tasks()
186+
super().shutdown()
187187
self.__socket.close()

test/back2back_test.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import pytest
1313

1414
import can
15+
from can import CanInterfaceNotImplementedError
1516
from can.interfaces.udp_multicast import UdpMulticastBus
1617

1718
from .config import (
@@ -294,7 +295,7 @@ class BasicTestUdpMulticastBusIPv4(Back2BackTestCase):
294295
CHANNEL_2 = UdpMulticastBus.DEFAULT_GROUP_IPv4
295296

296297
def test_unique_message_instances(self):
297-
with self.assertRaises(NotImplementedError):
298+
with self.assertRaises(CanInterfaceNotImplementedError):
298299
super().test_unique_message_instances()
299300

300301

@@ -313,15 +314,15 @@ class BasicTestUdpMulticastBusIPv6(Back2BackTestCase):
313314
CHANNEL_2 = HOST_LOCAL_MCAST_GROUP_IPv6
314315

315316
def test_unique_message_instances(self):
316-
with self.assertRaises(NotImplementedError):
317+
with self.assertRaises(CanInterfaceNotImplementedError):
317318
super().test_unique_message_instances()
318319

319320

320321
TEST_INTERFACE_ETAS = False
321322
try:
322323
bus_class = can.interface._get_class_for_interface("etas")
323324
TEST_INTERFACE_ETAS = True
324-
except can.exceptions.CanInterfaceNotImplementedError:
325+
except CanInterfaceNotImplementedError:
325326
pass
326327

327328

test/test_bus.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import gc
12
from unittest.mock import patch
23

34
import can
@@ -12,3 +13,11 @@ def test_bus_ignore_config():
1213

1314
_ = can.Bus(interface="virtual")
1415
assert can.util.load_config.called
16+
17+
18+
@patch.object(can.bus.BusABC, "shutdown")
19+
def test_bus_attempts_self_cleanup(mock_shutdown):
20+
bus = can.Bus(interface="virtual")
21+
del bus
22+
gc.collect()
23+
mock_shutdown.assert_called()

test/test_interface.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import importlib
2+
from unittest.mock import patch
3+
4+
import pytest
5+
6+
import can
7+
from can.interfaces import BACKENDS
8+
9+
10+
@pytest.fixture(params=(BACKENDS.keys()))
11+
def constructor(request):
12+
mod, cls = BACKENDS[request.param]
13+
14+
try:
15+
module = importlib.import_module(mod)
16+
constructor = getattr(module, cls)
17+
except:
18+
pytest.skip("Unable to load interface")
19+
20+
return constructor
21+
22+
23+
@pytest.fixture
24+
def interface(constructor):
25+
class MockInterface(constructor):
26+
def __init__(self):
27+
pass
28+
29+
def __del__(self):
30+
pass
31+
32+
return MockInterface()
33+
34+
35+
@patch.object(can.bus.BusABC, "shutdown")
36+
def test_interface_calls_parent_shutdown(mock_shutdown, interface):
37+
try:
38+
interface.shutdown()
39+
except:
40+
pass
41+
finally:
42+
mock_shutdown.assert_called()

test/test_interface_canalystii.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
#!/usr/bin/env python
22

3-
"""
4-
"""
5-
6-
import time
73
import unittest
8-
from unittest.mock import Mock, patch, call
4+
from unittest.mock import patch, call
95
from ctypes import c_ubyte
106

117
import canalystii as driver # low-level driver module, mock out this layer

0 commit comments

Comments
 (0)