Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve array support #8

Merged
merged 11 commits into from
Jun 2, 2022

Conversation

MrBlenny
Copy link
Contributor

@MrBlenny MrBlenny commented May 16, 2022

Thanks so much for your work here. This PR aims to improve support for arrays and sequences.

Example 1 - array.array[?]:

sensor_msgs/Image.msg has property uint8[] data

This generates the following in _image.py

 @property
    def data(self):
        """Message field 'data'."""
        return self._data

    @data.setter
    def data(self, value):
        if isinstance(value, array.array):
            assert value.typecode == 'B', \
                "The 'data' array.array() must have the type code of 'B'"
            self._data = value
            return
        if __debug__:
            from collections.abc import Sequence
            from collections.abc import Set
            from collections import UserList
            from collections import UserString
            assert \
                ((isinstance(value, Sequence) or
                  isinstance(value, Set) or
                  isinstance(value, UserList)) and
                 not isinstance(value, str) and
                 not isinstance(value, UserString) and
                 all(isinstance(v, int) for v in value) and
                 all(val >= 0 and val < 256 for val in value)), \
                "The 'data' field must be a set or sequence and each value of type 'int' and each unsigned integer in [0, 255]"
        self._data = array.array('B', value)

As such, the pyi should be:

    @property
    def data(self) -> array.array[int]: ...
    @data.setter
    def data(self, value: typing.Union[typing.Sequence[int], array.array[int]]) -> None: ...

Example 2 - np.ndarray:

sensor_msgs/Imu.msg has property float64[9] orientation_covariance

This generates the following in _imu.py

  @property
    def orientation_covariance(self):
        """Message field 'orientation_covariance'."""
        return self._orientation_covariance

    @orientation_covariance.setter
    def orientation_covariance(self, value):
        if isinstance(value, numpy.ndarray):
            assert value.dtype == numpy.float64, \
                "The 'orientation_covariance' numpy.ndarray() must have the dtype of 'numpy.float64'"
            assert value.size == 9, \
                "The 'orientation_covariance' numpy.ndarray() must have a size of 9"
            self._orientation_covariance = value
            return
        if __debug__:
            from collections.abc import Sequence
            from collections.abc import Set
            from collections import UserList
            from collections import UserString
            assert \
                ((isinstance(value, Sequence) or
                  isinstance(value, Set) or
                  isinstance(value, UserList)) and
                 not isinstance(value, str) and
                 not isinstance(value, UserString) and
                 len(value) == 9 and
                 all(isinstance(v, float) for v in value) and
                 True), \
                "The 'orientation_covariance' field must be a set or sequence with length 9 and each value of type 'float'"
        self._orientation_covariance = numpy.array(value, dtype=numpy.float64)

As such, the pyi should be:

    @property
    def orientation_covariance(self) -> np.ndarray: ...
    @orientation_covariance.setter
    def orientation_covariance(self, value: typing.Union[typing.Sequence[float], np.ndarray]) -> None: ...

This PR corrects the pyi to be as described above. Note that array and numpy is now imported into EVERY pyi file, even if it is not used.

Copy link
Member

@bonprosoft bonprosoft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much! 😃
I left some comments, please take a look!

rosidl_generator_mypy/__init__.py Outdated Show resolved Hide resolved
rosidl_generator_mypy/__init__.py Outdated Show resolved Hide resolved
rosidl_generator_mypy/__init__.py Outdated Show resolved Hide resolved
rosidl_generator_mypy/__init__.py Outdated Show resolved Hide resolved
@bonprosoft
Copy link
Member

bonprosoft commented May 17, 2022

FYI: I made a suggestion patch to this PR: 70dad0e
Please feel free to add anything! Thank you.

Copy link
Contributor Author

@MrBlenny MrBlenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic! I agee with all of that. I've made those changes.

