diff --git a/launch/launch/substitutions/__init__.py b/launch/launch/substitutions/__init__.py index d631bbbdd..8195ba242 100644 --- a/launch/launch/substitutions/__init__.py +++ b/launch/launch/substitutions/__init__.py @@ -15,6 +15,9 @@ """Package for substitutions.""" from .anon_name import AnonName +from .boolean_substitution import AndSubstitution +from .boolean_substitution import NotSubstitution +from .boolean_substitution import OrSubstitution from .command import Command from .environment_variable import EnvironmentVariable from .find_executable import FindExecutable @@ -28,12 +31,15 @@ from .this_launch_file_dir import ThisLaunchFileDir __all__ = [ + 'AndSubstitution', 'AnonName', 'Command', 'EnvironmentVariable', 'FindExecutable', 'LaunchConfiguration', 'LocalSubstitution', + 'NotSubstitution', + 'OrSubstitution', 'PathJoinSubstitution', 'PythonExpression', 'SubstitutionFailure', diff --git a/launch/launch/substitutions/boolean_substitution.py b/launch/launch/substitutions/boolean_substitution.py new file mode 100644 index 000000000..016c844fd --- /dev/null +++ b/launch/launch/substitutions/boolean_substitution.py @@ -0,0 +1,154 @@ +# Copyright 2022 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 boolean substitutions.""" + +from typing import Iterable +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.type_utils import perform_typed_substitution + + +@expose_substitution('not') +class NotSubstitution(Substitution): + """Substitution that returns 'not' of the input boolean value.""" + + def __init__(self, value: SomeSubstitutionsType) -> None: + """Create a NotSubstitution substitution.""" + super().__init__() + + self.__value = normalize_to_list_of_substitutions(value) + + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): + """Parse `NotSubstitution` substitution.""" + if len(data) != 1: + raise TypeError('not substitution expects 1 argument') + return cls, {'value': data[0]} + + @property + def value(self) -> Substitution: + """Getter for value.""" + return self.__value + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return f'NotSubstitution({self.value})' + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution.""" + try: + condition = perform_typed_substitution(context, self.value, bool) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) + + return str(not condition).lower() + + +@expose_substitution('and') +class AndSubstitution(Substitution): + """Substitution that returns 'and' of the input boolean values.""" + + def __init__(self, left: SomeSubstitutionsType, right: SomeSubstitutionsType) -> None: + """Create a AndSubstitution substitution.""" + super().__init__() + + self.__left = normalize_to_list_of_substitutions(left) + self.__right = normalize_to_list_of_substitutions(right) + + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): + """Parse `AndSubstitution` substitution.""" + if len(data) != 2: + raise TypeError('and substitution expects 2 arguments') + return cls, {'left': data[0], 'right': data[1]} + + @property + def left(self) -> Substitution: + """Getter for left.""" + return self.__left + + @property + def right(self) -> Substitution: + """Getter for right.""" + return self.__right + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return f'AndSubstitution({self.left} {self.right})' + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution.""" + try: + left_condition = perform_typed_substitution(context, self.left, bool) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) + try: + right_condition = perform_typed_substitution(context, self.right, bool) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) + + return str(left_condition and right_condition).lower() + + +@expose_substitution('or') +class OrSubstitution(Substitution): + """Substitution that returns 'or' of the input boolean values.""" + + def __init__(self, left: SomeSubstitutionsType, right: SomeSubstitutionsType) -> None: + """Create a AndSubstitution substitution.""" + super().__init__() + + self.__left = normalize_to_list_of_substitutions(left) + self.__right = normalize_to_list_of_substitutions(right) + + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): + """Parse `AndSubstitution` substitution.""" + if len(data) != 2: + raise TypeError('and substitution expects 2 arguments') + return cls, {'left': data[0], 'right': data[1]} + + @property + def left(self) -> Substitution: + """Getter for left.""" + return self.__left + + @property + def right(self) -> Substitution: + """Getter for right.""" + return self.__right + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return f'AndSubstitution({self.left} {self.right})' + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution.""" + try: + left_condition = perform_typed_substitution(context, self.left, bool) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) + try: + right_condition = perform_typed_substitution(context, self.right, bool) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) + + return str(left_condition or right_condition).lower() diff --git a/launch/test/launch/substitutions/test_boolean_substitution.py b/launch/test/launch/substitutions/test_boolean_substitution.py new file mode 100644 index 000000000..edd39cfb2 --- /dev/null +++ b/launch/test/launch/substitutions/test_boolean_substitution.py @@ -0,0 +1,56 @@ +# Copyright 2022 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 boolean substitution classes.""" + +from launch import LaunchContext + +from launch.substitutions import AndSubstitution +from launch.substitutions import NotSubstitution +from launch.substitutions import OrSubstitution +from launch.substitutions.substitution_failure import SubstitutionFailure + +import pytest + + +def test_not_substitution(): + lc = LaunchContext() + assert NotSubstitution('true').perform(lc) == 'false' + assert NotSubstitution('false').perform(lc) == 'true' + with pytest.raises(SubstitutionFailure): + NotSubstitution('not-condition-expression').perform(lc) + + +def test_and_substitution(): + lc = LaunchContext() + assert AndSubstitution('true', 'true').perform(lc) == 'true' + assert AndSubstitution('true', 'false').perform(lc) == 'false' + assert AndSubstitution('false', 'true').perform(lc) == 'false' + assert AndSubstitution('false', 'false').perform(lc) == 'false' + with pytest.raises(SubstitutionFailure): + AndSubstitution('not-condition-expression', 'true').perform(lc) + with pytest.raises(SubstitutionFailure): + AndSubstitution('true', 'not-condition-expression').perform(lc) + + +def test_or_substitution(): + lc = LaunchContext() + assert OrSubstitution('true', 'true').perform(lc) == 'true' + assert OrSubstitution('true', 'false').perform(lc) == 'true' + assert OrSubstitution('false', 'true').perform(lc) == 'true' + assert OrSubstitution('false', 'false').perform(lc) == 'false' + with pytest.raises(SubstitutionFailure): + OrSubstitution('not-condition-expression', 'true').perform(lc) + with pytest.raises(SubstitutionFailure): + OrSubstitution('true', 'not-condition-expression').perform(lc) diff --git a/launch_xml/test/launch_xml/test_boolean_substitution.py b/launch_xml/test/launch_xml/test_boolean_substitution.py new file mode 100644 index 000000000..59fecde14 --- /dev/null +++ b/launch_xml/test/launch_xml/test_boolean_substitution.py @@ -0,0 +1,85 @@ +# Copyright 2022 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. + +import io +import textwrap + +from launch import LaunchService +from launch.frontend import Parser +from launch.utilities import perform_substitutions + + +def test_boolean_substitution_xml(): + xml_file = textwrap.dedent( + r""" + + + + + + + + + + + + + + + + + + """ + ) + with io.StringIO(xml_file) as f: + check_boolean_substitution(f) + + +def check_boolean_substitution(file): + root_entity, parser = Parser.load(file) + ld = parser.parse_description(root_entity) + ls = LaunchService() + ls.include_launch_description(ld) + assert 0 == ls.run() + + def perform(substitution): + return perform_substitutions(ls.context, substitution) + + sub_entries = ld.describe_sub_entities() + + not_true = sub_entries[2] + not_false = sub_entries[3] + + and_true_true = sub_entries[4] + and_true_false = sub_entries[5] + and_false_true = sub_entries[6] + and_false_false = sub_entries[7] + + or_true_true = sub_entries[8] + or_true_false = sub_entries[9] + or_false_true = sub_entries[10] + or_false_false = sub_entries[11] + + assert perform(not_true.value) == 'false' + assert perform(not_false.value) == 'true' + + assert perform(and_true_true.value) == 'true' + assert perform(and_true_false.value) == 'false' + assert perform(and_false_true.value) == 'false' + assert perform(and_false_false.value) == 'false' + + assert perform(or_true_true.value) == 'true' + assert perform(or_true_false.value) == 'true' + assert perform(or_false_true.value) == 'true' + assert perform(or_false_false.value) == 'false' diff --git a/launch_yaml/test/launch_yaml/test_boolean_substitution.py b/launch_yaml/test/launch_yaml/test_boolean_substitution.py new file mode 100644 index 000000000..0eba61615 --- /dev/null +++ b/launch_yaml/test/launch_yaml/test_boolean_substitution.py @@ -0,0 +1,84 @@ +# Copyright 2022 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. + +import io +import textwrap + +from launch import LaunchService +from launch.frontend import Parser +from launch.utilities import perform_substitutions + + +def test_boolean_substitution_yaml(): + yaml_file = textwrap.dedent( + r""" + launch: + - let: { name: true_value, value: "true" } + - let: { name: false_value, value: "false" } + + - let: { name: not_true, value: "$(not $(var true_value))" } + - let: { name: not_false, value: "$(not $(var false_value))" } + + - let: { name: and_true_true, value: "$(and $(var true_value) $(var true_value))" } + - let: { name: and_true_false, value: "$(and $(var true_value) $(var false_value))" } + - let: { name: and_false_true, value: "$(and $(var false_value) $(var true_value))" } + - let: { name: and_false_false, value: "$(and $(var false_value) $(var false_value))" } + + - let: { name: or_true_true, value: "$(or $(var true_value) $(var true_value))" } + - let: { name: or_true_false, value: "$(or $(var true_value) $(var false_value))" } + - let: { name: or_false_true, value: "$(or $(var false_value) $(var true_value))" } + - let: { name: or_false_false, value: "$(or $(var false_value) $(var false_value))" } + """ + ) + with io.StringIO(yaml_file) as f: + check_boolean_substitution(f) + + +def check_boolean_substitution(file): + root_entity, parser = Parser.load(file) + ld = parser.parse_description(root_entity) + ls = LaunchService() + ls.include_launch_description(ld) + assert 0 == ls.run() + + def perform(substitution): + return perform_substitutions(ls.context, substitution) + + sub_entries = ld.describe_sub_entities() + + not_true = sub_entries[2] + not_false = sub_entries[3] + + and_true_true = sub_entries[4] + and_true_false = sub_entries[5] + and_false_true = sub_entries[6] + and_false_false = sub_entries[7] + + or_true_true = sub_entries[8] + or_true_false = sub_entries[9] + or_false_true = sub_entries[10] + or_false_false = sub_entries[11] + + assert perform(not_true.value) == 'false' + assert perform(not_false.value) == 'true' + + assert perform(and_true_true.value) == 'true' + assert perform(and_true_false.value) == 'false' + assert perform(and_false_true.value) == 'false' + assert perform(and_false_false.value) == 'false' + + assert perform(or_true_true.value) == 'true' + assert perform(or_true_false.value) == 'true' + assert perform(or_false_true.value) == 'true' + assert perform(or_false_false.value) == 'false'