From 5a8d54fc1397e081adbbc39d3ba03227b91d6410 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Sat, 10 Jul 2021 23:35:10 +0200 Subject: [PATCH] Add option transformation handler --- README.md | 16 ++++++++++++-- lbuild/main.py | 2 +- lbuild/option.py | 52 ++++++++++++++++++++++++++++----------------- test/option_test.py | 30 +++++++++++++++++++++++--- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ca4bdd1..6f6a165 100644 --- a/README.md +++ b/README.md @@ -711,6 +711,7 @@ def add_option_dependencies(value): This is the most generic option, allowing to input any string. You may, however, provide your own validator that may raise a `ValueError` if the input string does not match your expectations. +You may also pass a transformation function to convert the option value. The string is passed unmodified from the configuration to the module and the dependency handler. @@ -719,10 +720,14 @@ def validate_string(string): if "please" not in string: raise ValueError("Input does not contain the magic word!") +def transform_string(string): + return string.lower() + option = StringOption(name="option-name", description="inline", # or FileReader("file.md") default="default string", validate=validate_string, + transform=transform_string, dependencies=add_option_dependencies) ``` @@ -759,13 +764,20 @@ option = PathOption(name="option-name", #### BooleanOption -This option maps strings from `true`, `yes`, `1` to `bool(True)` and `false`, -`no`, `0` to `bool(False)`. The dependency handler is passed this `bool` value. +This option maps strings from `true`, `yes`, `1`, `enable` to `bool(True)` and +`false`, `no`, `0`, `disable` to `bool(False)`. You can extend this list with a +custom transform handler. The dependency handler is passed this `bool` value. ```python +def transform_boolean(string): + if string == 'y': return True; + if string == 'n': return False; + return string # hand over to built-in conversion + option = BooleanOption(name="option-name", description="boolean", default=True, + transform=transform_boolean, dependencies=add_option_dependencies) ``` diff --git a/lbuild/main.py b/lbuild/main.py index 5021cd6..5e865a2 100644 --- a/lbuild/main.py +++ b/lbuild/main.py @@ -22,7 +22,7 @@ from lbuild.api import Builder -__version__ = '1.17.0' +__version__ = '1.18.0' class InitAction: diff --git a/lbuild/option.py b/lbuild/option.py index 1b387b3..e0e82a4 100644 --- a/lbuild/option.py +++ b/lbuild/option.py @@ -25,7 +25,8 @@ class Option(BaseNode): - def __init__(self, name, description, default=None, dependencies=None, validate=None, + def __init__(self, name, description, default=None, + dependencies=None, validate=None, transform=None, convert_input=None, convert_output=None): BaseNode.__init__(self, name, BaseNode.Type.OPTION) self._dependency_handler = dependencies @@ -36,6 +37,7 @@ def __init__(self, name, description, default=None, dependencies=None, validate= self._output = None self._default = None self._validate = validate + self._transform = transform self._filename = os.path.join(os.getcwd(), "dummy") self._set_default(default) @@ -93,9 +95,10 @@ def format_values(self): class StringOption(Option): - def __init__(self, name, description, default=None, dependencies=None, validate=None): - Option.__init__(self, name, description, None, dependencies, validate, - convert_input=self._validate_string) + def __init__(self, name, description, default=None, dependencies=None, validate=None, transform=None): + Option.__init__(self, name, description, None, dependencies, validate, transform, + convert_input=self._validate_string, + convert_output=self._transform_string) self._set_default(default) def _validate_string(self, value): @@ -104,6 +107,12 @@ def _validate_string(self, value): self._validate(value) return value + def _transform_string(self, value): + value = str(value) + if self._transform is not None: + value = self._transform(value) + return value + class PathOption(Option): @@ -163,29 +172,36 @@ def format_values(self): class BooleanOption(Option): - def __init__(self, name, description, default=False, dependencies=None): - Option.__init__(self, name, description, default, dependencies, - convert_input=self.as_boolean, - convert_output=self.as_boolean) + def __init__(self, name, description, default=False, dependencies=None, transform=None): + self._transform = transform + Option.__init__(self, name, description, default, dependencies, transform=transform, + convert_input=self.input_boolean, + convert_output=self.output_boolean) @property def values(self): - return ["True", "False"] + return ["yes", "no"] def format_values(self): - if self._default: - return _cw("True").wrap("underlined") + _cw(", False") - return _cw("True, ") + _cw("False").wrap("underlined") + if self.output_boolean(self._default): + return _cw("yes").wrap("underlined") + _cw(", no") + return _cw("yes, ") + _cw("no").wrap("underlined") - @staticmethod - def as_boolean(value): + def input_boolean(self, value): + if isinstance(value, bool): + value = "yes" if value else "no" + return str(value).lower() + + def output_boolean(self, value): if value is None: return value + if self._transform is not None: + value = self._transform(value) if isinstance(value, bool): return value - if str(value).lower() in ['true', 'yes', '1']: + if str(value).strip().lower() in ['true', 'yes', '1', 'enable', 'enabled']: return True - if str(value).lower() in ['false', 'no', '0']: + if str(value).strip().lower() in ['false', 'no', '0', 'disable', 'disabled']: return False raise TypeError("Input must be boolean!") @@ -196,7 +212,6 @@ class NumericOption(Option): def __init__(self, name, description, minimum=None, maximum=None, default=None, dependencies=None, validate=None): Option.__init__(self, name, description, default, dependencies, validate, - convert_input=str, convert_output=self.as_numeric_value) self.minimum_input = str(minimum) self.maximum_input = str(maximum) @@ -263,8 +278,7 @@ def format_values(self): return default - @staticmethod - def as_numeric_value(value): + def as_numeric_value(self, value): if value is None: return value if isinstance(value, (int, float)): diff --git a/test/option_test.py b/test/option_test.py index 76ea3c2..2eec817 100644 --- a/test/option_test.py +++ b/test/option_test.py @@ -98,6 +98,13 @@ def test_should_be_constructable_from_string(self): option.value = False self.assertEqual("False", option.value) + option = StringOption("test", "description", default="hello", + transform=lambda v: v.lower()) + option.value = "HELLO" + self.assertEqual("hello", option.value) + option.value = False + self.assertEqual("false", option.value) + def validate_string(value): if not isinstance(value, str): raise TypeError("must be of type str") @@ -178,7 +185,8 @@ def validate_path(path): self.assertEqual("/absolute/filename.txt", option.value) def test_should_be_constructable_from_boolean(self): - option = BooleanOption("test", "description", False) + option = BooleanOption("test", "description", False, + transform=lambda v: True if v == "world" else v) self.assertIn("test [BooleanOption]", option.description) self.assertEqual(False, option.value) option.value = 1 @@ -191,6 +199,8 @@ def test_should_be_constructable_from_boolean(self): self.assertEqual(True, option.value) option.value = False self.assertEqual(False, option.value) + option.value = "world" + self.assertEqual(True, option.value) with self.assertRaises(le.LbuildOptionInputException): option.value = "hello" @@ -378,10 +388,24 @@ def test_should_be_constructable_from_set_set(self): str(lbuild.format.format_option_value_description(option))) def test_should_format_boolean_option(self): - option = BooleanOption("test", "description", default=True) + option = BooleanOption("test", "description", default=True, + transform=lambda v: True if v == "hello" else v) + + output = str(lbuild.format.format_option_value_description(option)) + self.assertIn("yes in [yes, no]", output, "Output") + + option.value = "hello" + self.assertEqual(True, option.value) + output = str(lbuild.format.format_option_value_description(option)) + self.assertIn("hello in [yes, no]", output, "Output") + + option.value = "1" + output = str(lbuild.format.format_option_value_description(option)) + self.assertIn("1 in [yes, no]", output, "Output") + option.value = "TRUE" output = str(lbuild.format.format_option_value_description(option)) - self.assertIn("True in [True, False]", output, "Output") + self.assertIn("true in [yes, no]", output, "Output") def test_should_format_numeric_option(self):