Skip to content

Commit 1181950

Browse files
tehampsonpull[bot]
authored andcommitted
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
1 parent 743aad2 commit 1181950

File tree

4 files changed

+174
-35
lines changed

4 files changed

+174
-35
lines changed

src/controller/python/chip/yaml/constraints.py

+17-14
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def is_met(self, response) -> bool:
3636
class _LoadableConstraint(BaseConstraint):
3737
'''Constraints where value might be stored in VariableStorage needing runtime load.'''
3838

39-
def __init__(self, value, field_type, variable_storage: VariableStorage):
39+
def __init__(self, value, field_type, variable_storage: VariableStorage, config_values: dict):
4040
self._variable_storage = variable_storage
4141
# When not none _indirect_value_key is binding a name to the constraint value, and the
4242
# actual value can only be looked-up dynamically, which is why this is a key name.
@@ -50,8 +50,8 @@ def __init__(self, value, field_type, variable_storage: VariableStorage):
5050
if isinstance(value, str) and self._variable_storage.is_key_saved(value):
5151
self._indirect_value_key = value
5252
else:
53-
self._value = Converter.convert_yaml_type(
54-
value, field_type)
53+
self._value = Converter.parse_and_convert_yaml_value(
54+
value, field_type, config_values)
5555

5656
def get_value(self):
5757
'''Gets the current value of the constraint.
@@ -112,17 +112,19 @@ def is_met(self, response) -> bool:
112112

113113

114114
class _ConstraintMinValue(_LoadableConstraint):
115-
def __init__(self, min_value, field_type, variable_storage: VariableStorage):
116-
super().__init__(min_value, field_type, variable_storage)
115+
def __init__(self, min_value, field_type, variable_storage: VariableStorage,
116+
config_values: dict):
117+
super().__init__(min_value, field_type, variable_storage, config_values)
117118

118119
def is_met(self, response) -> bool:
119120
min_value = self.get_value()
120121
return response >= min_value
121122

122123

123124
class _ConstraintMaxValue(_LoadableConstraint):
124-
def __init__(self, max_value, field_type, variable_storage: VariableStorage):
125-
super().__init__(max_value, field_type, variable_storage)
125+
def __init__(self, max_value, field_type, variable_storage: VariableStorage,
126+
config_values: dict):
127+
super().__init__(max_value, field_type, variable_storage, config_values)
126128

127129
def is_met(self, response) -> bool:
128130
max_value = self.get_value()
@@ -162,16 +164,17 @@ def is_met(self, response) -> bool:
162164

163165

164166
class _ConstraintNotValue(_LoadableConstraint):
165-
def __init__(self, not_value, field_type, variable_storage: VariableStorage):
166-
super().__init__(not_value, field_type, variable_storage)
167+
def __init__(self, not_value, field_type, variable_storage: VariableStorage,
168+
config_values: dict):
169+
super().__init__(not_value, field_type, variable_storage, config_values)
167170

168171
def is_met(self, response) -> bool:
169172
not_value = self.get_value()
170173
return response != not_value
171174

172175

173-
def get_constraints(constraints, field_type,
174-
variable_storage: VariableStorage) -> list[BaseConstraint]:
176+
def get_constraints(constraints, field_type, variable_storage: VariableStorage,
177+
config_values: dict) -> list[BaseConstraint]:
175178
_constraints = []
176179
if 'hasValue' in constraints:
177180
_constraints.append(_ConstraintHasValue(constraints.get('hasValue')))
@@ -193,11 +196,11 @@ def get_constraints(constraints, field_type,
193196

194197
if 'minValue' in constraints:
195198
_constraints.append(_ConstraintMinValue(
196-
constraints.get('minValue'), field_type, variable_storage))
199+
constraints.get('minValue'), field_type, variable_storage, config_values))
197200

198201
if 'maxValue' in constraints:
199202
_constraints.append(_ConstraintMaxValue(
200-
constraints.get('maxValue'), field_type, variable_storage))
203+
constraints.get('maxValue'), field_type, variable_storage, config_values))
201204

202205
if 'contains' in constraints:
203206
_constraints.append(_ConstraintContains(constraints.get('contains')))
@@ -213,6 +216,6 @@ def get_constraints(constraints, field_type,
213216

214217
if 'notValue' in constraints:
215218
_constraints.append(_ConstraintNotValue(
216-
constraints.get('notValue'), field_type, variable_storage))
219+
constraints.get('notValue'), field_type, variable_storage, config_values))
217220

218221
return _constraints

src/controller/python/chip/yaml/format_converter.py

+77-11
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,44 @@
2323
import binascii
2424

2525

26+
def substitute_in_config_variables(field_value, config_values: dict):
27+
''' Substitutes values that are config variables.
28+
29+
YAML values can contain a string of a configuration variable name. In these instances we
30+
substitute the configuration variable name with the actual value.
31+
32+
For examples see unittest src/controller/python/test/unit_tests/test_yaml_format_converter.py
33+
34+
# TODO This should also substitue any saveAs values as well as perform any required
35+
# evaluations.
36+
37+
Args:
38+
'field_value': Value as extracted from YAML.
39+
'config_values': Dictionary of global configuration variables.
40+
Returns:
41+
Value with all global configuration variables substituted with the real value.
42+
'''
43+
if isinstance(field_value, dict):
44+
return {key: substitute_in_config_variables(
45+
field_value[key], config_values) for key in field_value}
46+
if isinstance(field_value, list):
47+
return [substitute_in_config_variables(item, config_values) for item in field_value]
48+
if isinstance(field_value, str) and field_value in config_values:
49+
config_value = config_values[field_value]
50+
if isinstance(config_value, dict) and 'defaultValue' in config_value:
51+
# TODO currently we don't validate that if config_value['type'] is provided
52+
# that the type does in fact match our expectation.
53+
return config_value['defaultValue']
54+
return config_values[field_value]
55+
56+
return field_value
57+
58+
2659
def convert_yaml_octet_string_to_bytes(s: str) -> bytes:
27-
"""Convert YAML octet string body to bytes, handling any c-style hex escapes (e.g. \x5a) and hex: prefix"""
60+
'''Convert YAML octet string body to bytes.
61+
62+
Included handling any c-style hex escapes (e.g. \x5a) and 'hex:' prefix.
63+
'''
2864
# Step 1: handle explicit "hex:" prefix
2965
if s.startswith('hex:'):
3066
return binascii.unhexlify(s[4:])
@@ -60,14 +96,21 @@ def convert_name_value_pair_to_dict(arg_values):
6096
return ret_value
6197

6298

63-
def convert_yaml_type(field_value, field_type, use_from_dict=False):
64-
''' Converts yaml value to expected type.
99+
def convert_yaml_type(field_value, field_type, inline_cast_dict_to_struct):
100+
''' Converts yaml value to provided pythonic type.
101+
102+
The YAML representation when converted to a dictionary does not line up to
103+
the python type data model for the various command/attribute/event object
104+
types. This function converts 'field_value' to the appropriate provided
105+
'field_type'.
65106
66-
The YAML representation when converted to a Python dictionary does not
67-
quite line up in terms of type (see each of the specific if branches
68-
below for the rationale for the necessary fix-ups). This function does
69-
a fix-up given a field value (as present in the YAML) and its matching
70-
cluster object type and returns it.
107+
Args:
108+
'field_value': Value as extracted from yaml
109+
'field_type': Pythonic command/attribute/event object type that we
110+
are converting value to.
111+
'inline_cast_dict_to_struct': If true, for any dictionary 'field_value'
112+
types provided we will do a convertion to the corresponding data
113+
model class in `field_type` by doing field_type.FromDict(...).
71114
'''
72115
origin = typing.get_origin(field_type)
73116

