Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add boolean substitutions #598

Merged
merged 10 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions launch/launch/substitutions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,12 +31,15 @@
from .this_launch_file_dir import ThisLaunchFileDir

__all__ = [
'AndSubstitution',
'AnonName',
'Command',
'EnvironmentVariable',
'FindExecutable',
'LaunchConfiguration',
'LocalSubstitution',
'NotSubstitution',
'OrSubstitution',
'PathJoinSubstitution',
'PythonExpression',
'SubstitutionFailure',
Expand Down
154 changes: 154 additions & 0 deletions launch/launch/substitutions/boolean_substitution.py
Original file line number Diff line number Diff line change
@@ -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()
56 changes: 56 additions & 0 deletions launch/test/launch/substitutions/test_boolean_substitution.py
Original file line number Diff line number Diff line change
@@ -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 AnonName substitution class."""
kenji-miyake marked this conversation as resolved.
Show resolved Hide resolved

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)
kenji-miyake marked this conversation as resolved.
Show resolved Hide resolved
with pytest.raises(SubstitutionFailure):
OrSubstitution('True', 'not-condition-expression').perform(lc)
kenji-miyake marked this conversation as resolved.
Show resolved Hide resolved
85 changes: 85 additions & 0 deletions launch_xml/test/launch_xml/test_boolean_substitution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright 2022 Open Source Robotics Foundation, Inc.
kenji-miyake marked this conversation as resolved.
Show resolved Hide resolved
#
# 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"""
<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))" />
</launch>
"""
)
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'