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
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ set(target_dependencies
"${rosidl_generator_py_TEMPLATE_DIR}/_msg.py.em"
"${rosidl_generator_py_TEMPLATE_DIR}/_srv_pkg_typesupport_entry_point.c.em"
"${rosidl_generator_py_TEMPLATE_DIR}/_srv.py.em"
"${rosidl_generator_py_TEMPLATE_DIR}/abstract_nested_type_check.py.em"
${rosidl_generate_interfaces_ABS_IDL_FILES}
${_dependency_files})
foreach(dep ${target_dependencies})
Expand Down
148 changes: 53 additions & 95 deletions rosidl_generator_py/resource/_msg.py.em
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@# Included from rosidl_generator_py/resource/_idl.py.em
@{

from rosidl_pycommon import convert_camel_case_to_lower_case_underscore
from rosidl_generator_py.generate_py_impl import constant_value_to_py
from rosidl_generator_py.generate_py_impl import get_python_type
Expand Down Expand Up @@ -494,118 +493,73 @@ if isinstance(member.type, (Array, AbstractSequence)):

@@@(member.name).setter@(noqa_string)
def @(member.name)(self, value: @(type_annotations_setter[member.name])) -> None:@(noqa_string)

@[ if isinstance(member.type, AbstractNestedType)]@
from collections.abc import Set
if isinstance(value, Set):
if isinstance(value, collections.abc.Set):
import warnings
warnings.warn(
'Using set or subclass of set is deprecated,'
' please use a subclass of collections.abc.Sequence like list',
DeprecationWarning)
@[ end if]@
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]@
@[ if isinstance(member.type, Array)]@
if isinstance(value, numpy.ndarray):
assert value.dtype == @(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype']), \
"The '@(member.name)' numpy.ndarray() must have the dtype of '@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'])'"
assert value.size == @(member.type.size), \
"The '@(member.name)' numpy.ndarray() must have a size of @(member.type.size)"
self._@(member.name) = value
return
@[ elif isinstance(member.type, AbstractSequence)]@
if isinstance(value, array.array):
assert value.typecode == '@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', \
"The '@(member.name)' array.array() must have the type code of '@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])'"
@[ if isinstance(member.type, BoundedSequence)]@
assert len(value) <= @(member.type.maximum_size), \
"The '@(member.name)' array.array() must have a size <= @(member.type.maximum_size)"
@[ end if]@
self._@(member.name) = value
return
@[ end if]@
@[ end if]@
@[ if isinstance(type_, NamespacedType)]@
@[ if (
type_.name.endswith(ACTION_GOAL_SUFFIX) or
type_.name.endswith(ACTION_RESULT_SUFFIX) or
type_.name.endswith(ACTION_FEEDBACK_SUFFIX)
)]@
from @('.'.join(type_.namespaces))._@(convert_camel_case_to_lower_case_underscore(type_.name.rsplit('_', 1)[0])) import @(type_.name)
from @('.'.join(type_.namespaces))._@(convert_camel_case_to_lower_case_underscore(type_.name.rsplit('_', 1)[0])) import @(type_.name)
@[ else]@
from @('.'.join(type_.namespaces)) import @(type_.name)
from @('.'.join(type_.namespaces)) import @(type_.name)
@[ end if]@
@[ end if]@
@[ if isinstance(member.type, AbstractNestedType)]@
from collections.abc import Sequence
from collections import UserString
@[ elif isinstance(type_, AbstractGenericString) and type_.has_maximum_size()]@
from collections import UserString
@[ elif isinstance(type_, BasicType) and type_.typename in CHARACTER_TYPES]@
from collections import UserString
@[ end if]@
assert \
@[ if isinstance(member.type, AbstractNestedType)]@
((isinstance(value, Sequence) or
isinstance(value, Set)) and
not isinstance(value, str) and
not isinstance(value, UserString) and
@{assert_msg_suffixes = ['sequence']}@
@[ if isinstance(type_, AbstractGenericString) and type_.has_maximum_size()]@
all(len(val) <= @(type_.maximum_size) for val in value) and
@{assert_msg_suffixes.append('and each string value not longer than %d' % type_.maximum_size)}@
@[ end if]@
@[ if isinstance(member.type, (Array, BoundedSequence))]@
@[ if isinstance(member.type, BoundedSequence)]@
len(value) <= @(member.type.maximum_size) and
@{assert_msg_suffixes.insert(1, 'with length <= %d' % member.type.maximum_size)}@
@[ else]@
len(value) == @(member.type.size) and
@{assert_msg_suffixes.insert(1, 'with length %d' % member.type.size)}@
@[ if isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES]@
@[ if isinstance(member.type, Array)]@
if isinstance(value, numpy.ndarray):
if self._check_fields:
assert value.dtype == @(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype']), \
"The '@(member.name)' numpy.ndarray() must have the dtype of '@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'])'"
assert value.size == @(member.type.size), \
"The '@(member.name)' numpy.ndarray() must have a size of @(member.type.size)"
@[ elif isinstance(member.type, AbstractSequence)]@
if isinstance(value, array.array):
if self._check_fields:
assert value.typecode == '@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', \
"The '@(member.name)' array.array() must have the type code of '@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])'"
@[ if isinstance(member.type, BoundedSequence)]@
assert len(value) <= @(member.type.maximum_size), \
"The '@(member.name)' array.array() must have a size <= @(member.type.maximum_size)"
@[ end if]@
@[ end if]@
@[ end if]@
all(isinstance(v, @(get_python_type(type_))) for v in value) and
@{assert_msg_suffixes.append("and each value of type '%s'" % get_python_type(type_))}@
@[ if isinstance(type_, BasicType) and type_.typename in SIGNED_INTEGER_TYPES]@
@{
nbits = int(type_.typename[3:])
bound = 2**(nbits - 1)
}@
all(val >= -@(bound) and val < @(bound) for val in value)), \
@{assert_msg_suffixes.append('and each integer in [%d, %d]' % (-bound, bound - 1))}@
@[ elif isinstance(type_, BasicType) and type_.typename in UNSIGNED_INTEGER_TYPES]@
@{
nbits = int(type_.typename[4:])
bound = 2**nbits
}@
all(val >= 0 and val < @(bound) for val in value)), \
@{assert_msg_suffixes.append('and each unsigned integer in [0, %d]' % (bound - 1))}@
@[ elif isinstance(type_, BasicType) and type_.typename == 'char']@
all(ord(val) >= 0 and ord(val) < 256 for val in value)), \
@{assert_msg_suffixes.append('and each char in [0, 255]')}@
@[ elif isinstance(type_, BasicType) and type_.typename in FLOATING_POINT_TYPES]@
@[ if type_.typename == "float"]@
@[ else]@
if isinstance(value, list):
if self._check_fields:
assert \
@{
name = "float"
bound = 3.402823466e+38
TEMPLATE(
'abstract_nested_type_check.py.em',
member=member,
type_=type_,
indent=(' ' * 5)
)
}@
all(not (val < -@(bound) or val > @(bound)) or math.isinf(val) for val in value)), \
@{assert_msg_suffixes.append('and each float in [%f, %f]' % (-bound, bound))}@
@[ elif type_.typename == "double"]@
@[ end if]@
self._@(member.name) = value
return
@[ end if]@
if self._check_fields:
assert \
@[ if isinstance(member.type, AbstractNestedType)]@
@{
name = "double"
bound = 1.7976931348623157e+308
TEMPLATE(
'abstract_nested_type_check.py.em',
member=member,
type_=type_,
indent=(' ' * 4)
)
}@
all(not (val < -@(bound) or val > @(bound)) or math.isinf(val) for val in value)), \
@{assert_msg_suffixes.append('and each double in [%f, %f]' % (-bound, bound))}@
@[ end if]@
@[ else]@
True), \
@[ end if]@
"The '@(member.name)' field must be @(' '.join(assert_msg_suffixes))"
@[ elif isinstance(member.type, AbstractGenericString) and member.type.has_maximum_size()]@
(isinstance(value, (str, UserString)) and
(isinstance(value, (str, collections.UserString)) and
len(value) <= @(member.type.maximum_size)), \
"The '@(member.name)' field must be string value " \
'not longer than @(type_.maximum_size)'
Expand All @@ -617,9 +571,9 @@ bound = 1.7976931348623157e+308
len(value) == 1), \
"The '@(member.name)' field must be of type 'bytes' or 'ByteString' with length 1"
@[ elif isinstance(type_, BasicType) and type_.typename == 'char']@
(isinstance(value, (str, UserString)) and
(isinstance(value, (str, collections.UserString)) and
len(value) == 1 and ord(value) >= -128 and ord(value) < 128), \
"The '@(member.name)' field must be of type 'str' or 'UserString' " \
"The '@(member.name)' field must be of type 'str' or 'collections.UserString' " \
'with length 1 and the character ord() in [-128, 127]'
@[ elif isinstance(type_, AbstractGenericString)]@
isinstance(value, str), \
Expand Down Expand Up @@ -659,12 +613,16 @@ bound = 1.7976931348623157e+308
@[ else]@
False
@[ end if]@
@[ 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)]@
@[ if isinstance(member.type, AbstractNestedType)]@
@[ if isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES]@
@[ if isinstance(member.type, Array)]@
self._@(member.name) = numpy.array(value, dtype=@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype']))
@[ elif isinstance(member.type, AbstractSequence)]@
@[ elif isinstance(member.type, AbstractSequence)]@
# type ignore below fixed in mypy 1.17+ see mypy#19421
self._@(member.name) = array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', value) # type: ignore[assignment]
@[ end if]@
@[ else]@
self._@(member.name) = list(value)
@[ end if]@
@[ else]@
self._@(member.name) = value
Expand Down
67 changes: 67 additions & 0 deletions rosidl_generator_py/resource/abstract_nested_type_check.py.em
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@{
from rosidl_parser.definition import AbstractGenericString
from rosidl_parser.definition import Array
from rosidl_parser.definition import BasicType
from rosidl_parser.definition import BoundedSequence
from rosidl_parser.definition import FLOATING_POINT_TYPES
from rosidl_parser.definition import SIGNED_INTEGER_TYPES
from rosidl_parser.definition import UNSIGNED_INTEGER_TYPES
from rosidl_generator_py.generate_py_impl import get_python_type
}@
@(indent)((isinstance(value, collections.abc.Sequence) or
@(indent) isinstance(value, collections.abc.Set)) and
@(indent) not isinstance(value, str) and
@(indent) not isinstance(value, collections.UserString) and
@{assert_msg_suffixes = ['sequence']}@
@[ if isinstance(type_, AbstractGenericString) and type_.has_maximum_size()]@
@(indent) all(len(val) <= @(type_.maximum_size) for val in value) and
@{assert_msg_suffixes.append('and each string value not longer than %d' % type_.maximum_size)}@
@[ end if]@
@[ if isinstance(member.type, (Array, BoundedSequence))]@
@[ if isinstance(member.type, BoundedSequence)]@
@(indent) len(value) <= @(member.type.maximum_size) and
@{assert_msg_suffixes.insert(1, 'with length <= %d' % member.type.maximum_size)}@
@[ else]@
@(indent) len(value) == @(member.type.size) and
@{assert_msg_suffixes.insert(1, 'with length %d' % member.type.size)}@
@[ end if]@
@[ end if]@
@(indent) all(isinstance(v, @(get_python_type(type_))) for v in value) and
@{assert_msg_suffixes.append("and each value of type '%s'" % get_python_type(type_))}@
@[ if isinstance(type_, BasicType) and type_.typename in SIGNED_INTEGER_TYPES]@
@{
nbits = int(type_.typename[3:])
bound = 2**(nbits - 1)
}@
@(indent) all(val >= -@(bound) and val < @(bound) for val in value)), \
@{assert_msg_suffixes.append('and each integer in [%d, %d]' % (-bound, bound - 1))}@
@[ elif isinstance(type_, BasicType) and type_.typename in UNSIGNED_INTEGER_TYPES]@
@{
nbits = int(type_.typename[4:])
bound = 2**nbits
}@
@(indent) all(val >= 0 and val < @(bound) for val in value)), \
@{assert_msg_suffixes.append('and each unsigned integer in [0, %d]' % (bound - 1))}@
@[ elif isinstance(type_, BasicType) and type_.typename == 'char']@
@(indent) all(ord(val) >= 0 and ord(val) < 256 for val in value)), \
@{assert_msg_suffixes.append('and each char in [0, 255]')}@
@[ elif isinstance(type_, BasicType) and type_.typename in FLOATING_POINT_TYPES]@
@[ if type_.typename == "float"]@
@{
name = "float"
bound = 3.402823466e+38
}@
@(indent) all(not (val < -@(bound) or val > @(bound)) or math.isinf(val) for val in value)), \
@{assert_msg_suffixes.append('and each float in [%f, %f]' % (-bound, bound))}@
@[ elif type_.typename == "double"]@
@{
name = "double"
bound = 1.7976931348623157e+308
}@
@(indent) all(not (val < -@(bound) or val > @(bound)) or math.isinf(val) for val in value)), \
@{assert_msg_suffixes.append('and each double in [%f, %f]' % (-bound, bound))}@
@[ end if]@
@[ else]@
@(indent) True), \
@[ end if]@
@(indent)"The '@(member.name)' field must be @(' '.join(assert_msg_suffixes))"
27 changes: 18 additions & 9 deletions rosidl_generator_py/rosidl_generator_py/generate_py_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,23 +400,32 @@ def get_setter_and_getter_type(member: Member, type_imports: set[str]) -> tuple[
type_annotations_getter = ''

if (
isinstance(member.type, AbstractNestedType) and isinstance(type_, BasicType) and
type_.typename in SPECIAL_NESTED_BASIC_TYPES
isinstance(member.type, AbstractNestedType)
):
if isinstance(member.type, Array):
type_imports.add('import numpy.typing')
dtype = SPECIAL_NESTED_BASIC_TYPES[type_.typename]['dtype']
type_annotation = f'numpy.typing.NDArray[{dtype}]'
elif isinstance(member.type, AbstractSequence):
type_annotation = f'array.array[{python_type}]'
if (
isinstance(type_, BasicType) and
type_.typename in SPECIAL_NESTED_BASIC_TYPES
):
if isinstance(member.type, Array):
type_imports.add('import numpy.typing')
dtype = SPECIAL_NESTED_BASIC_TYPES[type_.typename]['dtype']
type_annotation = f'numpy.typing.NDArray[{dtype}]'
elif isinstance(member.type, AbstractSequence):
type_annotation = f'array.array[{python_type}]'
else:
type_annotation = f'list[{python_type}]'

# Using Annotated because of mypy#3004
type_annotations_getter = f'typing.Annotated[typing.Any, {type_annotation}]'

if isinstance(member.type, AbstractNestedType):
sequence_type = f'collections.abc.Sequence[{python_type}]'

if type_annotation != '':
if (
isinstance(type_, BasicType) and
type_.typename in SPECIAL_NESTED_BASIC_TYPES and
isinstance(member.type, Array)
):
type_annotation = f'typing.Union[{type_annotation}, {sequence_type}]'
else:
type_annotation = sequence_type
Expand Down
12 changes: 12 additions & 0 deletions rosidl_generator_py/test/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,10 @@ def test_arrays() -> None:
with pytest.warns(DeprecationWarning):
Arrays(string_values={'bar', 'baz', 'foo'})

msg.bool_values = (False, False, False)
assert isinstance(msg.bool_values, list)
assert msg.bool_values == [False, False, False]


def test_bounded_sequences() -> None:
msg = BoundedSequences(check_fields=True)
Expand Down Expand Up @@ -756,6 +760,10 @@ def test_bounded_sequences() -> None:
float64_ieee_max_next = numpy.nextafter(1.7976931348623157e+308, math.inf)
setattr(msg, 'float64_values', [-float64_ieee_max_next, 0.0, float64_ieee_max_next])

msg.bool_values = (True, False)
assert isinstance(msg.bool_values, list)
assert msg.bool_values == [True, False]


def test_unbounded_sequences() -> None:
msg = UnboundedSequences(check_fields=True)
Expand Down Expand Up @@ -905,6 +913,10 @@ def test_unbounded_sequences() -> None:
float64_ieee_max_next = numpy.nextafter(1.7976931348623157e+308, math.inf)
setattr(msg, 'float64_values', [-float64_ieee_max_next, 0.0, float64_ieee_max_next])

msg.bool_values = (True, False, True, False)
assert isinstance(msg.bool_values, list)
assert msg.bool_values == [True, False, True, False]


def test_slot_attributes() -> None:
msg = Nested(check_fields=True)
Expand Down