@@ -110,8 +153,8 @@ def convert_yaml_type(field_value, field_type, use_from_dict=False):
110153
f'Did not find field "{item}" in {str(field_type)}') from None
111154

112155
return_field_value[field_descriptor.Label] = convert_yaml_type(
113-
field_value[item], field_descriptor.Type, use_from_dict)
114-
if use_from_dict:
156+
field_value[item], field_descriptor.Type, inline_cast_dict_to_struct)
157+
if inline_cast_dict_to_struct:
115158
return field_type.FromDict(return_field_value)
116159
return return_field_value
117160
elif(type(field_value) is float):
@@ -122,7 +165,8 @@ def convert_yaml_type(field_value, field_type, use_from_dict=False):
122165

123166
# The field type passed in is the type of the list element and not list[T].
124167
for idx, item in enumerate(field_value):
125-
field_value[idx] = convert_yaml_type(item, list_element_type, use_from_dict)
168+
field_value[idx] = convert_yaml_type(item, list_element_type,
169+
inline_cast_dict_to_struct)
126170
return field_value
127171
# YAML conversion treats all numbers as ints. Convert to a uint type if the schema
128172
# type indicates so.
@@ -139,3 +183,25 @@ def convert_yaml_type(field_value, field_type, use_from_dict=False):
139183
# By default, just return the field_value casted to field_type.
140184
else:
141185
return field_type(field_value)
186+
187+
188+
def parse_and_convert_yaml_value(field_value, field_type, config_values: dict,
189+
inline_cast_dict_to_struct: bool = False):
190+
''' Parse and converts YAML type
191+
192+
Parsing the YAML value means performing required substitutions and evaluations. Parsing is
193+
then followed by converting from the YAML type done using yaml.safe_load() to the type used in
194+
the various command/attribute/event object data model types.
195+
196+
Args:
197+
'field_value': Value as extracted from yaml to be parsed
198+
'field_type': Pythonic command/attribute/event object type that we
199+
are converting value to.
200+
'config_values': Dictionary of global configuration variables.
201+
'inline_cast_dict_to_struct': If true, for any dictionary 'field_value'
202+
types provided we will do an inline convertion to the corresponding
203+
struct in `field_type` by doing field_type.FromDict(...).
204+
'''
205+
field_value_with_config_variables = substitute_in_config_variables(field_value, config_values)
206+
return convert_yaml_type(field_value_with_config_variables, field_type,
207+
inline_cast_dict_to_struct)

