pw_protobuf_compiler: Better proto repr formatting
If wrap=True, format the proto_repr() result with yapf. This greatly
improves the readability of long proto messages.
Change-Id: I825865a4286a76f9f45347092d53ba10ae6fd0a0
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/96007
Commit-Queue: Wyatt Hepler <hepler@google.com>
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
diff --git a/pw_protobuf_compiler/docs.rst b/pw_protobuf_compiler/docs.rst
index e1b6a7a..7066b4f 100644
--- a/pw_protobuf_compiler/docs.rst
+++ b/pw_protobuf_compiler/docs.rst
@@ -1,11 +1,15 @@
.. _module-pw_protobuf_compiler:
---------------------
+====================
pw_protobuf_compiler
---------------------
+====================
The Protobuf compiler module provides build system integration and wrapper
scripts for generating source code for Protobuf definitions.
+--------------------
+Protobuf compilation
+--------------------
+
Generator support
=================
Protobuf code generation is currently supported for the following generators:
@@ -445,8 +449,6 @@
// or
#include "my_protos/bar.nanopb_rpc.pb.h"
-
-
**Supported Codegen**
Bazel supports the following compiled proto libraries via the specified
@@ -458,4 +460,15 @@
* ``${NAME}.raw_rpc`` - Generated C++ raw pw_rpc code (no protobuf library)
* ``${NAME}.nanopb_rpc`` - Generated C++ Nanopb pw_rpc code
+----------------------
+Python proto libraries
+----------------------
+``pw_protobuf_compiler`` includes utilties for working with protocol buffers
+in Python. The tools facilitate using protos from their package names
+(``my.pkg.Message()``) rather than their generated module names
+(``proto_source_file_pb2.Message()``).
+``python_protos`` module
+========================
+.. automodule:: pw_protobuf_compiler.python_protos
+ :members: proto_repr, Library
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
index 905fe0d..5030051 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
@@ -25,6 +25,8 @@
from typing import (Dict, Generic, Iterable, Iterator, List, NamedTuple, Set,
Tuple, TypeVar, Union)
+from yapf.yapflib import yapf_api # type: ignore[import]
+
_LOG = logging.getLogger(__name__)
PathOrStr = Union[Path, str]
@@ -394,6 +396,20 @@
yield f'{field.name}={_field_repr(field, value)}'
-def proto_repr(message) -> str:
- """Creates a repr-like string for a protobuf."""
- return f'{message.DESCRIPTOR.full_name}({", ".join(_proto_repr(message))})'
+def proto_repr(message, *, wrap: bool = True) -> str:
+ """Creates a repr-like string for a protobuf.
+
+ In an interactive console that imports proto objects into the namespace, the
+ output of proto_repr() can be used as Python source to create a proto
+ object.
+
+ Args:
+ message: The protobuf message to format
+ wrap: If true, the output is line wrapped according to PEP8 using YAPF.
+ """
+ raw = f'{message.DESCRIPTOR.full_name}({", ".join(_proto_repr(message))})'
+
+ if wrap:
+ return yapf_api.FormatCode(raw, style_config='PEP8')[0].rstrip()
+
+ return raw
diff --git a/pw_protobuf_compiler/py/python_protos_test.py b/pw_protobuf_compiler/py/python_protos_test.py
index 74d2c8e..139800d 100755
--- a/pw_protobuf_compiler/py/python_protos_test.py
+++ b/pw_protobuf_compiler/py/python_protos_test.py
@@ -293,10 +293,10 @@
'regular_int=999, '
'optional_int=-1, '
'repeated_int=[0, 1, 2])',
- proto_repr(
- self.message(repeated_int=[0, 1, 2],
- regular_int=999,
- optional_int=-1)))
+ proto_repr(self.message(repeated_int=[0, 1, 2],
+ regular_int=999,
+ optional_int=-1),
+ wrap=False))
def test_bytes_fields(self):
self.assertEqual(
@@ -304,12 +304,12 @@
r"regular_bytes=b'\xFE\xED\xBE\xEF', "
r"optional_bytes=b'', "
r"repeated_bytes=[b'Hello\'\'\''])",
- proto_repr(
- self.message(
- regular_bytes=b'\xfe\xed\xbe\xef',
- optional_bytes=b'',
- repeated_bytes=[b"Hello'''"],
- )))
+ proto_repr(self.message(
+ regular_bytes=b'\xfe\xed\xbe\xef',
+ optional_bytes=b'',
+ repeated_bytes=[b"Hello'''"],
+ ),
+ wrap=False))
def test_string_fields(self):
self.assertEqual(
@@ -317,12 +317,12 @@
"regular_string='hi', "
"optional_string='', "
'repeated_string=["\'"])',
- proto_repr(
- self.message(
- regular_string='hi',
- optional_string='',
- repeated_string=[b"'"],
- )))
+ proto_repr(self.message(
+ regular_string='hi',
+ optional_string='',
+ repeated_string=[b"'"],
+ ),
+ wrap=False))
def test_enum_fields(self):
self.assertEqual('pw.test3.Nested(an_enum=pw.test3.Enum.ONE)',
@@ -332,7 +332,7 @@
self.assertEqual(
'pw.test3.Message(repeated_enum='
'[pw.test3.Enum.ONE, pw.test3.Enum.ONE, pw.test3.Enum.ZERO])',
- proto_repr(self.message(repeated_enum=[1, 1, 0])))
+ proto_repr(self.message(repeated_enum=[1, 1, 0]), wrap=False))
def test_message_fields(self):
self.assertEqual(
@@ -342,21 +342,21 @@
'pw.test3.Message('
'repeated_message=[pw.test3.Nested(value=[123]), '
'pw.test3.Nested()])',
- proto_repr(
- self.message(
- repeated_message=[self.nested(
- value=[123]), self.nested()])))
+ proto_repr(self.message(
+ repeated_message=[self.nested(
+ value=[123]), self.nested()]),
+ wrap=False))
def test_optional_shown_if_set_to_default(self):
self.assertEqual(
"pw.test3.Message("
"optional_int=0, optional_bytes=b'', optional_string='', "
"optional_enum=pw.test3.Enum.ZERO)",
- proto_repr(
- self.message(optional_int=0,
- optional_bytes=b'',
- optional_string='',
- optional_enum=0)))
+ proto_repr(self.message(optional_int=0,
+ optional_bytes=b'',
+ optional_string='',
+ optional_enum=0),
+ wrap=False))
def test_oneof(self):
self.assertEqual(proto_repr(self.message(oneof_1='test')),
@@ -379,7 +379,7 @@
msg.mapping['one'].MergeFrom(
self.nested(an_enum=self.enum.ONE, value=[1]))
- result = proto_repr(msg)
+ result = proto_repr(msg, wrap=False)
self.assertRegex(result, r'^pw.test3.Message\(mapping={.*}\)$')
self.assertIn("'zero': pw.test3.Nested()", result)
self.assertIn(
@@ -396,6 +396,25 @@
self.assertEqual(bytes_repr(b'\xfe\xed\xbe\xef12345'),
r"b'\xFE\xED\xBE\xEF12345'")
+ def test_wrap_multiple_lines(self):
+ self.assertEqual(
+ """\
+pw.test3.Message(optional_int=0,
+ optional_bytes=b'',
+ optional_string='',
+ optional_enum=pw.test3.Enum.ZERO)""",
+ proto_repr(self.message(optional_int=0,
+ optional_bytes=b'',
+ optional_string='',
+ optional_enum=0),
+ wrap=True))
+
+ def test_wrap_one_line(self):
+ self.assertEqual(
+ "pw.test3.Message(optional_int=0, optional_bytes=b'')",
+ proto_repr(self.message(optional_int=0, optional_bytes=b''),
+ wrap=True))
+
if __name__ == '__main__':
unittest.main()
diff --git a/pw_protobuf_compiler/py/setup.cfg b/pw_protobuf_compiler/py/setup.cfg
index a7e24b4..9c08240 100644
--- a/pw_protobuf_compiler/py/setup.cfg
+++ b/pw_protobuf_compiler/py/setup.cfg
@@ -28,6 +28,7 @@
mypy-protobuf==2.9
protobuf
types-protobuf
+ yapf
[options.package_data]
pw_protobuf_compiler = py.typed