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 ability to define and pass launch arguments to launch files #123

Merged
merged 26 commits into from
Sep 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c9fb73f
add self descriptions for substitutions
wjwwood Aug 14, 2018
2bb466e
add tracebacks back to the output by default
wjwwood Aug 14, 2018
dc3eaa6
add new actions for declaring launch arguments
wjwwood Aug 14, 2018
1af02d1
new method on LaunchDescription which gets all declared arguments within
wjwwood Aug 14, 2018
6213656
add ability to pass arguments when including a launch description
wjwwood Aug 14, 2018
5cfd710
add description for local variables used in Node action
wjwwood Aug 14, 2018
42bfcb9
fix bug in Node action
wjwwood Aug 14, 2018
4719cb4
cleanup error reporting in Node action
wjwwood Aug 14, 2018
6a9cc14
use launch arguments in examples
wjwwood Aug 14, 2018
6b4927b
add ability to show and pass launch arguments on the command line
wjwwood Aug 14, 2018
2977c5b
fix python 3.5 support
wjwwood Aug 22, 2018
1f5bf87
add accessor for the Condition of an Action
wjwwood Aug 23, 2018
c134906
do not automatically push/pop configs when including
wjwwood Aug 23, 2018
d27ea82
small refactor to get the launch file location
wjwwood Aug 23, 2018
89eab55
improve ability to detect declared arguments across launch file includes
wjwwood Aug 23, 2018
71a1dfb
add tests for the new DeclareLaunchArgument action
wjwwood Aug 23, 2018
8617c38
test new features of IncludeLaunchDescription
wjwwood Aug 23, 2018
c236b57
test new features of LaunchDescription class
wjwwood Aug 23, 2018
f5ba658
remove unused imports
wjwwood Aug 23, 2018
6ab4d88
improve output when showing arguments of a launch file
wjwwood Aug 23, 2018
60b72cf
fix the return type of LaunchService.run()
wjwwood Aug 23, 2018
43cb119
fix the checking for the asyncio event loop for the case where it is …
wjwwood Aug 23, 2018
97edc9f
typo
wjwwood Aug 30, 2018
b4969f4
restart event loop to allow proper shutdown when there's an unhandled…
wjwwood Aug 30, 2018
f077ab6
ExecuteProcess: unregister event handlers if rest of setup fails
wjwwood Aug 30, 2018
4dbd9bb
only put traceback in debug logging
wjwwood Aug 30, 2018
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
5 changes: 5 additions & 0 deletions launch/launch/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ def __init__(self, *, condition: Optional[Condition] = None) -> None:
"""
self.__condition = condition

@property
def condition(self) -> Optional[Condition]:
"""Getter for condition."""
return self.__condition

def describe(self) -> Text:
"""Return a description of this Action."""
return self.__repr__()
Expand Down
2 changes: 2 additions & 0 deletions launch/launch/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

"""actions Module."""

from .declare_launch_argument import DeclareLaunchArgument
from .emit_event import EmitEvent
from .execute_process import ExecuteProcess
from .include_launch_description import IncludeLaunchDescription
Expand All @@ -24,6 +25,7 @@
from .timer_action import TimerAction

__all__ = [
'DeclareLaunchArgument',
'EmitEvent',
'ExecuteProcess',
'IncludeLaunchDescription',
Expand Down
124 changes: 124 additions & 0 deletions launch/launch/actions/declare_launch_argument.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# 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 the DeclareLaunchArgument action."""

import logging
from typing import List
from typing import Optional
from typing import Text

from ..action import Action
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

_logger = logging.getLogger('launch.actions.DeclareLaunchArgument')


