Skip to content

Add back struct support #102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Add back struct support #102

wants to merge 1 commit into from

Conversation

AdrienVannson
Copy link
Contributor

No description provided.

@edaniels
Copy link
Contributor

edaniels commented Jun 15, 2025

Is this related to danielgtaylor/python-betterproto#599?

I'd love if Struct were smarter :D. Right now we do this:

def struct_from_dict(d: dict[str, Any]):
    """
    struct_from_dict converts the given dictionary into a proto Struct.
    This is a sadly needed because of https://github.com/danielgtaylor/python-betterproto/issues/599.
    """
    s = LegacyStruct()
    s.update(d)
    return Struct.FromString(s.SerializeToString())


def struct_to_dict(s: Struct) -> dict[str, Any]:
    """struct_to_dict converts a proto Struct into a python dictionary."""
    output: dict[str, Any] = {}
    for key, value in s.fields.items():
        ret = value_to_py(value)
        output[key] = ret
    return output

def value_to_py(proto_value: Value, include_default_values: bool = False) -> Any | None:
    """value_to_py converts a proto Value to a native python value."""
    field_name, actual_value = betterproto2.which_one_of(proto_value, "kind")
    match field_name:
        case "null_value" | "number_value" | "string_value" | "bool_value":
            return actual_value
        case "struct_value":
            return struct_to_dict(typing.cast(Struct, actual_value))
        case "list_value":
            return [value_to_py(list_val) for list_val in typing.cast(ListValue, actual_value).values]
        case _:
            return None

@leonardgerardatomicmachinescom

I am currently monkey patching the original betterproto to circumvent the bug I report I opened danielgtaylor/python-betterproto#599

"""
Monkey patch betterproto to have Struct behave as expected.

In particular, the from_dict and to_dict methods of betterproto.lib.google.protobuf.Struct
do not work as expected, as they expect the values to be messages of type Value, while
protobuf Struct behaves as if the values are inlined in the struct.

After calling monkeypatch_betterproto_struct(), you can use from_dict and to_dict as expected.
Example:
>>> monkeypatch_betterproto_struct()  # this is automatically done if you import amp.kit (or any module that imports it)
>>> from betterproto.lib.google.protobuf import Struct, Value
>>> import json
>>> s = Struct.from_dict({"a": 42, "b": "hello"})
>>> s.fields["b"]
Value(string_value='hello')
>>> json.loads(s.to_json()) == {"a": 42, "b": "hello"}
True

"""

from typing import Any, Mapping

import betterproto.lib.google.protobuf
from betterproto import Casing
from betterproto.lib.google.protobuf import Struct
from google.protobuf.json_format import MessageToDict as pb_MessageToDict
from google.protobuf.struct_pb2 import Struct as pb_Struct


def monkeypatch_betterproto_struct():
    """Monkey patch betterproto to have Struct behave"""

    def struct_to_dict_method(
        self: Struct,
        casing: Casing = Casing.CAMEL,  # type: ignore The Casing.CAMEL is not a real enum value...
        include_default_values: bool = False,
    ) -> dict[str, Any]:
        # Protobuf Struct special case things, in particular it behaves as if values are inlined
        # in the struct, while betterproto sticks to the .proto definition of Struct,
        # that is values in the dict have to be messages of type Value.
        # So to_dict and to_json (which is just json_dumps(to_dict(()) do not work as expected.
        #
        # Fix: we create the corresponding proto struct, then use proto tool to get the dict representation with inlined values.
        s = pb_Struct()
        s.ParseFromString(self.SerializeToString())
        return pb_MessageToDict(s, preserving_proto_field_name=True)

    # Monkey patch betterproto Struct to_dict and to_pydict (they are the same for Struct, no custom types expected)
    betterproto.lib.google.protobuf.Struct.to_dict = struct_to_dict_method
    betterproto.lib.google.protobuf.Struct.to_pydict = struct_to_dict_method

    def struct_from_dict_method(self: Struct, value: Mapping[str, Any]) -> Struct:
        # Same reasoning as to_dict above.
        # Issue: from_dict and from_json (which is just from_dict(json.loads()) do not work as expected (values not inlined).
        # Fix: we create the struct using protobuf, serialize it and then parse it with betterproto.
        s = pb_Struct()
        s.update(value)
        return self.FromString(s.SerializeToString())

    # Betterproto uses hybridmethod for from_dict, i.e. it works as class method and instance method.
    # so we have to create the hybridmethod correspondingly.

    from betterproto.utils import hybridmethod

    def struct_from_dict_classmethod(
        cls: type[Struct], value: Mapping[str, Any]
    ) -> Struct:
        return struct_from_dict_method(cls(), value)

    from_dict = hybridmethod(struct_from_dict_classmethod)
    from_dict.instance_func = struct_from_dict_method

    betterproto.lib.google.protobuf.Struct.from_dict = from_dict

@leonardgerardatomicmachinescom

My monkey patch is "working" but does not abide by a bunch of things like casing, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants