Skip to content

Commit 56a5946

Browse files
author
Roger Strain
committed
Refactor Node action into description classes
Distro A; OPSEC #4584 Signed-off-by: Roger Strain <[email protected]>
1 parent 06d049e commit 56a5946

File tree

8 files changed

+106
-63
lines changed

8 files changed

+106
-63
lines changed

launch_ros/launch_ros/actions/node.py

+13-32
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,12 @@
1414

1515
"""Module for the Node action."""
1616

17-
import os
18-
import pathlib
19-
from tempfile import NamedTemporaryFile
20-
from typing import Dict
2117
from typing import Iterable
2218
from typing import List
2319
from typing import Optional
2420
from typing import Text # noqa: F401
2521
from typing import Tuple # noqa: F401
26-
from typing import Union
2722

28-
from launch.action import Action
2923
from launch.actions import ExecuteLocal
3024
from launch.actions import ExecuteProcess
3125
from launch.frontend import Entity
@@ -34,32 +28,13 @@
3428
from launch.frontend.type_utils import get_data_type_from_identifier
3529

3630
from launch.launch_context import LaunchContext
37-
import launch.logging
3831
from launch.some_substitutions_type import SomeSubstitutionsType
39-
from launch.substitutions import LocalSubstitution
40-
from launch.utilities import ensure_argument_type
41-
from launch.utilities import normalize_to_list_of_substitutions
42-
from launch.utilities import perform_substitutions
4332

4433
from launch_ros.descriptions import Node as NodeDescription
45-
from launch_ros.descriptions import RosExecutable
46-
from launch_ros.descriptions import Parameter
4734
from launch_ros.descriptions import ParameterFile
35+
from launch_ros.descriptions import RosExecutable
4836
from launch_ros.parameters_type import SomeParameters
4937
from launch_ros.remap_rule_type import SomeRemapRules
50-
from launch_ros.substitutions import ExecutableInPackage
51-
from launch_ros.utilities import add_node_name
52-
from launch_ros.utilities import evaluate_parameters
53-
from launch_ros.utilities import get_node_name_count
54-
from launch_ros.utilities import make_namespace_absolute
55-
from launch_ros.utilities import normalize_parameters
56-
from launch_ros.utilities import normalize_remap_rules
57-
from launch_ros.utilities import prefix_namespace
58-
59-
from rclpy.validate_namespace import validate_namespace
60-
from rclpy.validate_node_name import validate_node_name
61-
62-
import yaml
6338

6439

