From c535e6c4124c7930f27819d5224b9a1711288f2d Mon Sep 17 00:00:00 2001 From: Vikrant Shah Date: Wed, 17 Jun 2026 06:50:45 -0700 Subject: [PATCH 1/2] fix(generator): add opt-in .msg-syntax output to get_fields_and_field_types() _fields_and_field_types / get_fields_and_field_types() emits OMG IDL 4.2 syntax for bounded strings and sequences: sequence, 32> (IDL) Some consumers -- e.g. the MCAP ros2msg schema encoder used by downstream bag-to-MCAP tooling -- require ROS 2 .msg syntax instead: string<=256[<=32] (.msg) Foxglove and other ros2msg-aware tools reject the IDL form. Rather than changing the default output (which would break rqt_topic and any other consumer relying on today's IDL strings, per feedback from @sloretz), this adds an opt-in `syntax` argument: get_fields_and_field_types(syntax='idl') # default, unchanged get_fields_and_field_types(syntax='msg') # new The IDL-to-.msg conversion is implemented once at runtime in a new rosidl_generator_py.field_type_syntax.idl_to_msg_syntax() helper, rather than duplicating the dict-literal generation logic in the _msg.py.em template for two syntaxes. This keeps _fields_and_field_types and the .em template's dict-generation block untouched. SLOT_TYPES (rosidl_parser.definition objects) remains the correct API for programmatic type inspection and is unaffected by this change. Signed-off-by: Vikrant Shah --- rosidl_generator_py/resource/_msg.py.em | 18 ++++++- .../rosidl_generator_py/__init__.py | 3 +- .../rosidl_generator_py/field_type_syntax.py | 52 +++++++++++++++++++ .../test/test_field_type_syntax.py | 35 +++++++++++++ rosidl_generator_py/test/test_interfaces.py | 34 ++++++++++++ 5 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 rosidl_generator_py/rosidl_generator_py/field_type_syntax.py create mode 100644 rosidl_generator_py/test/test_field_type_syntax.py diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index a8682df0..33936f35 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -457,9 +457,23 @@ if isinstance(type_, AbstractNestedType): return True @@classmethod - def get_fields_and_field_types(cls) -> dict[str, str]: + def get_fields_and_field_types(cls, syntax: str = 'idl') -> dict[str, str]: + """Return a dict of field name to field type string. + + :param syntax: Either 'idl' (default) for the OMG IDL 4.2 type + strings historically returned by this method (e.g. + ``sequence, 32>``), or 'msg' for ROS 2 .msg-syntax + type strings (e.g. ``string<=256[<=32]``) as expected by + .msg-based tooling such as the MCAP ros2msg schema encoding. + """ from copy import copy - return copy(cls._fields_and_field_types) + fields = copy(cls._fields_and_field_types) + if syntax == 'idl': + return fields + if syntax == 'msg': + from rosidl_generator_py import idl_to_msg_syntax + return {name: idl_to_msg_syntax(t) for name, t in fields.items()} + raise ValueError(f"Unknown syntax {syntax!r}: expected 'idl' or 'msg'") @[for member in message.structure.members]@ @[ if len(message.structure.members) == 1 and member.name == EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME]@ @[ continue]@ diff --git a/rosidl_generator_py/rosidl_generator_py/__init__.py b/rosidl_generator_py/rosidl_generator_py/__init__.py index ecf221fa..26811fca 100644 --- a/rosidl_generator_py/rosidl_generator_py/__init__.py +++ b/rosidl_generator_py/rosidl_generator_py/__init__.py @@ -15,9 +15,10 @@ import logging import traceback +from .field_type_syntax import idl_to_msg_syntax from .import_type_support_impl import import_type_support -__all__ = ['import_type_support'] +__all__ = ['idl_to_msg_syntax', 'import_type_support'] try: from .generate_py_impl import generate_py # noqa: F401 diff --git a/rosidl_generator_py/rosidl_generator_py/field_type_syntax.py b/rosidl_generator_py/rosidl_generator_py/field_type_syntax.py new file mode 100644 index 00000000..965adcf7 --- /dev/null +++ b/rosidl_generator_py/rosidl_generator_py/field_type_syntax.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. + +"""Convert OMG IDL 4.2 field-type strings to ROS 2 .msg-syntax field-type strings. + +Generated message classes store field types as OMG IDL 4.2 strings in +``_fields_and_field_types`` (e.g. ``sequence, 32>``). Some +tooling -- such as consumers of the MCAP ``ros2msg`` schema encoding -- +expects ROS 2 ``.msg`` syntax instead (e.g. ``string<=256[<=32]``). This +module implements that conversion so it can be requested on demand via +``get_fields_and_field_types(syntax='msg')`` without changing the stored +IDL representation. +""" + +import re + +_BOUNDED_STRING_RE = re.compile(r'(w?string)<(\d+)>') +_BOUNDED_SEQUENCE_RE = re.compile(r'^sequence<(.+), (\d+)>$') +_UNBOUNDED_SEQUENCE_RE = re.compile(r'^sequence<(.+)>$') + + +def idl_to_msg_syntax(idl_type: str) -> str: + """Convert a single OMG IDL 4.2 field-type string to ROS 2 .msg syntax. + + :param idl_type: A field-type string as stored in a generated message + class's ``_fields_and_field_types`` dict, e.g. + ``sequence, 32>``. + :return: The equivalent ROS 2 .msg-syntax string, e.g. + ``string<=256[<=32]``. + """ + match = _BOUNDED_SEQUENCE_RE.match(idl_type) + if match: + value_type, max_size = match.groups() + return f'{idl_to_msg_syntax(value_type)}[<={max_size}]' + + match = _UNBOUNDED_SEQUENCE_RE.match(idl_type) + if match: + value_type = match.group(1) + return f'{idl_to_msg_syntax(value_type)}[]' + + return _BOUNDED_STRING_RE.sub(r'\1<=\2', idl_type) diff --git a/rosidl_generator_py/test/test_field_type_syntax.py b/rosidl_generator_py/test/test_field_type_syntax.py new file mode 100644 index 00000000..e17dbd5b --- /dev/null +++ b/rosidl_generator_py/test/test_field_type_syntax.py @@ -0,0 +1,35 @@ +# 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. + +import pytest + +from rosidl_generator_py import idl_to_msg_syntax + + +@pytest.mark.parametrize( + 'idl_type,expected_msg_type', + [ + ('string<5>', 'string<=5'), + ('wstring<5>', 'wstring<=5'), + ('string', 'string'), + ('char', 'char'), + ('sequence', 'char[]'), + ('sequence', 'string[]'), + ('sequence', 'string[<=10]'), + ('sequence>', 'string<=5[]'), + ('sequence, 10>', 'string<=5[<=10]'), + ], +) +def test_idl_to_msg_syntax(idl_type: str, expected_msg_type: str) -> None: + assert idl_to_msg_syntax(idl_type) == expected_msg_type diff --git a/rosidl_generator_py/test/test_interfaces.py b/rosidl_generator_py/test/test_interfaces.py index 64225d9c..65d0789a 100644 --- a/rosidl_generator_py/test/test_interfaces.py +++ b/rosidl_generator_py/test/test_interfaces.py @@ -959,6 +959,40 @@ def test_string_slot_attributes() -> None: assert expected_slot_type == string_slot_types_dict[expected_field] +def test_string_slot_attributes_msg_syntax() -> None: + msg = StringArrays(check_fields=True) + assert hasattr(msg, 'get_fields_and_field_types') + string_slot_types_dict = getattr(msg, 'get_fields_and_field_types')(syntax='msg') + expected_string_slot_types_dict = { + 'ub_string_static_array_value': 'string<=5[3]', + 'ub_string_ub_array_value': 'string<=5[<=10]', + 'ub_string_dynamic_array_value': 'string<=5[]', + 'string_dynamic_array_value': 'string[]', + 'string_static_array_value': 'string[3]', + 'string_bounded_array_value': 'string[<=10]', + 'def_string_dynamic_array_value': 'string[]', + 'def_string_static_array_value': 'string[3]', + 'def_string_bounded_array_value': 'string[<=10]', + 'def_various_quotes': 'string[]', + 'def_various_commas': 'string[]', + } + + assert len(string_slot_types_dict) == len(expected_string_slot_types_dict) + + for expected_field, expected_slot_type in expected_string_slot_types_dict.items(): + assert expected_field in string_slot_types_dict.keys() + assert expected_slot_type == string_slot_types_dict[expected_field] + + +def test_get_fields_and_field_types_invalid_syntax() -> None: + msg = StringArrays(check_fields=True) + try: + getattr(msg, 'get_fields_and_field_types')(syntax='bogus') + assert False, 'expected ValueError for unknown syntax' + except ValueError: + pass + + def test_modifying_slot_fields_and_types() -> None: msg = StringArrays(check_fields=True) assert hasattr(msg, 'get_fields_and_field_types') From ea8eaa50f4b684ae0a56c971a682668591cb0566 Mon Sep 17 00:00:00 2001 From: Shane Loretz Date: Tue, 14 Jul 2026 22:37:58 +0000 Subject: [PATCH 2/2] use omg and ros as syntax values Signed-off-by: Shane Loretz --- rosidl_generator_py/CMakeLists.txt | 15 +++++++++++++ rosidl_generator_py/resource/_msg.py.em | 19 ++++++++-------- .../rosidl_generator_py/__init__.py | 4 ++-- .../rosidl_generator_py/field_type_syntax.py | 22 ++++++++++--------- .../test/test_field_type_syntax.py | 8 +++---- rosidl_generator_py/test/test_interfaces.py | 6 +++-- 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/rosidl_generator_py/CMakeLists.txt b/rosidl_generator_py/CMakeLists.txt index e5ff834f..0a878bec 100644 --- a/rosidl_generator_py/CMakeLists.txt +++ b/rosidl_generator_py/CMakeLists.txt @@ -53,6 +53,21 @@ if(BUILD_TESTING) SKIP_INSTALL ) + # Ensure build directory contains a complete package by symlinking/copying python files from source + foreach(_pypath + "__init__.py" + "cli.py" + "field_type_syntax.py" + "generate_py_impl.py" + "import_type_support_impl.py" + ) + file(CREATE_LINK + "${CMAKE_CURRENT_SOURCE_DIR}/rosidl_generator_py/${_pypath}" + "${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py/rosidl_generator_py/${_pypath}" + SYMBOLIC + COPY_ON_ERROR) + endforeach() + set(_append_library_dirs "") if(WIN32) set(_append_library_dirs "${CMAKE_CURRENT_BINARY_DIR}/$") diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 33936f35..07eb1a5b 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -457,23 +457,24 @@ if isinstance(type_, AbstractNestedType): return True @@classmethod - def get_fields_and_field_types(cls, syntax: str = 'idl') -> dict[str, str]: - """Return a dict of field name to field type string. + def get_fields_and_field_types(cls, *, syntax: str = 'omg') -> dict[str, str]: + """ + Return a dict of field name to field type string. - :param syntax: Either 'idl' (default) for the OMG IDL 4.2 type + :param syntax: Either 'omg' (default) for the OMG IDL 4.2 type strings historically returned by this method (e.g. - ``sequence, 32>``), or 'msg' for ROS 2 .msg-syntax + ``sequence, 32>``), or 'ros' for ROS 2 .msg-syntax type strings (e.g. ``string<=256[<=32]``) as expected by .msg-based tooling such as the MCAP ros2msg schema encoding. """ from copy import copy fields = copy(cls._fields_and_field_types) - if syntax == 'idl': + if syntax == 'omg': return fields - if syntax == 'msg': - from rosidl_generator_py import idl_to_msg_syntax - return {name: idl_to_msg_syntax(t) for name, t in fields.items()} - raise ValueError(f"Unknown syntax {syntax!r}: expected 'idl' or 'msg'") + if syntax == 'ros': + from rosidl_generator_py.field_type_syntax import omg_to_ros_syntax + return {name: omg_to_ros_syntax(t) for name, t in fields.items()} + raise ValueError(f"Unknown syntax {syntax!r}: expected 'omg' or 'ros'") @[for member in message.structure.members]@ @[ if len(message.structure.members) == 1 and member.name == EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME]@ @[ continue]@ diff --git a/rosidl_generator_py/rosidl_generator_py/__init__.py b/rosidl_generator_py/rosidl_generator_py/__init__.py index 26811fca..c2075183 100644 --- a/rosidl_generator_py/rosidl_generator_py/__init__.py +++ b/rosidl_generator_py/rosidl_generator_py/__init__.py @@ -15,10 +15,10 @@ import logging import traceback -from .field_type_syntax import idl_to_msg_syntax +from .field_type_syntax import omg_to_ros_syntax from .import_type_support_impl import import_type_support -__all__ = ['idl_to_msg_syntax', 'import_type_support'] +__all__ = ['omg_to_ros_syntax', 'import_type_support'] try: from .generate_py_impl import generate_py # noqa: F401 diff --git a/rosidl_generator_py/rosidl_generator_py/field_type_syntax.py b/rosidl_generator_py/rosidl_generator_py/field_type_syntax.py index 965adcf7..add944f0 100644 --- a/rosidl_generator_py/rosidl_generator_py/field_type_syntax.py +++ b/rosidl_generator_py/rosidl_generator_py/field_type_syntax.py @@ -12,14 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Convert OMG IDL 4.2 field-type strings to ROS 2 .msg-syntax field-type strings. +""" +Convert OMG IDL 4.2 field-type strings to ROS 2 .msg-syntax field-type strings. Generated message classes store field types as OMG IDL 4.2 strings in ``_fields_and_field_types`` (e.g. ``sequence, 32>``). Some tooling -- such as consumers of the MCAP ``ros2msg`` schema encoding -- expects ROS 2 ``.msg`` syntax instead (e.g. ``string<=256[<=32]``). This module implements that conversion so it can be requested on demand via -``get_fields_and_field_types(syntax='msg')`` without changing the stored +``get_fields_and_field_types(syntax='ros')`` without changing the stored IDL representation. """ @@ -30,23 +31,24 @@ _UNBOUNDED_SEQUENCE_RE = re.compile(r'^sequence<(.+)>$') -def idl_to_msg_syntax(idl_type: str) -> str: - """Convert a single OMG IDL 4.2 field-type string to ROS 2 .msg syntax. +def omg_to_ros_syntax(omg_type: str) -> str: + """ + Convert a single OMG IDL 4.2 field-type string to ROS 2 .msg syntax. - :param idl_type: A field-type string as stored in a generated message + :param omg_type: A field-type string as stored in a generated message class's ``_fields_and_field_types`` dict, e.g. ``sequence, 32>``. :return: The equivalent ROS 2 .msg-syntax string, e.g. ``string<=256[<=32]``. """ - match = _BOUNDED_SEQUENCE_RE.match(idl_type) + match = _BOUNDED_SEQUENCE_RE.match(omg_type) if match: value_type, max_size = match.groups() - return f'{idl_to_msg_syntax(value_type)}[<={max_size}]' + return f'{omg_to_ros_syntax(value_type)}[<={max_size}]' - match = _UNBOUNDED_SEQUENCE_RE.match(idl_type) + match = _UNBOUNDED_SEQUENCE_RE.match(omg_type) if match: value_type = match.group(1) - return f'{idl_to_msg_syntax(value_type)}[]' + return f'{omg_to_ros_syntax(value_type)}[]' - return _BOUNDED_STRING_RE.sub(r'\1<=\2', idl_type) + return _BOUNDED_STRING_RE.sub(r'\1<=\2', omg_type) diff --git a/rosidl_generator_py/test/test_field_type_syntax.py b/rosidl_generator_py/test/test_field_type_syntax.py index e17dbd5b..735c26ec 100644 --- a/rosidl_generator_py/test/test_field_type_syntax.py +++ b/rosidl_generator_py/test/test_field_type_syntax.py @@ -14,11 +14,11 @@ import pytest -from rosidl_generator_py import idl_to_msg_syntax +from rosidl_generator_py.field_type_syntax import omg_to_ros_syntax @pytest.mark.parametrize( - 'idl_type,expected_msg_type', + 'omg_type,expected_ros_type', [ ('string<5>', 'string<=5'), ('wstring<5>', 'wstring<=5'), @@ -31,5 +31,5 @@ ('sequence, 10>', 'string<=5[<=10]'), ], ) -def test_idl_to_msg_syntax(idl_type: str, expected_msg_type: str) -> None: - assert idl_to_msg_syntax(idl_type) == expected_msg_type +def test_omg_to_ros_syntax(omg_type: str, expected_ros_type: str) -> None: + assert omg_to_ros_syntax(omg_type) == expected_ros_type diff --git a/rosidl_generator_py/test/test_interfaces.py b/rosidl_generator_py/test/test_interfaces.py index 65d0789a..c3a90a7b 100644 --- a/rosidl_generator_py/test/test_interfaces.py +++ b/rosidl_generator_py/test/test_interfaces.py @@ -938,6 +938,8 @@ def test_string_slot_attributes() -> None: assert hasattr(msg, 'get_fields_and_field_types') assert hasattr(msg, '__slots__') string_slot_types_dict = getattr(msg, 'get_fields_and_field_types')() + string_slot_types_dict_explicit = getattr(msg, 'get_fields_and_field_types')(syntax='omg') + assert string_slot_types_dict == string_slot_types_dict_explicit expected_string_slot_types_dict = { 'ub_string_static_array_value': 'string<5>[3]', 'ub_string_ub_array_value': 'sequence, 10>', @@ -959,10 +961,10 @@ def test_string_slot_attributes() -> None: assert expected_slot_type == string_slot_types_dict[expected_field] -def test_string_slot_attributes_msg_syntax() -> None: +def test_string_slot_attributes_ros_syntax() -> None: msg = StringArrays(check_fields=True) assert hasattr(msg, 'get_fields_and_field_types') - string_slot_types_dict = getattr(msg, 'get_fields_and_field_types')(syntax='msg') + string_slot_types_dict = getattr(msg, 'get_fields_and_field_types')(syntax='ros') expected_string_slot_types_dict = { 'ub_string_static_array_value': 'string<=5[3]', 'ub_string_ub_array_value': 'string<=5[<=10]',