From 2cebdbcf4d3e3e12f5eb81c5de600ef7548a66ec Mon Sep 17 00:00:00 2001 From: Burak Keskin Date: Tue, 25 Aug 2026 16:25:50 +0300 Subject: [PATCH] Show launch argument names in print-description output. --print-description currently renders DeclareLaunchArgument and OpaqueFunction with the default object repr, so operators only see addresses instead of argument names or the wrapped callable. Signed-off-by: Burak Keskin --- .../launch/actions/declare_launch_argument.py | 12 +++++ launch/launch/actions/opaque_function.py | 7 +++ launch/launch/launch_description.py | 6 +++ launch/launch/launch_introspector.py | 4 +- .../actions/test_declare_launch_argument.py | 2 + .../test/launch/test_launch_introspector.py | 52 +++++++++++++++++++ 6 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 launch/test/launch/test_launch_introspector.py diff --git a/launch/launch/actions/declare_launch_argument.py b/launch/launch/actions/declare_launch_argument.py index 351caef1b..598021d5d 100644 --- a/launch/launch/actions/declare_launch_argument.py +++ b/launch/launch/actions/declare_launch_argument.py @@ -204,6 +204,18 @@ def choices(self) -> Optional[List[Text]]: """Getter for self.__choices.""" return self.__choices + def __repr__(self) -> Text: + """Return a description of this DeclareLaunchArgument as a string.""" + parts = [f'name={self.name!r}'] + if self.default_value is not None: + default = ''.join(sub.describe() for sub in self.default_value) + parts.append(f'default_value={default}') + if self.description: + parts.append(f'description={self.description!r}') + if self.choices is not None: + parts.append(f'choices={self.choices!r}') + return 'DeclareLaunchArgument({})'.format(', '.join(parts)) + def execute(self, context: LaunchContext) -> None: """Execute the action.""" if self.name not in context.launch_configurations: diff --git a/launch/launch/actions/opaque_function.py b/launch/launch/actions/opaque_function.py index ae525f9e9..52915abd2 100644 --- a/launch/launch/actions/opaque_function.py +++ b/launch/launch/actions/opaque_function.py @@ -70,6 +70,13 @@ def __init__( if kwargs is not None: self.__kwargs = kwargs + def __repr__(self) -> Text: + """Return a description of this OpaqueFunction as a string.""" + function_name = getattr(self.__function, '__qualname__', None) + if not function_name: + function_name = getattr(self.__function, '__name__', repr(self.__function)) + return f'OpaqueFunction(function={function_name})' + def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEntity]]: """Execute the action.""" return self.__function(context, *self.__args, **self.__kwargs) diff --git a/launch/launch/launch_description.py b/launch/launch/launch_description.py index 2a08a6b3b..bee26488c 100644 --- a/launch/launch/launch_description.py +++ b/launch/launch/launch_description.py @@ -61,6 +61,12 @@ def __init__( self.__entities = list(initial_entities) if initial_entities is not None else [] self.__deprecated_reason = deprecated_reason + def __repr__(self) -> Text: + """Return a description of this LaunchDescription as a string.""" + n = len(self.__entities) + noun = 'entity' if n == 1 else 'entities' + return f'LaunchDescription({n} {noun})' + def visit(self, context: LaunchContext) -> List[LaunchDescriptionEntity]: """Override visit from LaunchDescriptionEntity to visit contained entities.""" if self.__deprecated_reason is not None: diff --git a/launch/launch/launch_introspector.py b/launch/launch/launch_introspector.py index 5cdf28d2d..77b519be2 100644 --- a/launch/launch/launch_introspector.py +++ b/launch/launch/launch_introspector.py @@ -128,7 +128,9 @@ def format_action(action: Action) -> List[Text]: result.extend(indent(format_event_handler(typed_action2.event_handler))) return result else: - return ["Action('{}')".format(action)] + # Use describe() so actions with a useful representation show names + # instead of the default object id. + return [action.describe()] class LaunchIntrospector: diff --git a/launch/test/launch/actions/test_declare_launch_argument.py b/launch/test/launch/actions/test_declare_launch_argument.py index db5cea93d..0732e0303 100644 --- a/launch/test/launch/actions/test_declare_launch_argument.py +++ b/launch/test/launch/actions/test_declare_launch_argument.py @@ -37,6 +37,8 @@ def test_declare_launch_argument_methods(): assert dla1.description == 'description' assert dla1.choices is None assert 'DeclareLaunchArgument' in dla1.describe() + assert "name='name'" in dla1.describe() + assert 'default value' in dla1.describe() assert isinstance(dla1.describe_sub_entities(), list) assert isinstance(dla1.describe_conditional_sub_entities(), list) diff --git a/launch/test/launch/test_launch_introspector.py b/launch/test/launch/test_launch_introspector.py new file mode 100644 index 000000000..3a7f25240 --- /dev/null +++ b/launch/test/launch/test_launch_introspector.py @@ -0,0 +1,52 @@ +# Copyright 2026 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. + +"""Tests for LaunchIntrospector formatting used by --print-description.""" + +from launch import LaunchDescription +from launch import LaunchIntrospector +from launch.actions import DeclareLaunchArgument +from launch.actions import OpaqueFunction + + +def test_introspector_shows_declare_launch_argument_details(): + """Declared arguments should show name and default, not a default object repr.""" + ld = LaunchDescription([ + DeclareLaunchArgument( + 'use_sim_time', + default_value='false', + description='Use simulation clock', + ), + ]) + text = LaunchIntrospector().format_launch_description(ld) + assert 'use_sim_time' in text + assert 'false' in text + assert 'Use simulation clock' in text + assert 'DeclareLaunchArgument' in text + assert 'object at 0x' not in text + + +def test_introspector_shows_opaque_function_name(): + """OpaqueFunction should identify the wrapped callable instead of an object id.""" + def generate_nodes(context): + return [] + + ld = LaunchDescription([ + OpaqueFunction(function=generate_nodes), + ]) + text = LaunchIntrospector().format_launch_description(ld) + assert 'generate_nodes' in text + assert 'OpaqueFunction' in text + assert 'LaunchDescription(1 entity)' in text + assert 'object at 0x' not in text