Skip to content

Commit eabdf65

Browse files
authored
Implement code generation for gRPC (#9)
* Add grpcio-tools as a dependency * Add mypy-protobuf as a dependency * Use dataclass 'GenerationSpec' to hold gRPC details * Detect proto files using a basepath and subpath * Generate the protobuf descriptor set in the same call that generates the package - Add utility methods to GenerationSpec for inspecting a generated protobuf package * Add acceptance tests
1 parent b950c57 commit eabdf65

File tree

8 files changed

+877
-27
lines changed

8 files changed

+877
-27
lines changed

tools/grpc_generator/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,92 @@
33
`grpc_generator` is a Python tool that generates gRPC stubs from proto files.
44

55
It supports emitting stubs into namespace packages, transforming them as necessary from submodules to subpackages.
6+
7+
## Setup
8+
9+
```pwsh
10+
# Initialize the tool
11+
cd tools\grpc_generator
12+
poetry install
13+
```
14+
15+
## Generate
16+
17+
```pwsh
18+
# PowerShell
19+
poetry run grpc-generator `
20+
--proto-basepath ..\..\third_party\ni-apis `
21+
--proto-subpath ni\protobuf\types `
22+
--output-basepath ..\..\packages\ni.protobuf.types\src `
23+
--output-format submodule
24+
```
25+
26+
## Generate into a namespace package
27+
28+
```pwsh
29+
# PowerShell
30+
poetry run grpc-generator `
31+
--proto-basepath ..\..\third_party\ni-apis `
32+
--proto-subpath ni\measurementlink\pinmap\v1 `
33+
--output-basepath ..\..\packages\ni.measurementlink.pinmap.v1\src `
34+
--output-format subpackage
35+
```
36+
37+
## Options
38+
39+
**`grpc-generator --help`**
40+
```
41+
Usage: grpc-generator [OPTIONS]
42+
43+
Generate gRPC Python stubs from proto files.
44+
45+
Specifying input and output locations
46+
47+
This script uses the protobuf files from the folder specified by --proto-
48+
basepath and --proto-subpath and emits Python files into the folder
49+
specified by --output-basepath:
50+
51+
{proto-basepath}/{proto-subpath} --> {output-basepath}/{proto-subpath}
52+
53+
The script resolves gRPC imports from --proto-basepath by default. Include
54+
additional paths by using --proto-include-path for each required folder.
55+
56+
Specifying output format
57+
58+
The script supports generating gRPC packages as either subpackages or
59+
submodules with --output-format.
60+
61+
When generating submodules, the script creates Python files with names
62+
that match the source protobuf files:
63+
64+
waveform.proto --> waveform_pb2.py
65+
66+
When generating subpackages, the script creates folders with names that
67+
match the source protobuf files:
68+
69+
waveform.proto --> waveform_pb2/__init__.py
70+
71+
Clients use the same "import waveform_pb2" syntax.
72+
73+
Options:
74+
--output-basepath PATH Emit the generated gRPC files to PATH
75+
[required]
76+
--output-format [submodule|subpackage]
77+
Generate a Python submodule or subpackage
78+
[required]
79+
--proto-basepath PATH Use PATH as the base for --proto-subpath
80+
[default: C:\dev\ni\git\github\ni-apis-
81+
python\third_party\ni-apis]
82+
--proto-include-path PATH Add PATH to the import search list, can be
83+
used more than once [default:
84+
C:\dev\ni\git\github\ni-apis-
85+
python\third_party\ni-apis]
86+
--proto-subpath PATH Use the proto files under PATH as input
87+
[required]
88+
--help Show this message and exit.
89+
--version Show the version and exit.
90+
91+
Example:
92+
93+
grpc-generator --proto-subpath ni/protobuf/types --output-basepath ../../packages/ni.protobuf.types/src --output-format submodule
94+
```

tools/grpc_generator/poetry.lock

Lines changed: 326 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/grpc_generator/pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ grpc-generator = "grpc_generator.__main__:cli"
1313
[tool.poetry.dependencies]
1414
python = "^3.11"
1515
click = "^8.2.1"
16+
# When updating the grpcio-tools version, also update the minimum grpcio version
17+
# and regenerate gRPC stubs.
18+
grpcio-tools = [
19+
{ version = "1.49.1", python = ">=3.9,<3.12" },
20+
{ version = "1.59.0", python = ">=3.12,<3.13" },
21+
{ version = "1.67.0", python = "^3.13" },
22+
]
23+
mypy-protobuf = ">=3.6"
1624

1725
[tool.poetry.group.lint.dependencies]
1826
bandit = { version = ">=1.7", extras = ["toml"] }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
11
"""grpc_generator package."""
2+
3+
import warnings
4+
5+
warnings.filterwarnings(
6+
action="ignore",
7+
category=UserWarning,
8+
module=r"^grpc_tools",
9+
) # grpc_tools\protoc.py:21: UserWarning: pkg_resources is deprecated as an API

tools/grpc_generator/src/grpc_generator/__main__.py

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,105 @@
11
"""grpc_generator entry points."""
22

3-
import logging
3+
import pathlib
44

55
import click
66

77
from . import generator
88

99

10-
_logger = logging.getLogger(__name__)
11-
_logger.addHandler(logging.NullHandler())
10+
REPO_ROOT = next(
11+
(p for p in pathlib.Path(__file__).parents if (p / "third_party").exists()), pathlib.Path(".")
12+
)
1213

1314

14-
@click.command()
15+
@click.command(epilog=generator.USAGE_EXAMPLE)
16+
@click.option(
17+
"--output-basepath",
18+
metavar="PATH",
19+
type=click.Path(file_okay=False, writable=True, path_type=pathlib.Path),
20+
required=True,
21+
help="Emit the generated gRPC files to PATH",
22+
)
23+
@click.option(
24+
"--output-format",
25+
type=click.Choice(choices=[entry.value for entry in generator.OutputFormat]),
26+
required=True,
27+
help="Generate a Python submodule or subpackage",
28+
)
29+
@click.option(
30+
"--proto-basepath",
31+
metavar="PATH",
32+
type=click.Path(file_okay=False, path_type=pathlib.Path),
33+
default=REPO_ROOT.joinpath("third_party/ni-apis"),
34+
show_default=True,
35+
help="Use PATH as the base for --proto-subpath",
36+
)
37+
@click.option(
38+
"--proto-include-path",
39+
metavar="PATH",
40+
multiple=True,
41+
type=click.Path(file_okay=False, path_type=pathlib.Path),
42+
default=[REPO_ROOT.joinpath("third_party/ni-apis")],
43+
show_default=True,
44+
help="Add PATH to the import search list, can be used more than once",
45+
)
46+
@click.option(
47+
"--proto-subpath",
48+
metavar="PATH",
49+
type=click.Path(file_okay=False, path_type=pathlib.Path),
50+
required=True,
51+
help="Use the proto files under PATH as input",
52+
)
1553
@click.help_option()
16-
def cli() -> None:
17-
"""Generate gRPC Python stubs from proto files."""
18-
generator.handle_cli()
54+
@click.version_option()
55+
def cli(
56+
proto_basepath: pathlib.Path,
57+
proto_subpath: pathlib.Path,
58+
proto_include_path: list[pathlib.Path],
59+
output_basepath: pathlib.Path,
60+
output_format: str,
61+
) -> None:
62+
"""Generate gRPC Python stubs from proto files.
63+
64+
Specifying input and output locations
65+
66+
This script uses the protobuf files from the folder specified by
67+
--proto-basepath and --proto-subpath and emits Python files into the
68+
folder specified by --output-basepath:
69+
70+
\b
71+
{proto-basepath}/{proto-subpath} --> {output-basepath}/{proto-subpath}
72+
73+
The script resolves gRPC imports from --proto-basepath by default. Include
74+
additional paths by using --proto-include-path for each required folder.
75+
76+
Specifying output format
77+
78+
The script supports generating gRPC packages as either subpackages
79+
or submodules with --output-format.
80+
81+
When generating submodules, the script creates Python files with names
82+
that match the source protobuf files:
83+
84+
\b
85+
waveform.proto --> waveform_pb2.py
86+
87+
When generating subpackages, the script creates folders with names
88+
that match the source protobuf files:
89+
90+
\b
91+
waveform.proto --> waveform_pb2/__init__.py
92+
93+
Clients use the same "import waveform_pb2" syntax.
94+
95+
""" # noqa: D301 - Use r""" if any backslashes in a docstring
96+
generator.handle_cli(
97+
proto_basepath=proto_basepath,
98+
proto_subpath=proto_subpath,
99+
proto_include_paths=proto_include_path,
100+
output_basepath=output_basepath,
101+
output_format=generator.OutputFormat(output_format),
102+
)
19103

20104

21105
if __name__ == "__main__":

0 commit comments

Comments
 (0)