diff --git a/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake b/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake index 177b15c9..5379b0ec 100644 --- a/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake +++ b/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake @@ -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}) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 98285ecb..7a302a38 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -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 @@ -494,37 +493,13 @@ 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 ( @@ -532,80 +507,59 @@ if isinstance(member.type, (Array, AbstractSequence)): 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)' @@ -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), \ @@ -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 diff --git a/rosidl_generator_py/resource/abstract_nested_type_check.py.em b/rosidl_generator_py/resource/abstract_nested_type_check.py.em new file mode 100644 index 00000000..5d15f78e --- /dev/null +++ b/rosidl_generator_py/resource/abstract_nested_type_check.py.em @@ -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))" diff --git a/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py b/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py index c0cf7c9b..f6f4016b 100644 --- a/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py +++ b/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py @@ -400,15 +400,20 @@ 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}]' @@ -416,7 +421,11 @@ def get_setter_and_getter_type(member: Member, type_imports: set[str]) -> tuple[ 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 diff --git a/rosidl_generator_py/test/test_interfaces.py b/rosidl_generator_py/test/test_interfaces.py index 8d11d423..64225d9c 100644 --- a/rosidl_generator_py/test/test_interfaces.py +++ b/rosidl_generator_py/test/test_interfaces.py @@ -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) @@ -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) @@ -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)