From 5666393f9d10e13609d8eeac8d1ab3815dce5fd6 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 2 Jan 2024 15:16:15 -0500 Subject: [PATCH] betterproto: support `Struct` and `Value` (#551) * betterproto: support `Struct` and `Value` Signed-off-by: William Woodruff * betterproto: handle struct in to_dict as well Signed-off-by: William Woodruff * tests: add Struct roundtrip tests Signed-off-by: William Woodruff * specialize from_dict and to_dict on Struct ...rather than special-casing in the Message ABC. Signed-off-by: William Woodruff * betterproto: `poe format` Signed-off-by: William Woodruff * Update src/betterproto/__init__.py Co-authored-by: James Hilton-Balfe * remove future annotations Signed-off-by: William Woodruff * replace type[...] with typing.T Signed-off-by: William Woodruff * quote instead Signed-off-by: William Woodruff --------- Signed-off-by: William Woodruff Co-authored-by: James Hilton-Balfe --- .../lib/google/protobuf/__init__.py | 31 +++++++++++++++++++ tests/test_struct.py | 24 ++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/test_struct.py diff --git a/src/betterproto/lib/google/protobuf/__init__.py b/src/betterproto/lib/google/protobuf/__init__.py index f59e4a1..9976bda 100644 --- a/src/betterproto/lib/google/protobuf/__init__.py +++ b/src/betterproto/lib/google/protobuf/__init__.py @@ -2,14 +2,19 @@ # sources: google/protobuf/any.proto, google/protobuf/api.proto, google/protobuf/descriptor.proto, google/protobuf/duration.proto, google/protobuf/empty.proto, google/protobuf/field_mask.proto, google/protobuf/source_context.proto, google/protobuf/struct.proto, google/protobuf/timestamp.proto, google/protobuf/type.proto, google/protobuf/wrappers.proto # plugin: python-betterproto # This file has been @generated + import warnings from dataclasses import dataclass from typing import ( Dict, List, + Mapping, ) +from typing_extensions import Self + import betterproto +from betterproto.utils import hybridmethod class Syntax(betterproto.Enum): @@ -1458,6 +1463,32 @@ class Struct(betterproto.Message): ) """Unordered map of dynamically typed values.""" + @hybridmethod + def from_dict(cls: "type[Self]", value: Mapping[str, Any]) -> Self: # type: ignore + self = cls() + return self.from_dict(value) + + @from_dict.instancemethod + def from_dict(self, value: Mapping[str, Any]) -> Self: + fields = {**value} + for k in fields: + if hasattr(fields[k], "from_dict"): + fields[k] = fields[k].from_dict() + + self.fields = fields + return self + + def to_dict( + self, + casing: betterproto.Casing = betterproto.Casing.CAMEL, + include_default_values: bool = False, + ) -> Dict[str, Any]: + output = {**self.fields} + for k in self.fields: + if hasattr(self.fields[k], "to_dict"): + output[k] = self.fields[k].to_dict(casing, include_default_values) + return output + @dataclass(eq=False, repr=False) class Value(betterproto.Message): diff --git a/tests/test_struct.py b/tests/test_struct.py new file mode 100644 index 0000000..f266bc8 --- /dev/null +++ b/tests/test_struct.py @@ -0,0 +1,24 @@ +import json + +from betterproto.lib.google.protobuf import Struct + + +def test_struct_roundtrip(): + data = { + "foo": "bar", + "baz": None, + "quux": 123, + "zap": [1, {"two": 3}, "four"], + } + data_json = json.dumps(data) + + struct_from_dict = Struct().from_dict(data) + assert struct_from_dict.fields == data + assert struct_from_dict.to_dict() == data + assert struct_from_dict.to_json() == data_json + + struct_from_json = Struct().from_json(data_json) + assert struct_from_json.fields == data + assert struct_from_json.to_dict() == data + assert struct_from_json == struct_from_dict + assert struct_from_json.to_json() == data_json