Skip to content

Commit 2b1aae7

Browse files
committed
Unify protobuf generation configuration
A new module `protobuf` is added to handle the common `protobuf` generation documentation which is used by both the `setuptools` plugin to generate the Python files and the the `mkdocs` function to generate the API documentation pages. The `pyproject.toml` configuration key is changed from `tool.frequenz-repo-config.setuptools.grpc_tools` to `tool.frequenz-repo-config.protobuf` as it is more generic now. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 5572d9d commit 2b1aae7

File tree

4 files changed

+144
-95
lines changed

4 files changed

+144
-95
lines changed

src/frequenz/repo/config/__init__.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -205,32 +205,62 @@
205205
- path/to/my/custom/script.py
206206
```
207207
208-
If your project also provides *protobuf* files, you can also generate the API
209-
documentation for them adding one more line to the previous script:
208+
# APIs
209+
210+
## Protobuf configuation
211+
212+
Support is provided to generate files from *protobuf* files. To do this, it is possible
213+
to configure the options to use while generating the files for different purposes
214+
(language bindings, documentation, etc.).
215+
216+
The configuration can be done in the `pyproject.toml` file as follows:
217+
218+
```toml
219+
[tool.frequenz_repo_config.protobuf]
220+
# Location of the proto files relative to the root of the repository (default: "proto")
221+
proto_path = "proto_files"
222+
# Glob pattern to use to find the proto files in the proto_path (default: "*.proto")
223+
proto_glob = "*.prt" # Default: "*.proto"
224+
# List of paths to pass to the protoc compiler as include paths (default:
225+
# ["submodules/api-common-protos", "submodules/frequenz-api-common/proto"])
226+
include_paths = ["submodules/api-common-protos"]
227+
# Path where to generate the Python files (default: "py")
228+
py_path = "generated"
229+
# Path where to generate the documentation files (default: "protobuf-reference")
230+
docs_path = "API"
231+
```
232+
233+
If the defaults are not suitable for you (for example you need to use more or less
234+
submodules or your proto files are located somewhere else), please adjust the
235+
configuration to match your project structure.
236+
237+
### `mkdocs` API reference generation
238+
239+
If your project provides *protobuf* files, you can also generate the API
240+
documentation for them adding one more line to the script provided in the common
241+
section:
210242
211243
```python
212244
from frequenz.repo.config import mkdocs
213245
214246
mkdocs.generate_python_api_pages("my_sources", "API-py")
215-
mkdocs.generate_protobuf_api_pages("my_protos", "API-proto")
247+
mkdocs.generate_protobuf_api_pages()
216248
```
217249
218-
# APIs
250+
This will use the configuration in the `pyproject.toml` file and requires `docker` to
251+
run (it uses the `pseudomuto/protoc-gen-doc` docker image.
219252
220-
## `setuptools` gRPC support
253+
### `setuptools` gRPC support
221254
222255
When configuring APIs it is assumed that they have a gRPC interface.
223256
224-
The project structure is assumed to be as follows:
257+
The project structure is assumed to be as described in the *Protobuf configuration*
258+
section plus the following:
225259
226-
- `proto/`: Directory containing the `.proto` files.
227-
- `py/`: Directory containing the Python code. It should only provide
228-
a `py.typed` file and a `__init__.py` file. API repositories should not
229-
contain any other Python code.
230260
- `pytests/`: Directory containing the tests for the Python code.
231-
- `submodules/api-common-protos`: Directory containing the submodule with the
261+
- `submodules/api-common-protos`: Directory containing the Git submodule with the
232262
`google/api-common-protos` repository.
233-
- `submodules/frequenz-api-common`: Directory containing the submodule with the
263+
- `submodules/frequenz-api-common`: Directory containing the Git submodule with the
234264
`frequenz-floss/frequenz-api-common` repository.
235265
236266
Normally Frequenz APIs use basic types from
@@ -296,24 +326,6 @@
296326
recursive-include submodules/frequenz-api-common/proto *.proto
297327
```
298328
299-
If the defaults are not suitable for you (for example you need to use more or less
300-
submodules or your proto files are located somewhere else, you can customize how
301-
the protocol files are generated by adding the following section to your
302-
`pyproject.toml` file:
303-
304-
```toml
305-
[tool.frequenz_repo_config.setuptools.grpc_tools]
306-
# Location of the proto files relative to the root of the repository (default: "proto")
307-
proto_path = "proto_files"
308-
# Glob pattern to use to find the proto files in the proto_path (default: "*.proto")
309-
proto_glob = "*.prt" # Default: "*.proto"
310-
# List of paths to pass to the protoc compiler as include paths (default:
311-
# ["submodules/api-common-protos", "submodules/frequenz-api-common/proto"])
312-
include_paths = ["submodules/api-common-protos"]
313-
# Path where to generate the Python files (default: "py")
314-
py_path = "generated"
315-
```
316-
317329
Please adapt the instructions above to your project structure if you need to change the
318330
defaults.
319331
"""

src/frequenz/repo/config/mkdocs.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import mkdocs_gen_files
2121

22+
from . import protobuf as _protobuf
23+
2224

2325
def _is_internal(path_parts: Tuple[str, ...]) -> bool:
2426
"""Tell if the path is internal judging by the parts.
@@ -99,19 +101,21 @@ def generate_protobuf_api_pages(
99101
# type ignore because mkdocs_gen_files uses a very weird module-level
100102
# __getattr__() which messes up the type system
101103
nav = mkdocs_gen_files.Nav() # type: ignore
104+
config = _protobuf.ProtobufConfig.from_pyproject_toml(
105+
proto_path=src_path, docs_path=dst_path
106+
)
102107

103108
cwd = Path.cwd()
104109

105110
with tempfile.TemporaryDirectory(prefix="mkdocs-protobuf-reference-") as tmp_path:
106-
for path in sorted(Path(src_path).rglob("*.proto")):
107-
doc_path = path.relative_to(src_path).with_suffix(".md")
108-
full_doc_path = Path(dst_path, doc_path)
109-
parts = tuple(path.relative_to(src_path).parts)
111+
for path in sorted(Path(config.proto_path).rglob("*.proto")):
112+
doc_path = path.relative_to(config.proto_path).with_suffix(".md")
113+
full_doc_path = Path(config.docs_path, doc_path)
114+
parts = tuple(path.relative_to(config.proto_path).parts)
110115
nav[parts] = doc_path.as_posix()
111116
doc_tmp_path = tmp_path / doc_path
112117
doc_tmp_path.parent.mkdir(parents=True, exist_ok=True)
113118
try:
114-
# TODO: Get arguments from setuptools.grpc
115119
subprocess.run(
116120
[
117121
"docker",
@@ -120,9 +124,8 @@ def generate_protobuf_api_pages(
120124
f"-v{cwd}:{cwd}",
121125
f"-v{tmp_path}:{tmp_path}",
122126
"pseudomuto/protoc-gen-doc",
123-
f"-I{cwd / src_path}",
124-
f"-I{cwd}/submodules/api-common-protos",
125-
f"-I{cwd}/submodules/frequenz-api-common/proto",
127+
f"-I{cwd / config.proto_path}",
128+
*(f"-I{cwd / p}" for p in config.include_paths),
126129
f"--doc_opt=markdown,{doc_path.name}",
127130
f"--doc_out={tmp_path / doc_path.parent}",
128131
str(cwd / path),
@@ -139,5 +142,5 @@ def generate_protobuf_api_pages(
139142

140143
mkdocs_gen_files.set_edit_path(full_doc_path, Path("..") / path)
141144

142-
with mkdocs_gen_files.open(Path(dst_path) / "SUMMARY.md", "w") as nav_file:
145+
with mkdocs_gen_files.open(Path(config.docs_path) / "SUMMARY.md", "w") as nav_file:
143146
nav_file.writelines(nav.build_literate_nav())
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Manages the configuration to generate files from the protobuf files."""
5+
6+
import dataclasses
7+
import logging
8+
import pathlib
9+
import tomllib
10+
from collections.abc import Sequence
11+
from typing import Any, Self
12+
13+
_logger = logging.getLogger(__name__)
14+
15+
16+
@dataclasses.dataclass(frozen=True, kw_only=True)
17+
class ProtobufConfig:
18+
"""A configuration for the protobuf files.
19+
20+
The configuration can be loaded from the `pyproject.toml` file using the class
21+
method `from_pyproject_toml()`.
22+
"""
23+
24+
proto_path: str = "proto"
25+
"""The path of the root directory containing the protobuf files."""
26+
27+
proto_glob: str = "*.proto"
28+
"""The glob pattern to use to find the protobuf files."""
29+
30+
include_paths: Sequence[str] = (
31+
"submodules/api-common-protos",
32+
"submodules/frequenz-api-common/proto",
33+
)
34+
"""The paths to add to the include path when compiling the protobuf files."""
35+
36+
py_path: str = "py"
37+
"""The path of the root directory where the Python files will be generated."""
38+
39+
docs_path: str = "protobuf-reference"
40+
"""The path of the root directory where the documentation files will be generated."""
41+
42+
@classmethod
43+
def from_pyproject_toml(
44+
cls, /, path: str = "pyproject.toml", **defaults: Any
45+
) -> Self:
46+
"""Create a new configuration by loading the options from a `pyproject.toml` file.
47+
48+
The options are read from the `[tool.frequenz-repo-config.protobuf]`
49+
section of the `pyproject.toml` file.
50+
51+
Args:
52+
path: The path to the `pyproject.toml` file.
53+
**defaults: The default values for the options missing in the file. If
54+
a default is missing too, then the default in this class will be used.
55+
56+
Returns:
57+
The configuration.
58+
"""
59+
try:
60+
with pathlib.Path(path).open("rb") as toml_file:
61+
pyproject_toml = tomllib.load(toml_file)
62+
except FileNotFoundError:
63+
return cls(**defaults)
64+
except (IOError, OSError) as err:
65+
_logger.warning("WARNING: Failed to load pyproject.toml: %s", err)
66+
return cls(**defaults)
67+
68+
try:
69+
config = pyproject_toml["tool"]["frequenz-repo-config"]["protobuf"]
70+
except KeyError:
71+
return cls(**defaults)
72+
73+
known_keys = frozenset(defaults.keys())
74+
config_keys = frozenset(config.keys())
75+
if unknown_keys := config_keys - known_keys:
76+
_logger.warning(
77+
"WARNING: There are some configuration keys in pyproject.toml we don't "
78+
"know about and will be ignored: %s",
79+
", ".join(f"'{k}'" for k in unknown_keys),
80+
)
81+
82+
attrs = dict(defaults, **{k: config[k] for k in (known_keys & config_keys)})
83+
return cls(**attrs)

src/frequenz/repo/config/setuptools/grpc_tools.py

Lines changed: 7 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import setuptools as _setuptools
1919
import setuptools.command.build as _build_command
2020

21+
from .. import protobuf as _protobuf
22+
2123

2224
class CompileProto(_setuptools.Command):
2325
"""Build the Python protobuf files."""
@@ -57,69 +59,18 @@ class CompileProto(_setuptools.Command):
5759
]
5860
"""Options of the command."""
5961

60-
DEFAULT_OPTIONS: dict[str, str] = {
61-
"proto_path": "proto",
62-
"proto_glob": "*.proto",
63-
"include_paths": "submodules/api-common-protos,submodules/frequenz-api-common/proto",
64-
"py_path": "py",
65-
}
66-
6762
def initialize_options(self) -> None:
6863
"""Initialize options."""
69-
options = self._get_options_from_pyproject_toml(self.DEFAULT_OPTIONS)
64+
config = _protobuf.ProtobufConfig.from_pyproject_toml()
7065

71-
self.proto_path = options["proto_path"]
72-
self.proto_glob = options["proto_glob"]
73-
self.include_paths = options["include_paths"]
74-
self.py_path = options["py_path"]
66+
self.proto_path = config.proto_path
67+
self.proto_glob = config.proto_glob
68+
self.include_paths = ",".join(config.include_paths)
69+
self.py_path = config.py_path
7570

7671
def finalize_options(self) -> None:
7772
"""Finalize options."""
7873

79-
def _get_options_from_pyproject_toml(
80-
self, defaults: dict[str, str]
81-
) -> dict[str, str]:
82-
"""Get the options from the pyproject.toml file.
83-
84-
The options are read from the `[tool.frequenz-repo-config.setuptools.grpc_tools]`
85-
section of the pyproject.toml file.
86-
87-
Args:
88-
defaults: The default values for the options.
89-
90-
Returns:
91-
The options read from the pyproject.toml file.
92-
"""
93-
try:
94-
with _pathlib.Path("pyproject.toml").open("rb") as toml_file:
95-
pyproject_toml = _tomllib.load(toml_file)
96-
except FileNotFoundError:
97-
return defaults
98-
except (IOError, OSError) as err:
99-
print(f"WARNING: Failed to load pyproject.toml: {err}")
100-
return defaults
101-
102-
try:
103-
config = pyproject_toml["tool"]["frequenz-repo-config"]["setuptools"][
104-
"grpc_tools"
105-
]
106-
except KeyError:
107-
return defaults
108-
109-
known_keys = frozenset(defaults.keys())
110-
config_keys = frozenset(config.keys())
111-
if unknown_keys := config_keys - known_keys:
112-
print(
113-
"WARNING: There are some configuration keys in pyproject.toml we don't "
114-
"know about and will be ignored: "
115-
+ ", ".join(f"'{k}'" for k in unknown_keys)
116-
)
117-
118-
if "include_paths" in config:
119-
config["include_paths"] = ",".join(config["include_paths"])
120-
121-
return dict(defaults, **{k: config[k] for k in (known_keys & config_keys)})
122-
12374
def run(self) -> None:
12475
"""Compile the Python protobuf files."""
12576
include_paths = self.include_paths.split(",")

0 commit comments

Comments
 (0)