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 frontend parsing methods for Node, ExecutableInPackage and FindPackage substitution #23

Merged
merged 32 commits into from Jul 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8198671
Add node parsing method and test
ivanpauno Apr 23, 2019
3fbd877
Used parse_substitution in parse Node method
ivanpauno Apr 23, 2019
b93e86d
Corrected node parsing method and test with lastest changes
ivanpauno May 9, 2019
a0de256
Expose ExecutableInPackage substitution
ivanpauno May 10, 2019
9869bbd
Updated node parsing method with typed get_attr change.
ivanpauno May 10, 2019
011f188
Correct param and env parsing in node parsing method
ivanpauno May 14, 2019
1d1559b
Add yaml node example. Reoredered test folders
ivanpauno May 14, 2019
7ff4c2f
Address review comments
ivanpauno May 17, 2019
6c986fa
Allow code reusage with base class parsing method
ivanpauno May 17, 2019
f5f88c5
Parameterize node frontend tests. Add missing test dependencies in pa…
ivanpauno May 20, 2019
55df869
Handle parameters correctly after bb82f311255b02ee9306fc5fd9ccaa0f63f…
ivanpauno Jun 3, 2019
02184a3
Revert "Handle parameters correctly after bb82f311255b02ee9306fc5fd9c…
ivanpauno Jun 3, 2019
6799cdd
Deleted unused node.xml file
ivanpauno Jun 3, 2019
f884836
Parse parameter values as substitutions
ivanpauno Jun 3, 2019
e2e8f7a
Better testing for node parsing
ivanpauno Jun 3, 2019
cd8a01f
Updated after addressing comments in launch PR
ivanpauno Jun 11, 2019
df0b0dd
Address PR comments
ivanpauno Jun 28, 2019
3f67583
Updated after merging launch_frontend into launch
ivanpauno Jul 1, 2019
f58b863
Update after change of get_attr types argument in launch_frontend
ivanpauno Jul 2, 2019
0608922
Updated old import in test
ivanpauno Jul 2, 2019
0a16d4e
Correct param from option parsing. Add find-package substitution. Upd…
ivanpauno Jul 2, 2019
110fb1a
Avoid unnecessary installation of testing file
ivanpauno Jul 3, 2019
b8b6b14
Address PR comments
ivanpauno Jul 3, 2019
009041a
Renamed launch_frontend to launch
ivanpauno Jul 3, 2019
dfc2292
Correct Node parsing method after change in ExecuteProcess
ivanpauno Jul 4, 2019
7dede1f
Add more test cases for loading params
ivanpauno Jul 4, 2019
f202a63
Allow parameter name to be a substitution
ivanpauno Jul 5, 2019
a3ca07f
Rename launch_frontend test folder to frontend
ivanpauno Jul 8, 2019
07f162d
Add new tests for node parameters
ivanpauno Jul 8, 2019
c1c1d6a
Update get_attr argument from types to data_type
ivanpauno Jul 8, 2019
3a95e38
Correct parameters parsing and test
ivanpauno Jul 8, 2019
840b279
Change code style. Delete old comment
ivanpauno Jul 11, 2019
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
87 changes: 85 additions & 2 deletions launch_ros/launch_ros/actions/node.py
Expand Up @@ -25,12 +25,14 @@

from launch.action import Action
from launch.actions import ExecuteProcess
from launch.frontend import Entity
from launch.frontend import expose_action
from launch.frontend import Parser
from launch.launch_context import LaunchContext

import launch.logging

from launch.some_substitutions_type import SomeSubstitutionsType
from launch.substitutions import LocalSubstitution
from launch.substitutions import TextSubstitution
from launch.utilities import ensure_argument_type
from launch.utilities import normalize_to_list_of_substitutions
from launch.utilities import perform_substitutions
Expand All @@ -48,6 +50,7 @@
import yaml


