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
12 changes: 12 additions & 0 deletions launch/launch/actions/declare_launch_argument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions launch/launch/actions/opaque_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
6 changes: 6 additions & 0 deletions launch/launch/launch_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion launch/launch/launch_introspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions launch/test/launch/actions/test_declare_launch_argument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
52 changes: 52 additions & 0 deletions launch/test/launch/test_launch_introspector.py
Original file line number Diff line number Diff line change
@@ -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