Skip to content

Commit

Permalink
Add boolean substitutions (#598)
Browse files Browse the repository at this point in the history
* Add boolean substitutions

Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>

* Add lower() to the output

Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>

* Move test file

Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>

* Use perform_typed_substitution and simplify the code

Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>

* fix for flake8

Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>

* Add auto-expansion of LaunchConfiguration

Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>

* Revert "Add auto-expansion of LaunchConfiguration"

This reverts commit 8bdf336.

Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>

* Fix True to true

Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>

* Add tests to launch_yaml

Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>

* Update launch/test/launch/substitutions/test_boolean_substitution.py

Co-authored-by: Jacob Perron <jacob@openrobotics.org>
Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>

Co-authored-by: Jacob Perron <jacob@openrobotics.org>
  • Loading branch information
kenji-miyake and jacobperron authored Mar 29, 2022
1 parent d2eb2ba commit fbb5f51
Show file tree
Hide file tree
Showing 5 changed files with 385 additions and 0 deletions.
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 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)
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.
#
# 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'
Loading

0 comments on commit fbb5f51

Please sign in to comment.