@expose_action('node')
class Node(ExecuteProcess):
"""Action that executes a ROS node."""

Expand Down Expand Up @@ -172,6 +175,86 @@ def __init__(

self.__logger = launch.logging.get_logger(__name__)

@staticmethod
def parse_nested_parameters(params, parser):
"""Normalize parameters as expected by Node constructor argument."""
def get_nested_dictionary_from_nested_key_value_pairs(params):
"""Convert nested params in a nested dictionary."""
param_dict = {}
for param in params:
name = tuple(parser.parse_substitution(param.get_attr('name')))
value = param.get_attr('value', data_type=None, optional=True)
nested_params = param.get_attr('param', data_type=List[Entity], optional=True)
if value is not None and nested_params:
raise RuntimeError('param and value attributes are mutually exclusive')
elif value is not None:
def normalize_scalar_value(value):
if isinstance(value, str):
value = parser.parse_substitution(value)
if len(value) == 1 and isinstance(value[0], TextSubstitution):
value = value[0].text # python `str` are not converted like yaml
return value
if isinstance(value, list):
value = [normalize_scalar_value(x) for x in value]
else:
value = normalize_scalar_value(value)
param_dict[name] = value
elif nested_params:
param_dict.update({
name: get_nested_dictionary_from_nested_key_value_pairs(nested_params)
})
else:
raise RuntimeError('either a value attribute or nested params are needed')
return param_dict

normalized_params = []
params_without_from = []
for param in params:
from_attr = param.get_attr('from', optional=True)
name = param.get_attr('name', optional=True)
if from_attr is not None and name is not None:
raise RuntimeError('name and from attributes are mutually exclusive')
elif from_attr is not None:
# 'from' attribute ignores 'name' attribute,
# it's not accepted to be nested,
# and it can not have children.
normalized_params.append(parser.parse_substitution(from_attr))
continue
elif name is not None:
params_without_from.append(param)
continue
raise ValueError('param Entity should have name or from attribute')
normalized_params.append(
get_nested_dictionary_from_nested_key_value_pairs(params_without_from))
return normalized_params

@classmethod
def parse(cls, entity: Entity, parser: Parser):
"""Parse node."""
# See parse method of `ExecuteProcess`
_, kwargs = super().parse(entity, parser, 'args')
kwargs['arguments'] = kwargs['args']
del kwargs['args']
kwargs['node_name'] = kwargs['name']
del kwargs['name']
kwargs['package'] = parser.parse_substitution(entity.get_attr('package'))
kwargs['node_executable'] = parser.parse_substitution(entity.get_attr('executable'))
ns = entity.get_attr('namespace', optional=True)
if ns is not None:
kwargs['node_namespace'] = parser.parse_substitution(ns)
remappings = entity.get_attr('remap', optional=True)
if remappings is not None:
kwargs['remappings'] = [
(
parser.parse_substitution(remap.get_attr('from')),
parser.parse_substitution(remap.get_attr('to'))
) for remap in remappings
]
parameters = entity.get_attr('param', data_type=List[Entity], optional=True)
if parameters is not None:
kwargs['parameters'] = cls.parse_nested_parameters(parameters, parser)
return cls, kwargs

@property
def node_name(self):
"""Getter for node_name."""
Expand Down
11 changes: 11 additions & 0 deletions launch_ros/launch_ros/substitutions/executable_in_package.py
Expand Up @@ -15,9 +15,11 @@
"""Module for the ExecutableInPackage substitution."""

import os
from typing import Iterable
from typing import List
from typing import Text

from launch.frontend import expose_substitution
from launch.launch_context import LaunchContext
from launch.some_substitutions_type import SomeSubstitutionsType
from launch.substitution import Substitution
Expand All @@ -30,6 +32,7 @@
from .find_package import FindPackage


@expose_substitution('exec-in-package')
class ExecutableInPackage(FindPackage):
"""
Substitution that tries to locate an executable in the libexec directory of a ROS package.
Expand All @@ -47,6 +50,14 @@ def __init__(self, executable: SomeSubstitutionsType, package: SomeSubstitutions
super().__init__(package)
self.__executable = normalize_to_list_of_substitutions(executable)

@classmethod
def parse(cls, data: Iterable[SomeSubstitutionsType]):
"""Parse a ExecutableInPackage substitution."""
if not data or len(data) != 2:
raise AttributeError('exec-in-package substitution expects 2 arguments')
kwargs = {'executable': data[0], 'package': data[1]}
return cls, kwargs

@property
def executable(self) -> List[Substitution]:
"""Getter for executable."""
Expand Down
11 changes: 11 additions & 0 deletions launch_ros/launch_ros/substitutions/find_package.py
Expand Up @@ -14,18 +14,21 @@

"""Module for the FindPackage substitution."""

from typing import Iterable
from typing import List
from typing import Text

from ament_index_python.packages import get_package_prefix

from launch.frontend import expose_substitution
from launch.launch_context import LaunchContext
from launch.some_substitutions_type import SomeSubstitutionsType
from launch.substitution import Substitution
from launch.utilities import normalize_to_list_of_substitutions
from launch.utilities import perform_substitutions


@expose_substitution('find-package')
class FindPackage(Substitution):
"""
Substitution that tries to locate the package prefix of a ROS package.
Expand All @@ -41,6 +44,14 @@ def __init__(self, package: SomeSubstitutionsType) -> None:
super().__init__()
self.__package = normalize_to_list_of_substitutions(package)

@classmethod
def parse(cls, data: Iterable[SomeSubstitutionsType]):
"""Parse a FindPackage substitution."""
if not data or len(data) != 1:
raise AttributeError('find-package substitution expects 1 argument')
kwargs = {'package': data[0]}
return cls, kwargs

@property
def package(self) -> List[Substitution]:
"""Getter for package."""
Expand Down
5 changes: 5 additions & 0 deletions launch_ros/setup.py
Expand Up @@ -32,4 +32,9 @@
),
license='Apache License, Version 2.0',
tests_require=['pytest'],
entry_points={
'launch.frontend.launch_extension': [
'launch_ros = launch_ros',
],
}
)
2 changes: 2 additions & 0 deletions test_launch_ros/package.xml
Expand Up @@ -14,6 +14,8 @@
<test_depend>ament_pep257</test_depend>
<test_depend>demo_nodes_py</test_depend>
<test_depend>launch_ros</test_depend>
<test_depend>launch_xml</test_depend>
<test_depend>launch_yaml</test_depend>
<test_depend>python3-pytest</test_depend>
<test_depend>python3-yaml</test_depend>