src/controller/python/chip/yaml/parser.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ def __init__(self, value, response_type, context: _ExecutionContext):
6969
if isinstance(value, str) and self._variable_storage.is_key_saved(value):
7070
self._load_expected_response_in_verify = value
7171
else:
72-
self._expected_response = Converter.convert_yaml_type(
73-
value, response_type, use_from_dict=True)
72+
self._expected_response = Converter.parse_and_convert_yaml_value(
73+
value, response_type, context.config_values, inline_cast_dict_to_struct=True)
7474

7575
def verify(self, response):
7676
if (self._expected_response_type is None):
@@ -145,8 +145,8 @@ def __init__(self, item: dict, cluster: str, context: _ExecutionContext):
145145
request_data_as_dict = Converter.convert_name_value_pair_to_dict(args)
146146

147147
try:
148-
request_data = Converter.convert_yaml_type(
149-
request_data_as_dict, type(command_object))
148+
request_data = Converter.parse_and_convert_yaml_value(
149+
request_data_as_dict, type(command_object), context.config_values)
150150
except ValueError:
151151
raise ParsingError('Could not covert yaml type')
152152

@@ -166,8 +166,8 @@ def __init__(self, item: dict, cluster: str, context: _ExecutionContext):
166166
expected_response_args = self._expected_raw_response['values']
167167
expected_response_data_as_dict = Converter.convert_name_value_pair_to_dict(
168168
expected_response_args)
169-
expected_response_data = Converter.convert_yaml_type(
170-
expected_response_data_as_dict, expected_command)
169+
expected_response_data = Converter.parse_and_convert_yaml_value(
170+
expected_response_data_as_dict, expected_command, context.config_values)
171171
self._expected_response_object = expected_command.FromDict(expected_response_data)
172172

173173
def run_action(self, dev_ctrl: ChipDeviceCtrl, endpoint: int, node_id: int):
@@ -260,7 +260,8 @@ def __init__(self, item: dict, cluster: str, context: _ExecutionContext):
260260
if constraints:
261261
self._constraints = get_constraints(constraints,
262262
self._request_object.attribute_type.Type,
263-
context.variable_storage)
263+
context.variable_storage,
264+
context.config_values)
264265

