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 22 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 launch with ``ros2 launch ...``.
Copy link
Member

Choose a reason for hiding this comment

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

launched


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)
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]
66 changes: 65 additions & 1 deletion launch/launch/launch_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from typing import Optional

from .action import Action
from .actions import DeclareLaunchArgument
from .launch_context import LaunchContext
from .launch_description_entity import LaunchDescriptionEntity

Expand All @@ -29,11 +30,21 @@ class LaunchDescription(LaunchDescriptionEntity):

The description is expressed by a collection of entities which represent
the system architect's intentions.

The description may also have arguments, which are declared by
:py:class:`launch.actions.DeclareLaunchArgument` actions within this
launch description.

Arguments for this description may be accessed via the
:py:meth:`get_launch_arguments` method.
The arguments are gathered by searching through the entities in this
launch description and the descriptions of each entity (which may include
entities yielded by those entities).
"""

def __init__(
self,
initial_entities: Optional[Iterable[LaunchDescriptionEntity]] = None
initial_entities: Optional[Iterable[LaunchDescriptionEntity]] = None,
) -> None:
"""Constructor."""
self.__entities = list(initial_entities) if initial_entities is not None else []
Expand All @@ -42,6 +53,59 @@ def visit(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEntity
"""Override visit from LaunchDescriptionEntity to visit contained entities."""
return self.__entities

def describe_sub_entities(self) -> List[LaunchDescriptionEntity]:
"""Override describe_sub_entities from LaunchDescriptionEntity to return sub entities."""
return self.__entities

def get_launch_arguments(self, conditional_inclusion=False) -> List[DeclareLaunchArgument]:
"""
Return a list of :py:class:`launch.actions.DeclareLaunchArgument` actions.

This list is generated (never cached) by searching through this launch
description for any instances of the action that declares launch
arguments.

It will use :py:meth:`launch.LaunchDescriptionEntity.describe_sub_entities`
and :py:meth:`launch.LaunchDescriptionEntity.describe_conditional_sub_entities`
in order to discover as many instances of the declare launch argument
actions as is possible.
Also, specifically in the case of the
:py:class:`launch.actions.IncludeLaunchDescription` action, the method
:py:meth:`launch.LaunchDescriptionSource.try_get_launch_description_without_context`
is used to attempt to load launch descriptions without the "runtime"
context available.
This function may fail, e.g. if the path to the launch file to include
uses the values of launch configurations that have not been set yet,
and in that case the failure is ignored and the arugments defined in
those launch files will not be seen either.

Duplicate declarations of an argument are ignored, therefore the
default value and description from the first instance of the argument
declaration is used.
"""
declared_launch_arguments = [] # type: List[DeclareLaunchArgument]

def process_entities(entities, *, _conditional_inclusion):
for entity in entities:
if isinstance(entity, DeclareLaunchArgument):
# Avoid duplicate entries with the same name.
if entity.name in [e.name for e in declared_launch_arguments]:
continue
# Stuff this contextual information into the class for
# potential use in command-line descriptions or errors.
entity._conditionally_included = _conditional_inclusion
entity._conditionally_included |= entity.condition is not None
declared_launch_arguments.append(entity)
else:
process_entities(
entity.describe_sub_entities(), _conditional_inclusion=False)
process_entities(
entity.describe_conditional_sub_entities(), _conditional_inclusion=True)

process_entities(self.entities, _conditional_inclusion=conditional_inclusion)

return declared_launch_arguments

@property
def entities(self) -> List[LaunchDescriptionEntity]:
"""Getter for the entities."""
Expand Down
9 changes: 9 additions & 0 deletions launch/launch/launch_description_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ def __init__(
self.__location = location
self.__method = method

def try_get_launch_description_without_context(self) -> Optional[LaunchDescription]:
"""
Attempt to load the LaunchDescription without a context, return None if unsuccessful.

This method is useful for trying to introspect the included launch
description without visiting the user of this source.
"""
return self.__launch_description

def get_launch_description(self, context: LaunchContext) -> LaunchDescription:
"""Get the LaunchDescription, loading it if necessary."""
if self.__launch_description is None:
Expand Down
Loading