Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion can/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down
34 changes: 21 additions & 13 deletions can/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand All @@ -65,21 +65,25 @@ 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:
config.read([os.path.expanduser(path) for path in CONFIG_FILES])
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():
Expand All @@ -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):

Expand Down Expand Up @@ -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'::

Expand All @@ -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']:
Expand Down
27 changes: 27 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,33 @@ The configuration file sets the default interface and channel:
bitrate = <the bitrate in bits/s to use by default>


The configuration can also contain additional sections:

::

[default]
interface = <the name of the interface to use>
channel = <the channel to use by default>
bitrate = <the bitrate in bits/s to use by default>

[HS]
# All the values from the 'default' section are inherited
channel = <the channel to use>
bitrate = <the bitrate in bits/s to use. i.e. 500000>

[MS]
# All the values from the 'default' section are inherited
channel = <the channel to use>
bitrate = <the bitrate in bits/s to use. i.e. 125000>


::

from can.interfaces.interface import Bus

hs_bus = Bus(config_section='HS')
ms_bus = Bus(config_section='MS')

Environment Variables
---------------------

Expand Down
89 changes: 89 additions & 0 deletions test/test_load_file_config.py
Original file line number Diff line number Diff line change
@@ -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()