265266
def run_action(self, dev_ctrl: ChipDeviceCtrl, endpoint: int, node_id: int):
266267
try:
@@ -326,8 +327,8 @@ def __init__(self, item: dict, cluster: str, context: _ExecutionContext):
326327
if (item.get('arguments')):
327328
args = item['arguments']['value']
328329
try:
329-
request_data = Converter.convert_yaml_type(
330-
args, attribute.attribute_type.Type)
330+
request_data = Converter.parse_and_convert_yaml_value(
331+
args, attribute.attribute_type.Type, context.config_values)
331332
except ValueError:
332333
raise ParsingError('Could not covert yaml type')
333334

src/controller/python/test/unit_tests/test_yaml_format_converter.py

+70-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# limitations under the License.
1616
#
1717

18-
from chip.yaml.format_converter import convert_yaml_octet_string_to_bytes
18+
from chip.yaml.format_converter import convert_yaml_octet_string_to_bytes, substitute_in_config_variables
1919
from binascii import unhexlify
2020
import unittest
2121

@@ -44,6 +44,75 @@ def test_common_cases(self):
4444
convert_yaml_octet_string_to_bytes("hex:aa5")
4545

4646

47+
class TestSubstitueInConfigVariables(unittest.TestCase):
48+
49+
def setUp(self):
50+
self.common_config = {
51+
'arg1': {
52+
'defaultValue': 1
53+
},
54+
'arg2': {
55+
'defaultValue': 2
56+
},
57+
'no_explicit_default': 3
58+
}
59+
60+
def test_basic_substitution(self):
61+
self.assertEqual(substitute_in_config_variables('arg1', self.common_config), 1)
62+
self.assertEqual(substitute_in_config_variables('arg2', self.common_config), 2)
63+
self.assertEqual(substitute_in_config_variables('arg3', self.common_config), 'arg3')
64+
self.assertEqual(substitute_in_config_variables('no_explicit_default', self.common_config), 3)
65+
66+
def test_basis_dict_substitution(self):
67+
basic_dict = {
68+
'arg1': 'arg1',
69+
'arg2': 'arg2',
70+
'arg3': 'arg3',
71+
'no_explicit_default': 'no_explicit_default',
72+
}
73+
expected_dict = {
74+
'arg1': 1,
75+
'arg2': 2,
76+
'arg3': 'arg3',
77+
'no_explicit_default': 3,
78+
}
79+
self.assertEqual(substitute_in_config_variables(basic_dict, self.common_config), expected_dict)
80+
81+
def test_basis_list_substitution(self):
82+
basic_list = ['arg1', 'arg2', 'arg3', 'no_explicit_default']
83+
expected_list = [1, 2, 'arg3', 3]
84+
self.assertEqual(substitute_in_config_variables(basic_list, self.common_config), expected_list)
85+
86+
def test_complex_nested_type(self):
87+
complex_nested_type = {
88+
'arg1': ['arg1', 'arg2', 'arg3', 'no_explicit_default'],
89+
'arg2': 'arg22',
90+
'arg3': {
91+
'no_explicit_default': 'no_explicit_default',
92+
'arg2': 'arg2',
93+
'another_dict': {
94+
'arg1': ['arg1', 'arg1', 'arg1', 'no_explicit_default'],
95+
},
96+
'another_list': ['arg1', 'arg2', 'arg3', 'no_explicit_default']
97+
},
98+
'no_explicit_default': 'no_explicit_default',
99+
}
100+
expected_result = {
101+
'arg1': [1, 2, 'arg3', 3],
102+
'arg2': 'arg22',
103+
'arg3': {
104+
'no_explicit_default': 3,
105+
'arg2': 2,
106+
'another_dict': {
107+
'arg1': [1, 1, 1, 3],
108+
},
109+
'another_list': [1, 2, 'arg3', 3]
110+
},
111+
'no_explicit_default': 3,
112+
}
113+
self.assertEqual(substitute_in_config_variables(complex_nested_type, self.common_config), expected_result)
114+
115+
47116
def main():
48117
unittest.main()
49118

0 commit comments

Comments
 (0)