diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e59aac8..8f8b545 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,12 +16,12 @@ jobs: fail-fast: false matrix: os: [Ubuntu, MacOS, Windows] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} 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