class DeclareLaunchArgument(Action):
"""
Action that declares a new launch argument.

A launch arguments are stored in a "launch configuration" of the same name.
See :py:class:`launch.actions.SetLaunchConfiguration` and
:py:class:`launch.substitutions.LaunchConfiguration`.

Any launch arguments declared within a :py:class:`launch.LaunchDescription`
will be exposed as arguments when that launch description is included, e.g.
as additional parameters in the
:py:class:`launch.actions.IncludeLaunchDescription` action or as
command-line arguments when launched with ``ros2 launch ...``.

In addition to the name, which is also where the argument result is stored,
launch arguments may have a default value and a description.
If a default value is given, then the argument becomes optional and the
default value is placed in the launch configuration instead.
If no default value is given and no value is given when including the
launch description, then an error occurs.

The default value may use Substitutions, but the name and description can
only be Text, since they need a meaningful value before launching, e.g.
when listing the command-line arguments.

Note that declaring a launch argument needs to be in a part of the launch
description that is describable without launching.
For example, if you declare a launch argument with this action from within
a condition group or as a callback to an event handler, then it may not be
possible for a tool like ``ros2 launch`` to know about the argument before
launching the launch description.
In such cases, the argument will not be visible on the command line but
may raise an exception if that argument is not satisfied once visited (and
has no default value).

Put another way, the post-condition of this action being visited is either
that a launch configuration of the same name is set with a value or an
exception is raised because none is set and there is no default value.
However, the pre-condition does not guarantee that the argument was visible
if behind condition or situational inclusions.
"""

def __init__(
self,
name: Text,
*,
default_value: Optional[SomeSubstitutionsType] = None,
description: Text = 'no description given',
**kwargs
) -> None:
"""Constructor."""
super().__init__(**kwargs)
self.__name = name
if default_value is None:
self.__default_value = default_value
else:
self.__default_value = normalize_to_list_of_substitutions(default_value)
self.__description = description

# This is used later to determine if this launch argument will be
# conditionally visited.
# Its value will be read and set at different times and so the value
# may change depending at different times based on the context.
self._conditionally_included = False

@property
def name(self) -> Text:
"""Getter for self.__name."""
return self.__name

@property
def default_value(self) -> Optional[List[Substitution]]:
"""Getter for self.__default_value."""
return self.__default_value

@property
def description(self) -> Text:
"""Getter for self.__description."""
return self.__description

def execute(self, context: LaunchContext):
"""Execute the action."""
if self.name not in context.launch_configurations:
if self.default_value is None:
# Argument not already set and no default value given, error.
_logger.error(
"Required launch argument '{}' (description: '{}') was not provided"
.format(self.name, self.description)
)
raise RuntimeError(
"Required launch argument '{}' was not provided.".format(self.name))
context.launch_configurations[self.name] = \
perform_substitutions(context, self.default_value)
48 changes: 29 additions & 19 deletions launch/launch/actions/execute_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,25 +451,35 @@ def execute(self, context: LaunchContext) -> Optional[List['Action']]:
if self.__shutdown_received:
# If shutdown starts before execution can start, don't start execution.
return None
# TODO(wjwwood): unregister event handlers when that is possible
context.register_event_handler(EventHandler(
matcher=lambda event: is_a_subclass(event, ShutdownProcess),
entities=OpaqueFunction(function=self.__on_shutdown_process_event),
))
context.register_event_handler(EventHandler(
matcher=lambda event: is_a_subclass(event, SignalProcess),
entities=OpaqueFunction(function=self.__on_signal_process_event),
))
context.register_event_handler(EventHandler(
matcher=lambda event: is_a_subclass(event, ProcessStdin),
entities=OpaqueFunction(function=self.__on_process_stdin_event),
))
context.register_event_handler(OnShutdown(
on_shutdown=self.__on_shutdown,
))
self.__completed_future = create_future(context.asyncio_loop)
self.__expand_substitutions(context)
context.asyncio_loop.create_task(self.__execute_process(context))

event_handlers = [
EventHandler(
matcher=lambda event: is_a_subclass(event, ShutdownProcess),
entities=OpaqueFunction(function=self.__on_shutdown_process_event),
),
EventHandler(
matcher=lambda event: is_a_subclass(event, SignalProcess),
entities=OpaqueFunction(function=self.__on_signal_process_event),
),
EventHandler(
matcher=lambda event: is_a_subclass(event, ProcessStdin),
entities=OpaqueFunction(function=self.__on_process_stdin_event),
),
OnShutdown(
on_shutdown=self.__on_shutdown,
),
]
for event_handler in event_handlers:
context.register_event_handler(event_handler)

try:
self.__completed_future = create_future(context.asyncio_loop)
self.__expand_substitutions(context)
context.asyncio_loop.create_task(self.__execute_process(context))
except Exception:
for event_handler in event_handlers:
context.unregister_event_handler(event_handler)
raise
return None

def get_asyncio_future(self) -> Optional[asyncio.Future]:
Expand Down
92 changes: 85 additions & 7 deletions launch/launch/actions/include_launch_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,116 @@
"""Module for the IncludeLaunchDescription action."""

