From 495b87b048073c8260b32cc87b3b7d1a3ac8d6f8 Mon Sep 17 00:00:00 2001 From: Matteo Voges <98756476+MatteoVoges@users.noreply.github.com> Date: Mon, 20 Mar 2023 19:03:21 +0100 Subject: [PATCH 1/6] feat(parser): support ref-tags with default values Now you can write '${my:tag|default}'. if 'my:tag' could not resolved, the default value gets taken. Breaking Changes: Now you can't have the '|' - pipe symbol in your ref-tags anymore --- reclass/utils/dictpath.py | 14 ++++++++++++++ reclass/values/refitem.py | 2 ++ 2 files changed, 16 insertions(+) diff --git a/reclass/utils/dictpath.py b/reclass/utils/dictpath.py index 70c7bb51..ba8046c3 100644 --- a/reclass/utils/dictpath.py +++ b/reclass/utils/dictpath.py @@ -67,6 +67,8 @@ def __init__(self, delim, contents=None): elif isinstance(contents, list): self._parts = contents elif isinstance(contents, six.string_types): + # parse the default value from contents, strip default section (|...) from parts + contents, self.default = self._split_default(contents) self._parts = self._split_string(contents) elif isinstance(contents, tuple): self._parts = list(contents) @@ -115,6 +117,18 @@ def _get_innermost_container(self, base): def _split_string(self, string): return re.split(r'(? Date: Tue, 21 Mar 2023 10:42:38 +0100 Subject: [PATCH 2/6] fix(parser): change delimiter to '||' this fits better with the kapitan ecosystem and reduces cases where a single pipe would break --- reclass/utils/dictpath.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/reclass/utils/dictpath.py b/reclass/utils/dictpath.py index ba8046c3..2afd60fb 100644 --- a/reclass/utils/dictpath.py +++ b/reclass/utils/dictpath.py @@ -67,7 +67,7 @@ def __init__(self, delim, contents=None): elif isinstance(contents, list): self._parts = contents elif isinstance(contents, six.string_types): - # parse the default value from contents, strip default section (|...) from parts + # parse the default value from contents, strip default section (||...) from parts contents, self.default = self._split_default(contents) self._parts = self._split_string(contents) elif isinstance(contents, tuple): @@ -119,15 +119,15 @@ def _split_string(self, string): def _split_default(self, string): """ - splits reference tag 'my:tag|default-value' into two parts + splits reference tag 'my:tag||default-value' into two parts """ - parts = string.split("|") + parts = string.split("||") if not parts: return None # no contents at all elif len(parts) == 1: return parts[0], None # no default else: - return parts[:-1][0], parts[-1] # contain '|' in contents and use just the last '|' + return parts[:-1][0], parts[-1] # contain '||' in contents and use just the last '||' def key_parts(self): return self._parts[:-1] From 511960997d7bcdb9b6d29d3f51cfd0f5fea321f8 Mon Sep 17 00:00:00 2001 From: Matteo Voges <98756476+MatteoVoges@users.noreply.github.com> Date: Tue, 21 Mar 2023 12:16:00 +0100 Subject: [PATCH 3/6] fix(tests): Add test for resolving tags with default values --- reclass/values/tests/test_refitem.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/reclass/values/tests/test_refitem.py b/reclass/values/tests/test_refitem.py index 65814782..a500fee2 100644 --- a/reclass/values/tests/test_refitem.py +++ b/reclass/values/tests/test_refitem.py @@ -44,6 +44,11 @@ def test__resolve_ok(self): self.assertEquals(result, 1) + def test_default_resolve_ok(self): + reference = RefItem('', Settings({'delimiter': ':'})) + result = reference._resolve('non:existing||default', {'foo':'bar'}) + self.assertEquals(result, 'default') + def test__resolve_fails(self): refitem = RefItem('', Settings({'delimiter': ':'})) context = {'foo':{'bar': 1}} From 2fa70145d4f4dc6328759e718acd6e8ca2bddfed Mon Sep 17 00:00:00 2001 From: Matteo Voges <98756476+MatteoVoges@users.noreply.github.com> Date: Mon, 27 Mar 2023 18:03:00 +0200 Subject: [PATCH 4/6] tests: added missing cases to tests --- reclass/datatypes/tests/test_parameters.py | 14 ++++++++++++++ reclass/values/tests/test_refitem.py | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/reclass/datatypes/tests/test_parameters.py b/reclass/datatypes/tests/test_parameters.py index 80fd8de1..d32dba3b 100644 --- a/reclass/datatypes/tests/test_parameters.py +++ b/reclass/datatypes/tests/test_parameters.py @@ -414,6 +414,20 @@ def test_nested_deep_references(self): p.interpolate() self.assertEqual(p.as_dict(), r) + def test_nested_references_default_exists(self): + values = {"a": "value", "b": "${not:existing||${a}}"} + reference = {"a": "value", "b": "value"} + parameters = Parameters(values, SETTINGS, '') + parameters.interpolate() + self.assertEqual(parameters.as_dict(), reference) + + def test_nested_references_default_exists(self): + values = {"a": "value", "b": "${not:existing||${again||fallback}}"} + reference = {"a": "value", "b": "fallback"} + parameters = Parameters(values, SETTINGS, '') + parameters.interpolate() + self.assertEqual(parameters.as_dict(), reference) + def test_stray_occurrence_overwrites_during_interpolation(self): p1 = Parameters({'r' : 1, 'b': '${r}'}, SETTINGS, '') p2 = Parameters({'b' : 2}, SETTINGS, '') diff --git a/reclass/values/tests/test_refitem.py b/reclass/values/tests/test_refitem.py index a500fee2..692ec3d3 100644 --- a/reclass/values/tests/test_refitem.py +++ b/reclass/values/tests/test_refitem.py @@ -49,6 +49,11 @@ def test_default_resolve_ok(self): result = reference._resolve('non:existing||default', {'foo':'bar'}) self.assertEquals(result, 'default') + def test_default_exists_resolve_ok(self): + reference = RefItem('', Settings({'delimiter': ':'})) + result = reference._resolve('foo||default', {'foo':'bar'}) + self.assertEquals(result, 'bar') + def test__resolve_fails(self): refitem = RefItem('', Settings({'delimiter': ':'})) context = {'foo':{'bar': 1}} From 39a36fba263e71c4572dd9bcb3bb5248c8f6ee6a Mon Sep 17 00:00:00 2001 From: Matteo Voges <98756476+MatteoVoges@users.noreply.github.com> Date: Mon, 27 Mar 2023 18:17:38 +0200 Subject: [PATCH 5/6] docs: add new default value feature in the docs --- README-extensions.rst | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README-extensions.rst b/README-extensions.rst index da9ce85b..eb40f054 100644 --- a/README-extensions.rst +++ b/README-extensions.rst @@ -203,6 +203,46 @@ The ``${beta:${alpha:two}}`` construct first resolves the ``${alpha:two}`` refer the reference ``${beta:a}`` to the value 99. +Default values +----------------- + +References can now have a default value, if the reference couldn't get resolved, for example: + +.. code-block:: yaml + + # nodes/node1.yml + parameters: + myvalue: ${not:existing:ref||default} + +``reclass.py --nodeinfo node1`` then gives: + +.. code-block:: yaml + + parameters: + myvalue: default + +The ``${not:existing:ref||default}`` construct searches for a value at ``not:existing:ref``, and then because it can't find any, it will take the default value. + +This works for nested references as well: + +.. code-block:: yaml + + # nodes/node1.yml + parameters: + default: 123 + a: ${not:existing:ref||${default}} + b: ${not:existing:ref||${not:existing:default||fallback}} + +``reclass.py --nodeinfo node1`` then gives: + +.. code-block:: yaml + + parameters: + default: 123 + a: 123 + b: fallback + + Ignore overwritten missing references ------------------------------------- From 1d1d5def9953ccaacefaacd88181fe7f6a98a2cd Mon Sep 17 00:00:00 2001 From: Matteo Voges <98756476+MatteoVoges@users.noreply.github.com> Date: Wed, 3 May 2023 10:47:06 +0200 Subject: [PATCH 6/6] fix: reclass support empty default value --- reclass/values/refitem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/reclass/values/refitem.py b/reclass/values/refitem.py index c4cb7cdd..73880815 100644 --- a/reclass/values/refitem.py +++ b/reclass/values/refitem.py @@ -29,8 +29,10 @@ def _resolve(self, ref, context): try: return path.get_value(context) except (KeyError, TypeError) as e: - if path.default: - return path.default + if refpath.default is not None: + if refpath.default == '': + refpath.default = None + return refpath.default raise ResolveError(ref) def render(self, context, inventory):