Skip to content

Commit 821cdb0

Browse files
committed
Add Python generator support for deprecated interface fields
Depends on: ros2/rosidl#945 Signed-off-by: Pengkun-ZHU <q1091803103@gmail.com>
1 parent 01af609 commit 821cdb0

5 files changed

Lines changed: 110 additions & 3 deletions

File tree

rosidl_generator_py/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ if(BUILD_TESTING)
4949
msg/BuiltinTypeSequencesIdl.idl
5050
msg/StringArrays.msg
5151
msg/Property.msg
52+
msg/TestDeprecated.idl
5253
ADD_LINTER_TESTS
5354
SKIP_INSTALL
5455
)
@@ -75,6 +76,12 @@ if(BUILD_TESTING)
7576
APPEND_LIBRARY_DIRS "${_append_library_dirs}"
7677
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py"
7778
)
79+
80+
ament_add_pytest_test(test_deprecated_py test/test_deprecated.py
81+
APPEND_ENV "PYTHONPATH=${pythonpath}"
82+
APPEND_LIBRARY_DIRS "${_append_library_dirs}"
83+
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py"
84+
)
7885
endif()
7986
endif()
8087

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module rosidl_generator_py {
2+
module msg {
3+
struct TestDeprecated {
4+
@deprecated ( text="Use distance_meters instead")
5+
uint8 distance_cm;
6+
double distance_meters;
7+
};
8+
};
9+
};

rosidl_generator_py/resource/_msg.py.em

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ for member in message.structure.members:
8383
if member.name != EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME:
8484
imports.setdefault(
8585
'import builtins', []) # used for @builtins.property
86+
if member.has_annotation('deprecated'):
87+
imports.setdefault(
88+
'from typing_extensions import deprecated as _deprecated', [])
8689
if isinstance(type_, BasicType) and type_.typename in FLOATING_POINT_TYPES:
8790
imports.setdefault(
8891
'import math', []) # used for math.isinf
@@ -417,7 +420,7 @@ if isinstance(type_, AbstractNestedType):
417420
typename.append(self.__class__.__name__)
418421
args: list[str] = []
419422
for s, t in zip(self.get_fields_and_field_types().keys(), self.SLOT_TYPES):
420-
field = getattr(self, s)
423+
field = getattr(self, '_' + s)
421424
fieldstr = repr(field)
422425
# We use Python array type for fields that can be directly stored
423426
# in them, and "normal" sequences for everything else. If it is
@@ -446,9 +449,9 @@ if isinstance(type_, AbstractNestedType):
446449
@[ continue]@
447450
@[ end if]@
448451
@[ if isinstance(member.type, Array) and isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES]@
449-
if any(self.@(member.name) != other.@(member.name)):
452+
if any(self._@(member.name) != other._@(member.name)):
450453
@[ else]@
451-
if self.@(member.name) != other.@(member.name):
454+
if self._@(member.name) != other._@(member.name):
452455
@[ end if]@
453456
return False
454457
@[end for]@
@@ -476,12 +479,24 @@ array_type_commment = ''
476479
if isinstance(member.type, (Array, AbstractSequence)):
477480
array_type_commment = ' # typing.Annotated can be remove after mypy 1.16+ see mypy#3004'
478481
}@
482+
@[ if member.has_annotation('deprecated')]@
483+
@{
484+
deprecation_annotation = member.get_annotation_value('deprecated')
485+
deprecation_text = deprecation_annotation.get('text', '') if isinstance(deprecation_annotation, dict) else ''
486+
}@
487+
@[ end if]@
479488
@@builtins.property@(noqa_string)
489+
@[ if member.has_annotation('deprecated')]@
490+
@@_deprecated('@(deprecation_text)')@(noqa_string)
491+
@[ end if]@
480492
def @(member.name)(self) -> @(type_annotations_getter[member.name]):@(noqa_string)@(array_type_commment)
481493
"""Message field '@(member.name)'."""
482494
return self._@(member.name)
483495

