|
3 | 3 |
|
4 | 4 | """Setuptool hooks to build protobuf files. |
5 | 5 |
|
6 | | -This module provides a function that returns a dictionary with the required |
7 | | -machinery to build protobuf files via setuptools. |
| 6 | +This module contains a setuptools command that can be used to compile protocol |
| 7 | +buffer files in a project. |
| 8 | +
|
| 9 | +It also runs the command as the first sub-command for the build command, so |
| 10 | +protocol buffer files are compiled automatically before the project is built. |
8 | 11 | """ |
9 | 12 |
|
10 | 13 | import pathlib |
11 | 14 | import subprocess |
12 | 15 | import sys |
13 | | -from collections.abc import Iterable |
14 | | -from typing import Any |
15 | 16 |
|
16 | 17 | import setuptools |
17 | | -import setuptools.command.build_py |
18 | | - |
19 | | - |
20 | | -def build_proto_cmdclass( |
21 | | - *, |
22 | | - proto_path: str = "proto", |
23 | | - proto_glob: str = "*.proto", |
24 | | - include_paths: Iterable[str] = ("submodules/api-common-protos",), |
25 | | -) -> dict[str, Any]: |
26 | | - """Return a dictionary with the required machinery to build protobuf files. |
27 | | -
|
28 | | - This dictionary is meant to be passed as the `cmdclass` argument of |
29 | | - `setuptools.setup()`. |
30 | | -
|
31 | | - It will add the following commands to setuptools: |
32 | | -
|
33 | | - - `compile_proto`: Adds a command to compile the protobuf files to |
34 | | - Python files. |
35 | | - - `build_py`: Use the `compile_proto` command to build the python files |
36 | | - and run the regular `build_py` command, so the protobuf files are |
37 | | - create automatically when the python package is created. |
38 | | -
|
39 | | - Unless an explicit `include_paths` is passed, the |
40 | | - `submodules/api-common-protos` wiil be added to the include paths, so your |
41 | | - project should have a submodule with the common google api protos in that |
42 | | - path. |
43 | | -
|
44 | | - Args: |
45 | | - proto_path: Path of the root directory containing the protobuf files. |
46 | | - proto_glob: The glob pattern to use to find the protobuf files. |
47 | | - include_paths: Paths to include when compiling the protobuf files. |
48 | | -
|
49 | | - Returns: |
50 | | - Options to pass to `setuptools.setup()` `cmdclass` argument to build |
51 | | - protobuf files. |
52 | | - """ |
53 | | - |
54 | | - class CompileProto(setuptools.Command): |
55 | | - """Build the Python protobuf files.""" |
56 | | - |
57 | | - description: str = f"compile protobuf files in {proto_path}/**/{proto_glob}/" |
58 | | - """Description of the command.""" |
59 | | - |
60 | | - user_options: list[str] = [] |
61 | | - """Options of the command.""" |
62 | | - |
63 | | - def initialize_options(self) -> None: |
64 | | - """Initialize options.""" |
65 | | - |
66 | | - def finalize_options(self) -> None: |
67 | | - """Finalize options.""" |
68 | | - |
69 | | - def run(self) -> None: |
70 | | - """Compile the Python protobuf files.""" |
71 | | - proto_files = [str(p) for p in pathlib.Path(proto_path).rglob(proto_glob)] |
72 | | - protoc_cmd = ( |
73 | | - [sys.executable, "-m", "grpc_tools.protoc"] |
74 | | - + [f"-I{p}" for p in [*include_paths, proto_path]] |
75 | | - + """--python_out=py |
76 | | - --grpc_python_out=py |
77 | | - --mypy_out=py |
78 | | - --mypy_grpc_out=py |
79 | | - """.split() |
80 | | - + proto_files |
81 | | - ) |
82 | | - print(f"Compiling proto files via: {' '.join(protoc_cmd)}") |
83 | | - subprocess.run(protoc_cmd, check=True) |
84 | | - |
85 | | - class BuildPy(setuptools.command.build_py.build_py, CompileProto): |
86 | | - """Build the Python protobuf files and run the regular `build_py` command.""" |
87 | | - |
88 | | - def run(self) -> None: |
89 | | - """Compile the Python protobuf files and run regular `build_py`.""" |
90 | | - CompileProto.run(self) |
91 | | - setuptools.command.build_py.build_py.run(self) |
92 | | - |
93 | | - return { |
94 | | - "compile_proto": CompileProto, |
95 | | - # Compile the proto files to python files. This is done when building |
96 | | - # the wheel, the source distribution (sdist) contains the *.proto files |
97 | | - # only. Check the MANIFEST.in file to see which files are included in |
98 | | - # the sdist, and the tool.setuptools.package-dir, |
99 | | - # tool.setuptools.package-data, and tools.setuptools.packages |
100 | | - # configuration keys in pyproject.toml to see which files are included |
101 | | - # in the wheel package. |
102 | | - "build_py": BuildPy, |
103 | | - } |
| 18 | + |
| 19 | +# The typing stub for this module is missing |
| 20 | +import setuptools.command.build # type: ignore[import] |
| 21 | + |
| 22 | + |
| 23 | +class CompileProto(setuptools.Command): |
| 24 | + """Build the Python protobuf files.""" |
| 25 | + |
| 26 | + proto_path: str |
| 27 | + """The path of the root directory containing the protobuf files.""" |
| 28 | + |
| 29 | + proto_glob: str |
| 30 | + """The glob pattern to use to find the protobuf files.""" |
| 31 | + |
| 32 | + include_paths: str |
| 33 | + """Comma-separated list of paths to include when compiling the protobuf files.""" |
| 34 | + |
| 35 | + py_path: str |
| 36 | + """The path of the root directory where the Python files will be generated.""" |
| 37 | + |
| 38 | + description: str = "compile protobuf files" |
| 39 | + """Description of the command.""" |
| 40 | + |
| 41 | + user_options: list[tuple[str, str | None, str]] = [ |
| 42 | + ( |
| 43 | + "proto-path=", |
| 44 | + None, |
| 45 | + "path of the root directory containing the protobuf files", |
| 46 | + ), |
| 47 | + ("proto-glob=", None, "glob pattern to use to find the protobuf files"), |
| 48 | + ( |
| 49 | + "include-paths=", |
| 50 | + None, |
| 51 | + "comma-separated list of paths to include when compiling the protobuf files", |
| 52 | + ), |
| 53 | + ( |
| 54 | + "py-path=", |
| 55 | + None, |
| 56 | + "path of the root directory where the Python files will be generated", |
| 57 | + ), |
| 58 | + ] |
| 59 | + """Options of the command.""" |
| 60 | + |
| 61 | + def initialize_options(self) -> None: |
| 62 | + """Initialize options.""" |
| 63 | + self.proto_path = "proto" |
| 64 | + self.proto_glob = "*.proto" |
| 65 | + self.include_paths = "submodules/api-common-protos" |
| 66 | + self.py_path = "py" |
| 67 | + |
| 68 | + def finalize_options(self) -> None: |
| 69 | + """Finalize options.""" |
| 70 | + |
| 71 | + def run(self) -> None: |
| 72 | + """Compile the Python protobuf files.""" |
| 73 | + include_paths = self.include_paths.split(",") |
| 74 | + proto_files = [ |
| 75 | + str(p) for p in pathlib.Path(self.proto_path).rglob(self.proto_glob) |
| 76 | + ] |
| 77 | + |
| 78 | + if not proto_files: |
| 79 | + print(f"No proto files found in {self.proto_path}/**/{self.proto_glob}/") |
| 80 | + return |
| 81 | + |
| 82 | + protoc_cmd = ( |
| 83 | + [sys.executable, "-m", "grpc_tools.protoc"] |
| 84 | + + [f"-I{p}" for p in [*include_paths, self.proto_path]] |
| 85 | + + [ |
| 86 | + f"--{opt}={self.py_path}" |
| 87 | + for opt in "python_out grpc_python_out mypy_out mypy_grpc_out".split() |
| 88 | + ] |
| 89 | + + proto_files |
| 90 | + ) |
| 91 | + |
| 92 | + print(f"Compiling proto files via: {' '.join(protoc_cmd)}") |
| 93 | + subprocess.run(protoc_cmd, check=True) |
| 94 | + |
| 95 | + |
| 96 | +# This adds the compile_proto command to the build sub-command. |
| 97 | +# The name of the command is mapped to the class name in the pyproject.toml file, |
| 98 | +# in the [project.entry-points.distutils.commands] section. |
| 99 | +# The None value is an optional function that can be used to determine if the |
| 100 | +# sub-command should be executed or not. |
| 101 | +setuptools.command.build.build.sub_commands.insert(0, ("compile_proto", None)) |
0 commit comments