pw_protobuf_compiler: Simplify proto imports
Simplify importing protos from strings, which is a common use case in
tests.
Change-Id: Ib4b32f5e2d18009ec91fe2e1c30c1627dc844c9a
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/14260
Reviewed-by: Alexei Frolov <frolv@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
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 4c55bd3..9499520 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
@@ -13,6 +13,7 @@
# the License.
"""Tools for compiling and importing Python protos on the fly."""
+import importlib.util
import logging
import os
from pathlib import Path
@@ -20,8 +21,7 @@
import shlex
import tempfile
from types import ModuleType
-from typing import Dict, Iterable, List, Set, Tuple, Union
-import importlib.util
+from typing import Dict, Iterable, Iterator, List, Set, Tuple, Union
_LOG = logging.getLogger(__name__)
@@ -68,7 +68,7 @@
return module
-def import_modules(directory: PathOrStr) -> Iterable[ModuleType]:
+def import_modules(directory: PathOrStr) -> Iterator[ModuleType]:
"""Imports modules in a directory and yields them."""
parent = os.path.dirname(directory)
@@ -85,7 +85,7 @@
def compile_and_import(proto_files: Iterable[PathOrStr],
includes: Iterable[PathOrStr] = (),
- output_dir: PathOrStr = None) -> Iterable[ModuleType]:
+ output_dir: PathOrStr = None) -> Iterator[ModuleType]:
"""Compiles protos and imports their modules; yields the proto modules.
Args:
@@ -102,7 +102,7 @@
compile_protos(output_dir, proto_files, includes)
yield from import_modules(output_dir)
else:
- with tempfile.TemporaryDirectory(prefix='protos_') as tempdir:
+ with tempfile.TemporaryDirectory(prefix='compiled_protos_') as tempdir:
compile_protos(tempdir, proto_files, includes)
yield from import_modules(tempdir)
@@ -114,6 +114,28 @@
return next(iter(compile_and_import([proto_file], includes, output_dir)))
+def compile_and_import_strings(
+ contents: Iterable[str],
+ includes: Iterable[PathOrStr] = (),
+ output_dir: PathOrStr = None) -> Iterator[ModuleType]:
+ """Compiles protos in one or more strings."""
+
+ if isinstance(contents, str):
+ contents = [contents]
+
+ with tempfile.TemporaryDirectory(prefix='proto_sources_') as path:
+ protos = []
+
+ for proto in contents:
+ # Use a hash of the proto so the same contents map to the same file
+ # name. The protobuf package complains if it seems the same contents
+ # in files with different names.
+ protos.append(Path(path, f'protobuf_{hash(proto):x}.proto'))
+ protos[-1].write_text(proto)
+
+ yield from compile_and_import(protos, includes, output_dir)
+
+
class _ProtoPackage:
"""Used by the Library class for accessing protocol buffer modules."""
def __init__(self, package: str):
@@ -152,6 +174,13 @@
the list of modules in a particular package, and the modules() generator
for iterating over all modules.
"""
+ @classmethod
+ def from_strings(cls,
+ contents: Iterable[str],
+ includes: Iterable[PathOrStr] = (),
+ output_dir: PathOrStr = None) -> 'Library':
+ return cls(compile_and_import_strings(contents, includes, output_dir))
+
def __init__(self, modules: Iterable[ModuleType]):
"""Constructs a Library from an iterable of modules.
diff --git a/pw_protobuf_compiler/py/python_protos_test.py b/pw_protobuf_compiler/py/python_protos_test.py
index 2408fb3..f7dd75a 100755
--- a/pw_protobuf_compiler/py/python_protos_test.py
+++ b/pw_protobuf_compiler/py/python_protos_test.py
@@ -20,7 +20,7 @@
from pw_protobuf_compiler import python_protos
-PROTO_1 = b"""\
+PROTO_1 = """\
syntax = "proto3";
package pw.protobuf_compiler.test1;
@@ -48,7 +48,7 @@
}
"""
-PROTO_2 = b"""\
+PROTO_2 = """\
syntax = "proto2";
package pw.protobuf_compiler.test2;
@@ -69,7 +69,7 @@
}
"""
-PROTO_3 = b"""\
+PROTO_3 = """\
syntax = "proto3";
package pw.protobuf_compiler.test2;
@@ -93,7 +93,7 @@
for i, contents in enumerate([PROTO_1, PROTO_2, PROTO_3], 1):
self._protos.append(Path(self._proto_dir.name, f'test_{i}.proto'))
- self._protos[-1].write_bytes(contents)
+ self._protos[-1].write_text(contents)
def tearDown(self):
self._proto_dir.cleanup()
@@ -158,6 +158,24 @@
with self.assertRaises(KeyError):
_ = self._library.modules_by_package['pw.not_real']
+ def test_library_from_strings(self):
+ # Replace the package to avoid conflicts with the other proto imports
+ new_protos = [
+ p.replace('pw.protobuf_compiler', 'proto.library.test')
+ for p in [PROTO_1, PROTO_2, PROTO_3]
+ ]
+
+ library = python_protos.Library.from_strings(new_protos)
+
+ # Make sure we can safely import the same proto contents multiple times.
+ library = python_protos.Library.from_strings(new_protos)
+
+ msg = library.packages.proto.library.test.test2.Request
+ self.assertEqual(msg(magic_number=50).magic_number, 50)
+
+ val = library.packages.proto.library.test.test2.YO
+ self.assertEqual(val, 0)
+
if __name__ == '__main__':
unittest.main()