diff --git a/launch/launch/substitutions/string_join_substitution.py b/launch/launch/substitutions/string_join_substitution.py index be3a101d8..50dda3a27 100644 --- a/launch/launch/substitutions/string_join_substitution.py +++ b/launch/launch/substitutions/string_join_substitution.py @@ -14,14 +14,16 @@ """Module for the StringJoinSubstitution substitution.""" -from typing import Iterable, List, Text +from typing import Any, Dict, Iterable, List, Sequence, Text, Tuple, Type +from ..frontend.expose import expose_substitution from ..launch_context import LaunchContext from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution from ..utilities import perform_substitutions +@expose_substitution('string-join') class StringJoinSubstitution(Substitution): """ Substitution that joins strings and/or other substitutions. @@ -34,13 +36,31 @@ class StringJoinSubstitution(Substitution): .. code-block:: python - StringJoinSubstitution( - [['https', '://'], LaunchConfiguration('subdomain')], 'ros', 'org'], + subdomain = LaunchConfiguration(variable_name='subdomain', default='docs') + url = StringJoinSubstitution( + [['https', '://', subdomain], 'ros', 'org'], delimiter='.' ) - If the ``subdomain`` launch configuration was set to ``docs`` - and the ``delimiter`` to ``.``, this would result in a string equal to + .. code-block:: xml + + + + + + + .. code-block:: yaml + + launch: + - arg: + name: subdomain + default: "docs" + - let: + name: url + value: "$(string-join . https://$(var subdomain) ros org)" + + If the ``subdomain`` launch configuration was set to ``docs`` and the ``delimiter`` to ``.``, + then any of the above launch descriptions would result in a string equal to .. code-block:: python @@ -76,6 +96,18 @@ def delimiter(self) -> List[Substitution]: """Getter for delimiter.""" return self.__delimiter + @classmethod + def parse( + cls, data: Sequence[SomeSubstitutionsType] + ) -> Tuple[Type['StringJoinSubstitution'], Dict[str, Any]]: + """Parse `StringJoinSubstitution` substitution.""" + if len(data) < 2: + raise TypeError( + 'string-join substitution expects at least 2 arguments: ' + '1 delimiter + at least 1 component' + ) + return cls, {'delimiter': data[0], 'substitutions': data[1:]} + def __repr__(self) -> Text: """Return a description of this substitution as a string.""" string_components = [ diff --git a/launch_xml/test/launch_xml/test_string_join_substitution.py b/launch_xml/test/launch_xml/test_string_join_substitution.py new file mode 100644 index 000000000..4a4b3561c --- /dev/null +++ b/launch_xml/test/launch_xml/test_string_join_substitution.py @@ -0,0 +1,71 @@ +# Copyright 2025 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. + +"""Test parsing a StringJoinSubstitution in XML launch file.""" + +import io +import textwrap + +from launch.actions import DeclareLaunchArgument, SetLaunchConfiguration +from launch.frontend import Parser +from launch.launch_context import LaunchContext +from launch.substitutions import StringJoinSubstitution + + +def test_nested(): + xml_file = textwrap.dedent( + """ + + + + + """ + ) + root_entity, parser = Parser.load(io.StringIO(xml_file)) + ld = parser.parse_description(root_entity) + + assert len(ld.entities) == 2 + assert isinstance(ld.entities[0], DeclareLaunchArgument) + assert isinstance(ld.entities[1], SetLaunchConfiguration) + + lc = LaunchContext() + ld.entities[0].visit(lc) + + let = ld.entities[1] + assert isinstance(let.value[0], StringJoinSubstitution) + assert let.value[0].perform(lc) == 'https://wiki.ros.org' + + +def test_delimiter(): + yaml_file = textwrap.dedent( + """ + + + + + """ + ) + root_entity, parser = Parser.load(io.StringIO(yaml_file)) + ld = parser.parse_description(root_entity) + + assert len(ld.entities) == 2 + assert isinstance(ld.entities[0], DeclareLaunchArgument) + assert isinstance(ld.entities[1], SetLaunchConfiguration) + + lc = LaunchContext() + ld.entities[0].visit(lc) + + let = ld.entities[1] + assert isinstance(let.value[0], StringJoinSubstitution) + assert let.value[0].perform(lc) == 'a(^_^)b(^_^)c' diff --git a/launch_yaml/test/launch_yaml/test_string_join_substitution.py b/launch_yaml/test/launch_yaml/test_string_join_substitution.py new file mode 100644 index 000000000..761dbd1ee --- /dev/null +++ b/launch_yaml/test/launch_yaml/test_string_join_substitution.py @@ -0,0 +1,77 @@ +# Copyright 2025 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. + +"""Test parsing a StringJoinSubstitution in yaml launch file.""" + +import io +import textwrap + +from launch.actions import DeclareLaunchArgument, SetLaunchConfiguration +from launch.frontend import Parser +from launch.launch_context import LaunchContext +from launch.substitutions import StringJoinSubstitution + + +def test_nested(): + yaml_file = textwrap.dedent( + """ + launch: + - arg: + name: subdomain + default: "wiki" + - let: + name: url + value: "$(string-join . https://$(var subdomain) ros org)" + """ + ) + root_entity, parser = Parser.load(io.StringIO(yaml_file)) + ld = parser.parse_description(root_entity) + + assert len(ld.entities) == 2 + assert isinstance(ld.entities[0], DeclareLaunchArgument) + assert isinstance(ld.entities[1], SetLaunchConfiguration) + + lc = LaunchContext() + ld.entities[0].visit(lc) + + let = ld.entities[1] + assert isinstance(let.value[0], StringJoinSubstitution) + assert let.value[0].perform(lc) == 'https://wiki.ros.org' + + +def test_delimiter(): + yaml_file = textwrap.dedent( + """ + launch: + - arg: + name: delimiter + default: "^_^" + - let: + name: text + value: "$(string-join '($(var delimiter))' a b c)" + """ + ) + root_entity, parser = Parser.load(io.StringIO(yaml_file)) + ld = parser.parse_description(root_entity) + + assert len(ld.entities) == 2 + assert isinstance(ld.entities[0], DeclareLaunchArgument) + assert isinstance(ld.entities[1], SetLaunchConfiguration) + + lc = LaunchContext() + ld.entities[0].visit(lc) + + let = ld.entities[1] + assert isinstance(let.value[0], StringJoinSubstitution) + assert let.value[0].perform(lc) == 'a(^_^)b(^_^)c'