6540
@expose_action('node')
@@ -138,11 +113,19 @@ def __init__(
138113
passed to the node as ROS remapping rules
139114
:param: arguments list of extra arguments for the node
140115
"""
141-
142-
self.__node_desc = NodeDescription(node_name=name, namespace=namespace, parameters=parameters, remappings=remappings, arguments=arguments)
143-
self.__ros_exec = RosExecutable(package=package, executable_name=executable, nodes=[self.__node_desc])
116+
self.__node_desc = NodeDescription(node_name=name, node_namespace=namespace,
117+
parameters=parameters, remappings=remappings,
118+
arguments=arguments)
119+
self.__ros_exec = RosExecutable(package=package, executable=executable,
120+
nodes=[self.__node_desc])
144121
super().__init__(process_description=self.__ros_exec, **kwargs)
145122

123+
def _perform_substitutions(self, lc: LaunchContext):
124+
self.__node_desc.prepare(lc, self.__ros_exec)
125+
126+
def is_node_name_fully_specified(self):
127+
return self.__node_desc.is_node_name_fully_specified()
128+
146129
@staticmethod
147130
def parse_nested_parameters(params, parser):
148131
"""Normalize parameters as expected by Node constructor argument."""
@@ -249,9 +232,7 @@ def parse(cls, entity: Entity, parser: Parser):
249232
@property
250233
def node_name(self):
251234
"""Getter for node_name."""
252-
if self.__node_desc.final_node_name is None:
253-
raise RuntimeError("cannot access 'node_name' before executing action")
254-
return self.__node_desc.final_node_name
235+
return self.__node_desc.node_name
255236

256237
@property
257238
def expanded_node_namespace(self):

launch_ros/launch_ros/descriptions/node.py

+51-21
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,39 @@
2424

2525
import os
2626
import pathlib
27-
import yaml
2827

2928
from tempfile import NamedTemporaryFile
3029

30+
from typing import Dict
3131
from typing import Iterable
32+
from typing import List
3233
from typing import Optional
3334
from typing import Text
35+
from typing import Tuple
36+
from typing import Union
3437

3538
from launch import LaunchContext
3639
from launch import SomeSubstitutionsType
3740
from launch.descriptions import Executable
3841
import launch.logging
3942
from launch.substitutions import LocalSubstitution
43+
from launch.utilities import ensure_argument_type
4044
from launch.utilities import normalize_to_list_of_substitutions
4145
from launch.utilities import perform_substitutions
4246

47+
from launch_ros.utilities import add_node_name
4348
from launch_ros.utilities import evaluate_parameters
49+
from launch_ros.utilities import get_node_name_count
4450
from launch_ros.utilities import make_namespace_absolute
51+
from launch_ros.utilities import normalize_parameters
52+
from launch_ros.utilities import normalize_remap_rules
4553
from launch_ros.utilities import prefix_namespace
4654

4755
from rclpy.validate_namespace import validate_namespace
4856
from rclpy.validate_node_name import validate_node_name
4957

58+
import yaml
59+
5060
from .node_trait import NodeTrait
5161
from ..parameter_descriptions import Parameter
5262
from ..parameters_type import SomeParameters
@@ -114,11 +124,16 @@ def __init__(
114124
:param: arguments list of extra arguments for the node
115125
:param: traits list of special traits of the node
116126
"""
127+
if parameters is not None:
128+
ensure_argument_type(parameters, (list), 'parameters', 'Node')
129+
# All elements in the list are paths to files with parameters (or substitutions that
130+
# evaluate to paths), or dictionaries of parameters (fields can be substitutions).
131+
normalized_params = normalize_parameters(parameters)
117132

118133
self.__node_name = node_name
119134
self.__node_namespace = node_namespace
120-
self.__parameters = parameters
121-
self.__remappings = remappings
135+
self.__parameters = [] if parameters is None else normalized_params
136+
self.__remappings = [] if remappings is None else list(normalize_remap_rules(remappings))
122137
self.__arguments = arguments
123138
self.__traits = traits
124139

@@ -135,7 +150,9 @@ def __init__(
135150
@property
136151
def node_name(self):
137152
"""Getter for node_name."""
138-
return self.__node_name
153+
if self.__final_node_name is None:
154+
raise RuntimeError("cannot access 'node_name' before executing action")
155+
return self.__final_node_name
139156

140157
@property
141158
def node_namespace(self):
@@ -177,13 +194,6 @@ def expanded_parameter_arguments(self):
177194
"""Getter for expanded_parameter_arguments."""
178195
return self.__expanded_parameter_arguments
179196

180-
@property
181-
def final_node_name(self):
182-
"""Getter for final_node_name."""
183-
if self.__substitutions_performed == True:
184-
return self.__final_node_name
185-
return None
186-
187197
@property
188198
def expanded_remappings(self):
189199
"""Getter for expanded_remappings."""
@@ -206,13 +216,15 @@ def _create_params_file_from_dict(self, params):
206216
def _get_parameter_rule(self, param: 'Parameter', context: LaunchContext):
207217
name, value = param.evaluate(context)
208218
return f'{name}:={yaml.dump(value)}'
209-
219+
210220
def prepare(self, context: LaunchContext, executable: Executable) -> None:
211221
try:
212222
if self.__substitutions_performed:
213223
# This function may have already been called by a subclass' `execute`, for example.
214224
return
215225
self.__substitutions_performed = True
226+
cmd_ext = ['--ros-args'] # Prepend ros specific arguments with --ros-args flag
227+
executable.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_ext])
216228
if self.__node_name is not None:
217229
self.__expanded_node_name = perform_substitutions(
218230
context, normalize_to_list_of_substitutions(self.__node_name))
@@ -227,8 +239,8 @@ def prepare(self, context: LaunchContext, executable: Executable) -> None:
227239
prefix_namespace(base_ns, expanded_node_namespace))
228240
if expanded_node_namespace is not None:
229241
self.__expanded_node_namespace = expanded_node_namespace
230-
cmd_extension = ['-r', LocalSubstitution("ros_specific_arguments['ns']")]
231-
executable.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
242+
cmd_ext = ['-r', LocalSubstitution("ros_specific_arguments['ns']")]
243+
executable.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_ext])
232244
validate_namespace(self.__expanded_node_namespace)
233245
except Exception:
234246
self.__logger.error(
@@ -249,8 +261,8 @@ def prepare(self, context: LaunchContext, executable: Executable) -> None:
249261
if global_params is not None:
250262
param_file_path = self._create_params_file_from_dict(global_params)
251263
self.__expanded_parameter_arguments.append((param_file_path, True))
252-
cmd_extension = ['--params-file', f'{param_file_path}']
253-
executable.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
264+
cmd_ext = ['--params-file', f'{param_file_path}']
265+
executable.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_ext])
254266
assert os.path.isfile(param_file_path)
255267
# expand parameters too
256268
if self.__parameters is not None:
@@ -274,8 +286,8 @@ def prepare(self, context: LaunchContext, executable: Executable) -> None:
274286
)
275287
continue
276288
self.__expanded_parameter_arguments.append((param_argument, is_file))
277-
cmd_extension = ['--params-file' if is_file else '-p', f'{param_argument}']
278-
executable.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
289+
cmd_ext = ['--params-file' if is_file else '-p', f'{param_argument}']
290+
executable.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_ext])
279291
# expand remappings too
280292
global_remaps = context.launch_configurations.get('ros_remaps', None)
281293
if global_remaps or self.__remappings:
@@ -288,7 +300,25 @@ def prepare(self, context: LaunchContext, executable: Executable) -> None:
288300
for src, dst in self.__remappings
289301
])
290302
if self.__expanded_remappings:
291-
cmd_extension = []
303+
cmd_ext = []
292304
for src, dst in self.__expanded_remappings:
293-
cmd_extension.extend(['-r', f'{src}:={dst}'])
294-
executable.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
305+
cmd_ext.extend(['-r', f'{src}:={dst}'])
306+
executable.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_ext])
307+
# Prepare the ros_specific_arguments list and add it to the context so that the
308+
# LocalSubstitution placeholders added to the the cmd can be expanded using the contents.
309+
ros_specific_arguments: Dict[str, Union[str, List[str]]] = {}
310+
if self.__node_name is not None:
311+
ros_specific_arguments['name'] = '__node:={}'.format(self.__expanded_node_name)
312+
if self.__expanded_node_namespace != '':
313+
ros_specific_arguments['ns'] = '__ns:={}'.format(self.__expanded_node_namespace)
314+
context.extend_locals({'ros_specific_arguments': ros_specific_arguments})
315+
316+
if self.is_node_name_fully_specified():
317+
add_node_name(context, self.node_name)
318+
node_name_count = get_node_name_count(context, self.node_name)
319+
if node_name_count > 1:
320+
execute_process_logger = launch.logging.get_logger(self.name)
321+
execute_process_logger.warning(
322+
'there are now at least {} nodes with the name {} created within this '
323+
'launch context'.format(node_name_count, self.node_name)
324+
)

