pw_protobuf_compiler: Protobuf type annotations
- Use the mypy-protobufs protoc plugin to generate type annotations for
Python protobufs.
- Update Python proto package generation to include py.typed files.
Change-Id: I75e658d38b56853135005af6f35624de5df93a7e
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/28960
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
Reviewed-by: Rob Mohr <mohrr@google.com>
diff --git a/pw_protobuf_compiler/BUILD.gn b/pw_protobuf_compiler/BUILD.gn
index 58dce83..0d678a0 100644
--- a/pw_protobuf_compiler/BUILD.gn
+++ b/pw_protobuf_compiler/BUILD.gn
@@ -14,7 +14,7 @@
import("//build_overrides/pigweed.gni")
-import("$dir_pw_build/input_group.gni")
+import("$dir_pw_build/python.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_unit_test/test.gni")
@@ -34,5 +34,17 @@
}
pw_proto_library("nanopb_test_protos") {
- sources = [ "pw_protobuf_compiler_protos/nanopb_test.proto" ]
+ sources = [ "pw_protobuf_compiler_nanopb_protos/nanopb_test.proto" ]
+}
+
+pw_proto_library("test_protos") {
+ sources = [
+ "pw_protobuf_compiler_protos/nested/more_nesting/test.proto",
+ "pw_protobuf_compiler_protos/test.proto",
+ ]
+}
+
+# PyPI Requirements needed to install Python protobuf packages.
+pw_python_requirements("protobuf_requirements") {
+ requirements = [ "mypy-protobuf" ]
}
diff --git a/pw_protobuf_compiler/nanopb_test.cc b/pw_protobuf_compiler/nanopb_test.cc
index 4df3520..e916b49 100644
--- a/pw_protobuf_compiler/nanopb_test.cc
+++ b/pw_protobuf_compiler/nanopb_test.cc
@@ -13,7 +13,7 @@
// the License.
#include "gtest/gtest.h"
-#include "pw_protobuf_compiler_protos/nanopb_test.pb.h"
+#include "pw_protobuf_compiler_nanopb_protos/nanopb_test.pb.h"
TEST(Nanopb, CompilesProtobufs) {
pw_protobuf_compiler_Point point = {4, 8, "point"};
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index 94d0c6c..03ff3c4 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -254,9 +254,11 @@
forward_variables_from(invoker, "*", _forwarded_vars)
language = "python"
output_extensions = [ "_pb2.py" ]
+ deps += [ "$dir_pw_protobuf_compiler:protobuf_requirements.install" ]
}
_setup_py = "${invoker.gen_dir}/setup.py"
+ _generated_files = get_target_outputs(":$target_name._gen")
# Create the setup and init files for the Python package.
pw_python_action(target_name + "._package_gen") {
@@ -266,7 +268,8 @@
rebase_path(_setup_py),
"--package",
_package_dir,
- ] + rebase_path(get_path_info(invoker.sources, "dir"), ".")
+ ] + rebase_path(_generated_files, invoker.gen_dir)
+
public_deps = [ ":$_target._gen" ]
stamp = true
}
diff --git a/pw_protobuf_compiler/pw_protobuf_compiler_protos/nanopb_test.proto b/pw_protobuf_compiler/pw_protobuf_compiler_nanopb_protos/nanopb_test.proto
similarity index 100%
rename from pw_protobuf_compiler/pw_protobuf_compiler_protos/nanopb_test.proto
rename to pw_protobuf_compiler/pw_protobuf_compiler_nanopb_protos/nanopb_test.proto
diff --git a/pw_protobuf_compiler/pw_protobuf_compiler_protos/nested/more_nesting/test.proto b/pw_protobuf_compiler/pw_protobuf_compiler_protos/nested/more_nesting/test.proto
new file mode 100644
index 0000000..6bb1417
--- /dev/null
+++ b/pw_protobuf_compiler/pw_protobuf_compiler_protos/nested/more_nesting/test.proto
@@ -0,0 +1,21 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+syntax = "proto3";
+
+package pw.protobuf_compiler.test;
+
+message Message {
+ int32 field = 1;
+}
diff --git a/pw_protobuf_compiler/pw_protobuf_compiler_protos/test.proto b/pw_protobuf_compiler/pw_protobuf_compiler_protos/test.proto
new file mode 100644
index 0000000..27bf6b4
--- /dev/null
+++ b/pw_protobuf_compiler/pw_protobuf_compiler_protos/test.proto
@@ -0,0 +1,22 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+syntax = "proto3";
+
+package pw.protobuf_compiler.test;
+
+enum Enum {
+ FOO = 0;
+ BAR = 1;
+}
diff --git a/pw_protobuf_compiler/py/BUILD.gn b/pw_protobuf_compiler/py/BUILD.gn
index 9c4187d..f2231e0 100644
--- a/pw_protobuf_compiler/py/BUILD.gn
+++ b/pw_protobuf_compiler/py/BUILD.gn
@@ -25,7 +25,11 @@
"pw_protobuf_compiler/proto_target_invalid.py",
"pw_protobuf_compiler/python_protos.py",
]
- tests = [ "python_protos_test.py" ]
+ tests = [
+ "compiled_protos_test.py",
+ "python_protos_test.py",
+ ]
python_deps = [ "$dir_pw_cli/py" ]
+ python_test_deps = [ "..:test_protos.python" ]
pylintrc = "$dir_pigweed/.pylintrc"
}
diff --git a/pw_protobuf_compiler/py/compiled_protos_test.py b/pw_protobuf_compiler/py/compiled_protos_test.py
new file mode 100755
index 0000000..88f8e62
--- /dev/null
+++ b/pw_protobuf_compiler/py/compiled_protos_test.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests compiling and importing Python protos on the fly."""
+
+import unittest
+
+from pw_protobuf_compiler_protos import test_pb2 as top_level
+from pw_protobuf_compiler_protos.nested.more_nesting import test_pb2
+
+
+class TestCompileAndImport(unittest.TestCase):
+ def test_access_compiled_protobufs(self):
+ self.assertNotEqual(top_level.FOO, top_level.BAR)
+
+ message = test_pb2.Message(field=123)
+ self.assertEqual(message.field, 123)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
index 5c07d2c..7bfd76b 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
@@ -113,7 +113,7 @@
def protoc_python_args(args: argparse.Namespace) -> List[str]:
- return ['--python_out', args.out_dir]
+ return ['--python_out', args.out_dir, '--mypy_out', args.out_dir]
# Default additional protoc arguments for each supported language.
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_python_package.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_python_package.py
index b3e1b12..4c3e865 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_python_package.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_python_package.py
@@ -14,9 +14,10 @@
"""Generates a setup.py and __init__.py for a Python package."""
import argparse
+from collections import defaultdict
from pathlib import Path
import sys
-from typing import List
+from typing import Dict, List, Set
# Make sure dependencies are optional, since this script may be run when
# installing Python package dependencies through GN.
@@ -29,13 +30,16 @@
import setuptools
setuptools.setup(
- name='<PACKAGE_NAME>',
+ name={name!r},
version='0.0.1',
author='Pigweed Authors',
author_email='pigweed-developers@googlegroups.com',
description='Generated protobuf files',
- packages=setuptools.find_packages(),
+ packages={packages!r},
+ package_data={package_data!r},
+ include_package_data=True,
zip_safe=False,
+ install_requires=['protobuf'],
)
"""
@@ -50,23 +54,47 @@
required=True,
type=Path,
help='Path to setup.py file')
- parser.add_argument('subpackages',
+ parser.add_argument('sources',
type=Path,
nargs='+',
- help='Subpackage paths within the package')
-
+ help='Relative paths to sources in the package')
return parser.parse_args()
-def main(package: str, setup: Path, subpackages: List[Path]) -> int:
- setup.parent.mkdir(exist_ok=True)
+def main(package: str, setup: Path, sources: List[Path]) -> int:
+ """Generates __init__.py and py.typed files and a setup.py."""
+ base = setup.parent.resolve()
+ base.mkdir(exist_ok=True)
- for subpackage in set(subpackages):
- package_dir = setup.parent / subpackage
- package_dir.mkdir(exist_ok=True, parents=True)
- package_dir.joinpath('__init__.py').touch()
+ # Find all directories in the package, including empty ones.
+ subpackages: Set[Path] = set()
+ for source in sources:
+ subpackages.update(base / path for path in source.parents)
+ subpackages.remove(base)
- setup.write_text(_SETUP_TEMPLATE.replace('<PACKAGE_NAME>', package))
+ pkg_data: Dict[str, List[str]] = defaultdict(list)
+
+ # Create __init__.py and py.typed files for each subdirectory.
+ for pkg in subpackages:
+ pkg.mkdir(exist_ok=True, parents=True)
+ pkg.joinpath('__init__.py').touch()
+
+ package_name = '.'.join(pkg.relative_to(base).as_posix().split('/'))
+ pkg.joinpath('py.typed').touch()
+ pkg_data[package_name].append('py.typed')
+
+ # Add the .pyi for each source file.
+ for source in sources:
+ pkg = base / source.parent
+ package_name = '.'.join(pkg.relative_to(base).as_posix().split('/'))
+
+ path = base.joinpath(source).relative_to(pkg).with_suffix('.pyi')
+ pkg_data[package_name].append(str(path))
+
+ setup.write_text(
+ _SETUP_TEMPLATE.format(name=package,
+ packages=list(pkg_data),
+ package_data=dict(pkg_data)))
return 0
diff --git a/pw_protobuf_compiler/py/setup.py b/pw_protobuf_compiler/py/setup.py
index 189992f..b0d8657 100644
--- a/pw_protobuf_compiler/py/setup.py
+++ b/pw_protobuf_compiler/py/setup.py
@@ -24,11 +24,8 @@
packages=setuptools.find_packages(),
package_data={'pw_protobuf_compiler': ['py.typed']},
zip_safe=False,
- entry_points={
- 'console_scripts':
- ['generate_protos = pw_protobuf_compiler.generate_protos:main']
- },
install_requires=[
+ 'mypy-protobuf',
'protobuf',
'pw_cli',
],
diff --git a/pw_unit_test/py/pw_unit_test/rpc.py b/pw_unit_test/py/pw_unit_test/rpc.py
index 1d1cb5f..c2ec22b 100644
--- a/pw_unit_test/py/pw_unit_test/rpc.py
+++ b/pw_unit_test/py/pw_unit_test/rpc.py
@@ -18,9 +18,8 @@
import logging
from typing import Iterable
-from pw_unit_test_proto import unit_test_pb2 # type: ignore
-
import pw_rpc.client
+from pw_unit_test_proto import unit_test_pb2
_LOG = logging.getLogger(__name__)
@@ -66,8 +65,7 @@
"""Called when a new test case is started."""
@abc.abstractmethod
- def test_case_end(self, test_case: TestCase,
- result: unit_test_pb2.TestCaseResult):
+ def test_case_end(self, test_case: TestCase, result: int):
"""Called when a test case completes with its overall result."""
@abc.abstractmethod
@@ -94,8 +92,7 @@
def test_case_start(self, test_case: TestCase):
_LOG.info('[ RUN ] %s', test_case)
- def test_case_end(self, test_case: TestCase,
- result: unit_test_pb2.TestCaseResult):
+ def test_case_end(self, test_case: TestCase, result: int):
if result == unit_test_pb2.TestCaseResult.SUCCESS:
_LOG.info('[ OK ] %s', test_case)
else:
diff --git a/pw_unit_test/py/setup.py b/pw_unit_test/py/setup.py
index 0a7db0b..7ae8df1 100644
--- a/pw_unit_test/py/setup.py
+++ b/pw_unit_test/py/setup.py
@@ -27,5 +27,6 @@
install_requires=[
'pw_cli',
'pw_rpc',
+ 'pw_unit_test_proto',
],
)