From c566e99942b958a799eb1f337e4fa63a34464a4d Mon Sep 17 00:00:00 2001 From: Emerson Knapp <537409+emersonknapp@users.noreply.github.com> Date: Mon, 5 May 2025 10:55:20 -0700 Subject: [PATCH 1/2] Add a `/` path join operator for `PathJoinSubstitution` (#868) Signed-off-by: Emerson Knapp --- launch/launch/substitutions/__init__.py | 2 + .../substitutions/path_join_substitution.py | 85 +++++++++++++++++-- .../test_path_join_substitution.py | 18 +++- 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/launch/launch/substitutions/__init__.py b/launch/launch/substitutions/__init__.py index 8195ba242..d0776d5f7 100644 --- a/launch/launch/substitutions/__init__.py +++ b/launch/launch/substitutions/__init__.py @@ -24,6 +24,7 @@ from .launch_configuration import LaunchConfiguration from .local_substitution import LocalSubstitution from .path_join_substitution import PathJoinSubstitution +from .path_join_substitution import PathSubstitution from .python_expression import PythonExpression from .substitution_failure import SubstitutionFailure from .text_substitution import TextSubstitution @@ -41,6 +42,7 @@ 'NotSubstitution', 'OrSubstitution', 'PathJoinSubstitution', + 'PathSubstitution', 'PythonExpression', 'SubstitutionFailure', 'TextSubstitution', diff --git a/launch/launch/substitutions/path_join_substitution.py b/launch/launch/substitutions/path_join_substitution.py index ab3d6f273..1607b5b54 100644 --- a/launch/launch/substitutions/path_join_substitution.py +++ b/launch/launch/substitutions/path_join_substitution.py @@ -16,15 +16,48 @@ import os from typing import Iterable +from typing import List from typing import Text 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 class PathJoinSubstitution(Substitution): - """Substitution that join paths, in a platform independent way.""" + """ + Substitution that join paths, in a platform independent way. + + This takes in a list of path components as substitutions. The substitutions for each path + component are performed and concatenated, and then all path components are joined. + + For example: + + .. code-block:: python + + PathJoinSubstitution([ + EnvironmentVariable('SOME_DIR'), + 'cfg', + ['config_', LaunchConfiguration('map'), '.yml'] + ]) + + Or: + + .. code-block:: python + + cfg_dir = PathJoinSubstitution([EnvironmentVariable('SOME_DIR'), 'cfg']) + cfg_file = cfg_dir / ['config_', LaunchConfiguration('map'), '.yml'] + + If the ``SOME_DIR`` environment variable was set to ``/home/user/dir`` and the ``map`` launch + configuration was set to ``my_map``, this would result in a path equal equivalent to (depending + on the platform): + + .. code-block:: python + + '/home/user/dir/cfg/config_my_map.yml' + """ def __init__(self, substitutions: Iterable[SomeSubstitutionsType]) -> None: """Create a PathJoinSubstitution.""" @@ -32,15 +65,53 @@ def __init__(self, substitutions: Iterable[SomeSubstitutionsType]) -> None: self.__substitutions = normalize_to_list_of_substitutions(substitutions) @property - def substitutions(self) -> Iterable[Substitution]: + def substitutions(self) -> List[List[Substitution]]: """Getter for variable_name.""" return self.__substitutions - def describe(self) -> Text: + def __repr__(self) -> Text: """Return a description of this substitution as a string.""" - return "LocalVar('{}')".format(' + '.join([s.describe() for s in self.substitutions])) + path_components = [ + ' + '.join([s.describe() for s in component_substitutions]) + for component_substitutions in self.substitutions + ] + return f"PathJoinSubstitution('{', '.join(path_components)}')" "LocalVar('{}')".format(' + '.join([s.describe() for s in self.substitutions])) def perform(self, context: LaunchContext) -> Text: - """Perform the substitution by retrieving the local variable.""" - performed_substitutions = [sub.perform(context) for sub in self.__substitutions] - return os.path.join(*performed_substitutions) + """Perform the substitutions and join into a path.""" + path_components = [ + perform_substitutions(context, component_substitutions) + for component_substitutions in self.substitutions + ] + return os.path.join(*path_components) + + def __truediv__(self, additional_path: SomeSubstitutionsType) -> 'PathJoinSubstitution': + """Join path substitutions using the / operator, mimicking pathlib.Path operation.""" + return PathJoinSubstitution( + self.substitutions + [normalize_to_list_of_substitutions(additional_path)]) + + +class PathSubstitution(PathJoinSubstitution): + """ + Thin wrapper on PathJoinSubstitution for more pathlib.Path-like construction. + + .. code-block:: python + + PathSubstitution(LaunchConfiguration('base_dir')) / 'sub_dir' / 'file_name' + + Which, for ``base_dir:=/my_dir``, results in (depending on the platform): + + .. code-block:: python + + /my_dir/sub_dir/file_name + + """ + + def __init__(self, path: SomeSubstitutionsType): + """ + Create a PathSubstitution. + + :param path: May be a single text or Substitution element, + or an Iterable of them which are then joined + """ + super().__init__(normalize_to_list_of_substitutions(path)) diff --git a/launch/test/launch/substitutions/test_path_join_substitution.py b/launch/test/launch/substitutions/test_path_join_substitution.py index 1f6bec38d..2405f83f3 100644 --- a/launch/test/launch/substitutions/test_path_join_substitution.py +++ b/launch/test/launch/substitutions/test_path_join_substitution.py @@ -16,10 +16,24 @@ import os -from launch.substitutions import PathJoinSubstitution +from launch import LaunchContext +from launch.substitutions import PathJoinSubstitution, PathSubstitution +from launch.substitutions import TextSubstitution def test_this_launch_file_path(): + context = LaunchContext() + path = ['asd', 'bsd', 'cds'] sub = PathJoinSubstitution(path) - assert sub.perform(None) == os.path.join(*path) + assert sub.perform(context) == os.path.join(*path) + + path = ['path', ['to'], ['my_', TextSubstitution(text='file'), '.yaml']] + sub = PathJoinSubstitution(path) + assert sub.perform(context) == os.path.join('path', 'to', 'my_file.yaml') + + sub = PathSubstitution('some') / 'path' + sub = sub / PathJoinSubstitution(['to', 'some', 'dir']) + sub = sub / (TextSubstitution(text='my_model'), '.xacro') + assert sub.perform(context) == os.path.join( + 'some', 'path', 'to', 'some', 'dir', 'my_model.xacro') From 596e4afef0e30cfe8a87c1523761c854ee299bf2 Mon Sep 17 00:00:00 2001 From: Emerson Knapp Date: Wed, 14 May 2025 23:07:10 -0700 Subject: [PATCH 2/2] Fix merge mistake, and port substitution normalizing construction Signed-off-by: Emerson Knapp --- launch/launch/substitutions/path_join_substitution.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/launch/launch/substitutions/path_join_substitution.py b/launch/launch/substitutions/path_join_substitution.py index 1607b5b54..05117ae39 100644 --- a/launch/launch/substitutions/path_join_substitution.py +++ b/launch/launch/substitutions/path_join_substitution.py @@ -62,7 +62,10 @@ class PathJoinSubstitution(Substitution): def __init__(self, substitutions: Iterable[SomeSubstitutionsType]) -> None: """Create a PathJoinSubstitution.""" from ..utilities import normalize_to_list_of_substitutions - self.__substitutions = normalize_to_list_of_substitutions(substitutions) + self.__substitutions = [ + normalize_to_list_of_substitutions(path_component_substitutions) + for path_component_substitutions in substitutions + ] @property def substitutions(self) -> List[List[Substitution]]: @@ -75,7 +78,7 @@ def __repr__(self) -> Text: ' + '.join([s.describe() for s in component_substitutions]) for component_substitutions in self.substitutions ] - return f"PathJoinSubstitution('{', '.join(path_components)}')" "LocalVar('{}')".format(' + '.join([s.describe() for s in self.substitutions])) + return f"PathJoinSubstitution('{', '.join(path_components)}')" def perform(self, context: LaunchContext) -> Text: """Perform the substitutions and join into a path."""