Allow for substituting YAML value to global config variables (#23743)
* Allow for subsituting YAML value to config variables
* Address PR comments
* Fix conflict after master merge
diff --git a/src/controller/python/chip/yaml/constraints.py b/src/controller/python/chip/yaml/constraints.py
index 933d74f..49ac8c9 100644
--- a/src/controller/python/chip/yaml/constraints.py
+++ b/src/controller/python/chip/yaml/constraints.py
@@ -36,7 +36,7 @@
class _LoadableConstraint(BaseConstraint):
'''Constraints where value might be stored in VariableStorage needing runtime load.'''
- def __init__(self, value, field_type, variable_storage: VariableStorage):
+ def __init__(self, value, field_type, variable_storage: VariableStorage, config_values: dict):
self._variable_storage = variable_storage
# When not none _indirect_value_key is binding a name to the constraint value, and the
# actual value can only be looked-up dynamically, which is why this is a key name.
@@ -50,8 +50,8 @@
if isinstance(value, str) and self._variable_storage.is_key_saved(value):
self._indirect_value_key = value
else:
- self._value = Converter.convert_yaml_type(
- value, field_type)
+ self._value = Converter.parse_and_convert_yaml_value(
+ value, field_type, config_values)
def get_value(self):
'''Gets the current value of the constraint.
@@ -112,8 +112,9 @@
class _ConstraintMinValue(_LoadableConstraint):
- def __init__(self, min_value, field_type, variable_storage: VariableStorage):
- super().__init__(min_value, field_type, variable_storage)
+ def __init__(self, min_value, field_type, variable_storage: VariableStorage,
+ config_values: dict):
+ super().__init__(min_value, field_type, variable_storage, config_values)
def is_met(self, response) -> bool:
min_value = self.get_value()
@@ -121,8 +122,9 @@
class _ConstraintMaxValue(_LoadableConstraint):
- def __init__(self, max_value, field_type, variable_storage: VariableStorage):
- super().__init__(max_value, field_type, variable_storage)
+ def __init__(self, max_value, field_type, variable_storage: VariableStorage,
+ config_values: dict):
+ super().__init__(max_value, field_type, variable_storage, config_values)
def is_met(self, response) -> bool:
max_value = self.get_value()
@@ -162,16 +164,17 @@
class _ConstraintNotValue(_LoadableConstraint):
- def __init__(self, not_value, field_type, variable_storage: VariableStorage):
- super().__init__(not_value, field_type, variable_storage)
+ def __init__(self, not_value, field_type, variable_storage: VariableStorage,
+ config_values: dict):
+ super().__init__(not_value, field_type, variable_storage, config_values)
def is_met(self, response) -> bool:
not_value = self.get_value()
return response != not_value
-def get_constraints(constraints, field_type,
- variable_storage: VariableStorage) -> list[BaseConstraint]:
+def get_constraints(constraints, field_type, variable_storage: VariableStorage,
+ config_values: dict) -> list[BaseConstraint]:
_constraints = []
if 'hasValue' in constraints:
_constraints.append(_ConstraintHasValue(constraints.get('hasValue')))
@@ -193,11 +196,11 @@
if 'minValue' in constraints:
_constraints.append(_ConstraintMinValue(
- constraints.get('minValue'), field_type, variable_storage))
+ constraints.get('minValue'), field_type, variable_storage, config_values))
if 'maxValue' in constraints:
_constraints.append(_ConstraintMaxValue(
- constraints.get('maxValue'), field_type, variable_storage))
+ constraints.get('maxValue'), field_type, variable_storage, config_values))
if 'contains' in constraints:
_constraints.append(_ConstraintContains(constraints.get('contains')))
@@ -213,6 +216,6 @@
if 'notValue' in constraints:
_constraints.append(_ConstraintNotValue(
- constraints.get('notValue'), field_type, variable_storage))
+ constraints.get('notValue'), field_type, variable_storage, config_values))
return _constraints
diff --git a/src/controller/python/chip/yaml/format_converter.py b/src/controller/python/chip/yaml/format_converter.py
index f6402c5..fc3c5a1 100644
--- a/src/controller/python/chip/yaml/format_converter.py
+++ b/src/controller/python/chip/yaml/format_converter.py
@@ -23,8 +23,44 @@
import binascii
+def substitute_in_config_variables(field_value, config_values: dict):
+ ''' Substitutes values that are config variables.
+
+ YAML values can contain a string of a configuration variable name. In these instances we
+ substitute the configuration variable name with the actual value.
+
+ For examples see unittest src/controller/python/test/unit_tests/test_yaml_format_converter.py
+
+ # TODO This should also substitue any saveAs values as well as perform any required
+ # evaluations.
+
+ Args:
+ 'field_value': Value as extracted from YAML.
+ 'config_values': Dictionary of global configuration variables.
+ Returns:
+ Value with all global configuration variables substituted with the real value.
+ '''
+ if isinstance(field_value, dict):
+ return {key: substitute_in_config_variables(
+ field_value[key], config_values) for key in field_value}
+ if isinstance(field_value, list):
+ return [substitute_in_config_variables(item, config_values) for item in field_value]
+ if isinstance(field_value, str) and field_value in config_values:
+ config_value = config_values[field_value]
+ if isinstance(config_value, dict) and 'defaultValue' in config_value:
+ # TODO currently we don't validate that if config_value['type'] is provided
+ # that the type does in fact match our expectation.
+ return config_value['defaultValue']
+ return config_values[field_value]
+
+ return field_value
+
+
def convert_yaml_octet_string_to_bytes(s: str) -> bytes:
- """Convert YAML octet string body to bytes, handling any c-style hex escapes (e.g. \x5a) and hex: prefix"""
+ '''Convert YAML octet string body to bytes.
+
+ Included handling any c-style hex escapes (e.g. \x5a) and 'hex:' prefix.
+ '''
# Step 1: handle explicit "hex:" prefix
if s.startswith('hex:'):
return binascii.unhexlify(s[4:])
@@ -60,14 +96,21 @@
return ret_value
-def convert_yaml_type(field_value, field_type, use_from_dict=False):
- ''' Converts yaml value to expected type.
+def convert_yaml_type(field_value, field_type, inline_cast_dict_to_struct):
+ ''' Converts yaml value to provided pythonic type.
- The YAML representation when converted to a Python dictionary does not
- quite line up in terms of type (see each of the specific if branches
- below for the rationale for the necessary fix-ups). This function does
- a fix-up given a field value (as present in the YAML) and its matching
- cluster object type and returns it.
+ The YAML representation when converted to a dictionary does not line up to
+ the python type data model for the various command/attribute/event object
+ types. This function converts 'field_value' to the appropriate provided
+ 'field_type'.
+
+ Args:
+ 'field_value': Value as extracted from yaml
+ 'field_type': Pythonic command/attribute/event object type that we
+ are converting value to.
+ 'inline_cast_dict_to_struct': If true, for any dictionary 'field_value'
+ types provided we will do a convertion to the corresponding data
+ model class in `field_type` by doing field_type.FromDict(...).
'''
origin = typing.get_origin(field_type)
@@ -110,8 +153,8 @@
f'Did not find field "{item}" in {str(field_type)}') from None
return_field_value[field_descriptor.Label] = convert_yaml_type(
- field_value[item], field_descriptor.Type, use_from_dict)
- if use_from_dict:
+ field_value[item], field_descriptor.Type, inline_cast_dict_to_struct)
+ if inline_cast_dict_to_struct:
return field_type.FromDict(return_field_value)
return return_field_value
elif(type(field_value) is float):
@@ -122,7 +165,8 @@
# The field type passed in is the type of the list element and not list[T].
for idx, item in enumerate(field_value):
- field_value[idx] = convert_yaml_type(item, list_element_type, use_from_dict)
+ field_value[idx] = convert_yaml_type(item, list_element_type,
+ inline_cast_dict_to_struct)
return field_value
# YAML conversion treats all numbers as ints. Convert to a uint type if the schema
# type indicates so.
@@ -139,3 +183,25 @@
# By default, just return the field_value casted to field_type.
else:
return field_type(field_value)
+
+
+def parse_and_convert_yaml_value(field_value, field_type, config_values: dict,
+ inline_cast_dict_to_struct: bool = False):
+ ''' Parse and converts YAML type
+
+ Parsing the YAML value means performing required substitutions and evaluations. Parsing is
+ then followed by converting from the YAML type done using yaml.safe_load() to the type used in
+ the various command/attribute/event object data model types.
+
+ Args:
+ 'field_value': Value as extracted from yaml to be parsed
+ 'field_type': Pythonic command/attribute/event object type that we
+ are converting value to.
+ 'config_values': Dictionary of global configuration variables.
+ 'inline_cast_dict_to_struct': If true, for any dictionary 'field_value'
+ types provided we will do an inline convertion to the corresponding
+ struct in `field_type` by doing field_type.FromDict(...).
+ '''
+ field_value_with_config_variables = substitute_in_config_variables(field_value, config_values)
+ return convert_yaml_type(field_value_with_config_variables, field_type,
+ inline_cast_dict_to_struct)
diff --git a/src/controller/python/chip/yaml/parser.py b/src/controller/python/chip/yaml/parser.py
index a3f38f7..b70a743 100644
--- a/src/controller/python/chip/yaml/parser.py
+++ b/src/controller/python/chip/yaml/parser.py
@@ -69,8 +69,8 @@
if isinstance(value, str) and self._variable_storage.is_key_saved(value):
self._load_expected_response_in_verify = value
else:
- self._expected_response = Converter.convert_yaml_type(
- value, response_type, use_from_dict=True)
+ self._expected_response = Converter.parse_and_convert_yaml_value(
+ value, response_type, context.config_values, inline_cast_dict_to_struct=True)
def verify(self, response):
if (self._expected_response_type is None):
@@ -145,8 +145,8 @@
request_data_as_dict = Converter.convert_name_value_pair_to_dict(args)
try:
- request_data = Converter.convert_yaml_type(
- request_data_as_dict, type(command_object))
+ request_data = Converter.parse_and_convert_yaml_value(
+ request_data_as_dict, type(command_object), context.config_values)
except ValueError:
raise ParsingError('Could not covert yaml type')
@@ -166,8 +166,8 @@
expected_response_args = self._expected_raw_response['values']
expected_response_data_as_dict = Converter.convert_name_value_pair_to_dict(
expected_response_args)
- expected_response_data = Converter.convert_yaml_type(
- expected_response_data_as_dict, expected_command)
+ expected_response_data = Converter.parse_and_convert_yaml_value(
+ expected_response_data_as_dict, expected_command, context.config_values)
self._expected_response_object = expected_command.FromDict(expected_response_data)
def run_action(self, dev_ctrl: ChipDeviceCtrl, endpoint: int, node_id: int):
@@ -260,7 +260,8 @@
if constraints:
self._constraints = get_constraints(constraints,
self._request_object.attribute_type.Type,
- context.variable_storage)
+ context.variable_storage,
+ context.config_values)
def run_action(self, dev_ctrl: ChipDeviceCtrl, endpoint: int, node_id: int):
try:
@@ -326,8 +327,8 @@
if (item.get('arguments')):
args = item['arguments']['value']
try:
- request_data = Converter.convert_yaml_type(
- args, attribute.attribute_type.Type)
+ request_data = Converter.parse_and_convert_yaml_value(
+ args, attribute.attribute_type.Type, context.config_values)
except ValueError:
raise ParsingError('Could not covert yaml type')
diff --git a/src/controller/python/test/unit_tests/test_yaml_format_converter.py b/src/controller/python/test/unit_tests/test_yaml_format_converter.py
index fa46705..05f58d2 100644
--- a/src/controller/python/test/unit_tests/test_yaml_format_converter.py
+++ b/src/controller/python/test/unit_tests/test_yaml_format_converter.py
@@ -15,7 +15,7 @@
# limitations under the License.
#
-from chip.yaml.format_converter import convert_yaml_octet_string_to_bytes
+from chip.yaml.format_converter import convert_yaml_octet_string_to_bytes, substitute_in_config_variables
from binascii import unhexlify
import unittest
@@ -44,6 +44,75 @@
convert_yaml_octet_string_to_bytes("hex:aa5")
+class TestSubstitueInConfigVariables(unittest.TestCase):
+
+ def setUp(self):
+ self.common_config = {
+ 'arg1': {
+ 'defaultValue': 1
+ },
+ 'arg2': {
+ 'defaultValue': 2
+ },
+ 'no_explicit_default': 3
+ }
+
+ def test_basic_substitution(self):
+ self.assertEqual(substitute_in_config_variables('arg1', self.common_config), 1)
+ self.assertEqual(substitute_in_config_variables('arg2', self.common_config), 2)
+ self.assertEqual(substitute_in_config_variables('arg3', self.common_config), 'arg3')
+ self.assertEqual(substitute_in_config_variables('no_explicit_default', self.common_config), 3)
+
+ def test_basis_dict_substitution(self):
+ basic_dict = {
+ 'arg1': 'arg1',
+ 'arg2': 'arg2',
+ 'arg3': 'arg3',
+ 'no_explicit_default': 'no_explicit_default',
+ }
+ expected_dict = {
+ 'arg1': 1,
+ 'arg2': 2,
+ 'arg3': 'arg3',
+ 'no_explicit_default': 3,
+ }
+ self.assertEqual(substitute_in_config_variables(basic_dict, self.common_config), expected_dict)
+
+ def test_basis_list_substitution(self):
+ basic_list = ['arg1', 'arg2', 'arg3', 'no_explicit_default']
+ expected_list = [1, 2, 'arg3', 3]
+ self.assertEqual(substitute_in_config_variables(basic_list, self.common_config), expected_list)
+
+ def test_complex_nested_type(self):
+ complex_nested_type = {
+ 'arg1': ['arg1', 'arg2', 'arg3', 'no_explicit_default'],
+ 'arg2': 'arg22',
+ 'arg3': {
+ 'no_explicit_default': 'no_explicit_default',
+ 'arg2': 'arg2',
+ 'another_dict': {
+ 'arg1': ['arg1', 'arg1', 'arg1', 'no_explicit_default'],
+ },
+ 'another_list': ['arg1', 'arg2', 'arg3', 'no_explicit_default']
+ },
+ 'no_explicit_default': 'no_explicit_default',
+ }
+ expected_result = {
+ 'arg1': [1, 2, 'arg3', 3],
+ 'arg2': 'arg22',
+ 'arg3': {
+ 'no_explicit_default': 3,
+ 'arg2': 2,
+ 'another_dict': {
+ 'arg1': [1, 1, 1, 3],
+ },
+ 'another_list': [1, 2, 'arg3', 3]
+ },
+ 'no_explicit_default': 3,
+ }
+ self.assertEqual(substitute_in_config_variables(complex_nested_type, self.common_config), expected_result)
+
+
def main():
unittest.main()