Skip to content

Commit ead8657

Browse files
committed
Initial implementation of ScopedIncludeLaunchDescription
Signed-off-by: Jasper van Brakel <[email protected]>
1 parent 3694dc2 commit ead8657

File tree

3 files changed

+171
-0
lines changed

3 files changed

+171
-0
lines changed

launch/launch/actions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .for_loop import ForLoop
2424
from .group_action import GroupAction
2525
from .include_launch_description import IncludeLaunchDescription
26+
from .include_scoped_launch_description import ScopedIncludeLaunchDescription
2627
from .log_info import LogInfo
2728
from .opaque_coroutine import OpaqueCoroutine
2829
from .opaque_function import OpaqueFunction
@@ -51,6 +52,7 @@
5152
'ForLoop',
5253
'GroupAction',
5354
'IncludeLaunchDescription',
55+
'ScopedIncludeLaunchDescription',
5456
'LogInfo',
5557
'OpaqueCoroutine',
5658
'OpaqueFunction',
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
"""Module for the ScopedIncludeLaunchDescription action."""
16+
17+
# from typing import override # Available starting from Python3.12
18+
from typing import List
19+
from typing import Text
20+
# from itertools import zip_longest
21+
22+
from .include_launch_description import IncludeLaunchDescription
23+
from .pop_environment import PopEnvironment
24+
from .pop_launch_configurations import PopLaunchConfigurations
25+
from .push_environment import PushEnvironment
26+
from .push_launch_configurations import PushLaunchConfigurations
27+
from .reset_environment import ResetEnvironment
28+
from .reset_launch_configurations import ResetLaunchConfigurations
29+
from .set_launch_configuration import SetLaunchConfiguration
30+
from ..frontend import expose_action
31+
from ..launch_context import LaunchContext
32+
from ..launch_description_entity import LaunchDescriptionEntity
33+
from ..utilities import normalize_to_list_of_substitutions
34+
from ..utilities import perform_substitutions
35+
36+
37+
@expose_action('scoped_include')
38+
class ScopedIncludeLaunchDescription(IncludeLaunchDescription):
39+
# TODO(SuperJappie08) Propper Documentation
40+
41+
# NOTE(SuperJappie08) __init__ is not required since the function signature will be the same
42+
# However maybe it is interresting for documentation purposes
43+
44+
def get_sub_entities(self):
45+
"""Get subentities."""
46+
ret = super().get_sub_entities()
47+
# TODO(SuperJappie08)? Do these internals need to be hidden?
48+
return [
49+
PushLaunchConfigurations(),
50+
PushEnvironment(),
51+
ResetEnvironment(),
52+
# NOTE(SuperJappie08) Need weird remap, since AnySubstitution type can be a List which
53+
# is not Hashable.
54+
ResetLaunchConfigurations({k[0]: v for k, v in self.launch_arguments})
55+
* ret,
56+
PopEnvironment(),
57+
PopLaunchConfigurations(),
58+
]
59+
60+
def execute(self, context: LaunchContext) -> List[LaunchDescriptionEntity]:
61+
"""Execute the action."""
62+
# NOTE(SuperJappie08) Originally this returend something based on the used actions
63+
# However after further consideration the context might not be correct that way.
64+
context._push_launch_configurations()
65+
context._push_environment()
66+
67+
context._reset_environment()
68+
69+
# Reset Launch Configuration
70+
evaluated_configurations = {}
71+
for k, v in self.launch_arguments:
72+
evaluated_k = perform_substitutions(context, normalize_to_list_of_substitutions(k))
73+
evaluated_v = perform_substitutions(context, normalize_to_list_of_substitutions(v))
74+
evaluated_configurations[evaluated_k] = evaluated_v
75+
76+
context.launch_configurations.clear()
77+
context.launch_configurations.update(evaluated_configurations)
78+
79+
launch_description = self.launch_description_source.get_launch_description(context)
80+
81+
# If the location does not exist, then it's likely set to '<script>' or something.
82+
context.extend_locals({
83+
'current_launch_file_path': self._get_launch_file(),
84+
})
85+
context.extend_locals({
86+
'current_launch_file_directory': self._get_launch_file_directory(),
87+
})
88+
89+
# Do best effort checking to see if non-optional, non-default declared arguments
90+
# are being satisfied.
91+
my_argument_names = [
92+
perform_substitutions(context, normalize_to_list_of_substitutions(arg_name))
93+
for arg_name, arg_value in self.launch_arguments
94+
]
95+
try:
96+
declared_launch_arguments = (
97+
launch_description.get_launch_arguments_with_include_launch_description_actions())
98+
except Exception as exc:
99+
if hasattr(exc, 'add_note'):
100+
exc.add_note(f'while executing {self.describe()}') # type: ignore
101+
raise
102+
for argument, ild_actions in declared_launch_arguments:
103+
if argument._conditionally_included or argument.default_value is not None:
104+
continue
105+
argument_names = my_argument_names
106+
if ild_actions is not None:
107+
for ild_action in ild_actions:
108+
argument_names.extend(ild_action._try_get_arguments_names_without_context())
109+
if argument.name not in argument_names:
110+
raise RuntimeError(
111+
"Included launch description missing required argument '{}' "
112+
"(description: '{}'), given: [{}]"
113+
.format(argument.name, argument.description, ', '.join(argument_names))
114+
)
115+
116+
# Create actions to set the launch arguments into the launch configurations.
117+
set_launch_configuration_actions = []
118+
for name, value in self.launch_arguments:
119+
set_launch_configuration_actions.append(SetLaunchConfiguration(name, value))
120+
121+
# Set launch arguments as launch configurations and then include the launch description.
122+
return [
123+
*set_launch_configuration_actions,
124+
launch_description,
125+
PopEnvironment(),
126+
PopLaunchConfigurations(),
127+
]
128+
129+
def __repr__(self) -> Text:
130+
"""Return a description of this ScopedIncludeLaunchDescription as a string."""
131+
return f'ScopedIncludeLaunchDescription({self.launch_description_source.location})'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
"""Tests for the IncludeScopedLaunchDescription action class."""
16+
17+
# from launch import LaunchContext
18+
from launch import LaunchDescription
19+
from launch import LaunchDescriptionSource
20+
# from launch import LaunchService
21+
from launch.actions import IncludeScopedLaunchDescription
22+
23+
# import pytest
24+
25+
26+
def test_include_launch_description_constructors():
27+
"""Test the constructors for IncludeLaunchDescription class."""
28+
IncludeScopedLaunchDescription(LaunchDescriptionSource(LaunchDescription()))
29+
IncludeScopedLaunchDescription(
30+
LaunchDescriptionSource(LaunchDescription()),
31+
launch_arguments={'foo': 'FOO'}.items())
32+
33+
34+
def test_correct_scoping():
35+
"""Test for verifying scoping behavior."""
36+
NotImplemented()
37+
38+
# TODO(SuperJappie08) Add tests to verify behavior

0 commit comments

Comments
 (0)