launch_ros/launch_ros/descriptions/node_trait.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from launch import Action
2626
from launch import LaunchContext
2727

28+
2829
class NodeTrait:
2930
"""Describes a trait of a node."""
3031

@@ -40,4 +41,4 @@ def __init__(self) -> None:
4041
pass
4142

4243
def prepare(self, node, context: LaunchContext, action: Action):
43-
"""Performs any actions necessary to prepare the node for execution."""
44+
"""Perform any actions necessary to prepare the node for execution."""

launch_ros/launch_ros/descriptions/ros_executable.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333

3434
from ..descriptions import Node
3535

36+
3637
class RosExecutable(Executable):
3738
"""Describes an executable with ROS features which may be run by the launch system."""
39+
3840
def __init__(
3941
self, *,
4042
executable: Optional[SomeSubstitutionsType] = None,
@@ -50,7 +52,6 @@ def __init__(
5052
:param: package the package in which the node executable can be found
5153
:param: nodes the ROS node(s) included in the executable
5254
"""
53-
5455
if package is not None:
5556
cmd = [ExecutableInPackage(package=package, executable=executable)]
5657
else:
@@ -84,9 +85,7 @@ def apply_context(self, context: LaunchContext):
8485
- prepares all nodes
8586
- performs substitutions on various properties
8687
"""
87-
8888
for node in self.__nodes:
8989
node.prepare(context, self)
9090

9191
super().apply_context(context)
92-

test_launch_ros/test/test_launch_ros/actions/test_node.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def test_launch_node_with_remappings(self):
8787
self._assert_launch_no_errors([node_action])
8888

8989
# Check the expanded parameters.
90-
expanded_remappings = node_action._Node__expanded_remappings
90+
expanded_remappings = node_action._Node__node_desc.expanded_remappings
9191
assert len(expanded_remappings) == 2
9292
for i in range(2):
9393
assert expanded_remappings[i] == ('chatter', 'new_chatter')
@@ -176,7 +176,7 @@ def test_launch_node_with_parameter_descriptions(self):
176176
)
177177
self._assert_launch_no_errors([node_action])
178178

179-
expanded_parameter_arguments = node_action._Node__expanded_parameter_arguments
179+
expanded_parameter_arguments = node_action._Node__node_desc.expanded_parameter_arguments
180180
assert len(expanded_parameter_arguments) == 5
181181
parameters = []
182182
for item, is_file in expanded_parameter_arguments:
@@ -213,7 +213,7 @@ def test_launch_node_with_parameter_dict(self):
213213
self._assert_launch_no_errors([node_action])
214214

215215
# Check the expanded parameters (will be written to a file).
216-
expanded_parameter_arguments = node_action._Node__expanded_parameter_arguments
216+
expanded_parameter_arguments = node_action._Node__node_desc.expanded_parameter_arguments
217217
assert len(expanded_parameter_arguments) == 1
218218
file_path, is_file = expanded_parameter_arguments[0]
219219
assert is_file

test_launch_ros/test/test_launch_ros/actions/test_push_ros_namespace.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414

1515
"""Tests for the PushRosNamespace Action."""
1616

17+
from _collections import defaultdict
18+
1719
from launch_ros.actions import Node
1820
from launch_ros.actions import PushRosNamespace
1921
from launch_ros.actions.load_composable_nodes import get_composable_node_load_request
2022
from launch_ros.descriptions import ComposableNode
23+
from launch_ros.descriptions import Node as NodeDescription
2124

2225
import pytest
2326

@@ -26,6 +29,14 @@ class MockContext:
2629

2730
def __init__(self):
2831
self.launch_configurations = {}
32+
self.locals = lambda: None
33+
self.locals.unique_ros_node_names = defaultdict(int)
34+
35+
def extend_globals(self, val):
36+
pass
37+
38+
def extend_locals(self, val):
39+
pass
2940

3041
def perform_substitution(self, sub):
3142
return sub.perform(None)
@@ -118,10 +129,11 @@ def test_push_ros_namespace(config):
118129
)
119130
node._perform_substitutions(lc)
120131
expected_ns = (
121-
config.expected_ns if config.expected_ns is not None else Node.UNSPECIFIED_NODE_NAMESPACE
132+
config.expected_ns if config.expected_ns is not None else
133+
NodeDescription.UNSPECIFIED_NODE_NAMESPACE
122134
)
123135
expected_name = (
124-
config.node_name if config.node_name is not None else Node.UNSPECIFIED_NODE_NAME
136+
config.node_name if config.node_name is not None else NodeDescription.UNSPECIFIED_NODE_NAME
125137
)
126138
expected_fqn = expected_ns.rstrip('/') + '/' + expected_name
127139
assert expected_ns == node.expanded_node_namespace

