Skip to content

Commit ff8bb93

Browse files
Scope launch file dir/path locals to included launch file
Signed-off-by: Christophe Bedard <bedard.christophe@gmail.com>
1 parent 628d61e commit ff8bb93

File tree

2 files changed

+48
-15
lines changed

2 files changed

+48
-15
lines changed

launch/launch/actions/include_launch_description.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import launch.logging
2626

27+
from .opaque_function import OpaqueFunction
2728
from .set_launch_configuration import SetLaunchConfiguration
2829
from ..action import Action
2930
from ..frontend import Entity
@@ -147,13 +148,7 @@ def _try_get_arguments_names_without_context(self):
147148
def execute(self, context: LaunchContext) -> List[LaunchDescriptionEntity]:
148149
"""Execute the action."""
149150
launch_description = self.__launch_description_source.get_launch_description(context)
150-
# If the location does not exist, then it's likely set to '<script>' or something.
151-
context.extend_locals({
152-
'current_launch_file_path': self._get_launch_file(),
153-
})
154-
context.extend_locals({
155-
'current_launch_file_directory': self._get_launch_file_directory(),
156-
})
151+
self._set_launch_file_location_locals(context)
157152

158153
# Do best effort checking to see if non-optional, non-default declared arguments
159154
# are being satisfied.
@@ -188,7 +183,44 @@ def execute(self, context: LaunchContext) -> List[LaunchDescriptionEntity]:
188183
set_launch_configuration_actions.append(SetLaunchConfiguration(name, value))
189184

190185
# Set launch arguments as launch configurations and then include the launch description.
191-
return [*set_launch_configuration_actions, launch_description]
186+
return [
187+
*set_launch_configuration_actions,
188+
launch_description,
189+
OpaqueFunction(function=self._restore_launch_file_location_locals),
190+
]
191+
192+
def _set_launch_file_location_locals(self, context: LaunchContext) -> None:
193+
context._push_locals()
194+
# Keep the previous launch file path/dir locals so that we can restore them after
195+
self.__previous_launch_file_path = context.get_locals_as_dict().get('current_launch_file_path' , None)
196+
self.__previous_launch_file_directory = context.get_locals_as_dict().get('current_launch_file_directory', None)
197+
context.extend_locals({
198+
'current_launch_file_path': self._get_launch_file(),
199+
})
200+
context.extend_locals({
201+
'current_launch_file_directory': self._get_launch_file_directory(),
202+
})
203+
204+
def _restore_launch_file_location_locals(self, context: LaunchContext) -> None:
205+
# We want to keep the state of the context locals even after the include, since included
206+
# launch descriptions are meant to act as if they were included literally in the parent
207+
# launch description.
208+
# However, we want to restore the launch file path/dir locals to their previous state, and
209+
# we may have to just delete them if we're now going back to a launch script (i.e., not a
210+
# launch file). However, there is no easy way to delete context locals, so save current
211+
# locals, reset to the state before the include previous state and then re-apply locals,
212+
# potentially minus the launch file path/dir locals.
213+
locals = context.get_locals_as_dict()
214+
if self.__previous_launch_file_path is None:
215+
del locals['current_launch_file_path']
216+
else:
217+
locals['current_launch_file_path'] = self.__previous_launch_file_path
218+
if self.__previous_launch_file_directory is None:
219+
del locals['current_launch_file_directory']
220+
else:
221+
locals['current_launch_file_directory'] = self.__previous_launch_file_directory
222+
context._pop_locals()
223+
context.extend_locals(locals)
192224

193225
def __repr__(self) -> Text:
194226
"""Return a description of this IncludeLaunchDescription as a string."""

launch/test/launch/actions/test_include_launch_description.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_include_launch_description_methods():
5050
assert isinstance(action.describe_sub_entities(), list)
5151
assert isinstance(action.describe_conditional_sub_entities(), list)
5252
# Result should only contain the launch description as there are no launch arguments.
53-
assert action.visit(LaunchContext()) == [ld]
53+
assert action.visit(LaunchContext())[0] == ld
5454
assert action.get_asyncio_future() is None
5555
assert len(action.launch_arguments) == 0
5656

@@ -60,7 +60,7 @@ def test_include_launch_description_methods():
6060
assert isinstance(action2.describe_sub_entities(), list)
6161
assert isinstance(action2.describe_conditional_sub_entities(), list)
6262
# Result should only contain the launch description as there are no launch arguments.
63-
assert action2.visit(LaunchContext()) == [ld2]
63+
assert action2.visit(LaunchContext())[0] == ld2
6464
assert action2.get_asyncio_future() is None
6565
assert len(action2.launch_arguments) == 0
6666

@@ -74,7 +74,7 @@ def test_include_launch_description_launch_file_location():
7474
assert isinstance(action.describe_conditional_sub_entities(), list)
7575
lc1 = LaunchContext()
7676
# Result should only contain the launch description as there are no launch arguments.
77-
assert action.visit(lc1) == [ld]
77+
assert action.visit(lc1)[0] == ld
7878
assert lc1.locals.current_launch_file_directory == '<script>'
7979
assert action.get_asyncio_future() is None
8080

@@ -86,7 +86,7 @@ def test_include_launch_description_launch_file_location():
8686
assert isinstance(action2.describe_conditional_sub_entities(), list)
8787
lc2 = LaunchContext()
8888
# Result should only contain the launch description as there are no launch arguments.
89-
assert action2.visit(lc2) == [ld2]
89+
assert action2.visit(lc2)[0] == ld2
9090
assert lc2.locals.current_launch_file_directory == os.path.dirname(this_file)
9191
assert action2.get_asyncio_future() is None
9292

@@ -149,7 +149,7 @@ def test_include_launch_description_launch_arguments():
149149
assert len(action1.launch_arguments) == 1
150150
lc1 = LaunchContext()
151151
result1 = action1.visit(lc1)
152-
assert len(result1) == 2
152+
assert len(result1) == 3
153153
assert isinstance(result1[0], SetLaunchConfiguration)
154154
assert perform_substitutions(lc1, result1[0].name) == 'foo'
155155
assert perform_substitutions(lc1, result1[0].value) == 'FOO'
@@ -278,8 +278,9 @@ def test_include_python():
278278
assert 'IncludeLaunchDescription' in action.describe()
279279
assert isinstance(action.describe_sub_entities(), list)
280280
assert isinstance(action.describe_conditional_sub_entities(), list)
281-
# Result should only contain a single launch description as there are no launch arguments.
282-
assert len(action.visit(LaunchContext())) == 1
281+
# Result should only contain a single launch description (+ internal action) as there are
282+
# no launch arguments.
283+
assert len(action.visit(LaunchContext())) == 2
283284
assert action.get_asyncio_future() is None
284285
assert len(action.launch_arguments) == 0
285286

0 commit comments

Comments
 (0)