Skip to content

Commit 4fe2166

Browse files
authored
Merge pull request #34 from ros2/rosidl_runtime_py
Add rosidl_runtime_py package
2 parents 4f11d87 + 4171c5a commit 4fe2166

File tree

10 files changed

+548
-0
lines changed

10 files changed

+548
-0
lines changed

rosidl_runtime_py/package.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0"?>
2+
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
3+
<package format="3">
4+
<name>rosidl_runtime_py</name>
5+
<version>0.6.2</version>
6+
<description>Runtime utilities for working with generated ROS interfaces in Python.</description>
7+
<maintainer email="[email protected]">Jacob Perron</maintainer>
8+
<license>Apache License 2.0</license>
9+
10+
<author email="[email protected]">Dirk Thomas</author>
11+
12+
<exec_depend>python3-numpy</exec_depend>
13+
<exec_depend>python3-yaml</exec_depend>
14+
15+
<test_depend>ament_copyright</test_depend>
16+
<test_depend>ament_flake8</test_depend>
17+
<test_depend>ament_pep257</test_depend>
18+
<test_depend>python3-pytest</test_depend>
19+
<test_depend>test_msgs</test_depend>
20+
21+
<export>
22+
<build_type>ament_python</build_type>
23+
</export>
24+
</package>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2019 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+
from .convert import message_to_csv
16+
from .convert import message_to_ordereddict
17+
from .convert import message_to_yaml
18+
from .set_message import set_message_fields
19+
20+
21+
__all__ = [
22+
'message_to_csv',
23+
'message_to_ordereddict',
24+
'message_to_yaml',
25+
'set_message_fields',
26+
]
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright 2016-2019 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 array
16+
from collections import OrderedDict
17+
import sys
18+
from typing import Any
19+
20+
import numpy
21+
import yaml
22+
23+
24+
__yaml_representer_registered = False
25+
26+
27+
# Custom representer for getting clean YAML output that preserves the order in an OrderedDict.
28+
# Inspired by: http://stackoverflow.com/a/16782282/7169408
29+
def __represent_ordereddict(dumper, data):
30+
items = []
31+
for k, v in data.items():
32+
items.append((dumper.represent_data(k), dumper.represent_data(v)))
33+
return yaml.nodes.MappingNode(u'tag:yaml.org,2002:map', items)
34+
35+
36+
def message_to_yaml(msg: Any, truncate_length: int = None) -> str:
37+
"""
38+
Convert a ROS message to a YAML string.
39+
40+
:param msg: The ROS message to convert.
41+
:param truncate_length: Truncate values for all message fields to this length.
42+
This does not truncate the list of message fields.
43+
:returns: A YAML string representation of the input ROS message.
44+
"""
45+
global __yaml_representer_registered
46+
47+
# Register our custom representer for YAML output
48+
if not __yaml_representer_registered:
49+
yaml.add_representer(OrderedDict, __represent_ordereddict)
50+
__yaml_representer_registered = True
51+
52+
return yaml.dump(
53+
message_to_ordereddict(msg, truncate_length=truncate_length),
54+
width=sys.maxsize,
55+
)
56+
57+
58+
def message_to_csv(msg: Any, truncate_length: int = None) -> str:
59+
"""
60+
Convert a ROS message to string of comma-separated values.
61+
62+
:param msg: The ROS message to convert.
63+
:param truncate_length: Truncate values for all message fields to this length.
64+
This does not truncate the list of message fields.
65+
:returns: A string of comma-separated values representing the input message.
66+
"""
67+
def to_string(val):
68+
nonlocal truncate_length
69+
r = ''
70+
if any(isinstance(val, t) for t in [list, tuple, array.array, numpy.ndarray]):
71+
for i, v in enumerate(val):
72+
if r:
73+
r += ','
74+
if truncate_length is not None and i >= truncate_length:
75+
r += '...'
76+
break
77+
r += to_string(v)
78+
elif any(isinstance(val, t) for t in [bool, bytes, float, int, str, numpy.number]):
79+
if any(isinstance(val, t) for t in [bytes, str]):
80+
if truncate_length is not None and len(val) > truncate_length:
81+
val = val[:truncate_length]
82+
if isinstance(val, bytes):
83+
val += b'...'
84+
else:
85+
val += '...'
86+
r = str(val)
87+
else:
88+
r = message_to_csv(val, truncate_length)
89+
return r
90+
result = ''
91+
# We rely on __slots__ retaining the order of the fields in the .msg file.
92+
for field_name in msg.__slots__:
93+
value = getattr(msg, field_name)
94+
if result:
95+
result += ','
96+
result += to_string(value)
97+
return result
98+
99+
100+
# Convert a msg to an OrderedDict. We do this instead of implementing a generic __dict__() method
101+
# in the msg because we want to preserve order of fields from the .msg file(s).
102+
def message_to_ordereddict(msg: Any, truncate_length: int = None) -> OrderedDict:
103+
"""
104+
Convert a ROS message to an OrderedDict.
105+
106+
:param msg: The ROS message to convert.
107+
:param truncate_length: Truncate values for all message fields to this length.
108+
This does not truncate the list of fields (ie. the dictionary keys).
109+
:returns: An OrderedDict where the keys are the ROS message fields and the values are
110+
set to the values of the input message.
111+
"""
112+
d = OrderedDict()
113+
# We rely on __slots__ retaining the order of the fields in the .msg file.
114+
for field_name in msg.__slots__:
115+
value = getattr(msg, field_name, None)
116+
value = _convert_value(value, truncate_length=truncate_length)
117+
# Remove leading underscore from field name
118+
d[field_name[1:]] = value
119+
return d
120+
121+
122+
def _convert_value(value, truncate_length=None):
123+
if isinstance(value, bytes):
124+
if truncate_length is not None and len(value) > truncate_length:
125+
value = ''.join([chr(c) for c in value[:truncate_length]]) + '...'
126+
else:
127+
value = ''.join([chr(c) for c in value])
128+
elif isinstance(value, str):
129+
if truncate_length is not None and len(value) > truncate_length:
130+
value = value[:truncate_length] + '...'
131+
elif (any(
132+
isinstance(value, t) for t in [list, tuple, array.array, numpy.ndarray]
133+
)):
134+
# Since arrays and ndarrays can't contain mixed types convert to list
135+
typename = tuple if isinstance(value, tuple) else list
136+
if truncate_length is not None and len(value) > truncate_length:
137+
# Truncate the sequence
138+
value = value[:truncate_length]
139+
# Truncate every item in the sequence
140+
value = typename(
141+
[_convert_value(v, truncate_length) for v in value] + ['...'])
142+
else:
143+
# Truncate every item in the list
144+
value = typename(
145+
[_convert_value(v, truncate_length) for v in value])
146+
elif isinstance(value, dict) or isinstance(value, OrderedDict):
147+
# Convert each key and value in the mapping
148+
new_value = {} if isinstance(value, dict) else OrderedDict()
149+
for k, v in value.items():
150+
# Don't truncate keys because that could result in key collisions and data loss
151+
new_value[_convert_value(k)] = _convert_value(v, truncate_length=truncate_length)
152+
value = new_value
153+
elif (
154+
not any(isinstance(value, t) for t in (bool, float, int, numpy.number))
155+
):
156+
# Assuming value is a message since it is neither a collection nor a primitive type
157+
value = message_to_ordereddict(value, truncate_length=truncate_length)
158+
return value
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright 2017-2019 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+
from typing import Any
16+
from typing import Dict
17+
18+
19+
def set_message_fields(msg: Any, values: Dict[str, str]) -> None:
20+
"""
21+
Set the fields of a ROS message.
22+
23+
:param msg: The ROS message to populate.
24+
:param values: The values to set in the ROS message. The keys of the dictionary represent
25+
fields of the message.
26+
:raises AttributeError: If the message does not have a field provided in the input dictionary.
27+
:raises ValueError: If a message value does not match its field type.
28+
"""
29+
for field_name, field_value in values.items():
30+
field_type = type(getattr(msg, field_name))
31+
try:
32+
value = field_type(field_value)
33+
except TypeError:
34+
value = field_type()
35+
set_message_fields(value, field_value)
36+
setattr(msg, field_name, value)

