Skip to content
This repository was archived by the owner on Jun 9, 2025. It is now read-only.

Commit d1e438b

Browse files
Message wrapping (#65)
* Change wrapping mechanism * Add missing file * Fix import for Python 3.10 * Fix typechecking * Wrap timestamp and duration * Improve timestamp support * Support durations * Support more well-known types * Fix typo * Remove useless wrap parameter * Rename parameter * Remove redondant code
1 parent 1fbc952 commit d1e438b

File tree

7 files changed

+435
-44
lines changed

7 files changed

+435
-44
lines changed

src/betterproto2_compiler/compile/importing.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
TYPE_CHECKING,
66
)
77

8-
from betterproto2_compiler.lib.google import protobuf as google_protobuf
8+
from betterproto2_compiler.known_types import WRAPPED_TYPES
99
from betterproto2_compiler.settings import Settings
1010

1111
from ..casing import safe_snake_case
@@ -14,18 +14,6 @@
1414
if TYPE_CHECKING:
1515
from ..plugin.models import PluginRequestCompiler
1616

17-
WRAPPER_TYPES: dict[str, type] = {
18-
".google.protobuf.DoubleValue": google_protobuf.DoubleValue,
19-
".google.protobuf.FloatValue": google_protobuf.FloatValue,
20-
".google.protobuf.Int32Value": google_protobuf.Int32Value,
21-
".google.protobuf.Int64Value": google_protobuf.Int64Value,
22-
".google.protobuf.UInt32Value": google_protobuf.UInt32Value,
23-
".google.protobuf.UInt64Value": google_protobuf.UInt64Value,
24-
".google.protobuf.BoolValue": google_protobuf.BoolValue,
25-
".google.protobuf.StringValue": google_protobuf.StringValue,
26-
".google.protobuf.BytesValue": google_protobuf.BytesValue,
27-
}
28-
2917

