Skip to content

Commit f5404a6

Browse files
authored
Add support for generating protobuf compliant DESCRIPTORs (#112)
* Add support for generate protobuf compliant DESCRIPTORs - Fixes danielgtaylor/python-betterproto#443 - Fixes #70 * fix for dependent pkgs * fix multiple descriptors * driveby * comment * order imports * fix nested descriptors and stop mutating file input * lint * fix * fix default groups * Revert "fix default groups" This reverts commit a85929c. * better descriptor * update readme * pr changes * lint * remove import_suffix * remove source code info from descriptors
1 parent f7997f0 commit f5404a6

File tree

22 files changed

+534
-58
lines changed

22 files changed

+534
-58
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949

5050
- name: Move compiled files to betterproto2
5151
shell: bash
52-
run: mv betterproto2_compiler/tests/output_betterproto betterproto2_compiler/tests/output_betterproto_pydantic betterproto2_compiler/tests/output_reference betterproto2/tests
52+
run: mv betterproto2_compiler/tests/output_betterproto betterproto2_compiler/tests/output_betterproto_pydantic betterproto2_compiler/tests/output_betterproto_descriptor betterproto2_compiler/tests/output_reference betterproto2/tests
5353

5454
- name: Execute test suite
5555
working-directory: ./betterproto2

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
.pytest_cache
77
.python-version
88
build/
9-
tests/output_*
9+
*/tests/output_*
1010
**/__pycache__
1111
dist
1212
**/*.egg-info

betterproto2/docs/descriptors.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Google Protobuf Descriptors
2+
3+
Google's protoc plugin for Python generated DESCRIPTOR fields that enable reflection capabilities in many libraries (e.g. grpc, grpclib, mcap).
4+
5+
By default, betterproto2 doesn't generate these as it introduces a dependency on `protobuf`. If you're okay with this dependency and want to generate DESCRIPTORs, use the compiler option `python_betterproto2_opt=google_protobuf_descriptors`.
6+
7+
8+
## grpclib Reflection
9+
10+
In order to properly use reflection right now, you will need to modify the `DescriptorPool` that is used by grpclib's `ServerReflection`. To do so, take a look at the use of `ServerReflection.extend` in the `test_grpclib_reflection` test in https://github.com/vmagamedov/grpclib/blob/master/tests/grpc/test_grpclib_reflection.py
11+
In the future, once https://github.com/vmagamedov/grpclib/pull/204 is merged, you will be able to pass the `default_google_proto_descriptor_pool` into the `ServerReflection.extend` class method.

betterproto2/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ nav:
1414
- Clients: tutorial/clients.md
1515
- API: api.md
1616
- Development: development.md
17+
- Protobuf Descriptors: descriptors.md
1718

1819

1920
plugins:

betterproto2/pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ Repository = "https://github.com/betterproto/python-betterproto2"
2222
grpcio = ["grpcio>=1.72.1"]
2323
grpclib = ["grpclib>=0.4.8"]
2424
pydantic = ["pydantic>=2.11.5"]
25-
all = ["grpclib>=0.4.8", "grpcio>=1.72.1", "pydantic>=2.11.5"]
25+
protobuf = ["protobuf>=5.29.3"]
26+
all = ["grpclib>=0.4.8", "grpcio>=1.72.1", "pydantic>=2.11.5", "protobuf>=5.29.3"]
2627

2728
[dependency-groups]
2829
dev = [
@@ -38,7 +39,6 @@ dev = [
3839
test = [
3940
"cachelib>=0.13.0",
4041
"poethepoet>=0.34.0",
41-
"protobuf>=5.29.3",
4242
"pytest>=8.4.0",
4343
"pytest-asyncio>=1.0.0",
4444
"pytest-cov>=6.1.1",
@@ -144,6 +144,7 @@ rm -rf tests/output_* &&
144144
git clone https://github.com/betterproto/python-betterproto2-compiler --branch compiled-test-files --single-branch compiled_files &&
145145
mv compiled_files/tests_betterproto tests/output_betterproto &&
146146
mv compiled_files/tests_betterproto_pydantic tests/output_betterproto_pydantic &&
147+
mv compiled_files/tests_betterproto_pydantic tests/output_betterproto_descriptor &&
147148
mv compiled_files/tests_reference tests/output_reference &&
148149
rm -rf compiled_files
149150
"""

betterproto2/src/betterproto2/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
__all__ = ["__version__", "check_compiler_version", "unwrap", "MessagePool", "validators"]
3+
__all__ = ["__version__", "check_compiler_version", "classproperty", "unwrap", "MessagePool", "validators"]
44

55
import dataclasses
66
import enum as builtin_enum
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import asyncio
2+
from typing import Generic, TypeVar
3+
4+
import pytest
5+
from google.protobuf import descriptor_pb2
6+
from grpclib.reflection.service import ServerReflection
7+
from grpclib.reflection.v1.reflection_grpc import ServerReflectionBase as ServerReflectionBaseV1
8+
from grpclib.reflection.v1alpha.reflection_grpc import ServerReflectionBase as ServerReflectionBaseV1Alpha
9+
from grpclib.testing import ChannelFor
10+
11+
from tests.output_betterproto.example_service import TestBase
12+
from tests.output_betterproto.grpc.reflection.v1 import (
13+
ErrorResponse,
14+
ListServiceResponse,
15+
ServerReflectionRequest,
16+
ServerReflectionStub,
17+
ServiceResponse,
18+
)
19+
from tests.output_betterproto_descriptor.google_proto_descriptor_pool import default_google_proto_descriptor_pool
20+
21+
22+
class TestService(TestBase):
23+
pass
24+
25+
26+
T = TypeVar("T")
27+
28+
29+
class AsyncIterableQueue(Generic[T]):
30+
CLOSED_SENTINEL = object()
31+
32+
def __init__(self):
33+
self._queue = asyncio.Queue()
34+
self._done = asyncio.Event()
35+
36+
def put(self, item: T):
37+
self._queue.put_nowait(item)
38+
39+
def close(self):
40+
self._queue.put_nowait(self.CLOSED_SENTINEL)
41+
42+
def __aiter__(self):
43+
return self
44+
45+
async def __anext__(self) -> T:
46+
val = await self._queue.get()
47+
if val is self.CLOSED_SENTINEL:
48+
raise StopAsyncIteration
49+
return val
50+
51+
52+
@pytest.mark.asyncio
53+
async def test_grpclib_reflection():
54+
service = TestService()
55+
services = ServerReflection.extend([service])
56+
for service in services:
57+
# This won't be needed once https://github.com/vmagamedov/grpclib/pull/204 is in.
58+
if isinstance(service, ServerReflectionBaseV1Alpha | ServerReflectionBaseV1):
59+
service._pool = default_google_proto_descriptor_pool
60+
61+
async with ChannelFor(services) as channel:
62+
requests = AsyncIterableQueue[ServerReflectionRequest]()
63+
responses = ServerReflectionStub(channel).server_reflection_info(requests)
64+
65+
# list services
66+
requests.put(ServerReflectionRequest(list_services=""))
67+
response = await anext(responses)
68+
assert response.list_services_response == ListServiceResponse(
69+
service=[ServiceResponse(name="example_service.Test")]
70+
)
71+
72+
# list methods
73+
74+
# should fail before we've added descriptors to the protobuf pool
75+
requests.put(ServerReflectionRequest(file_containing_symbol="example_service.Test"))
76+
response = await anext(responses)
77+
assert response.error_response == ErrorResponse(error_code=5, error_message="not found")
78+
assert response.file_descriptor_response is None
79+
80+
# now it should work
81+
import tests.output_betterproto_descriptor.example_service as example_service_with_desc
82+
83+
requests.put(ServerReflectionRequest(file_containing_symbol="example_service.Test"))
84+
response = await anext(responses)
85+
expected = descriptor_pb2.FileDescriptorProto.FromString(
86+
example_service_with_desc.EXAMPLE_SERVICE_PROTO_DESCRIPTOR.serialized_pb
87+
)
88+
assert response.error_response is None
89+
assert response.file_descriptor_response is not None
90+
assert len(response.file_descriptor_response.file_descriptor_proto) == 1
91+
actual = descriptor_pb2.FileDescriptorProto.FromString(
92+
response.file_descriptor_response.file_descriptor_proto[0]
93+
)
94+
assert actual == expected
95+
96+
requests.close()
97+
98+
await anext(responses, None)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
3+
from tests.output_betterproto.import_cousin_package_same_name.test.subpackage import Test
4+
5+
# importing the cousin should cause no descriptor pool errors since the subpackage imports it once already
6+
from tests.output_betterproto_descriptor.import_cousin_package_same_name.cousin.subpackage import CousinMessage
7+
from tests.output_betterproto_descriptor.import_cousin_package_same_name.test.subpackage import Test as TestWithDesc
8+
9+
10+
def test_message_enum_descriptors():
11+
# Normally descriptors are not available as they require protobuf support
12+
# to inteoperate with other libraries.
13+
with pytest.raises(AttributeError):
14+
Test.DESCRIPTOR.full_name
15+
16+
# But the python_betterproto2_opt=google_protobuf_descriptors option
17+
# will add them in as long as protobuf is depended on.
18+
assert TestWithDesc.DESCRIPTOR.full_name == "import_cousin_package_same_name.test.subpackage.Test"
19+
assert CousinMessage.DESCRIPTOR.full_name == "import_cousin_package_same_name.cousin.subpackage.CousinMessage"

betterproto2/tests/test_deprecated.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
Empty,
88
Message,
99
Test,
10+
TestNested,
1011
TestServiceStub,
1112
)
1213

@@ -26,6 +27,14 @@ def test_deprecated_message():
2627
assert str(record[0].message) == f"{Message.__name__} is deprecated"
2728

2829

30+
def test_deprecated_nested_message_field():
31+
with pytest.warns(DeprecationWarning) as record:
32+
TestNested(nested_value="hello")
33+
34+
assert len(record) == 1
35+
assert str(record[0].message) == f"TestNested.nested_value is deprecated"
36+
37+
2938
def test_message_with_deprecated_field(message):
3039
with pytest.warns(DeprecationWarning) as record:
3140
Test(message=message, value=10)

betterproto2/uv.lock

Lines changed: 7 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)