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
7 changes: 7 additions & 0 deletions rosidl_generator_py/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ if(BUILD_TESTING)
msg/BuiltinTypeSequencesIdl.idl
msg/StringArrays.msg
msg/Property.msg
msg/TestDeprecated.idl
ADD_LINTER_TESTS
SKIP_INSTALL
)
Expand All @@ -75,6 +76,12 @@ if(BUILD_TESTING)
APPEND_LIBRARY_DIRS "${_append_library_dirs}"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py"
)

ament_add_pytest_test(test_deprecated_py test/test_deprecated.py
APPEND_ENV "PYTHONPATH=${pythonpath}"
APPEND_LIBRARY_DIRS "${_append_library_dirs}"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py"
)
endif()
endif()

Expand Down
9 changes: 9 additions & 0 deletions rosidl_generator_py/msg/TestDeprecated.idl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module rosidl_generator_py {
module msg {
struct TestDeprecated {
@deprecated ( text="Use distance_meters instead")
uint8 distance_cm;
double distance_meters;
};
};
};
1 change: 1 addition & 0 deletions rosidl_generator_py/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

<exec_depend>ament_index_python</exec_depend>
<exec_depend>python3-numpy</exec_depend>
<exec_depend>python3-typing-extensions</exec_depend>
<exec_depend>rosidl_cli</exec_depend>
<exec_depend>rosidl_generator_c</exec_depend>
<exec_depend>rosidl_parser</exec_depend>
Expand Down
15 changes: 15 additions & 0 deletions rosidl_generator_py/resource/_msg.py.em
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ for member in message.structure.members:
if member.name != EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME:
imports.setdefault(
'import builtins', []) # used for @builtins.property
if member.has_annotation('deprecated'):
imports.setdefault(
'from typing_extensions import deprecated as _deprecated', [])
if isinstance(type_, BasicType) and type_.typename in FLOATING_POINT_TYPES:
imports.setdefault(
'import math', []) # used for math.isinf
Expand Down Expand Up @@ -429,12 +432,24 @@ noqa_string = ''
if member.name in dict(inspect.getmembers(builtins)).keys():
noqa_string = ' # noqa: A003'
}@
@[ if member.has_annotation('deprecated')]@
@{
deprecation_annotation = member.get_annotation_value('deprecated')
deprecation_text = deprecation_annotation.get('text', '') if isinstance(deprecation_annotation, dict) else ''
}@
@[ end if]@
@@builtins.property@(noqa_string)
@[ if member.has_annotation('deprecated')]@
@@_deprecated('@(deprecation_text)')@(noqa_string)
@[ end if]@
def @(member.name)(self):@(noqa_string)
"""Message field '@(member.name)'."""
return self._@(member.name)

@@@(member.name).setter@(noqa_string)
@[ if member.has_annotation('deprecated')]@
@@_deprecated('@(deprecation_text)')@(noqa_string)
@[ end if]@
def @(member.name)(self, value):@(noqa_string)
if self._check_fields:
@[ if isinstance(member.type, AbstractNestedType) and isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES]@
Expand Down
15 changes: 15 additions & 0 deletions rosidl_generator_py/resource/_msg_support.c.em
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ nested_header += '__functions.h'
@[ end for]@
// end nested array functions include
@[end if]@
@[if any(member.has_annotation('deprecated') for member in message.structure.members)]@
#include "rosidl_runtime_c/deprecation.h"
@[end if]@
@{
msg_typename = '__'.join(message.structure.namespaced_type.namespaced_name())
}@
Expand Down Expand Up @@ -202,6 +205,9 @@ if isinstance(type_, AbstractNestedType):
type_ = type_.value_type
}@
{ // @(member.name)
@[ if member.has_annotation('deprecated')]@
DISABLE_DEPRECATED_PUSH
@[ end if]@
PyObject * field = PyObject_GetAttrString(_pymsg, "@(member.name)");
if (!field) {
return false;
Expand Down Expand Up @@ -512,6 +518,9 @@ nested_type = '__'.join(type_.namespaced_name())
assert(false);
@[ end if]@
Py_DECREF(field);
@[ if member.has_annotation('deprecated')]@
DISABLE_DEPRECATED_POP
@[ end if]@
}
@[end for]@

Expand Down Expand Up @@ -550,6 +559,9 @@ if isinstance(type_, AbstractNestedType):
type_ = type_.value_type
}@
{ // @(member.name)
@[ if member.has_annotation('deprecated')]@
DISABLE_DEPRECATED_PUSH
@[ end if]@
PyObject * field = NULL;
@[ if isinstance(member.type, AbstractNestedType) and isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES]@
@[ if isinstance(member.type, Array)]@
Expand Down Expand Up @@ -795,6 +807,9 @@ nested_type = '__'.join(type_.namespaced_name())
}
}
@[ end if]@
@[ if member.has_annotation('deprecated')]@
DISABLE_DEPRECATED_POP
@[ end if]@
}
@[end for]@

Expand Down
64 changes: 64 additions & 0 deletions rosidl_generator_py/test/test_deprecated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# 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 warnings

import pytest

from rosidl_generator_py.msg import TestDeprecated


def test_deprecated_field_getter_emits_warning():
"""Test that accessing a deprecated field emits a DeprecationWarning."""
msg = TestDeprecated()

with pytest.warns(DeprecationWarning, match='Use distance_meters instead'):
_ = msg.distance_cm


def test_deprecated_field_setter_emits_warning():
"""Test that setting a deprecated field emits a DeprecationWarning."""
msg = TestDeprecated()

with pytest.warns(DeprecationWarning, match='Use distance_meters instead'):
msg.distance_cm = 42


def test_non_deprecated_field_no_warning():
"""Test that accessing non-deprecated fields does not emit a warning."""
msg = TestDeprecated()

with warnings.catch_warnings():
warnings.simplefilter('error', DeprecationWarning)
# Should not raise - distance_meters is not deprecated
_ = msg.distance_meters


def test_deprecated_field_values():
"""Test that deprecated fields still work correctly for values."""
msg = TestDeprecated()

# Suppress the deprecation warnings for value testing
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)

# Default value
assert msg.distance_cm == 0
assert msg.distance_meters == 0.0

# Set and get
msg.distance_cm = 10
msg.distance_meters = 1.5
assert msg.distance_cm == 10
assert msg.distance_meters == 1.5