diff --git a/can/interface.py b/can/interface.py index 291fec268..d823a3b5b 100644 --- a/can/interface.py +++ b/can/interface.py @@ -103,7 +103,12 @@ def __new__(cls, channel=None, *args, **config): # figure out the rest of the configuration; this might raise an error if channel is not None: config['channel'] = channel - config = load_config(config=config) + if 'context' in config: + context = config['context'] + del config['context'] + else: + context = None + config = load_config(config=config, context=context) # resolve the bus class to use for that interface cls = _get_class_for_interface(config['interface']) diff --git a/can/util.py b/can/util.py index 2424af6b0..e68257cce 100644 --- a/can/util.py +++ b/can/util.py @@ -54,7 +54,7 @@ ) -def load_file_config(path=None): +def load_file_config(path=None, section=None): """ Loads configuration from file with following content:: @@ -65,7 +65,8 @@ def load_file_config(path=None): :param path: path to config file. If not specified, several sensible default locations are tried depending on platform. - + :param section: + name of the section to read configuration from. """ config = ConfigParser() if path is None: @@ -73,13 +74,16 @@ def load_file_config(path=None): else: config.read(path) - if not config.has_section('default'): - return {} + _config = {} - return dict( - (key, val) - for key, val in config.items('default') - ) + section = section if section is not None else 'default' + if config.has_section(section): + if config.has_section('default'): + _config.update( + dict((key, val) for key, val in config.items('default'))) + _config.update(dict((key, val) for key, val in config.items(section))) + + return _config def load_environment_config(): @@ -103,7 +107,7 @@ def load_environment_config(): ) -def load_config(path=None, config=None): +def load_config(path=None, config=None, context=None): """ Returns a dict with configuration details which is loaded from (in this order): @@ -133,6 +137,10 @@ def load_config(path=None, config=None): A dict which may set the 'interface', and/or the 'channel', or neither. It may set other values that are passed through. + :param context: + Extra 'context' pass to config sources. This can be use to section + other than 'default' in the configuration file. + :return: A config dictionary that should contain 'interface' & 'channel':: @@ -152,21 +160,21 @@ def load_config(path=None, config=None): """ # start with an empty dict to apply filtering to all sources - given_config = config + given_config = config or {} config = {} # use the given dict for default values config_sources = [ given_config, can.rc, - load_environment_config, - lambda: load_file_config(path) + lambda _context: load_environment_config(), # context is not supported + lambda _context: load_file_config(path, _context) ] # Slightly complex here to only search for the file config if required for cfg in config_sources: if callable(cfg): - cfg = cfg() + cfg = cfg(context) # remove legacy operator (and copy to interface if not already present) if 'bustype' in cfg: if 'interface' not in cfg or not cfg['interface']: diff --git a/doc/configuration.rst b/doc/configuration.rst index 35eeab665..a7da791f6 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -57,6 +57,33 @@ The configuration file sets the default interface and channel: bitrate = +The configuration can also contain additional sections: + +:: + + [default] + interface = + channel = + bitrate = + + [HS] + # All the values from the 'default' section are inherited + channel = + bitrate = + + [MS] + # All the values from the 'default' section are inherited + channel = + bitrate = + + +:: + + from can.interfaces.interface import Bus + + hs_bus = Bus(config_section='HS') + ms_bus = Bus(config_section='MS') + Environment Variables --------------------- diff --git a/test/test_load_file_config.py b/test/test_load_file_config.py new file mode 100644 index 000000000..52a45d734 --- /dev/null +++ b/test/test_load_file_config.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# coding: utf-8 +import shutil +import tempfile +import unittest +from tempfile import NamedTemporaryFile + +import can + + +class LoadFileConfigTest(unittest.TestCase): + configuration = { + 'default': {'interface': 'virtual', 'channel': '0'}, + 'one': {'interface': 'virtual', 'channel': '1'}, + 'two': {'channel': '2'}, + 'three': {'extra': 'extra value'}, + } + + def setUp(self): + # Create a temporary directory + self.test_dir = tempfile.mkdtemp() + + def tearDown(self): + # Remove the directory after the test + shutil.rmtree(self.test_dir) + + def _gen_configration_file(self, sections): + with NamedTemporaryFile(mode='w', dir=self.test_dir, + delete=False) as tmp_config_file: + content = [] + for section in sections: + content.append("[{}]".format(section)) + for k, v in self.configuration[section].items(): + content.append("{} = {}".format(k, v)) + tmp_config_file.write('\n'.join(content)) + return tmp_config_file.name + + def test_config_file_with_default(self): + tmp_config = self._gen_configration_file(['default']) + config = can.util.load_file_config(path=tmp_config) + self.assertEqual(config, self.configuration['default']) + + def test_config_file_with_default_and_section(self): + tmp_config = self._gen_configration_file(['default', 'one']) + + default = can.util.load_file_config(path=tmp_config) + self.assertEqual(default, self.configuration['default']) + + one = can.util.load_file_config(path=tmp_config, section='one') + self.assertEqual(one, self.configuration['one']) + + def test_config_file_with_section_only(self): + tmp_config = self._gen_configration_file(['one']) + config = can.util.load_file_config(path=tmp_config, section='one') + self.assertEqual(config, self.configuration['one']) + + def test_config_file_with_section_and_key_in_default(self): + expected = self.configuration['default'].copy() + expected.update(self.configuration['two']) + + tmp_config = self._gen_configration_file(['default', 'two']) + config = can.util.load_file_config(path=tmp_config, section='two') + self.assertEqual(config, expected) + + def test_config_file_with_section_missing_interface(self): + expected = self.configuration['two'].copy() + tmp_config = self._gen_configration_file(['two']) + config = can.util.load_file_config(path=tmp_config, section='two') + self.assertEqual(config, expected) + + def test_config_file_extra(self): + expected = self.configuration['default'].copy() + expected.update(self.configuration['three']) + + tmp_config = self._gen_configration_file(['default', 'three']) + config = can.util.load_file_config(path=tmp_config, section='three') + self.assertEqual(config, expected) + + def test_config_file_with_non_existing_section(self): + expected = {} + + tmp_config = self._gen_configration_file([ + 'default', 'one', 'two', 'three']) + config = can.util.load_file_config(path=tmp_config, section='zero') + self.assertEqual(config, expected) + + +if __name__ == '__main__': + unittest.main()