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

implement Action conditions #121

Merged
merged 6 commits into from Jul 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion launch/doc/source/architecture.rst
Expand Up @@ -99,7 +99,7 @@ Base Action
All actions need to inherit from the :class:`launch.Action` base class, so that some common interface is available to the launch system when interacting with actions defined by external packages.
Since the base action class is a first class element in a launch description it also inherits from :class:`launch.LaunchDescriptionEntity`, which is the polymorphic type used when iterating over the elements in a launch description.

Also, the base action has a few features common to all actions, such as some introspection utilities, and the ability to be associated with a single :class:`launch.Conditional`, like the :class:`launch.IfCondition` class or the :class:`launch.UnlessCondition` class.
Also, the base action has a few features common to all actions, such as some introspection utilities, and the ability to be associated with a single :class:`launch.Condition`, like the :class:`launch.IfCondition` class or the :class:`launch.UnlessCondition` class.

The action configurations are supplied when the user uses an action and can be used to pass "arguments" to the action in order to influence its behavior, e.g. this is how you would pass the path to the executable in the execute process action.

Expand Down
4 changes: 4 additions & 0 deletions launch/launch/__init__.py
Expand Up @@ -15,9 +15,11 @@
"""Main entry point for the `launch` package."""

from . import actions
from . import conditions
from . import events
from . import legacy
from .action import Action
from .condition import Condition
from .event import Event
from .event_handler import EventHandler
from .launch_context import LaunchContext
Expand All @@ -33,9 +35,11 @@