Co-authored-by: Yuki Igarashi <[email protected]>
type_annotation = to_type_annotation(
current_namespace, defined_classes, type_.value_type
)
return Annotation(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the specification.
https://design.ros2.org/articles/idl_interface_definition.html

According to the Type Mapping section on the document, shouldn't we check type_annotation.getter in SPECIAL_NESTED_BASIC_TYPES in the same way as AbstractSequence?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure... I've tested this against most of the standard ros interfaces (eg sensor_msgs) and I think everything looked correct.
It could be prudent to write some tests which colcon build a bunch of messages and make assertions about the pyi code it generates but I'm not sure I'll have time to do that right now.

Copy link
Member

@bonprosoft bonprosoft May 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I referred to the rosidl_python implementation, and it actually follows the specification.

I think the member initialization part is helpful to understand this behavior:
https://github.com/ros2/rosidl_python/blob/6675823dd6123360912e233a830d1edab89c7651/rosidl_generator_py/resource/_msg.py.em#L320-L332

I've also tested with the following message definition and got the same result that I expected.

Definition

int64[] integer_array
int64[4] integer_fixed_array

std_msgs/Header[] header_array
std_msgs/Header[4] header_fixed_array

integer_array

    @integer_array.setter
    def integer_array(self, value):
        if isinstance(value, array.array):
            assert value.typecode == 'q', \
                "The 'integer_array' array.array() must have the type code of 'q'"
            self._integer_array = value
            return
        if __debug__:
            from collections.abc import Sequence
            from collections.abc import Set
            from collections import UserList
            from collections import UserString
            assert \
                ((isinstance(value, Sequence) or
                  isinstance(value, Set) or
                  isinstance(value, UserList)) and
                 not isinstance(value, str) and
                 not isinstance(value, UserString) and
                 all(isinstance(v, int) for v in value) and
                 all(val >= -9223372036854775808 and val < 9223372036854775808 for val in value)), \
                "The 'integer_array' field must be a set or sequence and each value of type 'int' and each integer in [-9223372036854775808, 9223372036854775807]"
        self._integer_array = array.array('q', value)

integer_fixed_array

    @integer_fixed_array.setter
    def integer_fixed_array(self, value):
        if isinstance(value, numpy.ndarray):
            assert value.dtype == numpy.int64, \
                "The 'integer_fixed_array' numpy.ndarray() must have the dtype of 'numpy.int64'"
            assert value.size == 4, \
                "The 'integer_fixed_array' numpy.ndarray() must have a size of 4"
            self._integer_fixed_array = value
            return
        if __debug__:
            from collections.abc import Sequence
            from collections.abc import Set
            from collections import UserList
            from collections import UserString
            assert \
                ((isinstance(value, Sequence) or
                  isinstance(value, Set) or
                  isinstance(value, UserList)) and
                 not isinstance(value, str) and
                 not isinstance(value, UserString) and
                 len(value) == 4 and
                 all(isinstance(v, int) for v in value) and
                 all(val >= -9223372036854775808 and val < 9223372036854775808 for val in value)), \
                "The 'integer_fixed_array' field must be a set or sequence with length 4 and each value of type 'int' and each integer in [-9223372036854775808, 9223372036854775807]"
        self._integer_fixed_array = numpy.array(value, dtype=numpy.int64)

header_array

    @header_array.setter
    def header_array(self, value):
        if __debug__:
            from std_msgs.msg import Header
            from collections.abc import Sequence
            from collections.abc import Set
            from collections import UserList
            from collections import UserString
            assert \
                ((isinstance(value, Sequence) or
                  isinstance(value, Set) or
                  isinstance(value, UserList)) and
                 not isinstance(value, str) and
                 not isinstance(value, UserString) and
                 all(isinstance(v, Header) for v in value) and
                 True), \
                "The 'header_array' field must be a set or sequence and each value of type 'Header'"
        self._header_array = value

header_fixed_array

    @header_fixed_array.setter
    def header_fixed_array(self, value):
        if __debug__:
            from std_msgs.msg import Header
            from collections.abc import Sequence
            from collections.abc import Set
            from collections import UserList
            from collections import UserString
            assert \
                ((isinstance(value, Sequence) or
                  isinstance(value, Set) or
                  isinstance(value, UserList)) and
                 not isinstance(value, str) and
                 not isinstance(value, UserString) and
                 len(value) == 4 and
                 all(isinstance(v, Header) for v in value) and
                 True), \
                "The 'header_fixed_array' field must be a set or sequence with length 4 and each value of type 'Header'"
        self._header_fixed_array = value

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MrBlenny I also encountered the same issue as you 😅
Actually, this PR is great, and It would be wonderful to merge this PR soon.
If you don't have time to update this PR, please let me know. I am happy to help you.

Copy link
Member

@bonprosoft bonprosoft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the update!
Sorry, I left another comment 🙇‍♂️

@bonprosoft
Copy link
Member

@MrBlenny I left a comment. Let me kindly ping you just in case...

@MrBlenny
Copy link
Contributor Author

Sorry @bonprosoft
Please see my latest commit

Copy link
Member

@bonprosoft bonprosoft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you!

@bonprosoft bonprosoft merged commit ea8a376 into rospypi:master Jun 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants