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