From 26342498f8a4705d6cc8573830b513723e5821ec Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Fri, 30 Apr 2021 18:10:43 +0900 Subject: [PATCH 01/16] Support container in frontend Signed-off-by: Kenji Miyake --- .../actions/composable_node_container.py | 24 +++++++ .../actions/load_composable_nodes.py | 21 ++++++ .../utilities/parse_composable_node.py | 68 +++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 launch_ros/launch_ros/utilities/parse_composable_node.py diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index bbdb00ee..91ee41f5 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -18,14 +18,19 @@ from typing import Optional from launch.action import Action +from launch.frontend import Entity +from launch.frontend import Parser +from launch.frontend import expose_action from launch.launch_context import LaunchContext from launch.some_substitutions_type import SomeSubstitutionsType from .node import Node from ..descriptions import ComposableNode +from ..utilities.parse_composable_node import parse_composable_node +@expose_action('node_container') class ComposableNodeContainer(Node): """Action that executes a container ROS node for composable ROS nodes.""" @@ -51,6 +56,25 @@ def __init__( super().__init__(name=name, namespace=namespace, **kwargs) self.__composable_node_descriptions = composable_node_descriptions + @classmethod + def parse(cls, entity: Entity, parser: Parser): + """Parse node_container.""" + _, kwargs = super().parse(entity, parser) + + kwargs['package'] = entity.get_attr('pkg', data_type=str) + kwargs['executable'] = entity.get_attr('exec', data_type=str) + kwargs['name'] = entity.get_attr('name', data_type=str) + kwargs['namespace'] = entity.get_attr('namespace', data_type=str) + + composable_nodes = entity.get_attr( + 'composable_node', data_type=List[Entity], optional=True) + if composable_nodes is not None: + kwargs['composable_node_descriptions'] = [ + parse_composable_node(parser, entity) for entity in composable_nodes + ] + + return cls, kwargs + def execute(self, context: LaunchContext) -> Optional[List[Action]]: """ Execute the action. diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index 9f8990bb..9c1bc7df 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -24,6 +24,9 @@ import composition_interfaces.srv from launch.action import Action +from launch.frontend import Entity +from launch.frontend import Parser +from launch.frontend import expose_action from launch.launch_context import LaunchContext import launch.logging from launch.some_substitutions_type import SomeSubstitutionsType @@ -44,8 +47,10 @@ from ..utilities import prefix_namespace from ..utilities import to_parameters_list from ..utilities.normalize_parameters import normalize_parameter_dict +from ..utilities.parse_composable_node import parse_composable_node +@expose_action('load_composable_node') class LoadComposableNodes(Action): """Action that loads composable ROS nodes into a running container.""" @@ -82,6 +87,22 @@ def __init__( self.__final_target_container_name = None # type: Optional[Text] self.__logger = launch.logging.get_logger(__name__) + @classmethod + def parse(cls, entity: Entity, parser: Parser): + """Parse load_composable_node.""" + _, kwargs = super().parse(entity, parser) + + kwargs['target_container'] = entity.get_attr('target', data_type=str) + + composable_nodes = entity.get_attr( + 'composable_node', data_type=List[Entity], optional=True) + if composable_nodes is not None: + kwargs['composable_node_descriptions'] = [ + parse_composable_node(parser, entity) for entity in composable_nodes + ] + + return cls, kwargs + def _load_node( self, request: composition_interfaces.srv.LoadNode.Request, diff --git a/launch_ros/launch_ros/utilities/parse_composable_node.py b/launch_ros/launch_ros/utilities/parse_composable_node.py new file mode 100644 index 00000000..6762b186 --- /dev/null +++ b/launch_ros/launch_ros/utilities/parse_composable_node.py @@ -0,0 +1,68 @@ +# Copyright 2021 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 containing parser for composable_node tag.""" + +from typing import List + +from launch.frontend import Entity +from launch.frontend import Parser +from launch_ros.actions.node import Node +from launch_ros.descriptions import ComposableNode + + +def parse_composable_node(parser: Parser, entity: Entity): + """Parse composable_node""" + + attr_namespace = entity.get_attr('namespace', optional=True) + if attr_namespace is not None: + namespace = parser.parse_substitution(attr_namespace) + else: + namespace = None + + attr_parameters = entity.get_attr('param', data_type=List[Entity], optional=True) + if attr_parameters is not None: + parameters = Node.parse_nested_parameters(attr_parameters, parser) + else: + parameters = None + + attr_remappings = entity.get_attr('remap', data_type=List[Entity], optional=True) + if attr_remappings is not None: + remappings = [ + ( + parser.parse_substitution(remap.get_attr('from')), + parser.parse_substitution(remap.get_attr('to')) + ) for remap in attr_remappings + ] + + for remap in attr_remappings: + remap.assert_entity_completely_parsed() + else: + remappings = None + + attr_extra_arguments = entity.get_attr('extra_arg', data_type=List[Entity], optional=True) + if attr_extra_arguments is not None: + extra_arguments = Node.parse_nested_parameters(attr_extra_arguments, parser) + else: + extra_arguments = None + + return ComposableNode( + package=parser.parse_substitution(entity.get_attr('pkg')), + plugin=parser.parse_substitution(entity.get_attr('plugin')), + name=parser.parse_substitution(entity.get_attr('name')), + namespace=namespace, + parameters=parameters, + remappings=remappings, + extra_arguments=extra_arguments, + ) From 155253e2fbee88613c2afbf84465b73061c2028c Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Fri, 7 May 2021 01:43:16 +0900 Subject: [PATCH 02/16] Add parser.parse_substitutions Signed-off-by: Kenji Miyake --- .../launch_ros/actions/composable_node_container.py | 9 +++++---- launch_ros/launch_ros/actions/load_composable_nodes.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index 91ee41f5..47851e6b 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -61,10 +61,11 @@ def parse(cls, entity: Entity, parser: Parser): """Parse node_container.""" _, kwargs = super().parse(entity, parser) - kwargs['package'] = entity.get_attr('pkg', data_type=str) - kwargs['executable'] = entity.get_attr('exec', data_type=str) - kwargs['name'] = entity.get_attr('name', data_type=str) - kwargs['namespace'] = entity.get_attr('namespace', data_type=str) + kwargs['package'] = parser.parse_substitution(entity.get_attr('pkg', data_type=str)) + kwargs['executable'] = parser.parse_substitution(entity.get_attr('exec', data_type=str)) + kwargs['name'] = parser.parse_substitution(entity.get_attr('name', data_type=str)) + kwargs['namespace'] = parser.parse_substitution( + entity.get_attr('namespace', data_type=str)) composable_nodes = entity.get_attr( 'composable_node', data_type=List[Entity], optional=True) diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index 9c1bc7df..db620561 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -92,7 +92,7 @@ def parse(cls, entity: Entity, parser: Parser): """Parse load_composable_node.""" _, kwargs = super().parse(entity, parser) - kwargs['target_container'] = entity.get_attr('target', data_type=str) + kwargs['target_container'] = parser.parse_substitution(entity.get_attr('target', data_type=str)) composable_nodes = entity.get_attr( 'composable_node', data_type=List[Entity], optional=True) From 0eceb088fc2cf7bf5232128d7d13d7647dcabcca Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Fri, 7 May 2021 14:50:32 +0900 Subject: [PATCH 03/16] Fix import order Signed-off-by: Kenji Miyake --- launch_ros/launch_ros/actions/composable_node_container.py | 2 +- launch_ros/launch_ros/actions/load_composable_nodes.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index 47851e6b..ad242e8f 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -19,8 +19,8 @@ from launch.action import Action from launch.frontend import Entity -from launch.frontend import Parser from launch.frontend import expose_action +from launch.frontend import Parser from launch.launch_context import LaunchContext from launch.some_substitutions_type import SomeSubstitutionsType diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index db620561..25c9df93 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -25,8 +25,8 @@ from launch.action import Action from launch.frontend import Entity -from launch.frontend import Parser 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 @@ -92,7 +92,8 @@ def parse(cls, entity: Entity, parser: Parser): """Parse load_composable_node.""" _, kwargs = super().parse(entity, parser) - kwargs['target_container'] = parser.parse_substitution(entity.get_attr('target', data_type=str)) + kwargs['target_container'] = parser.parse_substitution( + entity.get_attr('target', data_type=str)) composable_nodes = entity.get_attr( 'composable_node', data_type=List[Entity], optional=True) From 224813880672461f74e1b98c15438357c486b807 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Fri, 7 May 2021 16:40:02 +0900 Subject: [PATCH 04/16] Remove optional of composable_node in load_composable_node Signed-off-by: Kenji Miyake --- launch_ros/launch_ros/actions/load_composable_nodes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index 25c9df93..9d9bb81a 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -95,8 +95,7 @@ def parse(cls, entity: Entity, parser: Parser): kwargs['target_container'] = parser.parse_substitution( entity.get_attr('target', data_type=str)) - composable_nodes = entity.get_attr( - 'composable_node', data_type=List[Entity], optional=True) + composable_nodes = entity.get_attr('composable_node', data_type=List[Entity]) if composable_nodes is not None: kwargs['composable_node_descriptions'] = [ parse_composable_node(parser, entity) for entity in composable_nodes From b92e4c41c7eba226696b174b827b15ed15e5f264 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Fri, 7 May 2021 16:49:52 +0900 Subject: [PATCH 05/16] Remove unnecessary lines Signed-off-by: Kenji Miyake --- launch_ros/launch_ros/actions/composable_node_container.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index ad242e8f..b9379e97 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -61,12 +61,6 @@ def parse(cls, entity: Entity, parser: Parser): """Parse node_container.""" _, kwargs = super().parse(entity, parser) - kwargs['package'] = parser.parse_substitution(entity.get_attr('pkg', data_type=str)) - kwargs['executable'] = parser.parse_substitution(entity.get_attr('exec', data_type=str)) - kwargs['name'] = parser.parse_substitution(entity.get_attr('name', data_type=str)) - kwargs['namespace'] = parser.parse_substitution( - entity.get_attr('namespace', data_type=str)) - composable_nodes = entity.get_attr( 'composable_node', data_type=List[Entity], optional=True) if composable_nodes is not None: From 498469b618131b63cce59df78f9dbd32f70cde90 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Fri, 7 May 2021 17:19:57 +0900 Subject: [PATCH 06/16] Return (cls, kwargs) Signed-off-by: Kenji Miyake --- .../actions/composable_node_container.py | 5 +- .../actions/load_composable_nodes.py | 5 +- .../utilities/parse_composable_node.py | 53 ++++++++----------- 3 files changed, 25 insertions(+), 38 deletions(-) diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index b9379e97..17788e86 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -64,9 +64,8 @@ def parse(cls, entity: Entity, parser: Parser): composable_nodes = entity.get_attr( 'composable_node', data_type=List[Entity], optional=True) if composable_nodes is not None: - kwargs['composable_node_descriptions'] = [ - parse_composable_node(parser, entity) for entity in composable_nodes - ] + parsed_tuples = [parse_composable_node(parser, entity) for entity in composable_nodes] + kwargs['composable_node_descriptions'] = [t[0](**t[1]) for t in parsed_tuples] return cls, kwargs diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index 9d9bb81a..e4be76a9 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -97,9 +97,8 @@ def parse(cls, entity: Entity, parser: Parser): composable_nodes = entity.get_attr('composable_node', data_type=List[Entity]) if composable_nodes is not None: - kwargs['composable_node_descriptions'] = [ - parse_composable_node(parser, entity) for entity in composable_nodes - ] + parsed_tuples = [parse_composable_node(parser, entity) for entity in composable_nodes] + kwargs['composable_node_descriptions'] = [t[0](**t[1]) for t in parsed_tuples] return cls, kwargs diff --git a/launch_ros/launch_ros/utilities/parse_composable_node.py b/launch_ros/launch_ros/utilities/parse_composable_node.py index 6762b186..24aa463d 100644 --- a/launch_ros/launch_ros/utilities/parse_composable_node.py +++ b/launch_ros/launch_ros/utilities/parse_composable_node.py @@ -23,46 +23,35 @@ def parse_composable_node(parser: Parser, entity: Entity): - """Parse composable_node""" + """Parse composable_node.""" + kwargs = {} - attr_namespace = entity.get_attr('namespace', optional=True) - if attr_namespace is not None: - namespace = parser.parse_substitution(attr_namespace) - else: - namespace = None + kwargs['package'] = parser.parse_substitution(entity.get_attr('pkg')) + kwargs['plugin'] = parser.parse_substitution(entity.get_attr('plugin')) + kwargs['name'] = parser.parse_substitution(entity.get_attr('name')) - attr_parameters = entity.get_attr('param', data_type=List[Entity], optional=True) - if attr_parameters is not None: - parameters = Node.parse_nested_parameters(attr_parameters, parser) - else: - parameters = None + namespace = entity.get_attr('namespace', optional=True) + if namespace is not None: + kwargs['namespace'] = parser.parse_substitution(namespace) - attr_remappings = entity.get_attr('remap', data_type=List[Entity], optional=True) - if attr_remappings is not None: - remappings = [ + parameters = entity.get_attr('param', data_type=List[Entity], optional=True) + if parameters is not None: + kwargs['parameters'] = Node.parse_nested_parameters(parameters, parser) + + remappings = entity.get_attr('remap', data_type=List[Entity], 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 attr_remappings + ) for remap in remappings ] - for remap in attr_remappings: + for remap in remappings: remap.assert_entity_completely_parsed() - else: - remappings = None - attr_extra_arguments = entity.get_attr('extra_arg', data_type=List[Entity], optional=True) - if attr_extra_arguments is not None: - extra_arguments = Node.parse_nested_parameters(attr_extra_arguments, parser) - else: - extra_arguments = None + extra_arguments = entity.get_attr('extra_arg', data_type=List[Entity], optional=True) + if extra_arguments is not None: + kwargs['extra_arguments'] = Node.parse_nested_parameters(extra_arguments, parser) - return ComposableNode( - package=parser.parse_substitution(entity.get_attr('pkg')), - plugin=parser.parse_substitution(entity.get_attr('plugin')), - name=parser.parse_substitution(entity.get_attr('name')), - namespace=namespace, - parameters=parameters, - remappings=remappings, - extra_arguments=extra_arguments, - ) + return (ComposableNode, kwargs) From c119e94f5fa308bc873b89541743f743a621122e Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Sat, 8 May 2021 03:15:52 +0900 Subject: [PATCH 07/16] Move parse_composable_node to ComposableNode class Signed-off-by: Kenji Miyake --- .../actions/composable_node_container.py | 4 +- .../actions/load_composable_nodes.py | 4 +- .../descriptions/composable_node.py | 38 +++++++++++++ .../utilities/parse_composable_node.py | 57 ------------------- 4 files changed, 42 insertions(+), 61 deletions(-) delete mode 100644 launch_ros/launch_ros/utilities/parse_composable_node.py diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index 17788e86..98643d2b 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -27,7 +27,6 @@ from .node import Node from ..descriptions import ComposableNode -from ..utilities.parse_composable_node import parse_composable_node @expose_action('node_container') @@ -64,7 +63,8 @@ def parse(cls, entity: Entity, parser: Parser): composable_nodes = entity.get_attr( 'composable_node', data_type=List[Entity], optional=True) if composable_nodes is not None: - parsed_tuples = [parse_composable_node(parser, entity) for entity in composable_nodes] + parsed_tuples = [ComposableNode.parse_composable_node( + parser, entity) for entity in composable_nodes] kwargs['composable_node_descriptions'] = [t[0](**t[1]) for t in parsed_tuples] return cls, kwargs diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index e4be76a9..c63f9eb1 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -47,7 +47,6 @@ from ..utilities import prefix_namespace from ..utilities import to_parameters_list from ..utilities.normalize_parameters import normalize_parameter_dict -from ..utilities.parse_composable_node import parse_composable_node @expose_action('load_composable_node') @@ -97,7 +96,8 @@ def parse(cls, entity: Entity, parser: Parser): composable_nodes = entity.get_attr('composable_node', data_type=List[Entity]) if composable_nodes is not None: - parsed_tuples = [parse_composable_node(parser, entity) for entity in composable_nodes] + parsed_tuples = [ComposableNode.parse_composable_node( + parser, entity) for entity in composable_nodes] kwargs['composable_node_descriptions'] = [t[0](**t[1]) for t in parsed_tuples] return cls, kwargs diff --git a/launch_ros/launch_ros/descriptions/composable_node.py b/launch_ros/launch_ros/descriptions/composable_node.py index c2fa4abc..5a03dffb 100644 --- a/launch_ros/launch_ros/descriptions/composable_node.py +++ b/launch_ros/launch_ros/descriptions/composable_node.py @@ -17,6 +17,8 @@ from typing import List from typing import Optional +from launch.frontend import Entity +from launch.frontend import Parser from launch.some_substitutions_type import SomeSubstitutionsType from launch.substitution import Substitution # from launch.utilities import ensure_argument_type @@ -110,3 +112,39 @@ def remappings(self) -> Optional[RemapRules]: def extra_arguments(self) -> Optional[Parameters]: """Get container extra arguments YAML files or dicts with substitutions to be performed.""" return self.__extra_arguments + + @staticmethod + def parse_composable_node(parser: Parser, entity: Entity): + """Parse composable_node.""" + from launch_ros.actions import Node + kwargs = {} + + kwargs['package'] = parser.parse_substitution(entity.get_attr('pkg')) + kwargs['plugin'] = parser.parse_substitution(entity.get_attr('plugin')) + kwargs['name'] = parser.parse_substitution(entity.get_attr('name')) + + namespace = entity.get_attr('namespace', optional=True) + if namespace is not None: + kwargs['namespace'] = parser.parse_substitution(namespace) + + parameters = entity.get_attr('param', data_type=List[Entity], optional=True) + if parameters is not None: + kwargs['parameters'] = Node.parse_nested_parameters(parameters, parser) + + remappings = entity.get_attr('remap', data_type=List[Entity], 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 + ] + + for remap in remappings: + remap.assert_entity_completely_parsed() + + extra_arguments = entity.get_attr('extra_arg', data_type=List[Entity], optional=True) + if extra_arguments is not None: + kwargs['extra_arguments'] = Node.parse_nested_parameters(extra_arguments, parser) + + return (ComposableNode, kwargs) diff --git a/launch_ros/launch_ros/utilities/parse_composable_node.py b/launch_ros/launch_ros/utilities/parse_composable_node.py deleted file mode 100644 index 24aa463d..00000000 --- a/launch_ros/launch_ros/utilities/parse_composable_node.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2021 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 containing parser for composable_node tag.""" - -from typing import List - -from launch.frontend import Entity -from launch.frontend import Parser -from launch_ros.actions.node import Node -from launch_ros.descriptions import ComposableNode - - -def parse_composable_node(parser: Parser, entity: Entity): - """Parse composable_node.""" - kwargs = {} - - kwargs['package'] = parser.parse_substitution(entity.get_attr('pkg')) - kwargs['plugin'] = parser.parse_substitution(entity.get_attr('plugin')) - kwargs['name'] = parser.parse_substitution(entity.get_attr('name')) - - namespace = entity.get_attr('namespace', optional=True) - if namespace is not None: - kwargs['namespace'] = parser.parse_substitution(namespace) - - parameters = entity.get_attr('param', data_type=List[Entity], optional=True) - if parameters is not None: - kwargs['parameters'] = Node.parse_nested_parameters(parameters, parser) - - remappings = entity.get_attr('remap', data_type=List[Entity], 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 - ] - - for remap in remappings: - remap.assert_entity_completely_parsed() - - extra_arguments = entity.get_attr('extra_arg', data_type=List[Entity], optional=True) - if extra_arguments is not None: - kwargs['extra_arguments'] = Node.parse_nested_parameters(extra_arguments, parser) - - return (ComposableNode, kwargs) From b2f62e6c2ef25cf95a497a66788aae9f6cf67a20 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Sat, 8 May 2021 03:20:16 +0900 Subject: [PATCH 08/16] Change parse to classmethod Signed-off-by: Kenji Miyake --- .../actions/composable_node_container.py | 3 +- .../actions/load_composable_nodes.py | 3 +- .../descriptions/composable_node.py | 72 +++++++++---------- 3 files changed, 38 insertions(+), 40 deletions(-) diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index 98643d2b..61f0b1ba 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -63,8 +63,7 @@ def parse(cls, entity: Entity, parser: Parser): composable_nodes = entity.get_attr( 'composable_node', data_type=List[Entity], optional=True) if composable_nodes is not None: - parsed_tuples = [ComposableNode.parse_composable_node( - parser, entity) for entity in composable_nodes] + parsed_tuples = [ComposableNode.parse(parser, entity) for entity in composable_nodes] kwargs['composable_node_descriptions'] = [t[0](**t[1]) for t in parsed_tuples] return cls, kwargs diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index c63f9eb1..36f4083b 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -96,8 +96,7 @@ def parse(cls, entity: Entity, parser: Parser): composable_nodes = entity.get_attr('composable_node', data_type=List[Entity]) if composable_nodes is not None: - parsed_tuples = [ComposableNode.parse_composable_node( - parser, entity) for entity in composable_nodes] + parsed_tuples = [ComposableNode.parse(parser, entity) for entity in composable_nodes] kwargs['composable_node_descriptions'] = [t[0](**t[1]) for t in parsed_tuples] return cls, kwargs diff --git a/launch_ros/launch_ros/descriptions/composable_node.py b/launch_ros/launch_ros/descriptions/composable_node.py index 5a03dffb..cf0277cc 100644 --- a/launch_ros/launch_ros/descriptions/composable_node.py +++ b/launch_ros/launch_ros/descriptions/composable_node.py @@ -78,6 +78,42 @@ def __init__( if extra_arguments: self.__extra_arguments = normalize_parameters(extra_arguments) + @classmethod + def parse(cls, parser: Parser, entity: Entity): + """Parse composable_node.""" + from launch_ros.actions import Node + kwargs = {} + + kwargs['package'] = parser.parse_substitution(entity.get_attr('pkg')) + kwargs['plugin'] = parser.parse_substitution(entity.get_attr('plugin')) + kwargs['name'] = parser.parse_substitution(entity.get_attr('name')) + + namespace = entity.get_attr('namespace', optional=True) + if namespace is not None: + kwargs['namespace'] = parser.parse_substitution(namespace) + + parameters = entity.get_attr('param', data_type=List[Entity], optional=True) + if parameters is not None: + kwargs['parameters'] = Node.parse_nested_parameters(parameters, parser) + + remappings = entity.get_attr('remap', data_type=List[Entity], 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 + ] + + for remap in remappings: + remap.assert_entity_completely_parsed() + + extra_arguments = entity.get_attr('extra_arg', data_type=List[Entity], optional=True) + if extra_arguments is not None: + kwargs['extra_arguments'] = Node.parse_nested_parameters(extra_arguments, parser) + + return (ComposableNode, kwargs) + @property def package(self) -> List[Substitution]: """Get node package name as a sequence of substitutions to be performed.""" @@ -112,39 +148,3 @@ def remappings(self) -> Optional[RemapRules]: def extra_arguments(self) -> Optional[Parameters]: """Get container extra arguments YAML files or dicts with substitutions to be performed.""" return self.__extra_arguments - - @staticmethod - def parse_composable_node(parser: Parser, entity: Entity): - """Parse composable_node.""" - from launch_ros.actions import Node - kwargs = {} - - kwargs['package'] = parser.parse_substitution(entity.get_attr('pkg')) - kwargs['plugin'] = parser.parse_substitution(entity.get_attr('plugin')) - kwargs['name'] = parser.parse_substitution(entity.get_attr('name')) - - namespace = entity.get_attr('namespace', optional=True) - if namespace is not None: - kwargs['namespace'] = parser.parse_substitution(namespace) - - parameters = entity.get_attr('param', data_type=List[Entity], optional=True) - if parameters is not None: - kwargs['parameters'] = Node.parse_nested_parameters(parameters, parser) - - remappings = entity.get_attr('remap', data_type=List[Entity], 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 - ] - - for remap in remappings: - remap.assert_entity_completely_parsed() - - extra_arguments = entity.get_attr('extra_arg', data_type=List[Entity], optional=True) - if extra_arguments is not None: - kwargs['extra_arguments'] = Node.parse_nested_parameters(extra_arguments, parser) - - return (ComposableNode, kwargs) From 88e894bd311b4ec9e0848df8abc12db6f8ff3483 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Tue, 11 May 2021 11:54:52 +0900 Subject: [PATCH 09/16] Fix return style Signed-off-by: Kenji Miyake --- launch_ros/launch_ros/descriptions/composable_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launch_ros/launch_ros/descriptions/composable_node.py b/launch_ros/launch_ros/descriptions/composable_node.py index cf0277cc..9edc1cea 100644 --- a/launch_ros/launch_ros/descriptions/composable_node.py +++ b/launch_ros/launch_ros/descriptions/composable_node.py @@ -112,7 +112,7 @@ def parse(cls, parser: Parser, entity: Entity): if extra_arguments is not None: kwargs['extra_arguments'] = Node.parse_nested_parameters(extra_arguments, parser) - return (ComposableNode, kwargs) + return cls, kwargs @property def package(self) -> List[Substitution]: From cb67451b93e8718b75f19a84fe3175d07d0510d7 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Tue, 11 May 2021 13:11:23 +0900 Subject: [PATCH 10/16] Improve readability Signed-off-by: Kenji Miyake --- .../launch_ros/actions/composable_node_container.py | 7 +++++-- launch_ros/launch_ros/actions/load_composable_nodes.py | 8 +++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index 61f0b1ba..32fbbcbf 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -63,8 +63,11 @@ def parse(cls, entity: Entity, parser: Parser): composable_nodes = entity.get_attr( 'composable_node', data_type=List[Entity], optional=True) if composable_nodes is not None: - parsed_tuples = [ComposableNode.parse(parser, entity) for entity in composable_nodes] - kwargs['composable_node_descriptions'] = [t[0](**t[1]) for t in parsed_tuples] + kwargs['composable_node_descriptions'] = [] + for entity in composable_nodes: + _, composable_node_kwargs = ComposableNode.parse(parser, entity) + kwargs['composable_node_descriptions'].append( + ComposableNode(**composable_node_kwargs)) return cls, kwargs diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index 36f4083b..bb6089da 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -95,9 +95,11 @@ def parse(cls, entity: Entity, parser: Parser): entity.get_attr('target', data_type=str)) composable_nodes = entity.get_attr('composable_node', data_type=List[Entity]) - if composable_nodes is not None: - parsed_tuples = [ComposableNode.parse(parser, entity) for entity in composable_nodes] - kwargs['composable_node_descriptions'] = [t[0](**t[1]) for t in parsed_tuples] + kwargs['composable_node_descriptions'] = [] + for entity in composable_nodes: + _, composable_node_kwargs = ComposableNode.parse(parser, entity) + kwargs['composable_node_descriptions'].append( + ComposableNode(**composable_node_kwargs)) return cls, kwargs From b1412a09080e0b3eefc03e75796282f7e9fe08c0 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Tue, 11 May 2021 13:43:14 +0900 Subject: [PATCH 11/16] Add assert_entity_completely_parsed Signed-off-by: Kenji Miyake --- launch_ros/launch_ros/actions/composable_node_container.py | 1 + launch_ros/launch_ros/actions/load_composable_nodes.py | 1 + 2 files changed, 2 insertions(+) diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index 32fbbcbf..62e6b787 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -68,6 +68,7 @@ def parse(cls, entity: Entity, parser: Parser): _, composable_node_kwargs = ComposableNode.parse(parser, entity) kwargs['composable_node_descriptions'].append( ComposableNode(**composable_node_kwargs)) + entity.assert_entity_completely_parsed() return cls, kwargs diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index bb6089da..c182bdef 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -100,6 +100,7 @@ def parse(cls, entity: Entity, parser: Parser): _, composable_node_kwargs = ComposableNode.parse(parser, entity) kwargs['composable_node_descriptions'].append( ComposableNode(**composable_node_kwargs)) + entity.assert_entity_completely_parsed() return cls, kwargs From da1f68c72e336e091d9a9e2d6650ff05b9faa1de Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Wed, 12 May 2021 00:45:42 +0900 Subject: [PATCH 12/16] Fix parse of extra_arguments Signed-off-by: Kenji Miyake --- .../launch_ros/actions/composable_node_container.py | 1 - .../launch_ros/actions/load_composable_nodes.py | 1 - .../launch_ros/descriptions/composable_node.py | 12 +++++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index 62e6b787..32fbbcbf 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -68,7 +68,6 @@ def parse(cls, entity: Entity, parser: Parser): _, composable_node_kwargs = ComposableNode.parse(parser, entity) kwargs['composable_node_descriptions'].append( ComposableNode(**composable_node_kwargs)) - entity.assert_entity_completely_parsed() return cls, kwargs diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index c182bdef..bb6089da 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -100,7 +100,6 @@ def parse(cls, entity: Entity, parser: Parser): _, composable_node_kwargs = ComposableNode.parse(parser, entity) kwargs['composable_node_descriptions'].append( ComposableNode(**composable_node_kwargs)) - entity.assert_entity_completely_parsed() return cls, kwargs diff --git a/launch_ros/launch_ros/descriptions/composable_node.py b/launch_ros/launch_ros/descriptions/composable_node.py index 9edc1cea..7f1bcc61 100644 --- a/launch_ros/launch_ros/descriptions/composable_node.py +++ b/launch_ros/launch_ros/descriptions/composable_node.py @@ -110,7 +110,17 @@ def parse(cls, parser: Parser, entity: Entity): extra_arguments = entity.get_attr('extra_arg', data_type=List[Entity], optional=True) if extra_arguments is not None: - kwargs['extra_arguments'] = Node.parse_nested_parameters(extra_arguments, parser) + kwargs['extra_arguments'] = [ + { + tuple(parser.parse_substitution(extra_arg.get_attr('name'))): + parser.parse_substitution(extra_arg.get_attr('value')) + } for extra_arg in extra_arguments + ] + + for extra_arg in extra_arguments: + extra_arg.assert_entity_completely_parsed() + + entity.assert_entity_completely_parsed() return cls, kwargs From c7accb512e0540711c68ed6a5973615d97144a97 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Wed, 12 May 2021 12:26:29 +0900 Subject: [PATCH 13/16] Use returned class object Signed-off-by: Kenji Miyake --- launch_ros/launch_ros/actions/composable_node_container.py | 4 ++-- launch_ros/launch_ros/actions/load_composable_nodes.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index 32fbbcbf..d054d85b 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -65,9 +65,9 @@ def parse(cls, entity: Entity, parser: Parser): if composable_nodes is not None: kwargs['composable_node_descriptions'] = [] for entity in composable_nodes: - _, composable_node_kwargs = ComposableNode.parse(parser, entity) + composable_node_cls, composable_node_kwargs = ComposableNode.parse(parser, entity) kwargs['composable_node_descriptions'].append( - ComposableNode(**composable_node_kwargs)) + composable_node_cls(**composable_node_kwargs)) return cls, kwargs diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index bb6089da..fe6241e0 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -97,9 +97,9 @@ def parse(cls, entity: Entity, parser: Parser): composable_nodes = entity.get_attr('composable_node', data_type=List[Entity]) kwargs['composable_node_descriptions'] = [] for entity in composable_nodes: - _, composable_node_kwargs = ComposableNode.parse(parser, entity) + composable_node_cls, composable_node_kwargs = ComposableNode.parse(parser, entity) kwargs['composable_node_descriptions'].append( - ComposableNode(**composable_node_kwargs)) + composable_node_cls(**composable_node_kwargs)) return cls, kwargs From 7c5ecb91908cb1a1ccf4880d09116295b816c0eb Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Mon, 7 Jun 2021 03:39:29 +0900 Subject: [PATCH 14/16] Add test_component_container.py Signed-off-by: Kenji Miyake --- .../frontend/test_component_container.py | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 test_launch_ros/test/test_launch_ros/frontend/test_component_container.py diff --git a/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py b/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py new file mode 100644 index 00000000..d7a6ceb7 --- /dev/null +++ b/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py @@ -0,0 +1,161 @@ +# Copyright 2019 Open Source Robotics Foundation, Inc. +# Copyright 2020 Open Avatar 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 asyncio +import io +import textwrap + +import osrf_pycommon.process_utils +from launch import LaunchService +from launch.frontend import Parser +from launch_ros.utilities import evaluate_parameters +from launch.utilities import perform_substitutions + + +def test_launch_component_container_yaml(): + yaml_file = textwrap.dedent( + r""" + launch: + - node_container: + pkg: rclcpp_components + exec: component_container + name: my_container + namespace: '' + args: 'test_args' + composable_node: + - pkg: composition + plugin: composition::Talker + name: talker + namespace: test_namespace + remap: + - from: chatter + to: /remap/chatter + param: + - name: use_sim_time + value: true + extra_arg: + - name: use_intra_process_comms + value: 'true' + + - load_composable_node: + target: my_container + composable_node: + - pkg: composition + plugin: composition::Listener + name: listener + namespace: test_namespace + remap: + - from: chatter + to: /remap/chatter + param: + - name: use_sim_time + value: true + extra_arg: + - name: use_intra_process_comms + value: 'true' + """ + ) + with io.StringIO(yaml_file) as f: + check_launch_namespace(f) + + +def test_launch_component_container_xml(): + xml_file = textwrap.dedent( + r""" + + + + + + + + + + + + + + + + + + """ + ) + with io.StringIO(xml_file) as f: + check_launch_namespace(f) + + +def check_launch_namespace(file): + root_entity, parser = Parser.load(file) + ld = parser.parse_description(root_entity) + ls = LaunchService() + ls.include_launch_description(ld) + + loop = osrf_pycommon.process_utils.get_loop() + launch_task = loop.create_task(ls.run_async()) + + node_container, load_composable_node = ld.describe_sub_entities() + talker = node_container._ComposableNodeContainer__composable_node_descriptions[0] + listener = load_composable_node._LoadComposableNodes__composable_node_descriptions[0] + + def perform(substitution): + return perform_substitutions(ls.context, substitution) + + # Check container params + assert perform(node_container._Node__package) == 'rclcpp_components' + assert perform(node_container._Node__node_executable) == 'component_container' + assert perform(node_container._Node__node_name) == 'my_container' + assert perform(node_container._Node__node_namespace) == '' + assert perform(node_container._Node__arguments[0]) == 'test_args' + + assert perform(load_composable_node._LoadComposableNodes__target_container) == 'my_container' + + # Check node parameters + talker_remappings = list(talker._ComposableNode__remappings) + listener_remappings = list(listener._ComposableNode__remappings) + + talker_params = evaluate_parameters(ls.context, talker._ComposableNode__parameters) + listener_params = evaluate_parameters(ls.context, listener._ComposableNode__parameters) + + talker_extra_args = evaluate_parameters(ls.context, talker._ComposableNode__extra_arguments) + listener_extra_args = evaluate_parameters( + ls.context, listener._ComposableNode__extra_arguments) + + assert perform(talker._ComposableNode__package) == 'composition' + assert perform(talker._ComposableNode__node_plugin) == 'composition::Talker' + assert perform(talker._ComposableNode__node_name) == 'talker' + assert perform(talker._ComposableNode__node_namespace) == 'test_namespace' + assert (perform(talker_remappings[0][0]), + perform(talker_remappings[0][1])) == ('chatter', '/remap/chatter') + assert talker_params[0]['use_sim_time'] is True + + assert perform(listener._ComposableNode__package) == 'composition' + assert perform(listener._ComposableNode__node_plugin) == 'composition::Listener' + assert perform(listener._ComposableNode__node_name) == 'listener' + assert perform(listener._ComposableNode__node_namespace) == 'test_namespace' + assert (perform(listener_remappings[0][0]), + perform(listener_remappings[0][1])) == ('chatter', '/remap/chatter') + assert listener_params[0]['use_sim_time'] is True + + # Check extra arguments + assert talker_extra_args[0]['use_intra_process_comms'] is True + assert listener_extra_args[0]['use_intra_process_comms'] is True + + timeout_sec = 5 + loop.run_until_complete(asyncio.sleep(timeout_sec)) + if not launch_task.done(): + loop.create_task(ls.shutdown()) + loop.run_until_complete(launch_task) + assert 0 == launch_task.result() From 9fa3cd361ddf5c0ea82fbf7405c915c890d816f8 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Mon, 7 Jun 2021 04:03:31 +0900 Subject: [PATCH 15/16] Fix lint Signed-off-by: Kenji Miyake --- .../test_launch_ros/frontend/test_component_container.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py b/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py index d7a6ceb7..c29160ef 100644 --- a/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py +++ b/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py @@ -17,11 +17,11 @@ import io import textwrap -import osrf_pycommon.process_utils from launch import LaunchService from launch.frontend import Parser -from launch_ros.utilities import evaluate_parameters from launch.utilities import perform_substitutions +from launch_ros.utilities import evaluate_parameters +import osrf_pycommon.process_utils def test_launch_component_container_yaml(): @@ -91,7 +91,7 @@ def test_launch_component_container_xml(): - """ + """ # noqa: E501 ) with io.StringIO(xml_file) as f: check_launch_namespace(f) From 440dadc10da4745c784cc2889490cdaf53b46eb1 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Mon, 7 Jun 2021 22:27:22 +0900 Subject: [PATCH 16/16] Fix function name Signed-off-by: Kenji Miyake --- .../test_launch_ros/frontend/test_component_container.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py b/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py index c29160ef..0ed30485 100644 --- a/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py +++ b/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py @@ -68,7 +68,7 @@ def test_launch_component_container_yaml(): """ ) with io.StringIO(yaml_file) as f: - check_launch_namespace(f) + check_launch_component_container(f) def test_launch_component_container_xml(): @@ -94,10 +94,10 @@ def test_launch_component_container_xml(): """ # noqa: E501 ) with io.StringIO(xml_file) as f: - check_launch_namespace(f) + check_launch_component_container(f) -def check_launch_namespace(file): +def check_launch_component_container(file): root_entity, parser = Parser.load(file) ld = parser.parse_description(root_entity) ls = LaunchService()