From 088423df84de599c3d1b6844ca519d883e5ebe69 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 2 Mar 2018 22:05:54 +0100 Subject: [PATCH 01/27] factored out some common import functionality --- can/interface.py | 76 ++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/can/interface.py b/can/interface.py index 96a22c780..e983a2b78 100644 --- a/can/interface.py +++ b/can/interface.py @@ -39,6 +39,49 @@ for interface in iter_entry_points('python_can.interface') }) +def _get_class_for_configuration(channel, *args, **kwargs): + """ + Returns the main bus class for the given interface/configuration. + + :raises: TODO + """ + + # Figure out the configuration + config = load_config(config={ + 'interface': kwargs.get('bustype'), + 'channel': channel + }) + + if 'bustype' in kwargs: + # remove the bustype so it doesn't get passed to the backend + del kwargs['bustype'] + interface = config['interface'] + channel = config['channel'] + + # Find the correct backend + try: + (module_name, class_name) = BACKENDS[interface] + except KeyError: + raise NotImplementedError("CAN interface '{}' not supported".format(interface)) + + # Import the correct interface module + try: + module = importlib.import_module(module_name) + except Exception as e: + raise ImportError( + "Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e) + ) + + # Get the correct class + try: + return getattr(module, class_name) + except Exception as e: + raise ImportError( + "Cannot import class {} from module {} for CAN interface '{}': {}".format( + class_name, module_name, interface, e + ) + ) + class Bus(object): """ @@ -61,38 +104,7 @@ def __new__(cls, other, channel=None, *args, **kwargs): or set in the can.rc config. """ - config = load_config(config={ - 'interface': kwargs.get('bustype'), - 'channel': channel - }) - - if 'bustype' in kwargs: - # remove the bustype so it doesn't get passed to the backend - del kwargs['bustype'] - interface = config['interface'] - channel = config['channel'] - - # Import the correct Bus backend - try: - (module_name, class_name) = BACKENDS[interface] - except KeyError: - raise NotImplementedError("CAN interface '{}' not supported".format(interface)) - - try: - module = importlib.import_module(module_name) - except Exception as e: - raise ImportError( - "Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e) - ) - try: - cls = getattr(module, class_name) - except Exception as e: - raise ImportError( - "Cannot import class {} from module {} for CAN interface '{}': {}".format( - class_name, module_name, interface, e - ) - ) - + cls = _get_class_for_configuration(channel, args, kwargs) return cls(channel, **kwargs) From 828b10fb9eeb254e552f737359b704efec69f4d8 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 6 Mar 2018 15:22:04 +0100 Subject: [PATCH 02/27] cleaning up interface.py --- can/interface.py | 79 ++++++++++++++++++++------------------ can/interfaces/__init__.py | 1 + 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/can/interface.py b/can/interface.py index e983a2b78..281879497 100644 --- a/can/interface.py +++ b/can/interface.py @@ -11,56 +11,49 @@ import can import importlib +from pkg_resources import iter_entry_points from can.broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC -from pkg_resources import iter_entry_points from can.util import load_config + # interface_name => (module, classname) BACKENDS = { - 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), - 'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'), - 'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'), - 'serial': ('can.interfaces.serial.serial_can', 'SerialBus'), - 'pcan': ('can.interfaces.pcan', 'PcanBus'), - 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), - 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), - 'nican': ('can.interfaces.nican', 'NicanBus'), - 'iscan': ('can.interfaces.iscan', 'IscanBus'), - 'virtual': ('can.interfaces.virtual', 'VirtualBus'), - 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), - 'vector': ('can.interfaces.vector', 'VectorBus'), - 'slcan': ('can.interfaces.slcan', 'slcanBus') + 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), + 'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'), + 'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'), + 'serial': ('can.interfaces.serial.serial_can','SerialBus'), + 'pcan': ('can.interfaces.pcan', 'PcanBus'), + 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), + 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), + 'nican': ('can.interfaces.nican', 'NicanBus'), + 'iscan': ('can.interfaces.iscan', 'IscanBus'), + 'virtual': ('can.interfaces.virtual', 'VirtualBus'), + 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), + 'vector': ('can.interfaces.vector', 'VectorBus'), + 'slcan': ('can.interfaces.slcan', 'slcanBus') } - BACKENDS.update({ interface.name: (interface.module_name, interface.attrs[0]) for interface in iter_entry_points('python_can.interface') }) -def _get_class_for_configuration(channel, *args, **kwargs): - """ - Returns the main bus class for the given interface/configuration. - :raises: TODO +def _get_class_for_interface(interface): """ + Returns the main bus class for the given interface. - # Figure out the configuration - config = load_config(config={ - 'interface': kwargs.get('bustype'), - 'channel': channel - }) - - if 'bustype' in kwargs: - # remove the bustype so it doesn't get passed to the backend - del kwargs['bustype'] - interface = config['interface'] - channel = config['channel'] + :raises: + NotImplementedError if the interface is not known + :raises: + ImportError if there was a problem while importing the + interface or the bus class within that + """ # Find the correct backend try: - (module_name, class_name) = BACKENDS[interface] + module_name, class_name = BACKENDS[interface] except KeyError: raise NotImplementedError("CAN interface '{}' not supported".format(interface)) @@ -74,14 +67,15 @@ def _get_class_for_configuration(channel, *args, **kwargs): # Get the correct class try: - return getattr(module, class_name) + bus_class = getattr(module, class_name) except Exception as e: raise ImportError( - "Cannot import class {} from module {} for CAN interface '{}': {}".format( - class_name, module_name, interface, e - ) + "Cannot import class {} from module {} for CAN interface '{}': {}" + .format(class_name, module_name, interface, e) ) + return bus_class + class Bus(object): """ @@ -104,8 +98,19 @@ def __new__(cls, other, channel=None, *args, **kwargs): or set in the can.rc config. """ - cls = _get_class_for_configuration(channel, args, kwargs) - return cls(channel, **kwargs) + + # Figure out the configuration + config = load_config(config={ + 'interface': kwargs.get('bustype'), + 'channel': channel + }) + + # remove the bustype so it doesn't get passed to the backend + if 'bustype' in kwargs: + del kwargs['bustype'] + + cls = _get_class_for_interface(config['interface']) + return cls(channel=config['channel'], *args, **kwargs) class CyclicSendTask(CyclicSendTaskABC): diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 89ff2cdb7..b4a1e83e6 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -7,6 +7,7 @@ from pkg_resources import iter_entry_points +# TODO: isn't this a unnecessary information duplicate of `can/interface.py :: BACKENDS`? VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native', 'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat', 'nican', 'iscan', 'vector', 'virtual', 'neovi', From 405d56962b5421ae62f2091315e18f2ffe559901 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 6 Mar 2018 15:27:20 +0100 Subject: [PATCH 03/27] make class Bus in interface.py extend BusABC (IDEs can now see the attributes) --- can/interface.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/can/interface.py b/can/interface.py index 281879497..2d193942d 100644 --- a/can/interface.py +++ b/can/interface.py @@ -9,12 +9,13 @@ from __future__ import absolute_import -import can import importlib from pkg_resources import iter_entry_points -from can.broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC -from can.util import load_config +import can +from .bus import BusABC +from .broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC +from .util import load_config # interface_name => (module, classname) @@ -77,7 +78,7 @@ def _get_class_for_interface(interface): return bus_class -class Bus(object): +class Bus(BusABC): """ Instantiates a CAN Bus of the given `bustype`, falls back to reading a configuration file from default locations. From c60b0657195fabfc8f78655401b5cfdac7681c89 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 9 Mar 2018 14:21:06 +0100 Subject: [PATCH 04/27] added static BusABC._detect_available_configs() --- can/bus.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/can/bus.py b/can/bus.py index ede8a74d0..b650956a8 100644 --- a/can/bus.py +++ b/can/bus.py @@ -25,6 +25,11 @@ class BusABC(object): As well as setting the `channel_info` attribute to a string describing the interface. + + They may implement :meth:`~can.BusABC._detect_available_configs` to allow + the interface to report which configurations are currently available for + new connections. + """ #: a string describing the underlying bus channel @@ -146,4 +151,19 @@ def shutdown(self): """ self.flush_tx_buffer() + @staticmethod + def _detect_available_configs(): + """Detect all configurations/channels that this interface could + currently connect with. + + This might be quite time consuming. + + May not to be implemented by every interface. + + :rtype: Iterator[dict] + :return: an iterable of dicts, each being a configuration suitable + for usage in the interface's bus constructor. + """ + raise NotImplementedError() + __metaclass__ = ABCMeta From f093fca90c3e531c82704cfdff37f424206345ef Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 9 Mar 2018 14:45:00 +0100 Subject: [PATCH 05/27] added public detect_available_channels() method --- can/__init__.py | 2 +- can/bus.py | 2 +- can/interface.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index f612eeda4..da70b24cf 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -35,7 +35,7 @@ class CanError(IOError): from can.notifier import Notifier from can.interfaces import VALID_INTERFACES from . import interface -from .interface import Bus +from .interface import Bus, detect_available_channels from can.broadcastmanager import send_periodic, \ CyclicSendTaskABC, \ diff --git a/can/bus.py b/can/bus.py index b650956a8..5bb370382 100644 --- a/can/bus.py +++ b/can/bus.py @@ -158,7 +158,7 @@ def _detect_available_configs(): This might be quite time consuming. - May not to be implemented by every interface. + May not to be implemented by every interface on every platform. :rtype: Iterator[dict] :return: an iterable of dicts, each being a configuration suitable diff --git a/can/interface.py b/can/interface.py index 2d193942d..b1baa4ff6 100644 --- a/can/interface.py +++ b/can/interface.py @@ -9,6 +9,7 @@ from __future__ import absolute_import +import sys import importlib from pkg_resources import iter_entry_points @@ -17,6 +18,9 @@ from .broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC from .util import load_config +if sys.version_info.major > 2: + basestring = str + # interface_name => (module, classname) BACKENDS = { @@ -102,18 +106,70 @@ def __new__(cls, other, channel=None, *args, **kwargs): # Figure out the configuration config = load_config(config={ - 'interface': kwargs.get('bustype'), + 'interface': kwargs.get('bustype', kwargs.get('interface')), 'channel': channel }) - # remove the bustype so it doesn't get passed to the backend + # remove the bustype & interface so it doesn't get passed to the backend if 'bustype' in kwargs: del kwargs['bustype'] + if 'interface' in kwargs: + del kwargs['interface'] cls = _get_class_for_interface(config['interface']) return cls(channel=config['channel'], *args, **kwargs) +def detect_available_channels(search_only_in=None): + """Detect all configurations/channels that the interfaces could + currently connect with. + + This might be quite time consuming. + + Automated configuration detection may not be implemented by + every interface on every platform. + + :param search_only_in: either + - the name of an interface to be searched in as a string, + - an iterable of interface names to search in, or + - `None` to search in all known interfaces. + :rtype: Iterator[dict] + :return: an iterable of dicts, each suitable for usage in + :class:`~can.interface.Bus`'s constructor. + """ + + # Figure out where to search + if search_only_in is None: + # use an iterator over the keys so we do not have to copy it + search_only_in = BACKENDS.iterkeys() + elif isinstance(search_only_in, basestring): + search_only_in = [search_only_in, ] + # else it is supposed to be an iterable of strings + + result = [] + for interface in search_only_in: + + bus_class = _get_class_for_interface(interface) + + # get available channels + try: + available = bus_class._detect_available_channels() + except NotImplementedError: + log.debug('interface "%s" does not support detection of available configurations', interface) + else: + log.debug('interface "%s" detected %i available configurations', interface, len(available)) + + # add the interface name to the configs if it is not already present + for config in available: + if 'interface' not in config: + config['interface'] = interface + + # append to result + result += available + + return result + + class CyclicSendTask(CyclicSendTaskABC): @classmethod From 005a26d052560e43d5ed8fecd27c9def686a1d14 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 9 Mar 2018 15:27:42 +0100 Subject: [PATCH 06/27] added _detect_available_configs() to virtual bus and added locks around the global channels variable --- can/interfaces/virtual.py | 60 +++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 2a7186b77..a77a202bf 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -15,6 +15,8 @@ import queue except ImportError: import Queue as queue +from threading import RLock +import random from can.bus import BusABC @@ -23,22 +25,37 @@ # Channels are lists of queues, one for each connection channels = {} +channels_lock = RLock() class VirtualBus(BusABC): - """Virtual CAN bus using an internal message queue for testing.""" + """ + A virtual CAN bus using an internal message queue. It can be + used for example for testing. + + In this interface, a channel is an arbitarty object used as + an identifier for connected buses. + + Implements :meth:`can.BusABC._detect_available_configs`; see + :meth:`can.VirtualBus._detect_available_configs` for how it + behaves here. + """ def __init__(self, channel=None, receive_own_messages=False, **config): - self.channel_info = 'Virtual bus channel %s' % channel + # the channel identifier may be an arbitrary object + self.channel_id = channel + self.channel_info = 'Virtual bus channel %s' % channel_id self.receive_own_messages = receive_own_messages - # Create a new channel if one does not exist - if channel not in channels: - channels[channel] = [] + with channels_lock: + + # Create a new channel if one does not exist + if channel_id not in channels: + channels[channel_id] = [] + self.channel = channels[channel_id] - self.queue = queue.Queue() - self.channel = channels[channel] - self.channel.append(self.queue) + self.queue = queue.Queue() + self.channel.append(self.queue) def recv(self, timeout=None): try: @@ -58,4 +75,29 @@ def send(self, msg, timeout=None): logger.log(9, 'Transmitted message:\n%s', msg) def shutdown(self): - self.channel.remove(self.queue) + with channels_lock: + self.channel.remove(self.queue) + + # remove if emtpy + if not self.channel: + del channels[channel_id] + + @staticmethod + def _detect_available_configs(): + """ + Returns all currently used channels as well as + one other currently unused channel. + + This method will have problems if thousands of + autodetected busses are used at once. + """ + with channels_lock: + available_channels = channels.keys() + + # find a currently unused channel + get_extra = lambda: "channel-{}".format(random.randint(0, 9999)) + extra = get_extra() + while extra in available_channels: + extra = get_extra() + + return available_channels + [extra] From 91a50a074e1bc775b81ed9efe1051fb3ce074c5f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 9 Mar 2018 15:36:37 +0100 Subject: [PATCH 07/27] fixes for channel detection --- can/interface.py | 4 ++-- can/interfaces/virtual.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/can/interface.py b/can/interface.py index b1baa4ff6..ffe554d1f 100644 --- a/can/interface.py +++ b/can/interface.py @@ -120,7 +120,7 @@ def __new__(cls, other, channel=None, *args, **kwargs): return cls(channel=config['channel'], *args, **kwargs) -def detect_available_channels(search_only_in=None): +def detect_available_configs(search_only_in=None): """Detect all configurations/channels that the interfaces could currently connect with. @@ -153,7 +153,7 @@ def detect_available_channels(search_only_in=None): # get available channels try: - available = bus_class._detect_available_channels() + available = bus_class.detect_available_configs() except NotImplementedError: log.debug('interface "%s" does not support detection of available configurations', interface) else: diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index a77a202bf..f65f44c83 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -100,4 +100,9 @@ def _detect_available_configs(): while extra in available_channels: extra = get_extra() - return available_channels + [extra] + available_channels += [extra] + + return [ + {'interface': 'virtual', 'channel': channel} + for channel in available_channels + ] From 3ebdfec0fe6469be167b3637deb589d86f6b1344 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 10 Mar 2018 01:29:38 +0100 Subject: [PATCH 08/27] various fixes for channel detection --- can/__init__.py | 2 +- can/interface.py | 11 ++++++++--- can/interfaces/virtual.py | 10 +++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index da70b24cf..f784ae1b1 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -35,7 +35,7 @@ class CanError(IOError): from can.notifier import Notifier from can.interfaces import VALID_INTERFACES from . import interface -from .interface import Bus, detect_available_channels +from .interface import Bus, detect_available_configs from can.broadcastmanager import send_periodic, \ CyclicSendTaskABC, \ diff --git a/can/interface.py b/can/interface.py index ffe554d1f..a99b20316 100644 --- a/can/interface.py +++ b/can/interface.py @@ -12,6 +12,7 @@ import sys import importlib from pkg_resources import iter_entry_points +import logging import can from .bus import BusABC @@ -22,6 +23,9 @@ basestring = str +log = logging.getLogger('can.interface') + + # interface_name => (module, classname) BACKENDS = { 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), @@ -137,6 +141,7 @@ def detect_available_configs(search_only_in=None): :return: an iterable of dicts, each suitable for usage in :class:`~can.interface.Bus`'s constructor. """ + logger = log.getChild('detect_available_configs') # Figure out where to search if search_only_in is None: @@ -153,11 +158,11 @@ def detect_available_configs(search_only_in=None): # get available channels try: - available = bus_class.detect_available_configs() + available = bus_class._detect_available_configs() except NotImplementedError: - log.debug('interface "%s" does not support detection of available configurations', interface) + logger.debug('interface "%s" does not support detection of available configurations', interface) else: - log.debug('interface "%s" detected %i available configurations', interface, len(available)) + logger.debug('interface "%s" detected %i available configurations', interface, len(available)) # add the interface name to the configs if it is not already present for config in available: diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index f65f44c83..eba06b9ab 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -44,15 +44,15 @@ class VirtualBus(BusABC): def __init__(self, channel=None, receive_own_messages=False, **config): # the channel identifier may be an arbitrary object self.channel_id = channel - self.channel_info = 'Virtual bus channel %s' % channel_id + self.channel_info = 'Virtual bus channel %s' % self.channel_id self.receive_own_messages = receive_own_messages with channels_lock: # Create a new channel if one does not exist - if channel_id not in channels: - channels[channel_id] = [] - self.channel = channels[channel_id] + if self.channel_id not in channels: + channels[self.channel_id] = [] + self.channel = channels[self.channel_id] self.queue = queue.Queue() self.channel.append(self.queue) @@ -80,7 +80,7 @@ def shutdown(self): # remove if emtpy if not self.channel: - del channels[channel_id] + del channels[self.channel_id] @staticmethod def _detect_available_configs(): From c9c4b306a33cc393a7bf6c4a8901db8c0e67ac15 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 10 Mar 2018 01:30:28 +0100 Subject: [PATCH 09/27] added unit tests for detect_available_configs() --- test/test_detect_available_configs.py | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/test_detect_available_configs.py diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py new file mode 100644 index 000000000..33af5960f --- /dev/null +++ b/test/test_detect_available_configs.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests :meth:`can.BusABC._detect_available_configs` / +:meth:`can.BusABC.detect_available_configs`. +""" + +import sys +import unittest + +from can import detect_available_configs + +if sys.version_info.major > 2: + basestring = str + + +class TestDetectAvailableConfigs(unittest.TestCase): + + def test_count_returned(self): + # At least virtual has to always return at least one interface + self.assertGreaterEqual (len(detect_available_configs() ), 1) + self.assertEquals (len(detect_available_configs(search_only_in=[]) ), 0) + self.assertGreaterEqual (len(detect_available_configs(search_only_in='virtual') ), 1) + self.assertGreaterEqual (len(detect_available_configs(search_only_in=['virtual']) ), 1) + self.assertGreaterEqual (len(detect_available_configs(search_only_in=None) ), 1) + + def test_general_values(self): + returned = detect_available_configs() + for config in returned: + self.assertIn('interface', config) + self.assertIn('channel', config) + self.assertIsInstance(config['interface'], basestring) + + def test_content_virtual(self): + returned = detect_available_configs(search_only_in='virtual') + for config in returned: + self.assertEqual(config['interface'], 'virtual') + +if __name__ == '__main__': + unittest.main() From 17dd7585426dd8d10a5d144fd4bc60c2f732b55d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 10 Mar 2018 02:07:26 +0100 Subject: [PATCH 10/27] small fixes for Python 2/3 compatibility --- can/interface.py | 2 +- can/interfaces/virtual.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/can/interface.py b/can/interface.py index a99b20316..49a3ca3b1 100644 --- a/can/interface.py +++ b/can/interface.py @@ -146,7 +146,7 @@ def detect_available_configs(search_only_in=None): # Figure out where to search if search_only_in is None: # use an iterator over the keys so we do not have to copy it - search_only_in = BACKENDS.iterkeys() + search_only_in = BACKENDS.keys() elif isinstance(search_only_in, basestring): search_only_in = [search_only_in, ] # else it is supposed to be an iterable of strings diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index eba06b9ab..1c9db7640 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -92,7 +92,7 @@ def _detect_available_configs(): autodetected busses are used at once. """ with channels_lock: - available_channels = channels.keys() + available_channels = list(channels.keys()) # find a currently unused channel get_extra = lambda: "channel-{}".format(random.randint(0, 9999)) From b66a8d44bbe40c244e305e63d264690858ccb97c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 27 Apr 2018 17:00:09 +0200 Subject: [PATCH 11/27] added channel detection for socketcan --- can/interfaces/socketcan/__init__.py | 1 + can/interfaces/socketcan/socketcan_common.py | 32 ++++++++++++++++++++ setup.py | 10 +++--- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/can/interfaces/socketcan/__init__.py b/can/interfaces/socketcan/__init__.py index 2e0a9ad0f..a861b93f3 100644 --- a/can/interfaces/socketcan/__init__.py +++ b/can/interfaces/socketcan/__init__.py @@ -2,6 +2,7 @@ # coding: utf-8 """ +See: https://www.kernel.org/doc/Documentation/networking/can.txt """ from can.interfaces.socketcan import socketcan_constants as constants diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 05ce48b6c..0ce1d9cdc 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -5,10 +5,19 @@ Defines common socketcan functions. """ +import logging import struct +import os +import sys +if sys.version_info.major < 3: # and os.name == 'posix' + import subprocess32 as subprocess +else: + import subprocess + from can.interfaces.socketcan.socketcan_constants import CAN_EFF_FLAG +log = logging.getLogger('can.socketcan_common') def pack_filters(can_filters=None): if can_filters is None: @@ -32,3 +41,26 @@ def pack_filters(can_filters=None): filter_data.append(can_mask) return struct.pack(can_filter_fmt, *filter_data) + +def find_available_interfaces(): + """Returns the names of all open can/vcan interfaces using + the ``ip link list`` command. If the lookup fails, an error + is logged to the console and an empty list is returned. + + :rtype: Iterator[:class:`str`] + """ + + try: + # it might be good to add "type vcan", but that might (?) exclude physical can devices + command = ["ip", "-br", "-0", "link", "list", "up"] + output = subprocess.check_output(command, universal_newlines=True) + + except subprocess.CalledProcessError, e: + log.error("failed to fetch opened can devices: %s", e) + return [] + + else: + # output contains some lines like "vcan42 UNKNOWN " + # return the first entry of each line + for line in output.splitlines(): + yield line.split()[0] diff --git a/setup.py b/setup.py index 68ba1ad4d..411867c00 100644 --- a/setup.py +++ b/setup.py @@ -39,13 +39,13 @@ # Package data package_data={ - "": ["CONTRIBUTORS.txt", "LICENSE.txt"], + "": ["CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.txt"], "doc": ["*.*"] }, - # Installation + # Installation; see https://www.python.org/dev/peps/pep-0440/#version-specifiers install_requires=[ - #'Deprecated >= 1.1.0', + 'subprocess32 ~= 3.2.7', ], extras_require={ 'serial': ['pyserial >= 3.0'], @@ -55,8 +55,8 @@ # Testing test_suite="nose.collector", tests_require=[ - 'mock', - 'nose', + 'mock >= 2.0.0', + 'nose >= 1.3.7', 'pyserial >= 3.0' ], ) From 34e3c52b04dc3f5426f2984ef8f40bfa4bc32471 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 27 Apr 2018 17:36:22 +0200 Subject: [PATCH 12/27] added tests for socketcan channel detection --- can/interfaces/socketcan/socketcan_common.py | 2 +- test/sockectan_helpers.py | 16 ++++++++++++++-- test/test_detect_available_configs.py | 8 ++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index a635361d2..eaede2c0f 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -55,7 +55,7 @@ def find_available_interfaces(): command = ["ip", "-br", "-0", "link", "list", "up"] output = subprocess.check_output(command, universal_newlines=True) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: log.error("failed to fetch opened can devices: %s", e) return [] diff --git a/test/sockectan_helpers.py b/test/sockectan_helpers.py index c92a1943d..7c9cc22d5 100644 --- a/test/sockectan_helpers.py +++ b/test/sockectan_helpers.py @@ -2,18 +2,21 @@ # coding: utf-8 """ +Tests helpers in `can.interfaces.socketcan.socketcan_common`. """ from __future__ import absolute_import import unittest +from can.interfaces.socketcan.socketcan_common import \ + find_available_interfaces, error_code_to_str + from .config import * -from can.interfaces.socketcan.socketcan_common import error_code_to_str +@unittest.skipUnless(IS_UNIX, "skip if not on UNIX") class TestSocketCanHelpers(unittest.TestCase): - @unittest.skipUnless(IS_UNIX, "skip if not on UNIX") def test_error_code_to_str(self): """ Check that the function does not crash & always @@ -27,5 +30,14 @@ def test_error_code_to_str(self): string = error_code_to_str(error_code) self.assertTrue(string) # not None or empty + def test_find_available_interfaces(self): + result = list(find_available_interfaces()) + self.assertGreaterEqual(len(result), 1) + for entry in result: + self.assertRegexpMatches(entry, r"v?can\d+") + if IS_CI: + self.assertIn("vcan0", result) + + if __name__ == '__main__': unittest.main() diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index 33af5960f..cfa0930dd 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -37,5 +37,13 @@ def test_content_virtual(self): for config in returned: self.assertEqual(config['interface'], 'virtual') + def test_content_socketcan(self): + returned = detect_available_configs(search_only_in='socketcan') + for config in returned: + self.assertRegexpMatches(config['interface'], r"socketcan(_(ctypes|native))?") + + # see TestSocketCanHelpers.test_find_available_interfaces() + + if __name__ == '__main__': unittest.main() From f0c927fe4f90a912edff6cf558755d1a04ba84ae Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 27 Apr 2018 17:49:08 +0200 Subject: [PATCH 13/27] catch ImportErrors in detect_available_configs() --- can/interface.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/can/interface.py b/can/interface.py index 49a3ca3b1..c4bb5adb2 100644 --- a/can/interface.py +++ b/can/interface.py @@ -56,8 +56,8 @@ def _get_class_for_interface(interface): :raises: NotImplementedError if the interface is not known :raises: - ImportError if there was a problem while importing the - interface or the bus class within that + ImportError if there was a problem while importing the + interface or the bus class within that """ # Find the correct backend @@ -137,7 +137,7 @@ def detect_available_configs(search_only_in=None): - the name of an interface to be searched in as a string, - an iterable of interface names to search in, or - `None` to search in all known interfaces. - :rtype: Iterator[dict] + :rtype: list of `dict`s :return: an iterable of dicts, each suitable for usage in :class:`~can.interface.Bus`'s constructor. """ @@ -154,7 +154,11 @@ def detect_available_configs(search_only_in=None): result = [] for interface in search_only_in: - bus_class = _get_class_for_interface(interface) + try: + bus_class = _get_class_for_interface(interface) + except ImportError: + logger.debug('interface "%s" can not be loaded for detection of available configurations', interface) + continue # get available channels try: From f3b7cfce173ec5f76a7c74c4a300c2fc225177f1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 27 Apr 2018 18:25:39 +0200 Subject: [PATCH 14/27] fix loading of socketcan's special case --- can/interface.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/can/interface.py b/can/interface.py index c4bb5adb2..318608cd4 100644 --- a/can/interface.py +++ b/can/interface.py @@ -60,6 +60,10 @@ def _get_class_for_interface(interface): interface or the bus class within that """ + # filter out the socketcan special case + if interface == 'socketcan': + interface = can.util.choose_socketcan_implementation() + # Find the correct backend try: module_name, class_name = BACKENDS[interface] From c31770e6ede309d66c7fda24d1a26289ad86cb0d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 27 Apr 2018 18:47:48 +0200 Subject: [PATCH 15/27] various smaller fixes --- can/interface.py | 11 ++++++++--- can/interfaces/socketcan/socketcan_common.py | 5 ++--- test/test_detect_available_configs.py | 7 +++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/can/interface.py b/can/interface.py index 318608cd4..cc41f3dcb 100644 --- a/can/interface.py +++ b/can/interface.py @@ -62,7 +62,10 @@ def _get_class_for_interface(interface): # filter out the socketcan special case if interface == 'socketcan': - interface = can.util.choose_socketcan_implementation() + try: + interface = can.util.choose_socketcan_implementation() + except Exception as e: + raise ImportError("Cannot choose socketcan implementation: {}".format(e)) # Find the correct backend try: @@ -135,7 +138,9 @@ def detect_available_configs(search_only_in=None): This might be quite time consuming. Automated configuration detection may not be implemented by - every interface on every platform. + every interface on every platform. This method will not raise + an error in that case, but with rather return an empty list + for that interface. :param search_only_in: either - the name of an interface to be searched in as a string, @@ -166,7 +171,7 @@ def detect_available_configs(search_only_in=None): # get available channels try: - available = bus_class._detect_available_configs() + available = list(bus_class._detect_available_configs()) except NotImplementedError: logger.debug('interface "%s" does not support detection of available configurations', interface) else: diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index eaede2c0f..a6aba4b06 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -47,7 +47,7 @@ def find_available_interfaces(): the ``ip link list`` command. If the lookup fails, an error is logged to the console and an empty list is returned. - :rtype: Iterator[:class:`str`] + :rtype: an iterable of :class:`str` """ try: @@ -62,8 +62,7 @@ def find_available_interfaces(): else: # output contains some lines like "vcan42 UNKNOWN " # return the first entry of each line - for line in output.splitlines(): - yield line.split()[0] + return [line.split()[0] for line in output.splitlines()] def error_code_to_str(code): """ diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index cfa0930dd..67f445a1e 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -6,13 +6,16 @@ :meth:`can.BusABC.detect_available_configs`. """ +from __future__ import absolute_import + import sys import unittest +if sys.version_info.major > 2: + basestring = str from can import detect_available_configs -if sys.version_info.major > 2: - basestring = str +from .config import IS_LINUX class TestDetectAvailableConfigs(unittest.TestCase): From f0f8ae8861ab6dbd6eee68986eb2cb58f54eb53e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 27 Apr 2018 22:16:58 +0200 Subject: [PATCH 16/27] added link to helper method in socketcan bus classes --- can/interfaces/socketcan/socketcan_ctypes.py | 7 ++++++- can/interfaces/socketcan/socketcan_native.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index 484055ccc..775ed5742 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -18,7 +18,8 @@ from can.bus import BusABC from can.message import Message from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW -from can.interfaces.socketcan.socketcan_common import * +from can.interfaces.socketcan.socketcan_common import \ + pack_filters, find_available_interfaces, error_code_to_str # Set up logging log = logging.getLogger('can.socketcan.ctypes') @@ -164,6 +165,10 @@ def send_periodic(self, msg, period, duration=None): return task + @staticmethod + def _detect_available_configs(): + return find_available_interfaces() + class SOCKADDR(ctypes.Structure): # See /usr/include/i386-linux-gnu/bits/socket.h for original struct diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 3eda5299f..11a33e8bc 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -34,7 +34,8 @@ import can from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG -from can.interfaces.socketcan.socketcan_common import * +from can.interfaces.socketcan.socketcan_common import \ + pack_filters, find_available_interfaces, error_code_to_str from can import Message, BusABC from can.broadcastmanager import ModifiableCyclicTaskABC, RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC @@ -492,6 +493,10 @@ def set_filters(self, can_filters=None): socket.CAN_RAW_FILTER, filter_struct) + @staticmethod + def _detect_available_configs(): + return find_available_interfaces() + if __name__ == "__main__": # Create two sockets on vcan0 to test send and receive From 3f510845727fed0671239e7b5e6606c80b089fdd Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 27 Apr 2018 23:08:28 +0200 Subject: [PATCH 17/27] added some tests and fixed calls to helper --- can/interfaces/socketcan/socketcan_ctypes.py | 3 ++- can/interfaces/socketcan/socketcan_native.py | 3 ++- test/sockectan_helpers.py | 2 +- test/test_detect_available_configs.py | 19 ++++++++++++------- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index 775ed5742..8b5b3b0e6 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -167,7 +167,8 @@ def send_periodic(self, msg, period, duration=None): @staticmethod def _detect_available_configs(): - return find_available_interfaces() + return [{'interface': 'socketcan_ctypes', 'channel': channel} + for channel in find_available_interfaces()] class SOCKADDR(ctypes.Structure): diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 11a33e8bc..725176cb1 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -495,7 +495,8 @@ def set_filters(self, can_filters=None): @staticmethod def _detect_available_configs(): - return find_available_interfaces() + return [{'interface': 'socketcan_native', 'channel': channel} + for channel in find_available_interfaces()] if __name__ == "__main__": diff --git a/test/sockectan_helpers.py b/test/sockectan_helpers.py index 7c9cc22d5..502602c4c 100644 --- a/test/sockectan_helpers.py +++ b/test/sockectan_helpers.py @@ -14,7 +14,7 @@ from .config import * -@unittest.skipUnless(IS_UNIX, "skip if not on UNIX") +@unittest.skipUnless(IS_LINUX, "skip if not on UNIX") class TestSocketCanHelpers(unittest.TestCase): def test_error_code_to_str(self): diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index 67f445a1e..5de194ff3 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -29,21 +29,26 @@ def test_count_returned(self): self.assertGreaterEqual (len(detect_available_configs(search_only_in=None) ), 1) def test_general_values(self): - returned = detect_available_configs() - for config in returned: + configs = detect_available_configs() + for config in configs: self.assertIn('interface', config) self.assertIn('channel', config) self.assertIsInstance(config['interface'], basestring) def test_content_virtual(self): - returned = detect_available_configs(search_only_in='virtual') - for config in returned: + configs = detect_available_configs(search_only_in='virtual') + for config in configs: self.assertEqual(config['interface'], 'virtual') def test_content_socketcan(self): - returned = detect_available_configs(search_only_in='socketcan') - for config in returned: - self.assertRegexpMatches(config['interface'], r"socketcan(_(ctypes|native))?") + configs = detect_available_configs(search_only_in='socketcan') + for config in configs: + self.assertIn(config['interface'], ('socketcan_native', 'socketcan_ctypes')) + + def test_socketcan_on_ci_server(self): + configs = detect_available_configs(search_only_in='socketcan') + self.assertGreaterEqual(len(configs), 1) + self.assertIn('vcan0', [config['channel'] for config in configs]) # see TestSocketCanHelpers.test_find_available_interfaces() From de385136ff7268cc50f9814734a86fa6ed53a871 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 28 Apr 2018 12:12:59 +0200 Subject: [PATCH 18/27] windows fix --- can/interfaces/socketcan/socketcan_common.py | 2 +- can/interfaces/socketcan/socketcan_native.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index a6aba4b06..f84791bba 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -55,7 +55,7 @@ def find_available_interfaces(): command = ["ip", "-br", "-0", "link", "list", "up"] output = subprocess.check_output(command, universal_newlines=True) - except subprocess.CalledProcessError as e: + except (subprocess.SubprocessError, FileNotFoundError) as e: log.error("failed to fetch opened can devices: %s", e) return [] diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 725176cb1..83c6874d2 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -3,7 +3,9 @@ """ This implementation is for versions of Python that have native -can socket and can bcm socket support: >=3.5 +can socket and can bcm socket support. + +See :meth:`can.util.choose_socketcan_implementation()`. """ import logging @@ -32,13 +34,12 @@ log.error("CAN_* properties not found in socket module. These are required to use native socketcan") import can - -from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG +from can import Message, BusABC +from can.broadcastmanager import ModifiableCyclicTaskABC, \ + RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC +from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG from can.interfaces.socketcan.socketcan_common import \ pack_filters, find_available_interfaces, error_code_to_str -from can import Message, BusABC - -from can.broadcastmanager import ModifiableCyclicTaskABC, RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC # struct module defines a binary packing format: # https://docs.python.org/3/library/struct.html#struct-format-strings From bdf6a30c7b0a22a114b95a7b95be9cd5c4e444f3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 28 Apr 2018 12:27:23 +0200 Subject: [PATCH 19/27] skip socketcan test on windwos --- test/sockectan_helpers.py | 2 +- test/test_detect_available_configs.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/sockectan_helpers.py b/test/sockectan_helpers.py index 502602c4c..713095b11 100644 --- a/test/sockectan_helpers.py +++ b/test/sockectan_helpers.py @@ -14,7 +14,7 @@ from .config import * -@unittest.skipUnless(IS_LINUX, "skip if not on UNIX") +@unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") class TestSocketCanHelpers(unittest.TestCase): def test_error_code_to_str(self): diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index 5de194ff3..015adf3ea 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -45,6 +45,7 @@ def test_content_socketcan(self): for config in configs: self.assertIn(config['interface'], ('socketcan_native', 'socketcan_ctypes')) + @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") def test_socketcan_on_ci_server(self): configs = detect_available_configs(search_only_in='socketcan') self.assertGreaterEqual(len(configs), 1) From 81c8722c4703c8b8d9e5c18fb869e7bf6cfbae65 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 28 Apr 2018 13:28:05 +0200 Subject: [PATCH 20/27] Win + Python 2 fix --- can/interfaces/socketcan/socketcan_common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index f84791bba..2f9321a0a 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -10,7 +10,7 @@ import errno import struct import sys -if sys.version_info.major < 3: # and os.name == 'posix' +if sys.version_info[0] < 3 and os.name == 'posix': import subprocess32 as subprocess else: import subprocess @@ -55,7 +55,7 @@ def find_available_interfaces(): command = ["ip", "-br", "-0", "link", "list", "up"] output = subprocess.check_output(command, universal_newlines=True) - except (subprocess.SubprocessError, FileNotFoundError) as e: + except (subprocess.CalledProcessError, FileNotFoundError, Exception) as e: log.error("failed to fetch opened can devices: %s", e) return [] From 69404ba0d324eab15cd1ee8a69e17d74423e54b4 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 28 Apr 2018 14:53:59 +0200 Subject: [PATCH 21/27] use different command call in socketcan's find_available_interfaces() --- can/interfaces/socketcan/socketcan_common.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 2f9321a0a..35f7e1d5b 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -14,6 +14,7 @@ import subprocess32 as subprocess else: import subprocess +import re from can.interfaces.socketcan.socketcan_constants import CAN_EFF_FLAG @@ -42,6 +43,9 @@ def pack_filters(can_filters=None): return struct.pack(can_filter_fmt, *filter_data) + +_PATTERN_CAN_INTERFACE = re.compile(r"v?can\d+") + def find_available_interfaces(): """Returns the names of all open can/vcan interfaces using the ``ip link list`` command. If the lookup fails, an error @@ -52,17 +56,18 @@ def find_available_interfaces(): try: # it might be good to add "type vcan", but that might (?) exclude physical can devices - command = ["ip", "-br", "-0", "link", "list", "up"] + command = ["ip", "-o", "link", "list", "up"] output = subprocess.check_output(command, universal_newlines=True) - except (subprocess.CalledProcessError, FileNotFoundError, Exception) as e: + except Exception as e: # subprocess.CalledProcessError was too specific log.error("failed to fetch opened can devices: %s", e) return [] else: - # output contains some lines like "vcan42 UNKNOWN " - # return the first entry of each line - return [line.split()[0] for line in output.splitlines()] + # output contains some lines like "1: vcan42: ..." + # extract the "vcan42" of each line + interface_names = [line.split(": ", 3)[0] for line in output.splitlines()] + return filter(_PATTERN_CAN_INTERFACE.match, interface_names) def error_code_to_str(code): """ From 886bc9f2ff54d7c6fbe0c4c845dfda37a177486f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 28 Apr 2018 15:05:23 +0200 Subject: [PATCH 22/27] debugging commit --- can/interfaces/socketcan/socketcan_common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 35f7e1d5b..5c8c6a0e0 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -58,6 +58,7 @@ def find_available_interfaces(): # it might be good to add "type vcan", but that might (?) exclude physical can devices command = ["ip", "-o", "link", "list", "up"] output = subprocess.check_output(command, universal_newlines=True) + log.debug('find_available_interfaces(): output="%s"', output) except Exception as e: # subprocess.CalledProcessError was too specific log.error("failed to fetch opened can devices: %s", e) From e8c440714a6ed84cb38447089b6fd7d2ff6de4e1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 28 Apr 2018 15:13:48 +0200 Subject: [PATCH 23/27] debugging commit --- can/interfaces/socketcan/socketcan_common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 5c8c6a0e0..82aa9aed9 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -58,7 +58,7 @@ def find_available_interfaces(): # it might be good to add "type vcan", but that might (?) exclude physical can devices command = ["ip", "-o", "link", "list", "up"] output = subprocess.check_output(command, universal_newlines=True) - log.debug('find_available_interfaces(): output="%s"', output) + log.debug("find_available_interfaces(): output=\n%s", output) except Exception as e: # subprocess.CalledProcessError was too specific log.error("failed to fetch opened can devices: %s", e) @@ -68,6 +68,7 @@ def find_available_interfaces(): # output contains some lines like "1: vcan42: ..." # extract the "vcan42" of each line interface_names = [line.split(": ", 3)[0] for line in output.splitlines()] + log.debug("interface names: %s", interface_names) return filter(_PATTERN_CAN_INTERFACE.match, interface_names) def error_code_to_str(code): From 6732fbba856ead6cbf88b22276516c72a6d3bd0f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 29 Apr 2018 23:13:38 +0200 Subject: [PATCH 24/27] corrected index in command parsing --- can/interfaces/socketcan/socketcan_common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 82aa9aed9..364851310 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -67,8 +67,8 @@ def find_available_interfaces(): else: # output contains some lines like "1: vcan42: ..." # extract the "vcan42" of each line - interface_names = [line.split(": ", 3)[0] for line in output.splitlines()] - log.debug("interface names: %s", interface_names) + interface_names = [line.split(": ", 3)[1] for line in output.splitlines()] + log.debug("find_available_interfaces(): detected names: %s", interface_names) return filter(_PATTERN_CAN_INTERFACE.match, interface_names) def error_code_to_str(code): From 00f73ae1426f465d119eebf718181780e616d6fc Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 29 Apr 2018 23:19:48 +0200 Subject: [PATCH 25/27] cleanups --- can/interfaces/socketcan/socketcan_common.py | 4 ++-- test/sockectan_helpers.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 364851310..4e904f774 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -58,17 +58,17 @@ def find_available_interfaces(): # it might be good to add "type vcan", but that might (?) exclude physical can devices command = ["ip", "-o", "link", "list", "up"] output = subprocess.check_output(command, universal_newlines=True) - log.debug("find_available_interfaces(): output=\n%s", output) except Exception as e: # subprocess.CalledProcessError was too specific log.error("failed to fetch opened can devices: %s", e) return [] else: + #log.debug("find_available_interfaces(): output=\n%s", output) # output contains some lines like "1: vcan42: ..." # extract the "vcan42" of each line interface_names = [line.split(": ", 3)[1] for line in output.splitlines()] - log.debug("find_available_interfaces(): detected names: %s", interface_names) + log.debug("find_available_interfaces(): detected: %s", interface_names) return filter(_PATTERN_CAN_INTERFACE.match, interface_names) def error_code_to_str(code): diff --git a/test/sockectan_helpers.py b/test/sockectan_helpers.py index 713095b11..846de8647 100644 --- a/test/sockectan_helpers.py +++ b/test/sockectan_helpers.py @@ -14,9 +14,10 @@ from .config import * -@unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") + class TestSocketCanHelpers(unittest.TestCase): + @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") def test_error_code_to_str(self): """ Check that the function does not crash & always @@ -30,12 +31,14 @@ def test_error_code_to_str(self): string = error_code_to_str(error_code) self.assertTrue(string) # not None or empty + @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") def test_find_available_interfaces(self): result = list(find_available_interfaces()) - self.assertGreaterEqual(len(result), 1) + self.assertGreaterEqual(len(result), 0) for entry in result: self.assertRegexpMatches(entry, r"v?can\d+") if IS_CI: + self.assertGreaterEqual(len(result), 1) self.assertIn("vcan0", result) From 5bf6f031b22d77667ed727fc8f209c232ec31add Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 1 May 2018 19:26:10 +0200 Subject: [PATCH 26/27] renamed "search_only_in" to "interfaces" --- can/interface.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/can/interface.py b/can/interface.py index cc41f3dcb..f6ba2bc56 100644 --- a/can/interface.py +++ b/can/interface.py @@ -24,7 +24,7 @@ log = logging.getLogger('can.interface') - +log_autodetect = log.getChild('detect_available_configs') # interface_name => (module, classname) BACKENDS = { @@ -131,51 +131,50 @@ def __new__(cls, other, channel=None, *args, **kwargs): return cls(channel=config['channel'], *args, **kwargs) -def detect_available_configs(search_only_in=None): +def detect_available_configs(interfaces=None): """Detect all configurations/channels that the interfaces could currently connect with. - This might be quite time consuming. + This might be quite time consuming. Automated configuration detection may not be implemented by every interface on every platform. This method will not raise an error in that case, but with rather return an empty list for that interface. - :param search_only_in: either + :param interfaces: either - the name of an interface to be searched in as a string, - an iterable of interface names to search in, or - `None` to search in all known interfaces. :rtype: list of `dict`s :return: an iterable of dicts, each suitable for usage in - :class:`~can.interface.Bus`'s constructor. + :class:`can.interface.Bus`'s constructor. """ - logger = log.getChild('detect_available_configs') # Figure out where to search - if search_only_in is None: + if interfaces is None: # use an iterator over the keys so we do not have to copy it - search_only_in = BACKENDS.keys() - elif isinstance(search_only_in, basestring): - search_only_in = [search_only_in, ] + interfaces = BACKENDS.keys() + elif isinstance(interfaces, basestring): + interfaces = [interfaces, ] # else it is supposed to be an iterable of strings result = [] - for interface in search_only_in: + for interface in interfaces: try: bus_class = _get_class_for_interface(interface) except ImportError: - logger.debug('interface "%s" can not be loaded for detection of available configurations', interface) + log_autodetect.debug('interface "%s" can not be loaded for detection of available configurations', interface) continue # get available channels try: available = list(bus_class._detect_available_configs()) except NotImplementedError: - logger.debug('interface "%s" does not support detection of available configurations', interface) + log_autodetect.debug('interface "%s" does not support detection of available configurations', interface) else: - logger.debug('interface "%s" detected %i available configurations', interface, len(available)) + log_autodetect.debug('interface "%s" detected %i available configurations', interface, len(available)) # add the interface name to the configs if it is not already present for config in available: From e0212ea31bdb63f98f32dfb0183c6a1a954bd721 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 1 May 2018 23:52:42 +0200 Subject: [PATCH 27/27] fix method signature in unit test --- test/test_detect_available_configs.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index 015adf3ea..f9fa1079c 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -This module tests :meth:`can.BusABC._detect_available_configs` / +This module tests :meth:`can.BusABC._detect_available_configs` and :meth:`can.BusABC.detect_available_configs`. """ @@ -22,11 +22,11 @@ class TestDetectAvailableConfigs(unittest.TestCase): def test_count_returned(self): # At least virtual has to always return at least one interface - self.assertGreaterEqual (len(detect_available_configs() ), 1) - self.assertEquals (len(detect_available_configs(search_only_in=[]) ), 0) - self.assertGreaterEqual (len(detect_available_configs(search_only_in='virtual') ), 1) - self.assertGreaterEqual (len(detect_available_configs(search_only_in=['virtual']) ), 1) - self.assertGreaterEqual (len(detect_available_configs(search_only_in=None) ), 1) + self.assertGreaterEqual (len(detect_available_configs() ), 1) + self.assertEquals (len(detect_available_configs(interfaces=[]) ), 0) + self.assertGreaterEqual (len(detect_available_configs(interfaces='virtual') ), 1) + self.assertGreaterEqual (len(detect_available_configs(interfaces=['virtual']) ), 1) + self.assertGreaterEqual (len(detect_available_configs(interfaces=None) ), 1) def test_general_values(self): configs = detect_available_configs() @@ -36,18 +36,18 @@ def test_general_values(self): self.assertIsInstance(config['interface'], basestring) def test_content_virtual(self): - configs = detect_available_configs(search_only_in='virtual') + configs = detect_available_configs(interfaces='virtual') for config in configs: self.assertEqual(config['interface'], 'virtual') def test_content_socketcan(self): - configs = detect_available_configs(search_only_in='socketcan') + configs = detect_available_configs(interfaces='socketcan') for config in configs: self.assertIn(config['interface'], ('socketcan_native', 'socketcan_ctypes')) @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") def test_socketcan_on_ci_server(self): - configs = detect_available_configs(search_only_in='socketcan') + configs = detect_available_configs(interfaces='socketcan') self.assertGreaterEqual(len(configs), 1) self.assertIn('vcan0', [config['channel'] for config in configs])