__all__ = [
'actions',
'conditions',
'events',
'legacy',
'Action',
'Condition',
'Event',
'EventHandler',
'LaunchContext',
Expand Down
18 changes: 17 additions & 1 deletion launch/launch/action.py
Expand Up @@ -14,10 +14,12 @@

"""Module for Action class."""

from typing import cast
from typing import List
from typing import Optional
from typing import Text

from .condition import Condition
from .launch_context import LaunchContext
from .launch_description_entity import LaunchDescriptionEntity

Expand All @@ -30,13 +32,27 @@ class Action(LaunchDescriptionEntity):
executed given a :class:`launch.LaunchContext` at runtime.
"""

def __init__(self, *, condition: Optional[Condition] = None) -> None:
"""
Constructor.

If the conditions argument is not None, the condition object will be
evaluated while being visited and the action will only be executed if
the condition evaluates to True.

:param condition: Either a :py:class:`Condition` or None
"""
self.__condition = condition

def describe(self) -> Text:
"""Return a description of this Action."""
return self.__repr__()

def visit(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEntity]]:
"""Override visit from LaunchDescriptionEntity so that it executes."""
return self.execute(context) # type: ignore
if self.__condition is None or self.__condition.evaluate(context):
return cast(Optional[List[LaunchDescriptionEntity]], self.execute(context))
return None

def execute(self, context: LaunchContext) -> Optional[List['Action']]:
"""
Expand Down
4 changes: 2 additions & 2 deletions launch/launch/actions/emit_event.py
Expand Up @@ -23,9 +23,9 @@
class EmitEvent(Action):
"""Action that emits an event when executed."""

def __init__(self, *, event: Event) -> None:
def __init__(self, *, event: Event, **kwargs) -> None:
"""Constructor."""
super().__init__()
super().__init__(**kwargs)
if not is_a_subclass(event, Event):
raise RuntimeError("EmitEvent() expected an event instance, got '{}'.".format(event))
self.__event = event
Expand Down
17 changes: 11 additions & 6 deletions launch/launch/actions/execute_process.py
Expand Up @@ -29,6 +29,7 @@
from typing import List
from typing import Optional
from typing import Text
from typing import Tuple # noqa: F401
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is F401 needed here, looks like you are using Tuple?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we have to support python 3.5, we cannot use the type annotation for member variables syntax which was added in 3.6. So we use the type comment, which is the only place I use Tuple. So flake8 doesn’t count that as using it so I suppressed the unused import warning.


from osrf_pycommon.process_utils import async_execute_process
from osrf_pycommon.process_utils import AsyncSubprocessProtocol
Expand Down Expand Up @@ -71,16 +72,20 @@ class ExecuteProcess(Action):
"""Action that begins executing a process and sets up event handlers for the process."""

def __init__(
self, *,
self,
*,
cmd: Iterable[SomeSubstitutionsType],
cwd: Optional[SomeSubstitutionsType] = None,
env: Optional[Dict[SomeSubstitutionsType, SomeSubstitutionsType]] = None,
shell: bool = False,
sigterm_timeout: SomeSubstitutionsType = LaunchConfiguration('sigterm_timeout', default=5),
sigkill_timeout: SomeSubstitutionsType = LaunchConfiguration('sigkill_timeout', default=5),
sigterm_timeout: SomeSubstitutionsType = LaunchConfiguration(
'sigterm_timeout', default = 5),
sigkill_timeout: SomeSubstitutionsType = LaunchConfiguration(
'sigkill_timeout', default = 5),
prefix: Optional[SomeSubstitutionsType] = None,
output: Optional[Text] = None,
log_cmd: bool = False
log_cmd: bool = False,
**kwargs
) -> None:
"""
Construct an ExecuteProcess action.
Expand Down Expand Up @@ -154,10 +159,10 @@ def __init__(
process, which is useful for debugging when substitutions are
involved.
"""
super().__init__()
super().__init__(**kwargs)
self.__cmd = [normalize_to_list_of_substitutions(x) for x in cmd]
self.__cwd = cwd if cwd is None else normalize_to_list_of_substitutions(cwd)
self.__env = None # type: Optional[Dict[List[Substitution], List[Substitution]]]
self.__env = None # type: Optional[List[Tuple[List[Substitution], List[Substitution]]]]
if env is not None:
self.__env = []
for key, value in env.items():
Expand Down
4 changes: 2 additions & 2 deletions launch/launch/actions/log_info.py
Expand Up @@ -41,9 +41,9 @@ def __init__(self, *, msg: List[Union[Text, Substitution]]) -> None:
"""Construct with list of Text and Substitutions."""
...

def __init__(self, *, msg): # noqa: F811
def __init__(self, *, msg, **kwargs): # noqa: F811
"""Constructor."""
super().__init__()
super().__init__(**kwargs)

self.__msg = normalize_to_list_of_substitutions(msg)

Expand Down
5 changes: 3 additions & 2 deletions launch/launch/actions/opaque_function.py
Expand Up @@ -49,10 +49,11 @@ def __init__(
self, *,
function: Callable,
args: Optional[Iterable[Any]] = None,
kwargs: Optional[Dict[Text, Any]] = None
kwargs: Optional[Dict[Text, Any]] = None,
**left_over_kwargs
) -> None:
"""Constructor."""
super().__init__()
super().__init__(**left_over_kwargs)
if not callable(function):
raise TypeError("OpaqueFunction expected a callable for 'function', got '{}'".format(
type(function)
Expand Down
4 changes: 2 additions & 2 deletions launch/launch/actions/register_event_handler.py
Expand Up @@ -32,9 +32,9 @@ class RegisterEventHandler(Action):
place.
"""

def __init__(self, event_handler: EventHandler) -> None:
def __init__(self, event_handler: EventHandler, **kwargs) -> None:
"""Constructor."""
super().__init__()
super().__init__(**kwargs)
self.__event_handler = event_handler

@property
Expand Down
9 changes: 7 additions & 2 deletions launch/launch/actions/set_launch_configuration.py
Expand Up @@ -33,9 +33,14 @@ class SetLaunchConfiguration(Action):
LaunchDescription's, but can be scoped with groups.
"""

def __init__(self, name: SomeSubstitutionsType, value: SomeSubstitutionsType) -> None:
def __init__(
self,
name: SomeSubstitutionsType,
value: SomeSubstitutionsType,
**kwargs
) -> None:
"""Constructor."""
super().__init__()
super().__init__(**kwargs)
self.__name = normalize_to_list_of_substitutions(name)
self.__value = normalize_to_list_of_substitutions(value)

Expand Down
8 changes: 5 additions & 3 deletions launch/launch/actions/timer_action.py
Expand Up @@ -53,12 +53,14 @@ class TimerAction(Action):
"""

def __init__(
self, *,
self,
*,
period: Union[float, SomeSubstitutionsType],
actions: Iterable[LaunchDescriptionEntity]
actions: Iterable[LaunchDescriptionEntity],
**kwargs
) -> None:
"""Constructor."""
super().__init__()
super().__init__(**kwargs)
period_types = list(SomeSubstitutionsType_types_tuple) + [float]
ensure_argument_type(period, period_types, 'period', 'TimerAction')
ensure_argument_type(actions, collections.Iterable, 'actions', 'TimerAction')
Expand Down
45 changes: 45 additions & 0 deletions launch/launch/condition.py
@@ -0,0 +1,45 @@
# Copyright 2018 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 Condition class."""

from typing import Callable
from typing import Optional
from typing import Text

from .launch_context import LaunchContext


class Condition:
"""
Encapsulates a condition to be evaluated when launching.

The given predicate receives a launch context and is evaluated while
launching, but must return True or False.

If a predicate is not set when evaluated, False is returned.
"""

def __init__(self, *, predicate: Optional[Callable[[LaunchContext], bool]] = None) -> None:
self._predicate = predicate

def describe(self) -> Text:
"""Return a description of this Condition."""
return self.__repr__()

def evaluate(self, context: LaunchContext) -> bool:
"""Evaluate the condition."""
if self._predicate is not None:
return self._predicate(context)
return False
27 changes: 27 additions & 0 deletions launch/launch/conditions/__init__.py
@@ -0,0 +1,27 @@
# Copyright 2018 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.

"""conditions Module."""

from .evaluate_condition_expression_impl import evaluate_condition_expression
from .if_condition import IfCondition
from .invalid_condition_expression_error import InvalidConditionExpressionError
from .unless_condition import UnlessCondition

__all__ = [
'evaluate_condition_expression',
'IfCondition',
'InvalidConditionExpressionError',
'UnlessCondition',
]
48 changes: 48 additions & 0 deletions launch/launch/conditions/evaluate_condition_expression_impl.py
@@ -0,0 +1,48 @@
# Copyright 2018 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 utility functions related to evaluating condition expressions."""

from typing import List

from .invalid_condition_expression_error import InvalidConditionExpressionError
from ..launch_context import LaunchContext
from ..substitution import Substitution
from ..utilities import perform_substitutions


VALID_TRUE_EXPRESSIONS = ['true', '1']
VALID_FALSE_EXPRESSIONS = ['false', '0']


def evaluate_condition_expression(context: LaunchContext, expression: List[Substitution]) -> bool:
"""
Expand an expression and then evaluate it as a condition, returing true or false.

The expanded expression is stripped and has ``lower()`` called on it before
being logically evaluated as either true or false.
A string will be considered True if it matches 'true' or '1'.
A string will be considered False if it matches 'false' or '0'.
Any other string content (including empty string) will result in an error.

:raises: InvalidConditionExpressionError
"""
expanded_expression = perform_substitutions(context, expression)
expanded_expression = expanded_expression.strip().lower()
if expanded_expression in ['true', '1']:
return True
if expanded_expression in ['false', '0']:
return False
valid_expressions = VALID_TRUE_EXPRESSIONS + VALID_FALSE_EXPRESSIONS
raise InvalidConditionExpressionError(expanded_expression, expression, valid_expressions)
47 changes: 47 additions & 0 deletions launch/launch/conditions/if_condition.py
@@ -0,0 +1,47 @@
# Copyright 2018 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 IfCondition class."""

from typing import Text

from .evaluate_condition_expression_impl import evaluate_condition_expression
from ..condition import Condition
from ..launch_context import LaunchContext
from ..some_substitutions_type import SomeSubstitutionsType
from ..utilities import normalize_to_list_of_substitutions


class IfCondition(Condition):
"""
Encapsulates an if condition to be evaluated when launching.

This condition takes a string expression that is lexically evaluated as a
boolean, but the expression may consist of :py:class:`launch.Substitution`
instances.

See :py:func:`evaluate_condition_expression` to understand what constitutes
a valid condition expression.
"""

def __init__(self, predicate_expression: SomeSubstitutionsType) -> None:
self.__predicate_expression = normalize_to_list_of_substitutions(predicate_expression)
super().__init__(predicate=self._predicate_func)

def _predicate_func(self, context: LaunchContext) -> bool:
return evaluate_condition_expression(context, self.__predicate_expression)

def describe(self) -> Text:
"""Return a description of this Condition."""
return self.__repr__()