import os
from typing import Iterable
from typing import List
from typing import Optional
from typing import Tuple

from .set_launch_configuration import SetLaunchConfiguration
from ..action import Action
from ..launch_context import LaunchContext
from ..launch_description_entity import LaunchDescriptionEntity
from ..launch_description_source import LaunchDescriptionSource
from ..some_substitutions_type import SomeSubstitutionsType
from ..utilities import normalize_to_list_of_substitutions
from ..utilities import perform_substitutions


class IncludeLaunchDescription(Action):
"""Action that includes a launch description source and yields its entities when visited."""
"""
Action that includes a launch description source and yields its entities when visited.

def __init__(self, launch_description_source: LaunchDescriptionSource) -> None:
It is possible to pass arguments to the launch description, which it
declared with the :py:class:`launch.actions.DeclareLaunchArgument` action.

If any given arguments do not match the name of any declared launch
arguments, then they will still be set as Launch Configurations using the
:py:class:`launch.actions.SetLaunchConfiguration` action.
This is done because it's not always possible to detect all instances of
the declare launch argument class in the given launch description.

On the other side, an error will sometimes be raised if the given launch
description declares a launch argument and its value is not provided to
this action.
It will only produce this error, however, if the declared launch argument
is unconditional (sometimes the action that declares the launch argument
will only be visited in certain circumstances) and if it does not have a
default value on which to fall back.

Conditionally included launch arguments that do not have a default value
will eventually raise an error if this best effort argument checking is
unable to see an unsatisfied argument ahead of time.
"""

def __init__(
self,
launch_description_source: LaunchDescriptionSource,
*,
launch_arguments: Optional[
Iterable[Tuple[SomeSubstitutionsType, SomeSubstitutionsType]]
] = None
) -> None:
"""Constructor."""
super().__init__()
self.__launch_description_source = launch_description_source
self.__launch_arguments = launch_arguments

@property
def launch_description_source(self) -> LaunchDescriptionSource:
"""Getter for self.__launch_description_source."""
return self.__launch_description_source

def visit(self, context: LaunchContext) -> List[LaunchDescriptionEntity]:
"""Override visit to return an Entity rather than an action."""
launch_description = self.__launch_description_source.get_launch_description(context)
@property
def launch_arguments(self) -> Iterable[Tuple[SomeSubstitutionsType, SomeSubstitutionsType]]:
"""Getter for self.__launch_arguments."""
if self.__launch_arguments is None:
return []
else:
return self.__launch_arguments

def _get_launch_file_location(self):
launch_file_location = os.path.abspath(self.__launch_description_source.location)
if os.path.exists(launch_file_location):
launch_file_location = os.path.dirname(launch_file_location)
else:
# If the location does not exist, then it's likely set to '<script>' or something
# so just pass it along.
launch_file_location = self.__launch_description_source.location
return launch_file_location

def describe_sub_entities(self) -> List[LaunchDescriptionEntity]:
"""Override describe_sub_entities from LaunchDescriptionEntity to return sub entities."""
ret = self.__launch_description_source.try_get_launch_description_without_context()
return [ret] if ret is not None else []

def visit(self, context: LaunchContext) -> List[LaunchDescriptionEntity]:
"""Override visit to return an Entity rather than an action."""
launch_description = self.__launch_description_source.get_launch_description(context)
context.extend_locals({
'current_launch_file_directory': launch_file_location,
'current_launch_file_directory': self._get_launch_file_location(),
})
return [launch_description]

# Do best effort checking to see if non-optional, non-default declared arguments
# are being satisfied.
argument_names = [
perform_substitutions(context, normalize_to_list_of_substitutions(arg_name))
for arg_name, arg_value in self.launch_arguments
]
declared_launch_arguments = launch_description.get_launch_arguments()
for argument in declared_launch_arguments:
if argument._conditionally_included or argument.default_value is not None:
continue
if argument.name not in argument_names:
raise RuntimeError(
"Included launch description missing required argument '{}' "
"(description: '{}'), given: [{}]"
.format(argument.name, argument.description, ', '.join(argument_names))
)

# Create actions to set the launch arguments into the launch configurations.
set_launch_configuration_actions = []
for name, value in self.launch_arguments:
set_launch_configuration_actions.append(SetLaunchConfiguration(name, value))

# Set launch arguments as launch configurations and then include the launch description.
return [*set_launch_configuration_actions, launch_description]
Loading