Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion launch/launch/actions/include_launch_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def __init__(
def parse(cls, entity: Entity, parser: Parser):
"""Return `IncludeLaunchDescription` action and kwargs for constructing it."""
_, kwargs = super().parse(entity, parser)
file_path = parser.parse_substitution(entity.get_attr('file'))
file_attr = entity.bare_text or entity.get_attr('file')
file_path = parser.parse_substitution(file_attr)

kwargs['launch_description_source'] = file_path
args = entity.get_attr('arg', data_type=List[Entity], optional=True)
if args is not None:
Expand Down
5 changes: 5 additions & 0 deletions launch/launch/frontend/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,8 @@ def assert_entity_completely_parsed(self):
function completed.
"""
raise NotImplementedError()

@property
def bare_text(self) -> Optional[Text]:
"""Return the bare text of this element if it is a bare text element, or None."""
raise NotImplementedError()
Comment on lines +120 to +123
Copy link
Member

Choose a reason for hiding this comment

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

Can we find a more descriptive name for this, or just describe it using words other than "bare text" in the docstring, so that it's a bit clearer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, a different name is fine, i just wasn't sure what to call it exactly

11 changes: 11 additions & 0 deletions launch_xml/launch_xml/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ def __init__(
parent: 'Entity' = None
) -> Text:
"""Construct the Entity."""
self.__bare_text = xml_element.text.strip() if xml_element.text else None
num_children = len(list(xml_element))
if self.__bare_text and num_children:
raise ValueError(
f'Cannot provide XML text alongside children. Found text "{self.__bare_text}" '
f'and {num_children} child(ren) in element {xml_element}')

self.__xml_element = xml_element
self.__parent = parent
self.__read_attributes = set()
Expand Down Expand Up @@ -72,6 +79,10 @@ def assert_entity_completely_parsed(self):
f'{unparsed_attributes}'
)

@property
def bare_text(self) -> Optional[Text]:
return self.__bare_text

def get_attr(
self,
name: Text,
Expand Down
17 changes: 17 additions & 0 deletions launch_xml/test/launch_xml/test_include.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,22 @@ def test_include():
assert 0 == ls.run()


def test_include_bare_text():
path = (Path(__file__).parent / 'executable.xml').as_posix()
xml_file = f"""
<launch>
<include>{path}</include>
</launch>
"""
xml_file = textwrap.dedent(xml_file)
root_entity, parser = load_no_extensions(io.StringIO(xml_file))
ld = parser.parse_description(root_entity)
include = ld.entities[0]
assert isinstance(include, IncludeLaunchDescription)
assert isinstance(include.launch_description_source, AnyLaunchDescriptionSource)
# No need to run a second time, just testing parsing


if __name__ == '__main__':
test_include()
test_include_bare_text()
11 changes: 10 additions & 1 deletion launch_yaml/launch_yaml/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ class Entity(BaseEntity):

def __init__(
self,
element: dict,
element: Union[dict, List, Text],
type_name: Text = None,
*,
parent: 'Entity' = None
) -> Text:
"""Create an Entity."""
self.__bare_text = element if isinstance(element, str) else None
self.__type_name = type_name
self.__element = element
self.__parent = parent
Expand Down Expand Up @@ -70,6 +71,8 @@ def children(self) -> List['Entity']:
'list element')
self.__read_keys.add('children')
children = self.__element['children']
elif isinstance(self.__element, str):
return [self]
else:
children = self.__element
entities = []
Expand All @@ -83,6 +86,8 @@ def children(self) -> List['Entity']:
return entities

def assert_entity_completely_parsed(self):
if isinstance(self.__element, str):
return
if isinstance(self.__element, list):
if not self.__children_called:
raise ValueError(
Expand All @@ -95,6 +100,10 @@ def assert_entity_completely_parsed(self):
f'Unexpected key(s) found in `{self.__type_name}`: {unparsed_keys}'
)

@property
def bare_text(self) -> Optional[Text]:
return self.__bare_text

def get_attr(
self,
name: Text,
Expand Down
15 changes: 15 additions & 0 deletions launch_yaml/test/launch_yaml/executable.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
launch:
- executable:
cmd: ls -l -a -s
cwd: /
name: my_ls
shell: true
output: log
emulate_tty: true
sigkill_timeout: 4.0
sigterm_timeout: 7.0
launch-prefix: $(env LAUNCH_PREFIX '')
env:
- name: var
value: "1"
65 changes: 65 additions & 0 deletions launch_yaml/test/launch_yaml/test_include.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copyright 2025 Polymath Robotics, 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.

"""Test parsing a launch file inclusion."""

import io
from pathlib import Path
import textwrap

from launch import LaunchService
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import AnyLaunchDescriptionSource

from parser_no_extensions import load_no_extensions


def test_include():
"""Parse include yaml example."""
path = (Path(__file__).parent / 'executable.yaml').as_posix()
yaml_file = f"""\
launch:
- include:
file: {path}
"""
yaml_file = textwrap.dedent(yaml_file)
root_entity, parser = load_no_extensions(io.StringIO(yaml_file))
ld = parser.parse_description(root_entity)
include = ld.entities[0]
assert isinstance(include, IncludeLaunchDescription)
assert isinstance(include.launch_description_source, AnyLaunchDescriptionSource)
ls = LaunchService(debug=True)
ls.include_launch_description(ld)
assert 0 == ls.run()


def test_include_bare_text():
"""Parse include yaml example."""
path = (Path(__file__).parent / 'executable.yaml').as_posix()
yaml_file = f"""
launch:
- include: {path}
"""
yaml_file = textwrap.dedent(yaml_file)
root_entity, parser = load_no_extensions(io.StringIO(yaml_file))
ld = parser.parse_description(root_entity)
include = ld.entities[0]
assert isinstance(include, IncludeLaunchDescription)
assert isinstance(include.launch_description_source, AnyLaunchDescriptionSource)
# No need to run a second time, just testing parsing


if __name__ == '__main__':
test_include()
test_include_bare_text()