Skip to content

Commit

Permalink
Add conditional substitution (#734)
Browse files Browse the repository at this point in the history
* Add conditional substitution

Closes: #727

Signed-off-by: Nick Lamprianidis <[email protected]>
  • Loading branch information
nlamprian authored and wjwwood committed Sep 20, 2023
1 parent 7dd7aa8 commit 159ee9c
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 2 deletions.
4 changes: 4 additions & 0 deletions launch/doc/source/architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ There are many possible variations of a substitution, but here are some of the c

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

- :class:`launch.substitutions.IfElseSubstitution`

- 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.

- :class:`launch.substitutions.LaunchDescriptionArgument`

- This substitution gets the value of a launch description argument, as a string, by name.
Expand Down
2 changes: 2 additions & 0 deletions launch/launch/substitutions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .equals_substitution import EqualsSubstitution
from .file_content import FileContent
from .find_executable import FindExecutable
from .if_else_substitution import IfElseSubstitution
from .launch_configuration import LaunchConfiguration
from .launch_log_dir import LaunchLogDir
from .local_substitution import LocalSubstitution
Expand All @@ -46,6 +47,7 @@
'EnvironmentVariable',
'FileContent',
'FindExecutable',
'IfElseSubstitution',
'LaunchConfiguration',
'LaunchLogDir',
'LocalSubstitution',
Expand Down
117 changes: 117 additions & 0 deletions launch/launch/substitutions/if_else_substitution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright 2023 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Module for the IfElseSubstitution substitution."""

from typing import List
from typing import Sequence
from typing import Text

from .substitution_failure import SubstitutionFailure
from ..frontend import expose_substitution
from ..launch_context import LaunchContext
from ..some_substitutions_type import SomeSubstitutionsType
from ..substitution import Substitution
from ..utilities import normalize_to_list_of_substitutions
from ..utilities import perform_substitutions
from ..utilities.type_utils import perform_typed_substitution


@expose_substitution('if')
class IfElseSubstitution(Substitution):
"""
Substitution that conditionally returns one of two substitutions.
Depending on whether the condition substitution evaluates to true, either it returns
the if_value substitution or the else_value substitution.
Example with a boolean launch configuration:
.. doctest::
>>> from launch.substitutions import LaunchConfiguration
>>> subst = IfElseSubstitution(
... LaunchConfiguration("arg"),
... if_value="arg_evaluated_to_true",
... else_value="arg_evaluated_to_false")
Combine with boolean substitutions to create more complex conditions.
Example with multiple boolean launch configurations:
.. doctest::
>>> from launch.substitutions import AllSubstitution
>>> from launch.substitutions import EqualsSubstitution
>>> from launch.substitutions import LaunchConfiguration
>>> from launch.substitutions import NotSubstitution
>>> subst = IfElseSubstitution(
... AllSubstitution(EqualsSubstitution(LaunchConfiguration("arg1"),
... LaunchConfiguration("arg2")),
... NotSubstitution(LaunchConfiguration("arg3"))),
... if_value="all_args_evaluated_to_true",
... else_value="at_least_one_arg_evaluated_to_false")
"""

def __init__(self, condition: SomeSubstitutionsType,
if_value: SomeSubstitutionsType = '',
else_value: SomeSubstitutionsType = '') -> None:
"""Create a IfElseSubstitution substitution."""
super().__init__()
if if_value == else_value == '':
raise RuntimeError('One of if_value and else_value must be specified')
self._condition = normalize_to_list_of_substitutions(condition)
self._if_value = normalize_to_list_of_substitutions(if_value)
self._else_value = normalize_to_list_of_substitutions(else_value)

@classmethod
def parse(cls, data: Sequence[SomeSubstitutionsType]):
"""Parse `IfElseSubstitution` substitution."""
if len(data) < 2 or len(data) > 3:
raise TypeError('if substitution expects from 2 or 3 arguments')
kwargs = {'condition': data[0], 'if_value': data[1]}
if len(data) == 3:
kwargs['else_value'] = data[2]
return cls, kwargs

@property
def condition(self) -> List[Substitution]:
"""Getter for condition."""
return self._condition

@property
def if_value(self) -> List[Substitution]:
"""Getter for if value."""
return self._if_value

@property
def else_value(self) -> List[Substitution]:
"""Getter for else value."""
return self._else_value

def describe(self) -> Text:
"""Return a description of this substitution as a string."""
return f'IfElseSubstitution({self.condition}, {self.if_value}, {self.else_value})'

def perform(self, context: LaunchContext) -> Text:
"""Perform the substitution by evaluating the condition."""
try:
condition = perform_typed_substitution(context, self.condition, bool)
except (TypeError, ValueError) as e:
raise SubstitutionFailure(e)

if condition:
return perform_substitutions(context, self.if_value)
else:
return perform_substitutions(context, self.else_value)
4 changes: 2 additions & 2 deletions launch/launch/substitutions/python_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def expression(self) -> List[Substitution]:

@property
def python_modules(self) -> List[Substitution]:
"""Getter for expression."""
"""Getter for python modules."""
return self.__python_modules

def describe(self) -> Text:
Expand All @@ -108,7 +108,7 @@ def perform(self, context: LaunchContext) -> Text:
module_objects = [importlib.import_module(name) for name in module_names]
expression_locals = {}
for module in module_objects:
# For backwards compatility, we allow math definitions to be implicitly
# For backwards compatibility, we allow math definitions to be implicitly
# referenced in expressions, without prepending the math module name
# TODO: This may be removed in a future release.
if module.__name__ == 'math':
Expand Down
72 changes: 72 additions & 0 deletions launch/test/launch/substitutions/test_if_else_substitution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2023 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for the IfElseSubstitution substitution class."""

from launch import LaunchContext
from launch.substitutions import IfElseSubstitution

import pytest


def test_if_else_substitution_no_values():
"""Check that construction fails if no values are specified."""
# Should raise an error since neither the if value nor the else value is given
with pytest.raises(RuntimeError):
IfElseSubstitution('true')


def test_if_else_substitution_both_values():
"""Check that the right value is returned when both values are given."""
# Condition is true case
subst = IfElseSubstitution('true', 'ivalue', 'evalue')
result = subst.perform(LaunchContext())
assert result == 'ivalue'
subst = IfElseSubstitution('true', if_value='ivalue', else_value='evalue')
result = subst.perform(LaunchContext())
assert result == 'ivalue'

# Condition is false case
subst = IfElseSubstitution('false', 'ivalue', 'evalue')
result = subst.perform(LaunchContext())
assert result == 'evalue'


def test_if_else_substitution_if_value():
"""Check that the right value is returned when only the if value is given."""
# Condition is true case
subst = IfElseSubstitution('1', 'ivalue')
result = subst.perform(LaunchContext())
assert result == 'ivalue'
subst = IfElseSubstitution('1', if_value='ivalue')
result = subst.perform(LaunchContext())
assert result == 'ivalue'

# Condition is false case
subst = IfElseSubstitution('0', 'ivalue')
result = subst.perform(LaunchContext())
assert result == ''


def test_if_else_substitution_else_value():
"""Check that the right value is returned when only the else value is given."""
# Condition is true case
subst = IfElseSubstitution('on', else_value='evalue')
result = subst.perform(LaunchContext())
assert result == ''

# Condition is false case
subst = IfElseSubstitution('off', else_value='evalue')
result = subst.perform(LaunchContext())
assert result == 'evalue'

0 comments on commit 159ee9c

Please sign in to comment.