rosidl_runtime_py/setup.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from setuptools import find_packages
2+
from setuptools import setup
3+
4+
setup(
5+
name='rosidl_runtime_py',
6+
version='0.6.2',
7+
packages=find_packages(exclude=['test']),
8+
zip_safe=False,
9+
author='Dirk Thomas',
10+
author_email='[email protected]',
11+
maintainer='Jacob Perron',
12+
maintainer_email='[email protected]',
13+
url='https://github.com/ros2/rosidl_python/tree/master/rosidl_runtime_py',
14+
download_url='https://github.com/ros2/rosidl_python/releases',
15+
keywords=[],
16+
classifiers=[
17+
'Environment :: Console',
18+
'Intended Audience :: Developers',
19+
'License :: OSI Approved :: Apache Software License',
20+
'Programming Language :: Python',
21+
],
22+
description='Runtime utilities for working with generated ROS interfaces in Python.',
23+
long_description=(
24+
'This package provides functions for operations such as populating ROS messages '
25+
'and converting messages to different representations.'),
26+
license='Apache License, Version 2.0',
27+
tests_require=['pytest'],
28+
)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Copyright 2017-2019 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+
from collections import OrderedDict
16+
17+
from rosidl_runtime_py import message_to_csv
18+
from rosidl_runtime_py import message_to_ordereddict
19+
from rosidl_runtime_py import message_to_yaml
20+
from rosidl_runtime_py.convert import _convert_value
21+
22+
from test_msgs import message_fixtures
23+
24+
25+
def test_primitives():
26+
# Smoke-test the formatters on a bunch of messages
27+
msgs = []
28+
msgs.extend(message_fixtures.get_msg_bounded_array_nested())
29+
msgs.extend(message_fixtures.get_msg_bounded_array_primitives())
30+
msgs.extend(message_fixtures.get_msg_builtins())
31+
msgs.extend(message_fixtures.get_msg_dynamic_array_nested())
32+
msgs.extend(message_fixtures.get_msg_dynamic_array_primitives())
33+
msgs.extend(message_fixtures.get_msg_dynamic_array_primitives_nested())
34+
msgs.extend(message_fixtures.get_msg_empty())
35+
msgs.extend(message_fixtures.get_msg_nested())
36+
msgs.extend(message_fixtures.get_msg_primitives())
37+
msgs.extend(message_fixtures.get_msg_static_array_nested())
38+
msgs.extend(message_fixtures.get_msg_static_array_primitives())
39+
for m in msgs:
40+
message_to_csv(m, 100)
41+
message_to_csv(m, None)
42+
message_to_ordereddict(m, 100)
43+
message_to_ordereddict(m, None)
44+
message_to_yaml(m, 100)
45+
message_to_yaml(m, None)
46+
47+
48+
def test_convert_primitives():
49+
assert 5 == _convert_value(5)
50+
assert 5 == _convert_value(5, truncate_length=0)
51+
assert 5 == _convert_value(5, truncate_length=1)
52+
assert 5 == _convert_value(5, truncate_length=10000)
53+
assert 42.0 == _convert_value(42.0)
54+
assert 42.0 == _convert_value(42.0, truncate_length=0)
55+
assert 42.0 == _convert_value(42.0, truncate_length=1)
56+
assert 42.0 == _convert_value(42.0, truncate_length=10000)
57+
assert True is _convert_value(True)
58+
assert True is _convert_value(True, truncate_length=0)
59+
assert True is _convert_value(True, truncate_length=1)
60+
assert True is _convert_value(True, truncate_length=10000)
61+
assert False is _convert_value(False)
62+
assert False is _convert_value(False, truncate_length=0)
63+
assert False is _convert_value(False, truncate_length=1)
64+
assert False is _convert_value(False, truncate_length=10000)
65+
66+
67+
def test_convert_tuple():
68+
assert (1, 2, 3) == _convert_value((1, 2, 3))
69+
assert ('...',) == _convert_value((1, 2, 3), truncate_length=0)
70+
assert (1, 2, '...') == _convert_value((1, 2, 3), truncate_length=2)
71+
assert ('123', '456', '789') == _convert_value(('123', '456', '789'))
72+
assert ('12...', '45...', '...') == _convert_value(('123', '456', '789'), truncate_length=2)
73+
assert ('123', '456', '789') == _convert_value(('123', '456', '789'), truncate_length=5)
74+
75+
76+
def test_convert_list():
77+
assert [1, 2, 3] == _convert_value([1, 2, 3])
78+
assert ['...'] == _convert_value([1, 2, 3], truncate_length=0)
79+
assert [1, 2, '...'] == _convert_value([1, 2, 3], truncate_length=2)
80+
assert ['123', '456', '789'] == _convert_value(['123', '456', '789'])
81+
assert ['12...', '45...', '...'] == _convert_value(['123', '456', '789'], truncate_length=2)
82+
assert ['123', '456', '789'] == _convert_value(['123', '456', '789'], truncate_length=5)
83+
84+
85+
def test_convert_str():
86+
assert 'hello world' == _convert_value('hello world')
87+
assert 'hello...' == _convert_value('hello world', truncate_length=5)
88+
assert 'hello world' == _convert_value('hello world', truncate_length=1000)
89+
90+
91+
def test_convert_bytes():
92+
assert 'hello world' == _convert_value(b'hello world')
93+
assert 'hello...' == _convert_value(b'hello world', truncate_length=5)
94+
assert 'hello world' == _convert_value(b'hello world', truncate_length=1000)
95+
96+
97+
def test_convert_ordered_dict():
98+
assert OrderedDict([(1, 'a'), ('2', 'b')]) == _convert_value(
99+
OrderedDict([(1, 'a'), ('2', 'b')]))
100+
assert OrderedDict([(1, 'a'), ('2', 'b')]) == _convert_value(
101+
OrderedDict([(1, 'a'), ('2', 'b')]), truncate_length=1)
102+
assert OrderedDict([(1, 'a'), ('2', 'b')]) == _convert_value(
103+
OrderedDict([(1, 'a'), ('2', 'b')]), truncate_length=1000)
104+
assert OrderedDict([(1, 'a...'), ('234', 'b...')]) == _convert_value(
105+
OrderedDict([(1, 'abc'), ('234', 'bcd')]), truncate_length=1)
106+
107+
108+
def test_convert_dict():
109+
assert {1: 'a', '2': 'b'} == _convert_value({1: 'a', '2': 'b'})
110+
assert {1: 'a', '2': 'b'} == _convert_value({1: 'a', '2': 'b'}, truncate_length=1)
111+
assert {1: 'a', '2': 'b'} == _convert_value({1: 'a', '2': 'b'}, truncate_length=1000)
112+
assert {1: 'a...', '234': 'b...'} == _convert_value(
113+
{1: 'abc', '234': 'bcd'}, truncate_length=1)

0 commit comments

Comments
 (0)