Expand Down
5 changes: 5 additions & 0 deletions test_launch_ros/test/test_launch_ros/frontend/params.yaml
@@ -0,0 +1,5 @@
my_ns:
my_node:
ros__parameters:
param_from_file_1: 1
param_from_file_2: 'asd'
143 changes: 143 additions & 0 deletions test_launch_ros/test/test_launch_ros/frontend/test_node_frontend.py
@@ -0,0 +1,143 @@
# Copyright 2019 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.

"""Example of how to parse an xml."""

import io
import pathlib
import textwrap

from launch import LaunchService
from launch.frontend import Parser

from launch_ros.utilities import evaluate_parameters

import pytest

yaml_params = str(pathlib.Path(__file__).parent / 'params.yaml')
xml_file = \
r"""
<launch>
<let name="a_string" value="\'[2, 5, 8]\'"/>
<let name="a_list" value="[2, 5, 8]"/>
<node package="demo_nodes_py" executable="talker_qos" output="screen" name="my_node" namespace="my_ns" args="--number_of_cycles 1">
<param name="param1" value="ads"/>
<param name="param_group1">
<param name="param_group2">
<param name="param2" value="2"/>
</param>
<param name="param3" value="2, 5, 8" value-sep=", "/>
<param name="param4" value="$(var a_list)"/>
<param name="param5" value="$(var a_string)"/>
<param name="param6" value="2., 5., 8." value-sep=", "/>
<param name="param7" value="'2', '5', '8'" value-sep=", "/>
<param name="param8" value="''2'', ''5'', ''8''" value-sep=", "/>
<param name="param9" value="\'2\', \'5\', \'8\'" value-sep=", "/>
<param name="param10" value="''asd'', ''bsd'', ''csd''" value-sep=", "/>
<param name="param11" value="'\asd', '\bsd', '\csd'" value-sep=", "/>
</param>
<param from="{}"/>
<env name="var" value="1"/>
</node>
</launch>
""".format(yaml_params) # noqa: E501
xml_file = textwrap.dedent(xml_file)
yaml_file = \
r"""
launch:
- let:
name: 'a_string'
value: "'[2, 5, 8]'"
- let:
name: 'a_list'
value: '[2, 5, 8]'
- node:
package: demo_nodes_py
executable: talker_qos
output: screen
name: my_node
namespace: my_ns
args: '--number_of_cycles 1'
param:
- name: param1
value: ads
- name: param_group1
param:
- name: param_group2
param:
- name: param2
value: 2
- name: param3
value: [2, 5, 8]
- name: param4
value: $(var a_list)
- name: param5
value: $(var a_string)
- name: param6
value: [2., 5., 8.]
- name: param7
value: ['2', '5', '8']
- name: param8
value: ["'2'", "'5'", "'8'"]
- name: param9
value: ["\\'2\\'", "\\'5\\'", "\\'8\\'"]
- name: param10
value: ["'asd'", "'bsd'", "'csd'"]
- name: param11
value: ['\asd', '\bsd', '\csd']
- from: {}
env:
- name: var
value: '1'
""".format(yaml_params) # noqa: E501
yaml_file = textwrap.dedent(yaml_file)


