From aaa996985699ab95888f99e6873a181893d7e539 Mon Sep 17 00:00:00 2001 From: Marko Durkovic Date: Tue, 9 Nov 2021 10:40:25 +0100 Subject: [PATCH] Parse msg bounded fields and default values --- docs/topics/typesys.rst | 4 ++ src/rosbags/typesys/msg.py | 89 ++++++++++++++++++++++++++++++++++---- tests/test_parse.py | 64 +++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 8 deletions(-) diff --git a/docs/topics/typesys.rst b/docs/topics/typesys.rst index acb98f8c..26c19274 100644 --- a/docs/topics/typesys.rst +++ b/docs/topics/typesys.rst @@ -9,6 +9,10 @@ Message instances ----------------- The type system generates a dataclass for each message type. These dataclasses give direct read write access to all mutable fields of a message. Fields should be mutated with care as no type checking is applied during runtime. +.. note:: + + Limitation: While the type system parses message definitions with array bounds and/or default values, neither bounds nor default values are enforced or assigned to message instances. + Extending the type system ------------------------- Adding custom message types consists of two steps. First, message definitions are converted into parse trees using :py:func:`get_types_from_idl() ` or :py:func:`get_types_from_msg() `, and second the types are registered in the type system via :py:func:`register_types() `. The following example shows how to add messages type definitions from ``.msg`` and ``.idl`` files: diff --git a/src/rosbags/typesys/msg.py b/src/rosbags/typesys/msg.py index d47626db..71e5f20a 100644 --- a/src/rosbags/typesys/msg.py +++ b/src/rosbags/typesys/msg.py @@ -48,24 +48,28 @@ const_dcl / type_spec identifier '=' integer_literal field_dcl - = type_spec identifier + = type_spec identifier default_value? type_spec = array_type_spec + / bounded_array_type_spec / simple_type_spec array_type_spec = simple_type_spec array_size +bounded_array_type_spec + = simple_type_spec array_bounds + simple_type_spec - = scoped_name + = 'string' '<=' integer_literal + / scoped_name array_size = '[' integer_literal? ']' -integer_literal - = r'[-+]?[1-9][0-9]+' - / r'[-+]?[0-9]' +array_bounds + = '[<=' integer_literal ']' scoped_name = identifier '/' scoped_name @@ -73,6 +77,50 @@ scoped_name identifier = r'[a-zA-Z_][a-zA-Z_0-9]*' + +default_value + = literal + +literal + = boolean_literal + / float_literal + / integer_literal + / string_literal + / array_literal + +boolean_literal + = 'true' + / 'false' + +integer_literal + = hexadecimal_literal + / octal_literal + / decimal_literal + +decimal_literal + = r'[-+]?[1-9][0-9]+' + / r'[-+]?[0-9]' + +octal_literal + = r'[-+]?0[0-7]+' + +hexadecimal_literal + = r'[-+]?0[xX][a-fA-F0-9]+' + +float_literal + = r'[-+]?[0-9]*\.[0-9]+([eE][-+]?[0-9]+)?' + / r'[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)' + +string_literal + = '"' r'(\\"|[^"])*' '"' + / '\'' r'(\\\'|[^'])*' '\'' + +array_literal + = '[' array_elements? ']' + +array_elements + = literal ',' array_elements + / literal """ @@ -207,15 +255,20 @@ class VisitorMSG(Visitor): return Nodetype.ARRAY, (children[0], length[0]) return Nodetype.SEQUENCE, (children[0], None) + def visit_bounded_array_type_spec(self, children: Any) -> Any: + """Process bounded array type specifier.""" + return Nodetype.SEQUENCE, (children[0], None) + def visit_simple_type_spec(self, children: Any) -> Any: """Process simple type specifier.""" + typespec = children[0][1] if ('LITERAL', '<=') in children else children[1] dct = { 'time': 'builtin_interfaces/msg/Time', 'duration': 'builtin_interfaces/msg/Duration', 'byte': 'uint8', 'char': 'uint8', } - return Nodetype.NAME, dct.get(children[1], children[1]) + return Nodetype.NAME, dct.get(typespec, typespec) def visit_scoped_name(self, children: Any) -> Any: """Process scoped name.""" @@ -228,10 +281,30 @@ class VisitorMSG(Visitor): """Process identifier.""" return (Nodetype.NAME, children) - def visit_integer_literal(self, children: Any) -> Any: - """Process integer literal.""" + def visit_boolean_literal(self, children: Any) -> Any: + """Process boolean literal.""" + return children[1] == 'TRUE' + + def visit_float_literal(self, children: Any) -> Any: + """Process float literal.""" + return float(children) + + def visit_decimal_literal(self, children: Any) -> Any: + """Process decimal integer literal.""" return int(children) + def visit_octal_literal(self, children: Any) -> Any: + """Process octal integer literal.""" + return int(children, 8) + + def visit_hexadecimal_literal(self, children: Any) -> Any: + """Process hexadecimal integer literal.""" + return int(children, 16) + + def visit_string_literal(self, children: Any) -> Any: + """Process integer literal.""" + return children[1] + def get_types_from_msg(text: str, name: str) -> Typesdict: """Get type from msg message definition. diff --git a/tests/test_parse.py b/tests/test_parse.py index 59083b3e..6112ef6f 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -29,6 +29,30 @@ float64[] seq2 float64[4] array """ +MSG_BOUNDS = """ +int32[] unbounded_integer_array +int32[5] five_integers_array +int32[<=5] up_to_five_integers_array + +string string_of_unbounded_size +string<=10 up_to_ten_characters_string + +string[<=5] up_to_five_unbounded_strings +string<=10[] unbounded_array_of_string_up_to_ten_characters_each +string<=10[<=5] up_to_five_strings_up_to_ten_characters_each +""" + +MSG_DEFAULTS = """ +bool b false +uint8 i 42 +uint8 o 0377 +uint8 h 0xff +float32 y -314.15e-2 +string name1 "John" +string name2 'Ringo' +int32[] samples [-200, -100, 0, 100, 200] +""" + MULTI_MSG = """ std_msgs/Header header byte b @@ -124,6 +148,46 @@ def test_parse_empty_msg(): assert ret == {'std_msgs/msg/Empty': ([], [])} +def test_parse_bounds_msg(): + """Test msg parser.""" + ret = get_types_from_msg(MSG_BOUNDS, 'test_msgs/msg/Foo') + assert ret == { + 'test_msgs/msg/Foo': ( + [], + [ + ('unbounded_integer_array', (4, ((1, 'int32'), None))), + ('five_integers_array', (3, ((1, 'int32'), 5))), + ('up_to_five_integers_array', (4, ((1, 'int32'), None))), + ('string_of_unbounded_size', (1, 'string')), + ('up_to_ten_characters_string', (1, 'string')), + ('up_to_five_unbounded_strings', (4, ((1, 'string'), None))), + ('unbounded_array_of_string_up_to_ten_characters_each', (4, ((1, 'string'), None))), + ('up_to_five_strings_up_to_ten_characters_each', (4, ((1, 'string'), None))), + ], + ), + } + + +def test_parse_defaults_msg(): + """Test msg parser.""" + ret = get_types_from_msg(MSG_DEFAULTS, 'test_msgs/msg/Foo') + assert ret == { + 'test_msgs/msg/Foo': ( + [], + [ + ('b', (1, 'bool')), + ('i', (1, 'uint8')), + ('o', (1, 'uint8')), + ('h', (1, 'uint8')), + ('y', (1, 'float32')), + ('name1', (1, 'string')), + ('name2', (1, 'string')), + ('samples', (4, ((1, 'int32'), None))), + ], + ), + } + + def test_parse_msg(): """Test msg parser.""" with pytest.raises(TypesysError, match='Could not parse'):