From 6c411aabe2dcd56a85085e27ef8a3950fb0f721b Mon Sep 17 00:00:00 2001 From: Rebecca Butler Date: Fri, 13 Aug 2021 15:21:57 -0400 Subject: [PATCH 01/11] Support both param file configurations in to_parameters_list.py Signed-off-by: Rebecca Butler --- .../launch_ros/actions/load_composable_nodes.py | 6 ++++-- .../launch_ros/utilities/to_parameters_list.py | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index fe6241e0..f13e0568 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -283,7 +283,8 @@ def get_composable_node_load_request( if parameters: request.parameters = [ param.to_parameter_msg() for param in to_parameters_list( - context, evaluate_parameters( + context, request.node_name, expanded_ns, + evaluate_parameters( context, parameters ) ) @@ -291,7 +292,8 @@ def get_composable_node_load_request( if composable_node_description.extra_arguments is not None: request.extra_arguments = [ param.to_parameter_msg() for param in to_parameters_list( - context, evaluate_parameters( + context, request.node_name, expanded_ns, + evaluate_parameters( context, composable_node_description.extra_arguments ) ) diff --git a/launch_ros/launch_ros/utilities/to_parameters_list.py b/launch_ros/launch_ros/utilities/to_parameters_list.py index c7d13e27..9cfbd659 100644 --- a/launch_ros/launch_ros/utilities/to_parameters_list.py +++ b/launch_ros/launch_ros/utilities/to_parameters_list.py @@ -30,6 +30,8 @@ def to_parameters_list( context: LaunchContext, + node_name: str, + namespace: str, evaluated_parameters: EvaluatedParameters ) -> List[rclpy.parameter.Parameter]: """ @@ -37,6 +39,8 @@ def to_parameters_list( :param context: to carry out substitutions during normalization of parameter files. See `normalize_parameters()` documentation for further reference. + :param node_name: node name + :param namespace: node namespace :param parameters: parameters as either paths to parameter files or name/value pairs. See `evaluate_parameters()` documentation for further reference. :returns: a list of parameters @@ -45,8 +49,19 @@ def to_parameters_list( for params_set_or_path in evaluated_parameters: if isinstance(params_set_or_path, pathlib.Path): with open(str(params_set_or_path), 'r') as f: + if namespace is not None: + node_name = f"{namespace}/{node_name}" + node_name_with_slash = f"/{node_name}" + param_dict = yaml.safe_load(f) + try: + param_dict = param_dict[node_name]["ros__parameters"] + except KeyError: + try: + param_dict = param_dict[node_name_with_slash]["ros__parameters"] + except KeyError: + pass params_set = evaluate_parameter_dict( - context, normalize_parameter_dict(yaml.safe_load(f)) + context, normalize_parameter_dict(param_dict) ) else: params_set = params_set_or_path From 97ede21d903a66ee7ebbe6acad988167a95a1bc5 Mon Sep 17 00:00:00 2001 From: Rebecca Butler Date: Tue, 24 Aug 2021 13:59:22 -0400 Subject: [PATCH 02/11] Support another file config and add tests Signed-off-by: Rebecca Butler --- .../actions/load_composable_nodes.py | 2 +- .../utilities/to_parameters_list.py | 37 ++++-- .../actions/example_parameters_namespace.yaml | 3 + .../actions/example_parameters_no_name.yaml | 2 + .../example_parameters_no_namespace.yaml | 3 + .../actions/example_parameters_wildcard.yaml | 3 + .../actions/test_load_composable_nodes.py | 111 ++++++++++++++++++ 7 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 test_launch_ros/test/test_launch_ros/actions/example_parameters_namespace.yaml create mode 100644 test_launch_ros/test/test_launch_ros/actions/example_parameters_no_name.yaml create mode 100644 test_launch_ros/test/test_launch_ros/actions/example_parameters_no_namespace.yaml create mode 100644 test_launch_ros/test/test_launch_ros/actions/example_parameters_wildcard.yaml diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index f13e0568..d16fed95 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -243,7 +243,7 @@ def get_composable_node_load_request( composable_node_description: ComposableNode, context: LaunchContext ): - """Get the request that will be send to the composable node container.""" + """Get the request that will be sent to the composable node container.""" request = composition_interfaces.srv.LoadNode.Request() request.package_name = perform_substitutions( context, composable_node_description.package diff --git a/launch_ros/launch_ros/utilities/to_parameters_list.py b/launch_ros/launch_ros/utilities/to_parameters_list.py index 9cfbd659..697c6ccf 100644 --- a/launch_ros/launch_ros/utilities/to_parameters_list.py +++ b/launch_ros/launch_ros/utilities/to_parameters_list.py @@ -28,6 +28,18 @@ from ..parameters_type import EvaluatedParameters +def search_for_key(keys, dictionary): + """Search for the nested "ros__parameters" key and return the keys above it.""" + for key, value in dictionary.items(): + if key == "ros__parameters": + return (keys, value) + elif type(value) is dict: + keys.append(key) + return search_for_key(keys, value) + else: + return (None, None) + + def to_parameters_list( context: LaunchContext, node_name: str, @@ -49,17 +61,24 @@ def to_parameters_list( for params_set_or_path in evaluated_parameters: if isinstance(params_set_or_path, pathlib.Path): with open(str(params_set_or_path), 'r') as f: - if namespace is not None: + node_name.lstrip('/') + if namespace: + namespace.lstrip('/') node_name = f"{namespace}/{node_name}" - node_name_with_slash = f"/{node_name}" param_dict = yaml.safe_load(f) - try: - param_dict = param_dict[node_name]["ros__parameters"] - except KeyError: - try: - param_dict = param_dict[node_name_with_slash]["ros__parameters"] - except KeyError: - pass + + # Get all keys that come before "ros__parameters" + keys = [] + inner_dict = {} + (keys, inner_dict) = search_for_key(keys, param_dict) + + # If we found any, combine them to form the full name + if keys: + keys = [key.lstrip('/') for key in keys] + yaml_name = '/'.join(keys) + if (yaml_name == node_name or yaml_name == "**"): + param_dict = inner_dict + params_set = evaluate_parameter_dict( context, normalize_parameter_dict(param_dict) ) diff --git a/test_launch_ros/test/test_launch_ros/actions/example_parameters_namespace.yaml b/test_launch_ros/test/test_launch_ros/actions/example_parameters_namespace.yaml new file mode 100644 index 00000000..edad0e91 --- /dev/null +++ b/test_launch_ros/test/test_launch_ros/actions/example_parameters_namespace.yaml @@ -0,0 +1,3 @@ +ns/test_node_name: + ros__parameters: + param: 1 \ No newline at end of file diff --git a/test_launch_ros/test/test_launch_ros/actions/example_parameters_no_name.yaml b/test_launch_ros/test/test_launch_ros/actions/example_parameters_no_name.yaml new file mode 100644 index 00000000..91015e73 --- /dev/null +++ b/test_launch_ros/test/test_launch_ros/actions/example_parameters_no_name.yaml @@ -0,0 +1,2 @@ +some_int: 42 +a_string: "Hello world" \ No newline at end of file diff --git a/test_launch_ros/test/test_launch_ros/actions/example_parameters_no_namespace.yaml b/test_launch_ros/test/test_launch_ros/actions/example_parameters_no_namespace.yaml new file mode 100644 index 00000000..8631b7b4 --- /dev/null +++ b/test_launch_ros/test/test_launch_ros/actions/example_parameters_no_namespace.yaml @@ -0,0 +1,3 @@ +/test_node_name: + ros__parameters: + param: 2 diff --git a/test_launch_ros/test/test_launch_ros/actions/example_parameters_wildcard.yaml b/test_launch_ros/test/test_launch_ros/actions/example_parameters_wildcard.yaml new file mode 100644 index 00000000..85198d89 --- /dev/null +++ b/test_launch_ros/test/test_launch_ros/actions/example_parameters_wildcard.yaml @@ -0,0 +1,3 @@ +/**: + ros__parameters: + param: "wildcard" \ No newline at end of file diff --git a/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py b/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py index efb1472e..55c593ba 100644 --- a/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py +++ b/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py @@ -14,6 +14,7 @@ """Tests for the LoadComposableNodes Action.""" +import pathlib import threading from composition_interfaces.srv import LoadNode @@ -214,6 +215,116 @@ def test_load_node_with_params(mock_component_container): assert len(request.extra_arguments) == 0 +def test_load_node_with_param_file(mock_component_container): + """Test loading a node with with parameters specified in yaml files.""" + parameters_file_dir = pathlib.Path(__file__).resolve().parent + + # Case 1: no node name in yaml file + context = _assert_launch_no_errors([ + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='test_node_name', + namespace='test_node_namespace', + parameters=[ + parameters_file_dir / 'example_parameters_no_name.yaml' + ], + ) + ]) + request = mock_component_container.requests[0] + assert get_node_name_count(context, '/test_node_namespace/test_node_name') == 1 + assert request.node_name == 'test_node_name' + assert request.node_namespace == '/test_node_namespace' + assert len(request.parameters) == 2 + assert request.parameters[0].name == 'some_int' + assert request.parameters[0].value.integer_value == 42 + assert request.parameters[1].name == 'a_string' + assert request.parameters[1].value.string_value == 'Hello world' + + # Case 2: node name with namespace + context = _assert_launch_no_errors([ + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='test_node_name', + namespace='ns', + parameters=[ + parameters_file_dir / 'example_parameters_namespace.yaml' + ], + ) + ]) + request = mock_component_container.requests[1] + assert get_node_name_count(context, '/ns/test_node_name') == 1 + assert request.node_name == 'test_node_name' + assert request.node_namespace == '/ns' + assert len(request.parameters) == 1 + assert request.parameters[0].name == 'param' + assert request.parameters[0].value.integer_value == 1 + + # Case 3: nested node name with namespace + context = _assert_launch_no_errors([ + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='my_node', + namespace='my_ns', + parameters=[ + parameters_file_dir / 'example_parameters.yaml' + ], + ) + ]) + request = mock_component_container.requests[2] + assert get_node_name_count(context, '/my_ns/my_node') == 1 + assert request.node_name == 'my_node' + assert request.node_namespace == '/my_ns' + assert len(request.parameters) == 5 + assert request.parameters[0].name == 'some_int' + assert request.parameters[0].value.integer_value == 42 + assert request.parameters[1].name == 'a_string' + assert request.parameters[1].value.string_value == 'Hello world' + assert request.parameters[2].value.string_value == '' + + # Case 4: node name without namespace + context = _assert_launch_no_errors([ + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='test_node_name', + namespace='', + parameters=[ + parameters_file_dir / 'example_parameters_no_namespace.yaml' + ], + ) + ]) + request = mock_component_container.requests[3] + assert get_node_name_count(context, '//test_node_name') == 1 + assert request.node_name == 'test_node_name' + assert request.node_namespace == '/' + assert len(request.parameters) == 1 + assert request.parameters[0].name == 'param' + assert request.parameters[0].value.integer_value == 2 + + # Case 5: wildcard + context = _assert_launch_no_errors([ + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='my_node', + namespace='wildcard_ns', + parameters=[ + parameters_file_dir / 'example_parameters_wildcard.yaml' + ], + ) + ]) + request = mock_component_container.requests[4] + assert get_node_name_count(context, '/wildcard_ns/my_node') == 1 + assert request.node_name == 'my_node' + assert request.node_namespace == '/wildcard_ns' + assert len(request.parameters) == 1 + assert request.parameters[0].name == 'param' + assert request.parameters[0].value.string_value == 'wildcard' + + def test_load_node_with_global_remaps_in_group(mock_component_container): """Test loading a node with global remaps scoped to a group.""" context = _assert_launch_no_errors([ From 4aebebdd2473f922ad5e1a29c67b8c63079b279d Mon Sep 17 00:00:00 2001 From: Rebecca Butler Date: Thu, 26 Aug 2021 16:22:55 -0400 Subject: [PATCH 03/11] Update to work for multiple entries in yaml Signed-off-by: Rebecca Butler --- .../utilities/to_parameters_list.py | 52 +++++++++------- .../example_parameters_multiple_entries.yaml | 14 +++++ .../actions/example_parameters_namespace.yaml | 2 +- .../actions/example_parameters_no_name.yaml | 2 +- .../actions/example_parameters_wildcard.yaml | 2 +- .../actions/test_load_composable_nodes.py | 62 +++++++++++++++++++ 6 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 test_launch_ros/test/test_launch_ros/actions/example_parameters_multiple_entries.yaml diff --git a/launch_ros/launch_ros/utilities/to_parameters_list.py b/launch_ros/launch_ros/utilities/to_parameters_list.py index 697c6ccf..e324f786 100644 --- a/launch_ros/launch_ros/utilities/to_parameters_list.py +++ b/launch_ros/launch_ros/utilities/to_parameters_list.py @@ -28,16 +28,22 @@ from ..parameters_type import EvaluatedParameters -def search_for_key(keys, dictionary): - """Search for the nested "ros__parameters" key and return the keys above it.""" +def __format_dict(dictionary, keys, formatted_dict): + """ + Combine namespaces and their node names into one key and remove the "ros__parameters" key. + + Return an empty dict if the "ros__parameters" key is not found. + """ for key, value in dictionary.items(): - if key == "ros__parameters": - return (keys, value) + if key == 'ros__parameters': + node_name = '/'.join(keys) + formatted_dict[node_name] = value + return formatted_dict elif type(value) is dict: - keys.append(key) - return search_for_key(keys, value) - else: - return (None, None) + keys.append(key.lstrip('/')) + formatted_dict = __format_dict(value, keys, formatted_dict) + keys = [] + return formatted_dict def to_parameters_list( @@ -58,26 +64,26 @@ def to_parameters_list( :returns: a list of parameters """ parameters = [] # type: List[rclpy.parameter.Parameter] + if not node_name: + print('Warning: no node name was provided. Parameter file entries that contain a ' + 'node name will not be applied') + if namespace: + namespace = namespace.lstrip('/') + node_name = f'{namespace}/{node_name}' + for params_set_or_path in evaluated_parameters: if isinstance(params_set_or_path, pathlib.Path): with open(str(params_set_or_path), 'r') as f: - node_name.lstrip('/') - if namespace: - namespace.lstrip('/') - node_name = f"{namespace}/{node_name}" param_dict = yaml.safe_load(f) + formatted_dict = __format_dict(param_dict, keys=[], formatted_dict={}) - # Get all keys that come before "ros__parameters" - keys = [] - inner_dict = {} - (keys, inner_dict) = search_for_key(keys, param_dict) - - # If we found any, combine them to form the full name - if keys: - keys = [key.lstrip('/') for key in keys] - yaml_name = '/'.join(keys) - if (yaml_name == node_name or yaml_name == "**"): - param_dict = inner_dict + if formatted_dict: + param_dict.clear() + if '**' in formatted_dict: + param_dict = formatted_dict['**'] + if node_name in formatted_dict: + for param, value in formatted_dict[node_name].items(): + param_dict[param] = value params_set = evaluate_parameter_dict( context, normalize_parameter_dict(param_dict) diff --git a/test_launch_ros/test/test_launch_ros/actions/example_parameters_multiple_entries.yaml b/test_launch_ros/test/test_launch_ros/actions/example_parameters_multiple_entries.yaml new file mode 100644 index 00000000..a332fcda --- /dev/null +++ b/test_launch_ros/test/test_launch_ros/actions/example_parameters_multiple_entries.yaml @@ -0,0 +1,14 @@ +/ns_1: + /node_1: + ros__parameters: + param_1: 1 + param_2: 2 + +/**: + ros__parameters: + param_2: 22 + param_3: 33 + +ns_2/node_2: + ros__parameters: + param_3: 3 diff --git a/test_launch_ros/test/test_launch_ros/actions/example_parameters_namespace.yaml b/test_launch_ros/test/test_launch_ros/actions/example_parameters_namespace.yaml index edad0e91..093083b6 100644 --- a/test_launch_ros/test/test_launch_ros/actions/example_parameters_namespace.yaml +++ b/test_launch_ros/test/test_launch_ros/actions/example_parameters_namespace.yaml @@ -1,3 +1,3 @@ ns/test_node_name: ros__parameters: - param: 1 \ No newline at end of file + param: 1 diff --git a/test_launch_ros/test/test_launch_ros/actions/example_parameters_no_name.yaml b/test_launch_ros/test/test_launch_ros/actions/example_parameters_no_name.yaml index 91015e73..353cde91 100644 --- a/test_launch_ros/test/test_launch_ros/actions/example_parameters_no_name.yaml +++ b/test_launch_ros/test/test_launch_ros/actions/example_parameters_no_name.yaml @@ -1,2 +1,2 @@ some_int: 42 -a_string: "Hello world" \ No newline at end of file +a_string: "Hello world" diff --git a/test_launch_ros/test/test_launch_ros/actions/example_parameters_wildcard.yaml b/test_launch_ros/test/test_launch_ros/actions/example_parameters_wildcard.yaml index 85198d89..92cb9316 100644 --- a/test_launch_ros/test/test_launch_ros/actions/example_parameters_wildcard.yaml +++ b/test_launch_ros/test/test_launch_ros/actions/example_parameters_wildcard.yaml @@ -1,3 +1,3 @@ /**: ros__parameters: - param: "wildcard" \ No newline at end of file + param: "wildcard" diff --git a/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py b/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py index 55c593ba..e895178a 100644 --- a/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py +++ b/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py @@ -39,6 +39,7 @@ TEST_CONTAINER_NAME = 'mock_component_container' TEST_NODE_NAME = 'test_load_composable_nodes_node' +parameters_file_dir = pathlib.Path(__file__).resolve().parent class MockComponentContainer(rclpy.node.Node): @@ -324,6 +325,67 @@ def test_load_node_with_param_file(mock_component_container): assert request.parameters[0].name == 'param' assert request.parameters[0].value.string_value == 'wildcard' + # Case 6: multiple entries (params with node name should take precedence over wildcard params) + context = _assert_launch_no_errors([ + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='node_1', + namespace='ns_1', + parameters=[ + parameters_file_dir / 'example_parameters_multiple_entries.yaml' + ], + ), + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='node_2', + namespace='ns_2', + parameters=[ + parameters_file_dir / 'example_parameters_multiple_entries.yaml' + ], + ) + ]) + request = mock_component_container.requests[5] + assert get_node_name_count(context, '/ns_1/node_1') == 1 + assert request.node_name == 'node_1' + assert request.node_namespace == '/ns_1' + assert len(request.parameters) == 3 + assert request.parameters[0].name == 'param_2' + assert request.parameters[0].value.integer_value == 2 + assert request.parameters[1].name == 'param_3' + assert request.parameters[1].value.integer_value == 33 + assert request.parameters[2].name == 'param_1' + assert request.parameters[2].value.integer_value == 1 + + request = mock_component_container.requests[6] + assert get_node_name_count(context, '/ns_2/node_2') == 1 + assert request.node_name == 'node_2' + assert request.node_namespace == '/ns_2' + assert len(request.parameters) == 2 + assert request.parameters[0].name == 'param_2' + assert request.parameters[0].value.integer_value == 22 + assert request.parameters[1].name == 'param_3' + assert request.parameters[1].value.integer_value == 3 + + # Case 7: node name not found + context = _assert_launch_no_errors([ + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='wrong_node_name', + namespace='ns', + parameters=[ + parameters_file_dir / 'example_parameters_no_namespace.yaml' + ], + ) + ]) + request = mock_component_container.requests[7] + assert get_node_name_count(context, '/ns/wrong_node_name') == 1 + assert request.node_name == 'wrong_node_name' + assert request.node_namespace == '/ns' + assert len(request.parameters) == 0 + def test_load_node_with_global_remaps_in_group(mock_component_container): """Test loading a node with global remaps scoped to a group.""" From 3eabbe4cc5e32bb5cf0fec719c4c5fee8dae2658 Mon Sep 17 00:00:00 2001 From: Rebecca Butler Date: Thu, 26 Aug 2021 16:25:45 -0400 Subject: [PATCH 04/11] Fix deleted line Signed-off-by: Rebecca Butler --- launch_ros/launch_ros/utilities/to_parameters_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/launch_ros/launch_ros/utilities/to_parameters_list.py b/launch_ros/launch_ros/utilities/to_parameters_list.py index e324f786..fcb4ef7e 100644 --- a/launch_ros/launch_ros/utilities/to_parameters_list.py +++ b/launch_ros/launch_ros/utilities/to_parameters_list.py @@ -64,6 +64,7 @@ def to_parameters_list( :returns: a list of parameters """ parameters = [] # type: List[rclpy.parameter.Parameter] + node_name = node_name.lstrip('/') if not node_name: print('Warning: no node name was provided. Parameter file entries that contain a ' 'node name will not be applied') From 0ddbb4686be924f326f3dd6d1aa84611643699fe Mon Sep 17 00:00:00 2001 From: Rebecca Butler Date: Thu, 26 Aug 2021 16:28:12 -0400 Subject: [PATCH 05/11] Remove duplicated line Signed-off-by: Rebecca Butler --- .../test/test_launch_ros/actions/test_load_composable_nodes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py b/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py index e895178a..d7c510d0 100644 --- a/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py +++ b/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py @@ -39,7 +39,6 @@ TEST_CONTAINER_NAME = 'mock_component_container' TEST_NODE_NAME = 'test_load_composable_nodes_node' -parameters_file_dir = pathlib.Path(__file__).resolve().parent class MockComponentContainer(rclpy.node.Node): From e6de2116496d7a962ec6b580a9e13000d8b91fcf Mon Sep 17 00:00:00 2001 From: Jacob Perron Date: Fri, 10 Sep 2021 12:07:43 -0700 Subject: [PATCH 06/11] Address reviewer comments Signed-off-by: Jacob Perron --- launch_ros/launch_ros/utilities/to_parameters_list.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/launch_ros/launch_ros/utilities/to_parameters_list.py b/launch_ros/launch_ros/utilities/to_parameters_list.py index fcb4ef7e..dbfa8fcf 100644 --- a/launch_ros/launch_ros/utilities/to_parameters_list.py +++ b/launch_ros/launch_ros/utilities/to_parameters_list.py @@ -16,6 +16,7 @@ import pathlib from typing import List +import warnings from launch.launch_context import LaunchContext import rclpy.parameter @@ -66,8 +67,10 @@ def to_parameters_list( parameters = [] # type: List[rclpy.parameter.Parameter] node_name = node_name.lstrip('/') if not node_name: - print('Warning: no node name was provided. Parameter file entries that contain a ' - 'node name will not be applied') + warnings.warn( + 'node name not provided to launch; parameter file entries will not be applied', + UserWarning + ) if namespace: namespace = namespace.lstrip('/') node_name = f'{namespace}/{node_name}' @@ -83,8 +86,7 @@ def to_parameters_list( if '**' in formatted_dict: param_dict = formatted_dict['**'] if node_name in formatted_dict: - for param, value in formatted_dict[node_name].items(): - param_dict[param] = value + param_dict.update(formatted_dict[node_name]) params_set = evaluate_parameter_dict( context, normalize_parameter_dict(param_dict) From 1f1ed6ef87704eae94590d02b30f9a29a675c8a9 Mon Sep 17 00:00:00 2001 From: Jacob Perron Date: Fri, 10 Sep 2021 12:37:37 -0700 Subject: [PATCH 07/11] Emit warning iff no node name and ros__parameters foramt is used Signed-off-by: Jacob Perron --- .../launch_ros/utilities/to_parameters_list.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/launch_ros/launch_ros/utilities/to_parameters_list.py b/launch_ros/launch_ros/utilities/to_parameters_list.py index dbfa8fcf..7839dd6b 100644 --- a/launch_ros/launch_ros/utilities/to_parameters_list.py +++ b/launch_ros/launch_ros/utilities/to_parameters_list.py @@ -66,15 +66,11 @@ def to_parameters_list( """ parameters = [] # type: List[rclpy.parameter.Parameter] node_name = node_name.lstrip('/') - if not node_name: - warnings.warn( - 'node name not provided to launch; parameter file entries will not be applied', - UserWarning - ) if namespace: namespace = namespace.lstrip('/') node_name = f'{namespace}/{node_name}' + warned_once = False for params_set_or_path in evaluated_parameters: if isinstance(params_set_or_path, pathlib.Path): with open(str(params_set_or_path), 'r') as f: @@ -87,6 +83,12 @@ def to_parameters_list( param_dict = formatted_dict['**'] if node_name in formatted_dict: param_dict.update(formatted_dict[node_name]) + if not warned_once and not node_name: + warnings.warn( + 'node name not provided to launch; parameter files will not apply', + UserWarning + ) + warned_once = True params_set = evaluate_parameter_dict( context, normalize_parameter_dict(param_dict) From 9de076764d24ee693a89ebb53e50af30713e2f98 Mon Sep 17 00:00:00 2001 From: Jacob Perron Date: Mon, 20 Sep 2021 21:36:48 -0700 Subject: [PATCH 08/11] Refactor after review - Rename private function for dealing with ros__parameters entries - Keep recursive parameters internal to function - Skip evaluating parameters if dictionary is empty - Use isinstance - Strip trailing and leading '/' from node namespace Signed-off-by: Jacob Perron --- .../utilities/to_parameters_list.py | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/launch_ros/launch_ros/utilities/to_parameters_list.py b/launch_ros/launch_ros/utilities/to_parameters_list.py index 7839dd6b..55156901 100644 --- a/launch_ros/launch_ros/utilities/to_parameters_list.py +++ b/launch_ros/launch_ros/utilities/to_parameters_list.py @@ -29,22 +29,30 @@ from ..parameters_type import EvaluatedParameters -def __format_dict(dictionary, keys, formatted_dict): +def __normalize_parameters_dict(dictionary): """ Combine namespaces and their node names into one key and remove the "ros__parameters" key. Return an empty dict if the "ros__parameters" key is not found. """ - for key, value in dictionary.items(): - if key == 'ros__parameters': - node_name = '/'.join(keys) - formatted_dict[node_name] = value - return formatted_dict - elif type(value) is dict: - keys.append(key.lstrip('/')) - formatted_dict = __format_dict(value, keys, formatted_dict) - keys = [] - return formatted_dict + # Recursive function + def normalize_parameters_dict(dictionary, keys, result_dict): + for key, value in dictionary.items(): + # Base case: found 'ros__parameters' key + # Return + if key == 'ros__parameters': + node_name = '/'.join(keys) + result_dict[node_name] = value + return result_dict + + if isinstance(value, dict): + keys.append(key.lstrip('/')) + result_dict = normalize_parameters_dict(value, keys, result_dict) + keys = [] + + return result_dict + + return normalize_parameters_dict(dictionary, [], {}) def to_parameters_list( @@ -67,22 +75,23 @@ def to_parameters_list( parameters = [] # type: List[rclpy.parameter.Parameter] node_name = node_name.lstrip('/') if namespace: - namespace = namespace.lstrip('/') + namespace = namespace.strip('/') node_name = f'{namespace}/{node_name}' + params_set = {} warned_once = False for params_set_or_path in evaluated_parameters: if isinstance(params_set_or_path, pathlib.Path): with open(str(params_set_or_path), 'r') as f: param_dict = yaml.safe_load(f) - formatted_dict = __format_dict(param_dict, keys=[], formatted_dict={}) + normalized_param_dict = __normalize_parameters_dict(param_dict) - if formatted_dict: + if normalized_param_dict: param_dict.clear() - if '**' in formatted_dict: - param_dict = formatted_dict['**'] - if node_name in formatted_dict: - param_dict.update(formatted_dict[node_name]) + if '**' in normalized_param_dict: + param_dict = normalized_param_dict['**'] + if node_name in normalized_param_dict: + param_dict.update(normalized_param_dict[node_name]) if not warned_once and not node_name: warnings.warn( 'node name not provided to launch; parameter files will not apply', @@ -90,9 +99,10 @@ def to_parameters_list( ) warned_once = True - params_set = evaluate_parameter_dict( - context, normalize_parameter_dict(param_dict) - ) + if param_dict: + params_set = evaluate_parameter_dict( + context, normalize_parameter_dict(param_dict) + ) else: params_set = params_set_or_path if not isinstance(params_set, dict): From 5adffa8e99561a831a4d6521a946cf51a71b87b5 Mon Sep 17 00:00:00 2001 From: Jacob Perron Date: Thu, 30 Sep 2021 17:14:46 -0700 Subject: [PATCH 09/11] Remove unnecessary line Signed-off-by: Jacob Perron --- launch_ros/launch_ros/utilities/to_parameters_list.py | 1 - 1 file changed, 1 deletion(-) diff --git a/launch_ros/launch_ros/utilities/to_parameters_list.py b/launch_ros/launch_ros/utilities/to_parameters_list.py index 55156901..bd10aece 100644 --- a/launch_ros/launch_ros/utilities/to_parameters_list.py +++ b/launch_ros/launch_ros/utilities/to_parameters_list.py @@ -48,7 +48,6 @@ def normalize_parameters_dict(dictionary, keys, result_dict): if isinstance(value, dict): keys.append(key.lstrip('/')) result_dict = normalize_parameters_dict(value, keys, result_dict) - keys = [] return result_dict From c7844b54984d0b916c3d9a2c4dbcc039038c050f Mon Sep 17 00:00:00 2001 From: Jacob Perron Date: Tue, 19 Oct 2021 14:01:09 -0700 Subject: [PATCH 10/11] Reset keys list in case of dictionary value We actually do this for the case of multiple entries like this: /ns_1: /node_1: ros__parameters: param_1: 1 param_2: 2 /**: ros__parameters: param_2: 22 param_3: 33 Signed-off-by: Jacob Perron --- launch_ros/launch_ros/utilities/to_parameters_list.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launch_ros/launch_ros/utilities/to_parameters_list.py b/launch_ros/launch_ros/utilities/to_parameters_list.py index bd10aece..b3d78959 100644 --- a/launch_ros/launch_ros/utilities/to_parameters_list.py +++ b/launch_ros/launch_ros/utilities/to_parameters_list.py @@ -48,6 +48,8 @@ def normalize_parameters_dict(dictionary, keys, result_dict): if isinstance(value, dict): keys.append(key.lstrip('/')) result_dict = normalize_parameters_dict(value, keys, result_dict) + # Reset keys in case there are multiple ros__parameter entries + keys = [] return result_dict From bd1bc63d0b618e5b5944d7b46ec0be3dfd24b6d2 Mon Sep 17 00:00:00 2001 From: Jacob Perron Date: Tue, 19 Oct 2021 15:53:42 -0700 Subject: [PATCH 11/11] Use separate test file for test_load_composable_nodes Rather than reusing the test file that is used in test_node. Signed-off-by: Jacob Perron --- .../actions/example_parameters_nested_namespace.yaml | 10 ++++++++++ .../actions/test_load_composable_nodes.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 test_launch_ros/test/test_launch_ros/actions/example_parameters_nested_namespace.yaml diff --git a/test_launch_ros/test/test_launch_ros/actions/example_parameters_nested_namespace.yaml b/test_launch_ros/test/test_launch_ros/actions/example_parameters_nested_namespace.yaml new file mode 100644 index 00000000..a4166eb8 --- /dev/null +++ b/test_launch_ros/test/test_launch_ros/actions/example_parameters_nested_namespace.yaml @@ -0,0 +1,10 @@ +/my_ns: + my_node: + ros__parameters: + some_int: 42 + a_string: "Hello world" + no_string: "" + some_list: + sub_list: + some_integers: [1, 2, 3, 4] + some_doubles : [3.14, 2.718] diff --git a/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py b/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py index d7c510d0..d16ea33a 100644 --- a/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py +++ b/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py @@ -269,7 +269,7 @@ def test_load_node_with_param_file(mock_component_container): name='my_node', namespace='my_ns', parameters=[ - parameters_file_dir / 'example_parameters.yaml' + parameters_file_dir / 'example_parameters_nested_namespace.yaml' ], ) ])