Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions rclpy/rclpy/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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],
Expand Down
40 changes: 37 additions & 3 deletions rclpy/rclpy/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]',
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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_:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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_:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions rclpy/src/rclpy/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions rclpy/test/test_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import os
from tempfile import NamedTemporaryFile
import unittest
import yaml

import pytest
from rcl_interfaces.msg import Parameter as ParameterMsg
Expand Down Expand Up @@ -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])
Expand Down