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
15 changes: 15 additions & 0 deletions rosidl_generator_py/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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}/$<CONFIG>")
Expand Down
19 changes: 17 additions & 2 deletions rosidl_generator_py/resource/_msg.py.em
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,24 @@ 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 = 'omg') -> dict[str, str]:
"""
Return a dict of field name to field type string.

:param syntax: Either 'omg' (default) for the OMG IDL 4.2 type
strings historically returned by this method (e.g.
``sequence<string<256>, 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
return copy(cls._fields_and_field_types)
fields = copy(cls._fields_and_field_types)
if syntax == 'omg':
return fields
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]@
Expand Down
3 changes: 2 additions & 1 deletion rosidl_generator_py/rosidl_generator_py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
import logging
import traceback

from .field_type_syntax import omg_to_ros_syntax
from .import_type_support_impl import import_type_support

__all__ = ['import_type_support']
__all__ = ['omg_to_ros_syntax', 'import_type_support']

try:
from .generate_py_impl import generate_py # noqa: F401
Expand Down
54 changes: 54 additions & 0 deletions rosidl_generator_py/rosidl_generator_py/field_type_syntax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 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<string<256>, 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='ros')`` 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 omg_to_ros_syntax(omg_type: str) -> str:
"""
Convert a single OMG IDL 4.2 field-type string to ROS 2 .msg syntax.

:param omg_type: A field-type string as stored in a generated message
class's ``_fields_and_field_types`` dict, e.g.
``sequence<string<256>, 32>``.
:return: The equivalent ROS 2 .msg-syntax string, e.g.
``string<=256[<=32]``.
"""
match = _BOUNDED_SEQUENCE_RE.match(omg_type)
if match:
value_type, max_size = match.groups()
return f'{omg_to_ros_syntax(value_type)}[<={max_size}]'

match = _UNBOUNDED_SEQUENCE_RE.match(omg_type)
if match:
value_type = match.group(1)
return f'{omg_to_ros_syntax(value_type)}[]'

return _BOUNDED_STRING_RE.sub(r'\1<=\2', omg_type)
35 changes: 35 additions & 0 deletions rosidl_generator_py/test/test_field_type_syntax.py
Original file line number Diff line number Diff line change
@@ -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.field_type_syntax import omg_to_ros_syntax


@pytest.mark.parametrize(
'omg_type,expected_ros_type',
[
('string<5>', 'string<=5'),
('wstring<5>', 'wstring<=5'),
('string', 'string'),
('char', 'char'),
('sequence<char>', 'char[]'),
('sequence<string>', 'string[]'),
('sequence<string, 10>', 'string[<=10]'),
('sequence<string<5>>', 'string<=5[]'),
('sequence<string<5>, 10>', 'string<=5[<=10]'),
],
)
def test_omg_to_ros_syntax(omg_type: str, expected_ros_type: str) -> None:
assert omg_to_ros_syntax(omg_type) == expected_ros_type
36 changes: 36 additions & 0 deletions rosidl_generator_py/test/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<string<5>, 10>',
Expand All @@ -959,6 +961,40 @@ def test_string_slot_attributes() -> None:
assert expected_slot_type == string_slot_types_dict[expected_field]


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='ros')
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')
Expand Down