From 587b2441435a04a0462a53b48654af286d5271f2 Mon Sep 17 00:00:00 2001 From: Stefan Fabian Date: Thu, 23 Jan 2025 11:07:33 +0100 Subject: [PATCH 1/2] Add --namespace argument to launch command to push a namespace when launching a launch file. Signed-off-by: Stefan Fabian Signed-off-by: Markus Kramer --- ros2launch/ros2launch/api/api.py | 13 +++++++++---- ros2launch/ros2launch/command/launch.py | 7 ++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ros2launch/ros2launch/api/api.py b/ros2launch/ros2launch/api/api.py index 3bdb5bd1b..f1d356a52 100644 --- a/ros2launch/ros2launch/api/api.py +++ b/ros2launch/ros2launch/api/api.py @@ -25,6 +25,7 @@ import launch from launch.frontend import Parser from launch.launch_description_sources import get_launch_description_from_any_launch_file +from launch_ros.actions import PushROSNamespace class MultipleLaunchFilesError(Exception): @@ -145,7 +146,8 @@ def launch_a_launch_file( noninteractive=False, args=None, option_extensions={}, - debug=False + debug=False, + namespace=None ): """Launch a given launch file (by path) and pass it the given launch file arguments.""" for name in sorted(option_extensions.keys()): @@ -167,14 +169,17 @@ def launch_a_launch_file( parsed_launch_arguments = parse_launch_arguments(launch_file_arguments) # Include the user provided launch file using IncludeLaunchDescription so that the # location of the current launch file is set. - launch_description = launch.LaunchDescription([ + launch_description = launch.LaunchDescription() + if namespace is not None: + launch_description.add_action(PushROSNamespace(namespace)) + launch_description.add_action( launch.actions.IncludeLaunchDescription( launch.launch_description_sources.AnyLaunchDescriptionSource( launch_file_path ), launch_arguments=parsed_launch_arguments, - ), - ]) + ) + ) for name in sorted(option_extensions.keys()): result = option_extensions[name].prelaunch( launch_description, diff --git a/ros2launch/ros2launch/command/launch.py b/ros2launch/ros2launch/command/launch.py index 1767d6e35..7e3277192 100644 --- a/ros2launch/ros2launch/command/launch.py +++ b/ros2launch/ros2launch/command/launch.py @@ -102,6 +102,10 @@ def add_arguments(self, parser, cli_name): help=('Regex pattern for filtering which executables the --launch-prefix is applied ' 'to by matching the executable name.') ) + parser.add_argument( + '--namespace', + help=('A namespace to push to the actions/nodes started by the launch file.') + ) arg = parser.add_argument( 'package_name', help='Name of the ROS package which contains the launch file') @@ -175,5 +179,6 @@ def main(self, *, parser, args): noninteractive=args.noninteractive, args=args, option_extensions=self._option_extensions, - debug=args.debug + debug=args.debug, + namespace=args.namespace ) From f43715d19d23fa2803944994619a1f1a62cd9c7e Mon Sep 17 00:00:00 2001 From: Markus Kramer Date: Fri, 28 Mar 2025 16:28:47 +0100 Subject: [PATCH 2/2] added testcase for namespace Signed-off-by: Markus Kramer --- launch_ros/test/test_cli_namespace.py | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 launch_ros/test/test_cli_namespace.py diff --git a/launch_ros/test/test_cli_namespace.py b/launch_ros/test/test_cli_namespace.py new file mode 100644 index 000000000..510044373 --- /dev/null +++ b/launch_ros/test/test_cli_namespace.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +# 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. + +import unittest +import subprocess + +class TestNamespaceArgument(unittest.TestCase): + """Test the --namespace command line argument for ros2 launch.""" + + def test_namespace_argument(self): + """Test that the --namespace argument correctly namespaces topics.""" + try: + # Start the talker_listener launch file with namespace argument + launch_proc_remapped = subprocess.Popen( + ['ros2', 'launch', 'demo_nodes_cpp', 'talker_listener_launch.py', + '--namespace', 'test_namespace'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # Run ros2 topic list to get the remapped topics + result = subprocess.run(['ros2', 'topic', 'list'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True) + remapped_topics = result.stdout.strip().split('\n') + + # Verify /chatter is NOT in the list + self.assertNotIn('/chatter', remapped_topics, + f"Did not expect to find /chatter after remapping. Topics: {remapped_topics}") + + # Verify /test_namespace/chatter IS in the list + self.assertIn('/test_namespace/chatter', remapped_topics, + f"Expected to find /test_namespace/chatter after remapping. Topics: {remapped_topics}") + + finally: + # Clean up + if 'launch_proc_remapped' in locals(): + launch_proc_remapped.terminate() + try: + launch_proc_remapped.wait(timeout=5) + except subprocess.TimeoutExpired: + launch_proc_remapped.kill() + launch_proc_remapped.wait() + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file