484496
@@@(member.name).setter@(noqa_string)
497+
@[ if member.has_annotation('deprecated')]@
498+
@@_deprecated('@(deprecation_text)')@(noqa_string)
499+
@[ end if]@
485500
def @(member.name)(self, value: @(type_annotations_setter[member.name])) -> None:@(noqa_string)
486501
@[ if isinstance(member.type, AbstractNestedType)]@
487502
if isinstance(value, collections.abc.Set):

rosidl_generator_py/resource/_msg_support.c.em

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ if isinstance(type_, AbstractNestedType):
202202
type_ = type_.value_type
203203
}@
204204
{ // @(member.name)
205+
@[ if member.has_annotation('deprecated')]@
206+
DISABLE_DEPRECATED_PUSH
207+
@[ end if]@
205208
PyObject * field = PyObject_GetAttrString(_pymsg, "@(member.name)");
206209
if (!field) {
207210
return false;
@@ -512,6 +515,9 @@ nested_type = '__'.join(type_.namespaced_name())
512515
assert(false);
513516
@[ end if]@
514517
Py_DECREF(field);
518+
@[ if member.has_annotation('deprecated')]@
519+
DISABLE_DEPRECATED_POP
520+
@[ end if]@
515521
}
516522
@[end for]@
517523

@@ -550,6 +556,9 @@ if isinstance(type_, AbstractNestedType):
550556
type_ = type_.value_type
551557
}@
552558
{ // @(member.name)
559+
@[ if member.has_annotation('deprecated')]@
560+
DISABLE_DEPRECATED_PUSH
561+
@[ end if]@
553562
PyObject * field = NULL;
554563
@[ if isinstance(member.type, AbstractNestedType) and isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES]@
555564
@[ if isinstance(member.type, Array)]@
@@ -795,6 +804,9 @@ nested_type = '__'.join(type_.namespaced_name())
795804
}
796805
}
797806
@[ end if]@
807+
@[ if member.has_annotation('deprecated')]@
808+
DISABLE_DEPRECATED_POP
809+
@[ end if]@
798810
}
799811
@[end for]@
800812

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2026 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import warnings
16+
17+
import pytest
18+
19+
from rosidl_generator_py.msg import TestDeprecated
20+
21+
22+
def test_deprecated_field_getter_emits_warning():
23+
"""Test that accessing a deprecated field emits a DeprecationWarning."""
24+
msg = TestDeprecated()
25+
26+
with pytest.warns(DeprecationWarning, match='Use distance_meters instead'):
27+
_ = msg.distance_cm
28+
29+
30+
def test_deprecated_field_setter_emits_warning():
31+
"""Test that setting a deprecated field emits a DeprecationWarning."""
32+
msg = TestDeprecated()
33+
34+
with pytest.warns(DeprecationWarning, match='Use distance_meters instead'):
35+
msg.distance_cm = 42
36+
37+
38+
def test_non_deprecated_field_no_warning():
39+
"""Test that accessing non-deprecated fields does not emit a warning."""
40+
msg = TestDeprecated()
41+
42+
with warnings.catch_warnings():
43+
warnings.simplefilter('error', DeprecationWarning)
44+
# Should not raise - distance_meters is not deprecated
45+
_ = msg.distance_meters
46+
47+
48+
def test_deprecated_field_values():
49+
"""Test that deprecated fields still work correctly for values."""
50+
msg = TestDeprecated()
51+
52+
# Suppress the deprecation warnings for value testing
53+
with warnings.catch_warnings():
54+
warnings.simplefilter('ignore', DeprecationWarning)
55+
56+
# Default value
57+
assert msg.distance_cm == 0
58+
assert msg.distance_meters == 0.0
59+
60+
# Set and get
61+
msg.distance_cm = 10
62+
msg.distance_meters = 1.5
63+
assert msg.distance_cm == 10
64+
assert msg.distance_meters == 1.5

0 commit comments

Comments
 (0)