Skip to content

Commit ec481fb

Browse files
djchoppwjwwoodhidmic
authored
Feature clear launch configs (#515)
* Initial commit for clear_launch_configuration Signed-off-by: djchopp <[email protected]> * Cleanup and implement forwarded configurations Signed-off-by: djchopp <[email protected]> * Implemented config isolation for group actions Signed-off-by: djchopp <[email protected]> * style: flake adherence Signed-off-by: djchopp <[email protected]> * doc: fix init docstring Signed-off-by: djchopp <[email protected]> * Add: new implementation, uses string key names instead of LaunchConfiguration objects Signed-off-by: djchopp <[email protected]> * Add: new implementation, mathes new ClearLaunchConfiguration implementation Signed-off-by: djchopp <[email protected]> * fix: scoped being overwritten by forwarded attribute Signed-off-by: djchopp <[email protected]> * fix: default value of launch_configurations_to_not_be_cleared to None Signed-off-by: djchopp <[email protected]> * add: Tests for new features Signed-off-by: djchopp <[email protected]> * drop: ClearLaunchConfigurations Signed-off-by: djchopp <[email protected]> * add: ResetLaunchConfigurations to replace ClearLaunchConfigurations Signed-off-by: djchopp <[email protected]> * fix: update GroupAction to use new ResetLaunchConfigurations Signed-off-by: djchopp <[email protected]> * doc: update documentation to match ResetLaunchConfigurations usage Signed-off-by: djchopp <[email protected]> * Update launch/launch/actions/group_action.py doc: fix docblock for newline style. Co-authored-by: William Woodall <[email protected]> Signed-off-by: djchopp <[email protected]> * Update launch/launch/actions/reset_launch_configurations.py doc: fix punctuation Co-authored-by: William Woodall <[email protected]> Signed-off-by: djchopp <[email protected]> * doc: update docblock to match style guide Signed-off-by: djchopp <[email protected]> * refactor: expand variable name abbreviations Signed-off-by: djchopp <[email protected]> * doc: fix whitespace Signed-off-by: djchopp <[email protected]> * doc: better wording Co-authored-by: Michel Hidalgo <[email protected]> Signed-off-by: djchopp <[email protected]> * doc: better wording Co-authored-by: Michel Hidalgo <[email protected]> Signed-off-by: djchopp <[email protected]> * refactor: use python dictionary update method Signed-off-by: djchopp <[email protected]> * add: verbose key and value checking Signed-off-by: djchopp <[email protected]> * refactor: corrected import order Signed-off-by: djchopp <[email protected]> * add: parser supporting configuration reset Signed-off-by: djchopp <[email protected]> * fix: parse arg names as tuples (hashable) for dict key Signed-off-by: djchopp <[email protected]> * refactor: 'arg' to 'keep' tag Signed-off-by: djchopp <[email protected]> * refactor: match dict creation method of other actions Signed-off-by: djchopp <[email protected]> * add: xml, yaml tests for reset launch configuration functionality Signed-off-by: djchopp <[email protected]> * fix: use substitution instead of yaml node achor for tests Signed-off-by: djchopp <[email protected]> Co-authored-by: William Woodall <[email protected]> Co-authored-by: Michel Hidalgo <[email protected]>
1 parent 6fd719c commit ec481fb

File tree

9 files changed

+553
-30
lines changed

9 files changed

+553
-30
lines changed

launch/launch/actions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .pop_launch_configurations import PopLaunchConfigurations
2626
from .push_launch_configurations import PushLaunchConfigurations
2727
from .register_event_handler import RegisterEventHandler
28+
from .reset_launch_configurations import ResetLaunchConfigurations
2829
from .set_environment_variable import SetEnvironmentVariable
2930
from .set_launch_configuration import SetLaunchConfiguration
3031
from .shutdown_action import Shutdown
@@ -44,6 +45,7 @@
4445
'OpaqueFunction',
4546
'PopLaunchConfigurations',
4647
'PushLaunchConfigurations',
48+
'ResetLaunchConfigurations',
4749
'RegisterEventHandler',
4850
'SetEnvironmentVariable',
4951
'SetLaunchConfiguration',

launch/launch/actions/group_action.py

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from .pop_launch_configurations import PopLaunchConfigurations
2323
from .push_launch_configurations import PushLaunchConfigurations
24+
from .reset_launch_configurations import ResetLaunchConfigurations
2425
from .set_launch_configuration import SetLaunchConfiguration
2526
from ..action import Action
2627
from ..frontend import Entity
@@ -34,26 +35,47 @@
3435
@expose_action('group')
3536
class GroupAction(Action):
3637
"""
37-
Action that yields other actions, optionally scoping launch configurations.
38+
Action that yields other actions, optionally scoping and forwarding launch configurations.
3839
3940
This action is used to nest other actions without including a separate
4041
launch description, while also optionally having a condition (like all
41-
other actions), scoping launch configurations, and/or declaring launch
42-
configurations for just the group and its yielded actions.
42+
other actions), scoping launch configurations, forwarding launch
43+
configurations, and/or declaring launch configurations for just the
44+
group and its yielded actions.
45+
46+
When scoped=True, changes to launch configurations are limited to the
47+
scope of actions in the group action.
48+
49+
When scoped=True and forwarding=True, all existing launch configurations
50+
are available in the scoped context.
51+
52+
When scoped=True and forwarding=False, all existing launch configurations
53+
are removed from the scoped context.
54+
55+
Any launch configuration defined in the launch_configurations dictionary
56+
will be set in the current context.
57+
When scoped=False these configurations will persist even after the
58+
GroupAction has completed.
59+
When scoped=True these configurations will only be available to actions in
60+
the GroupAction.
61+
When scoped=True and forwarding=False, the launch_configurations dictionary
62+
is evaluated before clearing, and then re-set in the cleared scoped context.
4363
"""
4464

4565
def __init__(
4666
self,
4767
actions: Iterable[Action],
4868
*,
4969
scoped: bool = True,
70+
forwarding: bool = True,
5071
launch_configurations: Optional[Dict[SomeSubstitutionsType, SomeSubstitutionsType]] = None,
5172
**left_over_kwargs
5273
) -> None:
5374
"""Create a GroupAction."""
5475
super().__init__(**left_over_kwargs)
5576
self.__actions = actions
5677
self.__scoped = scoped
78+
self.__forwarding = forwarding
5779
if launch_configurations is not None:
5880
self.__launch_configurations = launch_configurations
5981
else:
@@ -65,24 +87,49 @@ def parse(cls, entity: Entity, parser: Parser):
6587
"""Return `GroupAction` action and kwargs for constructing it."""
6688
_, kwargs = super().parse(entity, parser)
6789
scoped = entity.get_attr('scoped', data_type=bool, optional=True)
90+
forwarding = entity.get_attr('forwarding', data_type=bool, optional=True)
91+
keeps = entity.get_attr('keep', data_type=List[Entity], optional=True)
6892
if scoped is not None:
6993
kwargs['scoped'] = scoped
70-
kwargs['actions'] = [parser.parse_action(e) for e in entity.children]
94+
if forwarding is not None:
95+
kwargs['forwarding'] = forwarding
96+
if keeps is not None:
97+
kwargs['launch_configurations'] = {
98+
tuple(parser.parse_substitution(e.get_attr('name'))):
99+
parser.parse_substitution(e.get_attr('value')) for e in keeps
100+
}
101+
for e in keeps:
102+
e.assert_entity_completely_parsed()
103+
kwargs['actions'] = [parser.parse_action(e) for e in entity.children
104+
if e.type_name != 'keep']
71105
return cls, kwargs
72106

73107
def get_sub_entities(self) -> List[LaunchDescriptionEntity]:
74108
"""Return subentities."""
75109
if self.__actions_to_return is None:
76-
self.__actions_to_return = [] # type: List[Action]
77-
self.__actions_to_return += [
110+
self.__actions_to_return = list(self.__actions)
111+
configuration_sets = [
78112
SetLaunchConfiguration(k, v) for k, v in self.__launch_configurations.items()
79113
]
80-
self.__actions_to_return += list(self.__actions)
81114
if self.__scoped:
115+
if self.__forwarding:
116+
self.__actions_to_return = [
117+
PushLaunchConfigurations(),
118+
*configuration_sets,
119+
*self.__actions_to_return,
120+
PopLaunchConfigurations()
121+
]
122+
else:
123+
self.__actions_to_return = [
124+
PushLaunchConfigurations(),
125+
ResetLaunchConfigurations(self.__launch_configurations),
126+
*self.__actions_to_return,
127+
PopLaunchConfigurations()
128+
]
129+
else:
82130
self.__actions_to_return = [
83-
PushLaunchConfigurations(),
84-
*self.__actions_to_return,
85-
PopLaunchConfigurations()
131+
*configuration_sets,
132+
*self.__actions_to_return
86133
]
87134
return self.__actions_to_return
88135

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright 2021 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 ResetLaunchConfigurations action."""
16+
17+
from typing import Dict
18+
from typing import List
19+
from typing import Optional
20+
21+
from ..action import Action
22+
from ..frontend import Entity
23+
from ..frontend import expose_action
24+
from ..frontend import Parser
25+
from ..launch_context import LaunchContext
26+
from ..some_substitutions_type import SomeSubstitutionsType
27+
from ..utilities import normalize_to_list_of_substitutions
28+
from ..utilities import perform_substitutions
29+
30+
31+
@expose_action('reset')
32+
class ResetLaunchConfigurations(Action):
33+
"""
34+
Action that resets launch configurations in the current context.
35+
36+
This action can be used to clear the launch configurations from the
37+
context it was called in.
38+
It optionally can be given a dictionary with launch configurations
39+
to be set after clearing.
40+
Launch configurations given in the dictionary are evaluated before
41+
the context launch configurations are cleared.
42+
This allows launch configurations to be passed through the clearing
43+
of the context.
44+
45+
If launch_configurations is None or an empty dict then all launch configurations
46+
will be cleared.
47+
48+
If launch_configurations has entries (i.e. {'foo': 'FOO'}) then these will be
49+
set after the clearing operation.
50+
"""
51+
52+
def __init__(
53+
self,
54+
launch_configurations: Optional[Dict[SomeSubstitutionsType, SomeSubstitutionsType]] = None,
55+
**kwargs
56+
) -> None:
57+
"""Create an ResetLaunchConfigurations action."""
58+
super().__init__(**kwargs)
59+
self.__launch_configurations = launch_configurations
60+
61+
@classmethod
62+
def parse(cls, entity: Entity, parser: Parser):
63+
"""Return `ResetLaunchConfigurations` action and kwargs for constructing it."""
64+
_, kwargs = super().parse(entity, parser)
65+
keeps = entity.get_attr('keep', data_type=List[Entity], optional=True)
66+
if keeps is not None:
67+
kwargs['launch_configurations'] = {
68+
tuple(parser.parse_substitution(e.get_attr('name'))):
69+
parser.parse_substitution(e.get_attr('value')) for e in keeps
70+
}
71+
for e in keeps:
72+
e.assert_entity_completely_parsed()
73+
return cls, kwargs
74+
75+
def execute(self, context: LaunchContext):
76+
"""Execute the action."""
77+
if self.__launch_configurations is None:
78+
context.launch_configurations.clear()
79+
else:
80+
evaluated_configurations = {}
81+
for k, v in self.__launch_configurations.items():
82+
evaluated_k = perform_substitutions(context, normalize_to_list_of_substitutions(k))
83+
evaluated_v = perform_substitutions(context, normalize_to_list_of_substitutions(v))
84+
evaluated_configurations[evaluated_k] = evaluated_v
85+
86+
context.launch_configurations.clear()
87+
context.launch_configurations.update(evaluated_configurations)

launch/test/launch/actions/test_group_action.py

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@
1919
from launch.actions import GroupAction
2020
from launch.actions import PopLaunchConfigurations
2121
from launch.actions import PushLaunchConfigurations
22+
from launch.actions import ResetLaunchConfigurations
2223
from launch.actions import SetLaunchConfiguration
24+
from launch.substitutions import LaunchConfiguration
2325

2426

2527
def test_group_action_constructors():
2628
"""Test the constructors for the GroupAction class."""
2729
GroupAction([])
2830
GroupAction([Action()])
2931
GroupAction([Action()], scoped=False)
30-
GroupAction([Action()], scoped=False, launch_configurations={'foo': 'FOO'})
32+
GroupAction([Action()], scoped=False, forwarding=False)
33+
GroupAction([Action()], scoped=False, forwarding=False, launch_configurations={'foo': 'FOO'})
3134

3235

3336
def test_group_action_execute():
@@ -39,16 +42,26 @@ def test_group_action_execute():
3942
assert len(lc1.launch_configurations) == 0
4043

4144
assert len(lc1.launch_configurations) == 0
42-
result = GroupAction([]).visit(lc1)
43-
assert len(result) == 2 # push and pop actions, due to scope=True
45+
result = GroupAction([], forwarding=True).visit(lc1)
46+
assert len(result) == 2 # push and pop actions, due to scope=True, forwarded=True
4447
assert isinstance(result[0], PushLaunchConfigurations)
4548
assert isinstance(result[1], PopLaunchConfigurations)
4649
for a in result:
4750
a.visit(lc1)
4851
assert len(lc1.launch_configurations) == 0
4952

5053
assert len(lc1.launch_configurations) == 0
51-
result = GroupAction([], launch_configurations={'foo': 'FOO'}).visit(lc1)
54+
result = GroupAction([], forwarding=False).visit(lc1)
55+
assert len(result) == 3 # push, reset, pop actions, due to scope=True, forwarded=False
56+
assert isinstance(result[0], PushLaunchConfigurations)
57+
assert isinstance(result[1], ResetLaunchConfigurations)
58+
assert isinstance(result[2], PopLaunchConfigurations)
59+
for a in result:
60+
a.visit(lc1)
61+
assert len(lc1.launch_configurations) == 0
62+
63+
assert len(lc1.launch_configurations) == 0
64+
result = GroupAction([], forwarding=True, launch_configurations={'foo': 'FOO'}).visit(lc1)
5265
assert len(result) == 3 # push, set 1 launch_configurations, and pop actions
5366
assert isinstance(result[0], PushLaunchConfigurations)
5467
assert isinstance(result[1], SetLaunchConfiguration)
@@ -58,7 +71,8 @@ def test_group_action_execute():
5871
assert len(lc1.launch_configurations) == 0
5972

6073
assert len(lc1.launch_configurations) == 0
61-
result = GroupAction([], launch_configurations={'foo': 'FOO', 'bar': 'BAR'}).visit(lc1)
74+
result = GroupAction([], forwarding=True,
75+
launch_configurations={'foo': 'FOO', 'bar': 'BAR'}).visit(lc1)
6276
assert len(result) == 4 # push, set 2 launch_configurations, and pop actions
6377
assert isinstance(result[0], PushLaunchConfigurations)
6478
assert isinstance(result[1], SetLaunchConfiguration)
@@ -68,6 +82,17 @@ def test_group_action_execute():
6882
a.visit(lc1)
6983
assert len(lc1.launch_configurations) == 0
7084

85+
assert len(lc1.launch_configurations) == 0
86+
result = GroupAction([], forwarding=False,
87+
launch_configurations={'foo': 'FOO', 'bar': 'BAR'}).visit(lc1)
88+
assert len(result) == 3 # push, reset (which will set launch_configurations), and pop actions
89+
assert isinstance(result[0], PushLaunchConfigurations)
90+
assert isinstance(result[1], ResetLaunchConfigurations)
91+
assert isinstance(result[2], PopLaunchConfigurations)
92+
for a in result:
93+
a.visit(lc1)
94+
assert len(lc1.launch_configurations) == 0
95+
7196
assert len(lc1.launch_configurations) == 0
7297
PushLaunchConfigurations().visit(lc1)
7398
result = GroupAction([], scoped=False, launch_configurations={'foo': 'FOO'}).visit(lc1)
@@ -92,7 +117,8 @@ def test_group_action_execute():
92117
assert len(lc1.launch_configurations) == 0
93118

94119
assert len(lc1.launch_configurations) == 0
95-
result = GroupAction([Action()], launch_configurations={'foo': 'FOO'}).visit(lc1)
120+
result = GroupAction([Action()], forwarding=True,
121+
launch_configurations={'foo': 'FOO'}).visit(lc1)
96122
assert len(result) == 4 # push, set 1 launch_configurations, the 1 action, and pop actions
97123
assert isinstance(result[0], PushLaunchConfigurations)
98124
assert isinstance(result[1], SetLaunchConfiguration)
@@ -101,3 +127,65 @@ def test_group_action_execute():
101127
for a in result:
102128
a.visit(lc1)
103129
assert len(lc1.launch_configurations) == 0
130+
131+
assert len(lc1.launch_configurations) == 0
132+
lc1.launch_configurations['foo'] = 'FOO'
133+
lc1.launch_configurations['bar'] = 'BAR'
134+
result = GroupAction([Action()], forwarding=False,
135+
launch_configurations={'bar': LaunchConfiguration('bar'),
136+
'baz': 'BAZ'}).visit(lc1)
137+
# push, reset (which will set launch_configurations), 1 action, and pop actions
138+
assert len(result) == 4
139+
assert isinstance(result[0], PushLaunchConfigurations)
140+
assert isinstance(result[1], ResetLaunchConfigurations)
141+
assert isinstance(result[2], Action)
142+
assert isinstance(result[3], PopLaunchConfigurations)
143+
result[0].visit(lc1) # Push
144+
assert 'foo' in lc1.launch_configurations.keys() # Copied to new scope, before Reset
145+
assert lc1.launch_configurations['foo'] == 'FOO'
146+
assert 'bar' in lc1.launch_configurations.keys() # Copied to new scope, before Reset
147+
assert lc1.launch_configurations['bar'] == 'BAR'
148+
result[1].visit(lc1) # Reset
149+
assert 'foo' not in lc1.launch_configurations.keys() # Cleared from scope in Reset
150+
assert 'bar' in lc1.launch_configurations.keys() # Evaluated and forwarded in Reset
151+
assert lc1.launch_configurations['bar'] == 'BAR'
152+
assert 'baz' in lc1.launch_configurations.keys() # Evaluated and added in Reset
153+
assert lc1.launch_configurations['baz'] == 'BAZ'
154+
result[2].visit(lc1) # Action
155+
result[3].visit(lc1) # Pop
156+
assert 'foo' in lc1.launch_configurations.keys() # Still in original scope
157+
assert lc1.launch_configurations['foo'] == 'FOO'
158+
assert 'bar' in lc1.launch_configurations.keys() # Still in original scope
159+
assert lc1.launch_configurations['bar'] == 'BAR'
160+
assert 'baz' not in lc1.launch_configurations.keys() # Out of scope from pop, no longer exists
161+
assert len(lc1.launch_configurations) == 2
162+
lc1.launch_configurations.clear()
163+
164+
assert len(lc1.launch_configurations) == 0
165+
lc1.launch_configurations['foo'] = 'FOO'
166+
lc1.launch_configurations['bar'] = 'BAR'
167+
result = GroupAction([Action()], forwarding=True,
168+
launch_configurations={'foo': 'OOF'}).visit(lc1)
169+
# push, 1 set (overwrite), 1 action, and pop actions
170+
assert len(result) == 4
171+
assert isinstance(result[0], PushLaunchConfigurations)
172+
assert isinstance(result[1], SetLaunchConfiguration)
173+
assert isinstance(result[2], Action)
174+
assert isinstance(result[3], PopLaunchConfigurations)
175+
result[0].visit(lc1) # Push
176+
assert 'foo' in lc1.launch_configurations.keys() # Copied to new scope, before Set
177+
assert lc1.launch_configurations['foo'] == 'FOO'
178+
assert 'bar' in lc1.launch_configurations.keys() # Copied to new scope
179+
assert lc1.launch_configurations['bar'] == 'BAR'
180+
result[1].visit(lc1) # Set
181+
assert 'foo' in lc1.launch_configurations.keys() # Overwritten in Set
182+
assert lc1.launch_configurations['foo'] == 'OOF'
183+
assert 'bar' in lc1.launch_configurations.keys() # Untouched
184+
assert lc1.launch_configurations['bar'] == 'BAR'
185+
result[2].visit(lc1) # Action
186+
result[3].visit(lc1) # Pop
187+
assert 'foo' in lc1.launch_configurations.keys() # Still in original scope with original value
188+
assert lc1.launch_configurations['foo'] == 'FOO'
189+
assert 'bar' in lc1.launch_configurations.keys() # Still in original scope with original value
190+
assert lc1.launch_configurations['bar'] == 'BAR'
191+
lc1.launch_configurations.clear()

0 commit comments

Comments
 (0)