Skip to content

Commit 9d0a311

Browse files
Allow providing launch args to include using let in frontends (#848)
Signed-off-by: Christophe Bedard <[email protected]>
1 parent 1abf55e commit 9d0a311

File tree

4 files changed

+151
-6
lines changed

4 files changed

+151
-6
lines changed

launch/launch/actions/include_launch_description.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,58 @@ class IncludeLaunchDescription(Action):
6868
Conditionally included launch arguments that do not have a default value
6969
will eventually raise an error if this best effort argument checking is
7070
unable to see an unsatisfied argument ahead of time.
71+
72+
For example, to include ``my_pkg``'s ``other_launch.py`` and set launch arguments for it:
73+
74+
.. code-block:: python
75+
76+
def generate_launch_description():
77+
return ([
78+
SetLaunchConfiguration('arg1', 'value1'),
79+
IncludeLaunchDescription(
80+
AnyLaunchDescriptionSource([
81+
PathJoinSubstitution([
82+
FindPackageShare('my_pkg'),
83+
'launch',
84+
'other_launch.py',
85+
]),
86+
]),
87+
launch_arguments={
88+
'other_arg1': LaunchConfiguration('arg1'),
89+
'other_arg2': 'value2',
90+
}.items(),
91+
),
92+
])
93+
94+
.. code-block:: xml
95+
96+
<launch>
97+
<let name="arg1" value="value1" />
98+
<include file="$(find-pkg-share my_pkg)/launch/other_launch.py">
99+
<let name="other_arg1" value="$(var arg1)" />
100+
<let name="other_arg2" value="value2" />
101+
</include>
102+
</launch>
103+
104+
.. code-block:: yaml
105+
106+
launch:
107+
- let:
108+
name: 'arg1'
109+
value: 'value1'
110+
- include:
111+
file: '$(find-pkg-share my_pkg)/launch/other_launch.py'
112+
let:
113+
- name: 'other_arg1'
114+
value: '$(var arg1)'
115+
- name: 'other_arg2'
116+
value: 'value2'
117+
118+
.. note::
119+
120+
While frontends currently support both ``let`` and ``arg`` for launch arguments, they are
121+
both converted into ``SetLaunchConfiguration`` actions (``let``). The same launch argument
122+
should not be defined using both ``let`` and ``arg``.
71123
"""
72124

73125
def __init__(
@@ -94,8 +146,14 @@ def parse(cls, entity: Entity, parser: Parser
94146
_, kwargs = super().parse(entity, parser)
95147
file_path = parser.parse_substitution(entity.get_attr('file'))
96148
kwargs['launch_description_source'] = file_path
97-
args = entity.get_attr('arg', data_type=List[Entity], optional=True)
98-
if args is not None:
149+
args = []
150+
args_arg = entity.get_attr('arg', data_type=List[Entity], optional=True)
151+
if args_arg is not None:
152+
args.extend(args_arg)
153+
args_let = entity.get_attr('let', data_type=List[Entity], optional=True)
154+
if args_let is not None:
155+
args.extend(args_let)
156+
if args:
99157
kwargs['launch_arguments'] = [
100158
(
101159
parser.parse_substitution(e.get_attr('name')),

launch_xml/test/launch_xml/test_include.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,32 @@
2626

2727

2828
def test_include():
29-
"""Parse node xml example."""
30-
# Always use posix style paths in launch XML files.
29+
"""Parse include XML example."""
3130
path = (Path(__file__).parent / 'executable.xml').as_posix()
3231
xml_file = \
3332
"""\
3433
<launch>
35-
<include file="{}"/>
34+
<let name="main_baz" value="BAZ" />
35+
<include file="{}">
36+
<arg name="foo" value="FOO" />
37+
<arg name="baz" value="overwritten" />
38+
<let name="bar" value="BAR" />
39+
<let name="baz" value="$(var main_baz)" />
40+
</include>
3641
</launch>
3742
""".format(path) # noqa: E501
3843
xml_file = textwrap.dedent(xml_file)
3944
root_entity, parser = load_no_extensions(io.StringIO(xml_file))
4045
ld = parser.parse_description(root_entity)
41-
include = ld.entities[0]
46+
include = ld.entities[1]
4247
assert isinstance(include, IncludeLaunchDescription)
4348
assert isinstance(include.launch_description_source, AnyLaunchDescriptionSource)
4449
ls = LaunchService(debug=True)
4550
ls.include_launch_description(ld)
4651
assert 0 == ls.run()
52+
assert ls.context.launch_configurations['foo'] == 'FOO'
53+
assert ls.context.launch_configurations['bar'] == 'BAR'
54+
assert ls.context.launch_configurations['baz'] == 'BAZ'
4755

4856

4957
if __name__ == '__main__':
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
launch:
2+
- executable:
3+
cmd: 'ls -l -a -s'
4+
cwd: '/'
5+
name: 'my_ls'
6+
shell: true
7+
output: 'log'
8+
emulate_tty: true
9+
sigkill_timeout: 4.0
10+
sigterm_timeout: 7.0
11+
launch-prefix: "$(env LAUNCH_PREFIX '')"
12+
env:
13+
- name: 'var'
14+
value: '1'
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright 2025 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test parsing an include action."""
16+
17+
import io
18+
from pathlib import Path
19+
import textwrap
20+
21+
from launch import LaunchService
22+
from launch.actions import IncludeLaunchDescription
23+
from launch.launch_description_sources import AnyLaunchDescriptionSource
24+
25+
from parser_no_extensions import load_no_extensions
26+
27+
28+
def test_include():
29+
"""Parse include YAML example."""
30+
path = (Path(__file__).parent / 'executable.yaml').as_posix()
31+
yaml_file = \
32+
"""\
33+
launch:
34+
- let:
35+
name: 'main_baz'
36+
value: 'BAZ'
37+
- include:
38+
file: '{}'
39+
arg:
40+
- name: 'foo'
41+
value: 'FOO'
42+
- name: 'baz'
43+
value: 'overwritten'
44+
let:
45+
- name: 'bar'
46+
value: 'BAR'
47+
- name: 'baz'
48+
value: '$(var main_baz)'
49+
""".format(path) # noqa: E501
50+
yaml_file = textwrap.dedent(yaml_file)
51+
root_entity, parser = load_no_extensions(io.StringIO(yaml_file))
52+
ld = parser.parse_description(root_entity)
53+
include = ld.entities[1]
54+
assert isinstance(include, IncludeLaunchDescription)
55+
assert isinstance(include.launch_description_source, AnyLaunchDescriptionSource)
56+
ls = LaunchService(debug=True)
57+
ls.include_launch_description(ld)
58+
assert 0 == ls.run()
59+
assert ls.context.launch_configurations['foo'] == 'FOO'
60+
assert ls.context.launch_configurations['bar'] == 'BAR'
61+
assert ls.context.launch_configurations['baz'] == 'BAZ'
62+
63+
64+
if __name__ == '__main__':
65+
test_include()

0 commit comments

Comments
 (0)