3018
def parse_source_type_name(field_type_name: str, request: PluginRequestCompiler) -> tuple[str, str]:
3119
"""
@@ -73,26 +61,18 @@ def get_type_reference(
7361
imports: set,
7462
source_type: str,
7563
request: PluginRequestCompiler,
76-
unwrap: bool = True,
64+
wrap: bool = True,
7765
settings: Settings,
7866
) -> str:
7967
"""
8068
Return a Python type name for a proto type reference. Adds the import if
8169
necessary. Unwraps well known type if required.
8270
"""
83-
if unwrap:
84-
if source_type in WRAPPER_TYPES:
85-
wrapped_type = type(WRAPPER_TYPES[source_type]().value)
86-
return f"{wrapped_type.__name__} | None"
87-
88-
if source_type == ".google.protobuf.Duration":
89-
return "datetime.timedelta"
90-
91-
elif source_type == ".google.protobuf.Timestamp":
92-
return "datetime.datetime"
93-
9471
source_package, source_type = parse_source_type_name(source_type, request)
9572

73+
if wrap and (source_package, source_type) in WRAPPED_TYPES:
74+
return WRAPPED_TYPES[(source_package, source_type)]
75+
9676
current_package: list[str] = package.split(".") if package else []
9777
py_package: list[str] = source_package.split(".") if source_package else []
9878
py_type: str = pythonize_class_name(source_type)

src/betterproto2_compiler/known_types/__init__.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,102 @@
22

33
from .any import Any
44
from .duration import Duration
5+
from .google_values import (
6+
BoolValue,
7+
BytesValue,
8+
DoubleValue,
9+
FloatValue,
10+
Int32Value,
11+
Int64Value,
12+
StringValue,
13+
UInt32Value,
14+
UInt64Value,
15+
)
516
from .timestamp import Timestamp
617

718
# For each (package, message name), lists the methods that should be added to the message definition.
819
# The source code of the method is read from the `known_types` folder. If imports are needed, they can be directly added
920
# to the template file: they will automatically be removed if not necessary.
1021
KNOWN_METHODS: dict[tuple[str, str], list[Callable]] = {
1122
("google.protobuf", "Any"): [Any.pack, Any.unpack, Any.to_dict],
12-
("google.protobuf", "Timestamp"): [Timestamp.from_datetime, Timestamp.to_datetime, Timestamp.timestamp_to_json],
13-
("google.protobuf", "Duration"): [Duration.from_timedelta, Duration.to_timedelta, Duration.delta_to_json],
23+
("google.protobuf", "Timestamp"): [
24+
Timestamp.from_datetime,
25+
Timestamp.to_datetime,
26+
Timestamp.timestamp_to_json,
27+
Timestamp.from_dict,
28+
Timestamp.to_dict,
29+
Timestamp.from_wrapped,
30+
Timestamp.to_wrapped,
31+
],
32+
("google.protobuf", "Duration"): [
33+
Duration.from_timedelta,
34+
Duration.to_timedelta,
35+
Duration.delta_to_json,
36+
Duration.from_dict,
37+
Duration.to_dict,
38+
Duration.from_wrapped,
39+
Duration.to_wrapped,
40+
],
41+
("google.protobuf", "BoolValue"): [
42+
BoolValue.from_dict,
43+
BoolValue.to_dict,
44+
BoolValue.from_wrapped,
45+
BoolValue.to_wrapped,
46+
],
47+
("google.protobuf", "Int32Value"): [
48+
Int32Value.from_dict,
49+
Int32Value.to_dict,
50+
Int32Value.from_wrapped,
51+
Int32Value.to_wrapped,
52+
],
53+
("google.protobuf", "Int64Value"): [
54+
Int64Value.from_dict,
55+
Int64Value.to_dict,
56+
Int64Value.from_wrapped,
57+
Int64Value.to_wrapped,
58+
],
59+
("google.protobuf", "UInt32Value"): [
60+
UInt32Value.from_dict,
61+
UInt32Value.to_dict,
62+
UInt32Value.from_wrapped,
63+
UInt32Value.to_wrapped,
64+
],
65+
("google.protobuf", "UInt64Value"): [
66+
UInt64Value.from_dict,
67+
UInt64Value.to_dict,
68+
UInt64Value.from_wrapped,
69+
UInt64Value.to_wrapped,
70+
],
71+
("google.protobuf", "FloatValue"): [
72+
FloatValue.from_dict,
73+
FloatValue.to_dict,
74+
FloatValue.from_wrapped,
75+
FloatValue.to_wrapped,
76+
],
77+
("google.protobuf", "DoubleValue"): [
78+
DoubleValue.from_dict,
79+
DoubleValue.to_dict,
80+
DoubleValue.from_wrapped,
81+
DoubleValue.to_wrapped,
82+
],
83+
("google.protobuf", "StringValue"): [
84+
StringValue.from_dict,
85+
StringValue.to_dict,
86+
StringValue.from_wrapped,
87+
StringValue.to_wrapped,
88+
],
89+
("google.protobuf", "BytesValue"): [
90+
BytesValue.from_dict,
91+
BytesValue.to_dict,
92+
BytesValue.from_wrapped,
93+
BytesValue.to_wrapped,
94+
],
95+
}
96+
97+
# A wrapped type is the type of a message that is automatically replaced by a known Python type.
98+
WRAPPED_TYPES: dict[tuple[str, str], str] = {
99+
("google.protobuf", "BoolValue"): "bool",
100+
("google.protobuf", "StringValue"): "str",
101+
("google.protobuf", "Timestamp"): "datetime.datetime",
102+
("google.protobuf", "Duration"): "datetime.timedelta",
14103
}

src/betterproto2_compiler/known_types/duration.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import datetime
2+
import re
3+
import typing
4+
5+
import betterproto2
26

37
from betterproto2_compiler.lib.google.protobuf import Duration as VanillaDuration
48

@@ -23,3 +27,44 @@ def delta_to_json(delta: datetime.timedelta) -> str:
2327
while len(parts[1]) not in (3, 6, 9):
2428
parts[1] = f"{parts[1]}0"
2529
return f"{'.'.join(parts)}s"
30+
31+
# TODO typing
32+
@classmethod
33+
def from_dict(cls, value):
34+
if isinstance(value, str):
35+
if not re.match(r"^\d+(\.\d+)?s$", value):
36+
raise ValueError(f"Invalid duration string: {value}")
37+
38+
seconds = float(value[:-1])
39+
return Duration(seconds=int(seconds), nanos=int((seconds - int(seconds)) * 1e9))
40+
41+
return super().from_dict(value)
42+
43+
# TODO typing
44+
def to_dict(
45+
self,
46+
*,
47+
output_format: betterproto2.OutputFormat = betterproto2.OutputFormat.PROTO_JSON,
48+
casing: betterproto2.Casing = betterproto2.Casing.CAMEL,
49+
include_default_values: bool = False,
50+
) -> dict[str, typing.Any] | typing.Any:
51+
# If the output format is PYTHON, we should have kept the wrapped type without building the real class
52+
assert output_format == betterproto2.OutputFormat.PROTO_JSON
53+
54+
assert 0 <= self.nanos < 1e9
55+
56+
if self.nanos == 0:
57+
return f"{self.seconds}s"
58+
59+
nanos = f"{self.nanos:09d}".rstrip("0")
60+
if len(nanos) < 3:
61+
nanos += "0" * (3 - len(nanos))
62+
63+
return f"{self.seconds}.{nanos}s"
64+
65+
@staticmethod
66+
def from_wrapped(wrapped: datetime.timedelta) -> "Duration":
67+
return Duration.from_timedelta(wrapped)
68+
69+
def to_wrapped(self) -> datetime.timedelta:
70+
return self.to_timedelta()

0 commit comments

Comments
 (0)