@pytest.mark.parametrize('file', (xml_file, yaml_file))
def test_node_frontend(file):
"""Parse node xml example."""
root_entity, parser = Parser.load(io.StringIO(file))
ld = parser.parse_description(root_entity)
ls = LaunchService()
ls.include_launch_description(ld)
assert(0 == ls.run())
evaluated_parameters = evaluate_parameters(
ls.context,
ld.describe_sub_entities()[2]._Node__parameters
)
assert isinstance(evaluated_parameters[0], pathlib.Path)
assert isinstance(evaluated_parameters[1], dict)
param_dict = evaluated_parameters[1]
assert 'param1' in param_dict
assert param_dict['param1'] == 'ads'
assert 'param_group1.param_group2.param2' in param_dict
assert 'param_group1.param3' in param_dict
assert 'param_group1.param4' in param_dict
assert 'param_group1.param5' in param_dict
assert 'param_group1.param6' in param_dict
assert 'param_group1.param7' in param_dict
assert 'param_group1.param8' in param_dict
assert 'param_group1.param9' in param_dict
assert 'param_group1.param10' in param_dict
assert 'param_group1.param11' in param_dict
assert param_dict['param_group1.param_group2.param2'] == 2
assert param_dict['param_group1.param3'] == (2, 5, 8)
assert param_dict['param_group1.param4'] == (2, 5, 8)
assert param_dict['param_group1.param5'] == '[2, 5, 8]'
assert param_dict['param_group1.param6'] == (2., 5., 8.)
assert param_dict['param_group1.param7'] == ('2', '5', '8')
assert param_dict['param_group1.param8'] == ("'2'", "'5'", "'8'")
assert param_dict['param_group1.param9'] == ("'2'", "'5'", "'8'")
assert param_dict['param_group1.param10'] == ("'asd'", "'bsd'", "'csd'")
assert param_dict['param_group1.param11'] == ('asd', 'bsd', 'csd')