From 8f13a46514e1d7653426c0db3c1021f9c794451a Mon Sep 17 00:00:00 2001 From: Holt Skinner <13262395+holtskinner@users.noreply.github.com> Date: Fri, 8 Sep 2023 11:06:27 -0500 Subject: [PATCH] feat: Add additional parameters to `to_json()` and `to_dict()` methods (#384) * feat: Add additional parameters to `to_json()` and `to_dict()` methods - These parameters are just passthroughs to [`MessageToJson`][1] and [`MessageToDict`][2] [1]: https://googleapis.dev/python/protobuf/latest/google/protobuf/json_format.html#google.protobuf.json_format.MessageToJson [2]: https://googleapis.dev/python/protobuf/latest/google/protobuf/json_format.html#google.protobuf.json_format.MessageToDict * chore: Update min protobuf version to 3.20.0 to support `ensure_ascii` * Removed `ensure_ascii` to prevent version compatibility errors * Update proto/message.py Co-authored-by: Anthonios Partheniou * test: Add tests for `sort_keys` and `float_precision` * Fix test checks * Remove indent from float_precision test and add re.compile to sortkeys regex * Add spaces to float precision check, add dotall to sortkeys check * Remove indent from sort_keys test, add space to float precision test * Changed match to search * test: Add test for `to_dict()` with `float_precision` * Added TODOs for `float_precision` issue --------- Co-authored-by: Anthonios Partheniou --- proto/message.py | 15 +++++++++++++-- tests/test_json.py | 24 ++++++++++++++++++++++++ tests/test_message.py | 11 +++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/proto/message.py b/proto/message.py index 253e6240..7232d42f 100644 --- a/proto/message.py +++ b/proto/message.py @@ -376,7 +376,9 @@ def to_json( use_integers_for_enums=True, including_default_value_fields=True, preserving_proto_field_name=False, + sort_keys=False, indent=2, + float_precision=None, ) -> str: """Given a message instance, serialize it to json @@ -389,10 +391,13 @@ def to_json( preserving_proto_field_name (Optional(bool)): An option that determines whether field name representations preserve proto case (snake_case) or use lowerCamelCase. Default is False. - indent: The JSON object will be pretty-printed with this indent level. + sort_keys (Optional(bool)): If True, then the output will be sorted by field names. + Default is False. + indent (Optional(int)): The JSON object will be pretty-printed with this indent level. An indent level of 0 or negative will only insert newlines. Pass None for the most compact representation without newlines. - + float_precision (Optional(int)): If set, use this to specify float field valid digits. + Default is None. Returns: str: The json string representation of the protocol buffer. """ @@ -401,7 +406,9 @@ def to_json( use_integers_for_enums=use_integers_for_enums, including_default_value_fields=including_default_value_fields, preserving_proto_field_name=preserving_proto_field_name, + sort_keys=sort_keys, indent=indent, + float_precision=float_precision, ) def from_json(cls, payload, *, ignore_unknown_fields=False) -> "Message": @@ -428,6 +435,7 @@ def to_dict( use_integers_for_enums=True, preserving_proto_field_name=True, including_default_value_fields=True, + float_precision=None, ) -> "Message": """Given a message instance, return its representation as a python dict. @@ -443,6 +451,8 @@ def to_dict( including_default_value_fields (Optional(bool)): An option that determines whether the default field values should be included in the results. Default is True. + float_precision (Optional(int)): If set, use this to specify float field valid digits. + Default is None. Returns: dict: A representation of the protocol buffer using pythonic data structures. @@ -454,6 +464,7 @@ def to_dict( including_default_value_fields=including_default_value_fields, preserving_proto_field_name=preserving_proto_field_name, use_integers_for_enums=use_integers_for_enums, + float_precision=float_precision, ) def copy_from(cls, instance, other): diff --git a/tests/test_json.py b/tests/test_json.py index 93ca936c..e94e935a 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -13,6 +13,7 @@ # limitations under the License. import pytest +import re import proto from google.protobuf.json_format import MessageToJson, Parse, ParseError @@ -172,3 +173,26 @@ class Squid(proto.Message): s_two = Squid.from_json(j) assert s == s_two + + +def test_json_sort_keys(): + class Squid(proto.Message): + name = proto.Field(proto.STRING, number=1) + mass_kg = proto.Field(proto.INT32, number=2) + + s = Squid(name="Steve", mass_kg=20) + j = Squid.to_json(s, sort_keys=True, indent=None) + + assert re.search(r"massKg.*name", j) + + +# TODO: https://github.com/googleapis/proto-plus-python/issues/390 +def test_json_float_precision(): + class Squid(proto.Message): + name = proto.Field(proto.STRING, number=1) + mass_kg = proto.Field(proto.FLOAT, number=2) + + s = Squid(name="Steve", mass_kg=3.14159265) + j = Squid.to_json(s, float_precision=3, indent=None) + + assert j == '{"name": "Steve", "massKg": 3.14}' diff --git a/tests/test_message.py b/tests/test_message.py index 3146f0bb..983cde82 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -271,6 +271,17 @@ class Color(proto.Enum): assert new_s == s +# TODO: https://github.com/googleapis/proto-plus-python/issues/390 +def test_serialize_to_dict_float_precision(): + class Squid(proto.Message): + mass_kg = proto.Field(proto.FLOAT, number=1) + + s = Squid(mass_kg=3.14159265) + + s_dict = Squid.to_dict(s, float_precision=3) + assert s_dict["mass_kg"] == 3.14 + + def test_unknown_field_deserialize(): # This is a somewhat common setup: a client uses an older proto definition, # while the server sends the newer definition. The client still needs to be