Skip to content

Commit 1dddc1d

Browse files
authored
Automatic type conversion for config values (#1499)
1 parent dcf15c9 commit 1dddc1d

File tree

4 files changed

+77
-36
lines changed

4 files changed

+77
-36
lines changed

can/logger.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import can
99
from can.io import BaseRotatingLogger
1010
from can.io.generic import MessageWriter
11+
from can.util import cast_from_string
1112
from . import Bus, BusState, Logger, SizedRotatingLogger
1213
from .typechecking import CanFilter, CanFilters
1314

@@ -134,18 +135,7 @@ def _split_arg(_arg: str) -> Tuple[str, str]:
134135

135136
args: Dict[str, Union[str, int, float, bool]] = {}
136137
for key, string_val in map(_split_arg, unknown_args):
137-
if re.match(r"^[-+]?\d+$", string_val):
138-
# value is integer
139-
args[key] = int(string_val)
140-
elif re.match(r"^[-+]?\d*\.\d+$", string_val):
141-
# value is float
142-
args[key] = float(string_val)
143-
elif re.match(r"^(?:True|False)$", string_val):
144-
# value is bool
145-
args[key] = string_val == "True"
146-
else:
147-
# value is string
148-
args[key] = string_val
138+
args[key] = cast_from_string(string_val)
149139
return args
150140

151141

can/util.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def load_config(
147147
It may set other values that are passed through.
148148
149149
:param context:
150-
Extra 'context' pass to config sources. This can be use to section
150+
Extra 'context' pass to config sources. This can be used to section
151151
other than 'default' in the configuration file.
152152
153153
:return:
@@ -197,9 +197,12 @@ def load_config(
197197
cfg["interface"] = cfg["bustype"]
198198
del cfg["bustype"]
199199
# copy all new parameters
200-
for key in cfg:
200+
for key, val in cfg.items():
201201
if key not in config:
202-
config[key] = cfg[key]
202+
if isinstance(val, str):
203+
config[key] = cast_from_string(val)
204+
else:
205+
config[key] = cfg[key]
203206

204207
bus_config = _create_bus_config(config)
205208
can.log.debug("can config: %s", bus_config)
@@ -257,12 +260,8 @@ def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig:
257260
except (ValueError, TypeError):
258261
pass
259262

260-
if "bitrate" in config:
261-
config["bitrate"] = int(config["bitrate"])
262263
if "fd" in config:
263-
config["fd"] = config["fd"] not in ("0", "False", "false", False)
264-
if "data_bitrate" in config:
265-
config["data_bitrate"] = int(config["data_bitrate"])
264+
config["fd"] = config["fd"] not in (0, False)
266265

267266
return cast(typechecking.BusConfig, config)
268267

@@ -478,6 +477,28 @@ def time_perfcounter_correlation() -> Tuple[float, float]:
478477
return t1, performance_counter
479478

480479

480+
def cast_from_string(string_val: str) -> Union[str, int, float, bool]:
481+
"""Perform trivial type conversion from :class:`str` values.
482+
483+
:param string_val:
484+
the string, that shall be converted
485+
"""
486+
if re.match(r"^[-+]?\d+$", string_val):
487+
# value is integer
488+
return int(string_val)
489+
490+
if re.match(r"^[-+]?\d*\.\d+(?:e[-+]?\d+)?$", string_val):
491+
# value is float
492+
return float(string_val)
493+
494+
if re.match(r"^(?:True|False)$", string_val, re.IGNORECASE):
495+
# value is bool
496+
return string_val.lower() == "true"
497+
498+
# value is string
499+
return string_val
500+
501+
481502
if __name__ == "__main__":
482503
print("Searching for configuration named:")
483504
print("\n".join(CONFIG_FILES))

test/test_load_config.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
#!/usr/bin/env python
22

3-
import os
43
import shutil
54
import tempfile
65
import unittest
6+
import unittest.mock
77
from tempfile import NamedTemporaryFile
88

99
import can
1010

1111

1212
class LoadConfigTest(unittest.TestCase):
13-
configuration = {
13+
configuration_in = {
1414
"default": {"interface": "serial", "channel": "0"},
1515
"one": {"interface": "kvaser", "channel": "1", "bitrate": 100000},
1616
"two": {"channel": "2"},
1717
}
18+
configuration_out = {
19+
"default": {"interface": "serial", "channel": 0},
20+
"one": {"interface": "kvaser", "channel": 1, "bitrate": 100000},
21+
"two": {"channel": 2},
22+
}
1823

1924
def setUp(self):
2025
# Create a temporary directory
@@ -31,7 +36,7 @@ def _gen_configration_file(self, sections):
3136
content = []
3237
for section in sections:
3338
content.append(f"[{section}]")
34-
for k, v in self.configuration[section].items():
39+
for k, v in self.configuration_in[section].items():
3540
content.append(f"{k} = {v}")
3641
tmp_config_file.write("\n".join(content))
3742
return tmp_config_file.name
@@ -42,43 +47,43 @@ def _dict_to_env(self, d):
4247
def test_config_default(self):
4348
tmp_config = self._gen_configration_file(["default"])
4449
config = can.util.load_config(path=tmp_config)
45-
self.assertEqual(config, self.configuration["default"])
50+
self.assertEqual(config, self.configuration_out["default"])
4651

4752
def test_config_whole_default(self):
48-
tmp_config = self._gen_configration_file(self.configuration)
53+
tmp_config = self._gen_configration_file(self.configuration_in)
4954
config = can.util.load_config(path=tmp_config)
50-
self.assertEqual(config, self.configuration["default"])
55+
self.assertEqual(config, self.configuration_out["default"])
5156

5257
def test_config_whole_context(self):
53-
tmp_config = self._gen_configration_file(self.configuration)
58+
tmp_config = self._gen_configration_file(self.configuration_in)
5459
config = can.util.load_config(path=tmp_config, context="one")
55-
self.assertEqual(config, self.configuration["one"])
60+
self.assertEqual(config, self.configuration_out["one"])
5661

5762
def test_config_merge_context(self):
58-
tmp_config = self._gen_configration_file(self.configuration)
63+
tmp_config = self._gen_configration_file(self.configuration_in)
5964
config = can.util.load_config(path=tmp_config, context="two")
60-
expected = self.configuration["default"]
61-
expected.update(self.configuration["two"])
65+
expected = self.configuration_out["default"].copy()
66+
expected.update(self.configuration_out["two"])
6267
self.assertEqual(config, expected)
6368

6469
def test_config_merge_environment_to_context(self):
65-
tmp_config = self._gen_configration_file(self.configuration)
70+
tmp_config = self._gen_configration_file(self.configuration_in)
6671
env_data = {"interface": "serial", "bitrate": 125000}
6772
env_dict = self._dict_to_env(env_data)
6873
with unittest.mock.patch.dict("os.environ", env_dict):
6974
config = can.util.load_config(path=tmp_config, context="one")
70-
expected = self.configuration["one"]
75+
expected = self.configuration_out["one"].copy()
7176
expected.update(env_data)
7277
self.assertEqual(config, expected)
7378

7479
def test_config_whole_environment(self):
75-
tmp_config = self._gen_configration_file(self.configuration)
80+
tmp_config = self._gen_configration_file(self.configuration_in)
7681
env_data = {"interface": "socketcan", "channel": "3", "bitrate": 250000}
7782
env_dict = self._dict_to_env(env_data)
7883
with unittest.mock.patch.dict("os.environ", env_dict):
7984
config = can.util.load_config(path=tmp_config, context="one")
80-
expected = self.configuration["one"]
81-
expected.update(env_data)
85+
expected = self.configuration_out["one"].copy()
86+
expected.update({"interface": "socketcan", "channel": 3, "bitrate": 250000})
8287
self.assertEqual(config, expected)
8388

8489

test/test_util.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
channel2int,
1414
deprecated_args_alias,
1515
check_or_adjust_timing_clock,
16+
cast_from_string,
1617
)
1718

1819

@@ -262,3 +263,27 @@ def test_adjust_timing_fd(self):
262263

263264
with pytest.raises(CanInitializationError):
264265
check_or_adjust_timing_clock(timing, valid_clocks=[8_000, 16_000])
266+
267+
268+
class TestCastFromString(unittest.TestCase):
269+
def test_cast_from_string(self) -> None:
270+
self.assertEqual(1, cast_from_string("1"))
271+
self.assertEqual(-1, cast_from_string("-1"))
272+
self.assertEqual(0, cast_from_string("-0"))
273+
self.assertEqual(1.1, cast_from_string("1.1"))
274+
self.assertEqual(-1.1, cast_from_string("-1.1"))
275+
self.assertEqual(0.1, cast_from_string(".1"))
276+
self.assertEqual(10.0, cast_from_string(".1e2"))
277+
self.assertEqual(0.001, cast_from_string(".1e-2"))
278+
self.assertEqual(-0.001, cast_from_string("-.1e-2"))
279+
self.assertEqual("text", cast_from_string("text"))
280+
self.assertEqual("", cast_from_string(""))
281+
self.assertEqual("can0", cast_from_string("can0"))
282+
self.assertEqual("0can", cast_from_string("0can"))
283+
self.assertEqual(False, cast_from_string("false"))
284+
self.assertEqual(False, cast_from_string("False"))
285+
self.assertEqual(True, cast_from_string("true"))
286+
self.assertEqual(True, cast_from_string("True"))
287+
288+
with self.assertRaises(TypeError):
289+
cast_from_string(None)

0 commit comments

Comments
 (0)