test_launch_ros/test/test_launch_ros/actions/test_set_parameter.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
"""Tests for the SetParameter Action."""
1616

17+
from _collections import defaultdict
18+
1719
from launch import LaunchContext
1820
from launch.actions import PopLaunchConfigurations
1921
from launch.actions import PushLaunchConfigurations
@@ -33,6 +35,14 @@ class MockContext:
3335

3436
def __init__(self):
3537
self.launch_configurations = {}
38+
self.locals = lambda: None
39+
self.locals.unique_ros_node_names = defaultdict(int)
40+
41+
def extend_globals(self, val):
42+
pass
43+
44+
def extend_locals(self, val):
45+
pass
3646

3747
def perform_substitution(self, sub):
3848
return sub.perform(None)
@@ -112,7 +122,7 @@ def test_set_param_with_node():
112122
set_param = SetParameter(name='my_param', value='my_value')
113123
set_param.execute(lc)
114124
node._perform_substitutions(lc)
115-
expanded_parameter_arguments = node._Node__expanded_parameter_arguments
125+
expanded_parameter_arguments = node._Node__node_desc.expanded_parameter_arguments
116126
assert len(expanded_parameter_arguments) == 2
117127
param_file_path, is_file = expanded_parameter_arguments[0]
118128
assert is_file

0 commit comments

Comments
 (0)