Skip to content

Commit 159ee9c

Browse files
nlamprianwjwwood
authored andcommitted
Add conditional substitution (#734)
* Add conditional substitution Closes: #727 Signed-off-by: Nick Lamprianidis <[email protected]>
1 parent 7dd7aa8 commit 159ee9c

File tree

5 files changed

+197
-2
lines changed

5 files changed

+197
-2
lines changed

launch/doc/source/architecture.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ There are many possible variations of a substitution, but here are some of the c
131131

132132
- This substitution gets a launch configuration value, as a string, by name.
133133

134+
- :class:`launch.substitutions.IfElseSubstitution`
135+
136+
- This substitution takes a substitution, and if it evaluates to true, then the result is the if_value, else the result is the else_value.
137+
134138
- :class:`launch.substitutions.LaunchDescriptionArgument`
135139

136140
- This substitution gets the value of a launch description argument, as a string, by name.

launch/launch/substitutions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .equals_substitution import EqualsSubstitution
2626
from .file_content import FileContent
2727
from .find_executable import FindExecutable
28+
from .if_else_substitution import IfElseSubstitution
2829
from .launch_configuration import LaunchConfiguration
2930
from .launch_log_dir import LaunchLogDir
3031
from .local_substitution import LocalSubstitution
@@ -46,6 +47,7 @@
4647
'EnvironmentVariable',
4748
'FileContent',
4849
'FindExecutable',
50+
'IfElseSubstitution',
4951
'LaunchConfiguration',
5052
'LaunchLogDir',
5153
'LocalSubstitution',
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Copyright 2023 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 IfElseSubstitution substitution."""
16+
17+
from typing import List
18+
from typing import Sequence
19+
from typing import Text
20+
21+
from .substitution_failure import SubstitutionFailure
22+
from ..frontend import expose_substitution
23+
from ..launch_context import LaunchContext
24+
from ..some_substitutions_type import SomeSubstitutionsType
25+
from ..substitution import Substitution
26+
from ..utilities import normalize_to_list_of_substitutions
27+
from ..utilities import perform_substitutions
28+
from ..utilities.type_utils import perform_typed_substitution
29+
30+
31+
@expose_substitution('if')
32+
class IfElseSubstitution(Substitution):
33+
"""
34+
Substitution that conditionally returns one of two substitutions.
35+
36+
Depending on whether the condition substitution evaluates to true, either it returns
37+
the if_value substitution or the else_value substitution.
38+
39+
Example with a boolean launch configuration:
40+
41+
.. doctest::
42+
43+
>>> from launch.substitutions import LaunchConfiguration
44+
>>> subst = IfElseSubstitution(
45+
... LaunchConfiguration("arg"),
46+
... if_value="arg_evaluated_to_true",
47+
... else_value="arg_evaluated_to_false")
48+
49+
Combine with boolean substitutions to create more complex conditions.
50+
Example with multiple boolean launch configurations:
51+
52+
.. doctest::
53+
54+
>>> from launch.substitutions import AllSubstitution
55+
>>> from launch.substitutions import EqualsSubstitution
56+
>>> from launch.substitutions import LaunchConfiguration
57+
>>> from launch.substitutions import NotSubstitution
58+
>>> subst = IfElseSubstitution(
59+
... AllSubstitution(EqualsSubstitution(LaunchConfiguration("arg1"),
60+
... LaunchConfiguration("arg2")),
61+
... NotSubstitution(LaunchConfiguration("arg3"))),
62+
... if_value="all_args_evaluated_to_true",
63+
... else_value="at_least_one_arg_evaluated_to_false")
64+
65+
"""
66+
67+
def __init__(self, condition: SomeSubstitutionsType,
68+
if_value: SomeSubstitutionsType = '',
69+
else_value: SomeSubstitutionsType = '') -> None:
70+
"""Create a IfElseSubstitution substitution."""
71+
super().__init__()
72+
if if_value == else_value == '':
73+
raise RuntimeError('One of if_value and else_value must be specified')
74+
self._condition = normalize_to_list_of_substitutions(condition)
75+
self._if_value = normalize_to_list_of_substitutions(if_value)
76+
self._else_value = normalize_to_list_of_substitutions(else_value)
77+
78+
@classmethod
79+
def parse(cls, data: Sequence[SomeSubstitutionsType]):
80+
"""Parse `IfElseSubstitution` substitution."""
81+
if len(data) < 2 or len(data) > 3:
82+
raise TypeError('if substitution expects from 2 or 3 arguments')
83+
kwargs = {'condition': data[0], 'if_value': data[1]}
84+
if len(data) == 3:
85+
kwargs['else_value'] = data[2]
86+
return cls, kwargs
87+
88+
@property
89+
def condition(self) -> List[Substitution]:
90+
"""Getter for condition."""
91+
return self._condition
92+
93+
@property
94+
def if_value(self) -> List[Substitution]:
95+
"""Getter for if value."""
96+
return self._if_value
97+
98+
@property
99+
def else_value(self) -> List[Substitution]:
100+
"""Getter for else value."""
101+
return self._else_value
102+
103+
def describe(self) -> Text:
104+
"""Return a description of this substitution as a string."""
105+
return f'IfElseSubstitution({self.condition}, {self.if_value}, {self.else_value})'
106+
107+
def perform(self, context: LaunchContext) -> Text:
108+
"""Perform the substitution by evaluating the condition."""
109+
try:
110+
condition = perform_typed_substitution(context, self.condition, bool)
111+
except (TypeError, ValueError) as e:
112+
raise SubstitutionFailure(e)
113+
114+
if condition:
115+
return perform_substitutions(context, self.if_value)
116+
else:
117+
return perform_substitutions(context, self.else_value)

launch/launch/substitutions/python_expression.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def expression(self) -> List[Substitution]:
9292

9393
@property
9494
def python_modules(self) -> List[Substitution]:
95-
"""Getter for expression."""
95+
"""Getter for python modules."""
9696
return self.__python_modules
9797

9898
def describe(self) -> Text:
@@ -108,7 +108,7 @@ def perform(self, context: LaunchContext) -> Text:
108108
module_objects = [importlib.import_module(name) for name in module_names]
109109
expression_locals = {}
110110
for module in module_objects:
111-
# For backwards compatility, we allow math definitions to be implicitly
111+
# For backwards compatibility, we allow math definitions to be implicitly
112112
# referenced in expressions, without prepending the math module name
113113
# TODO: This may be removed in a future release.
114114
if module.__name__ == 'math':
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2023 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 IfElseSubstitution substitution class."""
16+
17+
from launch import LaunchContext
18+
from launch.substitutions import IfElseSubstitution
19+
20+
import pytest
21+
22+
23+
def test_if_else_substitution_no_values():
24+
"""Check that construction fails if no values are specified."""
25+
# Should raise an error since neither the if value nor the else value is given
26+
with pytest.raises(RuntimeError):
27+
IfElseSubstitution('true')
28+
29+
30+
def test_if_else_substitution_both_values():
31+
"""Check that the right value is returned when both values are given."""
32+
# Condition is true case
33+
subst = IfElseSubstitution('true', 'ivalue', 'evalue')
34+
result = subst.perform(LaunchContext())
35+
assert result == 'ivalue'
36+
subst = IfElseSubstitution('true', if_value='ivalue', else_value='evalue')
37+
result = subst.perform(LaunchContext())
38+
assert result == 'ivalue'
39+
40+
# Condition is false case
41+
subst = IfElseSubstitution('false', 'ivalue', 'evalue')
42+
result = subst.perform(LaunchContext())
43+
assert result == 'evalue'
44+
45+
46+
def test_if_else_substitution_if_value():
47+
"""Check that the right value is returned when only the if value is given."""
48+
# Condition is true case
49+
subst = IfElseSubstitution('1', 'ivalue')
50+
result = subst.perform(LaunchContext())
51+
assert result == 'ivalue'
52+
subst = IfElseSubstitution('1', if_value='ivalue')
53+
result = subst.perform(LaunchContext())
54+
assert result == 'ivalue'
55+
56+
# Condition is false case
57+
subst = IfElseSubstitution('0', 'ivalue')
58+
result = subst.perform(LaunchContext())
59+
assert result == ''
60+
61+
62+
def test_if_else_substitution_else_value():
63+
"""Check that the right value is returned when only the else value is given."""
64+
# Condition is true case
65+
subst = IfElseSubstitution('on', else_value='evalue')
66+
result = subst.perform(LaunchContext())
67+
assert result == ''
68+
69+
# Condition is false case
70+
subst = IfElseSubstitution('off', else_value='evalue')
71+
result = subst.perform(LaunchContext())
72+
assert result == 'evalue'

0 commit comments

Comments
 (0)