diff --git a/rclpy/rclpy/node.py b/rclpy/rclpy/node.py index 1c2d8f588..996f5dbe7 100644 --- a/rclpy/rclpy/node.py +++ b/rclpy/rclpy/node.py @@ -1002,6 +1002,7 @@ def _set_parameters_atomically_common( # Descriptors have already been applied by this point. self._parameters[param.name] = param + self._handle_changes_inside_yaml(parameter=param) parameter_event.stamp = self._clock.now().to_msg() if self._parameter_event_publisher: @@ -1012,6 +1013,62 @@ def _set_parameters_atomically_common( return result + + def _traverse_yaml_and_change_value(self, yaml_dict : dict, parameter_path : List[str], parameter_value : ParameterValue) -> None: + """ + Iteratively traverse a nested dictionary to apply a key-value pair + """ + for p in parameter_path[1:-1]: + if (yaml_dict := yaml_dict.get(p)) is None: + break + else: + value_type = parameter_value.type + final_value = None + if Parameter.Type.BOOL == value_type: + final_value = parameter_value.bool_value + elif Parameter.Type.INTEGER == value_type: + final_value = parameter_value.integer_value + elif Parameter.Type.DOUBLE == value_type: + final_value = parameter_value.double_value + elif Parameter.Type.STRING == value_type: + final_value = parameter_value.string_value + elif Parameter.Type.YAML == value_type: + # Setting a nested yaml isnt supported yet + return + elif Parameter.Type.BYTE_ARRAY == value_type: + final_value= parameter_value.byte_array_value + elif Parameter.Type.BOOL_ARRAY == value_type: + final_value = parameter_value.bool_array_value + elif Parameter.Type.INTEGER_ARRAY == value_type: + final_value = parameter_value.integer_array_value + elif Parameter.Type.DOUBLE_ARRAY == value_type: + final_value = parameter_value.double_array_value + elif Parameter.Type.STRING_ARRAY == value_type: + final_value = parameter_value.string_array_value + yaml_dict[parameter_path[-1]] = final_value + return yaml_dict + + + yaml_dict[parameter_path[-1]] = value.get_parameter_value() + + + def _handle_changes_inside_yaml(self, parameter : Parameter) -> None: + name = parameter._name + parameter_path = name.split(".") + # Handle case where the parameter is not splittable into namespaces + if len(parameter_path) == 1 and parameter_path[0] == name: + return + + if parameter_path[0] not in self._parameters.keys(): + return + + if self._parameters[parameter_path[0]]._type_ != Parameter.Type.YAML: + return + yaml_dict = self._parameters[parameter_path[0]].get_yaml_parameter_as_dict() + self._traverse_yaml_and_change_value(yaml_dict, parameter_path, parameter.get_parameter_value()) + self._parameters[parameter_path[0]] = Parameter(name, value=yaml_dict) + + def list_parameters( self, prefixes: List[str], diff --git a/rclpy/rclpy/parameter.py b/rclpy/rclpy/parameter.py index 1fd7dab76..3b1286642 100644 --- a/rclpy/rclpy/parameter.py +++ b/rclpy/rclpy/parameter.py @@ -40,14 +40,14 @@ # Mypy does not handle string literals of array.array[int/str/float] very well # So if user has newer version of python can use proper array types. if sys.version_info > (3, 9): - AllowableParameterValue = Union[None, bool, int, float, str, + AllowableParameterValue = Union[None, bool, int, float, str, dict, list[bytes], Tuple[bytes, ...], list[bool], Tuple[bool, ...], list[int], Tuple[int, ...], array.array[int], list[float], Tuple[float, ...], array.array[float], list[str], Tuple[str, ...], array.array[str]] else: - AllowableParameterValue = Union[None, bool, int, float, str, + AllowableParameterValue = Union[None, bool, int, float, str, dict, List[bytes], Tuple[bytes, ...], List[bool], Tuple[bool, ...], List[int], Tuple[int, ...], 'array.array[int]', @@ -71,6 +71,7 @@ class Type(IntEnum): INTEGER = ParameterType.PARAMETER_INTEGER DOUBLE = ParameterType.PARAMETER_DOUBLE STRING = ParameterType.PARAMETER_STRING + YAML = ParameterType.PARAMETER_YAML BYTE_ARRAY = ParameterType.PARAMETER_BYTE_ARRAY BOOL_ARRAY = ParameterType.PARAMETER_BOOL_ARRAY INTEGER_ARRAY = ParameterType.PARAMETER_INTEGER_ARRAY @@ -97,6 +98,8 @@ def from_parameter_value(cls, return Parameter.Type.DOUBLE elif isinstance(parameter_value, str): return Parameter.Type.STRING + elif isinstance(parameter_value, dict): + return Parameter.Type.YAML elif isinstance(parameter_value, (list, tuple, array.array)): if all(isinstance(v, bytes) for v in parameter_value): return Parameter.Type.BYTE_ARRAY @@ -127,6 +130,8 @@ def check(self, parameter_value: AllowableParameterValue) -> bool: return isinstance(parameter_value, float) if Parameter.Type.STRING == self: return isinstance(parameter_value, str) + if Parameter.Type.YAML == self: + return isinstance(parameter_value, dict) or isinstance(parameter_value, str) if Parameter.Type.BYTE_ARRAY == self: return isinstance(parameter_value, (list, tuple)) and \ all(isinstance(v, bytes) and len(v) == 1 for v in parameter_value) @@ -156,6 +161,8 @@ def from_parameter_msg(cls, param_msg: ParameterMsg) -> Parameter[Any]: value = param_msg.value.double_value elif Parameter.Type.STRING == type_: value = param_msg.value.string_value + elif Parameter.Type.YAML == type_: + value = param_msg.value.yaml_value elif Parameter.Type.BYTE_ARRAY == type_: value = param_msg.value.byte_array_value elif Parameter.Type.BOOL_ARRAY == type_: @@ -191,6 +198,11 @@ def __init__(self: Parameter[float], name: str, type_: Literal[Parameter.Type.DO def __init__(self: Parameter[str], name: str, type_: Literal[Parameter.Type.STRING] ) -> None: ... + @overload + def __init__(self: Parameter[dict], name: str, type_: Literal[Parameter.Type.YAML] + ) -> None: ... + + @overload def __init__(self: Parameter[Union[list[bytes], Tuple[bytes, ...]]], name: str, @@ -228,6 +240,16 @@ def __init__(self, name: str, type_: Parameter.Type, value: AllowableParameterValueT) -> None: ... def __init__(self, name: str, type_: Optional[Parameter.Type] = None, value=None) -> None: + # If is string, try loading as a yaml + # If it throws an exception, its not a valid yaml string, so its probably just a normal string + if isinstance(value, str): + try: + value = yaml.safe_load(value) + except: + pass + else: + value = yaml.safe_load(yaml.safe_dump(value)) + if type_ is None: # This will raise a TypeError if it is not possible to get a type from the value. type_ = Parameter.Type.from_parameter_value(value) @@ -240,6 +262,9 @@ def __init__(self, name: str, type_: Optional[Parameter.Type] = None, value=None self._type_ = type_ self._name = name + + if type_ == Parameter.Type.YAML: + value = yaml.safe_dump(value) self._value = value @property @@ -253,6 +278,11 @@ def type_(self) -> 'Parameter.Type': @property def value(self) -> AllowableParameterValueT: return self._value + + + def get_yaml_parameter_as_dict(self) -> dict : + assert(self.type_ == Parameter.Type.YAML) + return yaml.safe_load(self.value) def get_parameter_value(self) -> ParameterValue: parameter_value = ParameterValue(type=self.type_.value) @@ -264,6 +294,8 @@ def get_parameter_value(self) -> ParameterValue: parameter_value.double_value = self.value elif Parameter.Type.STRING == self.type_: parameter_value.string_value = self.value + elif Parameter.Type.YAML == self.type_: + parameter_value.yaml_value = self.value elif Parameter.Type.BYTE_ARRAY == self.type_: parameter_value.byte_array_value = self.value elif Parameter.Type.BOOL_ARRAY == self.type_: @@ -292,7 +324,6 @@ def get_parameter_value(string_value: str) -> ParameterValue: yaml_value = yaml.safe_load(string_value) except yaml.parser.ParserError: yaml_value = string_value - if isinstance(yaml_value, bool): value.type = ParameterType.PARAMETER_BOOL value.bool_value = yaml_value @@ -302,6 +333,9 @@ def get_parameter_value(string_value: str) -> ParameterValue: elif isinstance(yaml_value, float): value.type = ParameterType.PARAMETER_DOUBLE value.double_value = yaml_value + elif isinstance(yaml_value, dict): + value.type = ParameterType.PARAMETER_YAML + value.yaml_value = yaml.safe_dump(yaml_value) elif isinstance(yaml_value, list): if all((isinstance(v, bool) for v in yaml_value)): value.type = ParameterType.PARAMETER_BOOL_ARRAY diff --git a/rclpy/src/rclpy/node.cpp b/rclpy/src/rclpy/node.cpp index cfb30e4b0..2b0df2a23 100644 --- a/rclpy/src/rclpy/node.cpp +++ b/rclpy/src/rclpy/node.cpp @@ -244,6 +244,9 @@ _parameter_from_rcl_variant( } else if (variant->string_value) { type_enum_value = rcl_interfaces__msg__ParameterType__PARAMETER_STRING; value = py::str(variant->string_value); + } else if (variant->yaml_value) { + type_enum_value = rcl_interfaces__msg__ParameterType__PARAMETER_YAML; + value = py::str(variant->yaml_value); } else if (variant->byte_array_value) { type_enum_value = rcl_interfaces__msg__ParameterType__PARAMETER_BYTE_ARRAY; value = py::bytes( diff --git a/rclpy/test/test_parameter.py b/rclpy/test/test_parameter.py index c0101c9d6..0f9628dd1 100644 --- a/rclpy/test/test_parameter.py +++ b/rclpy/test/test_parameter.py @@ -16,6 +16,7 @@ import os from tempfile import NamedTemporaryFile import unittest +import yaml import pytest from rcl_interfaces.msg import Parameter as ParameterMsg @@ -78,6 +79,16 @@ def test_create_string_parameter(self) -> None: self.assertEqual(p.type_, Parameter.Type.STRING) self.assertEqual(p.value, 'pvalue') + def test_create_yaml_parameter(self)-> None: + yaml_dict = {"a":1, "b":2} + p = Parameter('myparam', Parameter.Type.YAML, yaml_dict) + self.assertEqual(p.name, 'myparam') + self.assertEqual(yaml.safe_load(p.value), yaml_dict) + + p = Parameter('myparam', value = yaml_dict) + self.assertEqual(p.name, 'myparam') + self.assertEqual(yaml.safe_load(p.value), yaml_dict) + def test_create_boolean_array_parameter(self) -> None: p = Parameter('myparam', Parameter.Type.BOOL_ARRAY, [True, False, True]) self.assertEqual(p.value, [True, False, True])