diff --git a/proto/message.py b/proto/message.py index 10a6f42d..5533e99b 100644 --- a/proto/message.py +++ b/proto/message.py @@ -36,6 +36,9 @@ PROTOBUF_VERSION = google.protobuf.__version__ +# extract the major version code +_PROTOBUF_MAJOR_VERSION = PROTOBUF_VERSION.partition(".")[0] + _upb = has_upb() # Important to cache result here. @@ -383,7 +386,7 @@ def _warn_if_including_default_value_fields_is_used_protobuf_5( including_default_value_fields (Optional(bool)): The value of `including_default_value_fields` set by the user. """ if ( - PROTOBUF_VERSION[0] not in ("3", "4") + _PROTOBUF_MAJOR_VERSION not in ("3", "4") and including_default_value_fields is not None ): warnings.warn( @@ -491,7 +494,7 @@ def to_json( 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. + Default is None. [DEPRECATED] float_precision was removed in Protobuf 7.x. always_print_fields_with_no_presence (Optional(bool)): If True, fields without presence (implicit presence scalars, repeated fields, and map fields) will always be serialized. Any field that supports presence is not affected by @@ -501,36 +504,7 @@ def to_json( Returns: str: The json string representation of the protocol buffer. """ - - print_fields = cls._normalize_print_fields_without_presence( - always_print_fields_with_no_presence, including_default_value_fields - ) - - if PROTOBUF_VERSION[0] in ("3", "4"): - return MessageToJson( - cls.pb(instance), - use_integers_for_enums=use_integers_for_enums, - including_default_value_fields=print_fields, - preserving_proto_field_name=preserving_proto_field_name, - sort_keys=sort_keys, - indent=indent, - float_precision=float_precision, - ) - else: - # The `including_default_value_fields` argument was removed from protobuf 5.x - # and replaced with `always_print_fields_with_no_presence` which very similar but has - # handles optional fields consistently by not affecting them. - # The old flag accidentally had inconsistent behavior between proto2 - # optional and proto3 optional fields. - return MessageToJson( - cls.pb(instance), - use_integers_for_enums=use_integers_for_enums, - always_print_fields_with_no_presence=print_fields, - preserving_proto_field_name=preserving_proto_field_name, - sort_keys=sort_keys, - indent=indent, - float_precision=float_precision, - ) + return _message_to_map(map_fn=MessageToJson, **locals()) def from_json(cls, payload, *, ignore_unknown_fields=False) -> "Message": """Given a json string representing an instance, @@ -576,7 +550,7 @@ def to_dict( This value must match `always_print_fields_with_no_presence`, if both arguments are explicitly set. float_precision (Optional(int)): If set, use this to specify float field valid digits. - Default is None. + Default is None. [DEPRECATED] float_precision was removed in Protobuf 7.x. always_print_fields_with_no_presence (Optional(bool)): If True, fields without presence (implicit presence scalars, repeated fields, and map fields) will always be serialized. Any field that supports presence is not affected by @@ -588,32 +562,7 @@ def to_dict( Messages and map fields are represented as dicts, repeated fields are represented as lists. """ - - print_fields = cls._normalize_print_fields_without_presence( - always_print_fields_with_no_presence, including_default_value_fields - ) - - if PROTOBUF_VERSION[0] in ("3", "4"): - return MessageToDict( - cls.pb(instance), - including_default_value_fields=print_fields, - preserving_proto_field_name=preserving_proto_field_name, - use_integers_for_enums=use_integers_for_enums, - float_precision=float_precision, - ) - else: - # The `including_default_value_fields` argument was removed from protobuf 5.x - # and replaced with `always_print_fields_with_no_presence` which very similar but has - # handles optional fields consistently by not affecting them. - # The old flag accidentally had inconsistent behavior between proto2 - # optional and proto3 optional fields. - return MessageToDict( - cls.pb(instance), - always_print_fields_with_no_presence=print_fields, - preserving_proto_field_name=preserving_proto_field_name, - use_integers_for_enums=use_integers_for_enums, - float_precision=float_precision, - ) + return _message_to_map(map_fn=MessageToDict, **locals()) def copy_from(cls, instance, other): """Equivalent for protobuf.Message.CopyFrom @@ -966,4 +915,45 @@ def pb(self) -> Type[message.Message]: return self._pb +def _message_to_map( + cls, + map_fn, + instance, + *, + including_default_value_fields=None, + always_print_fields_with_no_presence=None, + float_precision=None, + **kwargs, +): + """ + Helper for logic for Message.to_dict and Message.to_json + """ + + # The `including_default_value_fields` argument was removed from protobuf 5.x + # and replaced with `always_print_fields_with_no_presence` which very similar but has + # handles optional fields consistently by not affecting them. + # The old flag accidentally had inconsistent behavior between proto2 + # optional and proto3 optional fields. + print_fields = cls._normalize_print_fields_without_presence( + always_print_fields_with_no_presence, including_default_value_fields + ) + if _PROTOBUF_MAJOR_VERSION in ("3", "4"): + kwargs["including_default_value_fields"] = print_fields + else: + kwargs["always_print_fields_with_no_presence"] = print_fields + + if float_precision: + # float_precision removed in protobuf 7 + if _PROTOBUF_MAJOR_VERSION in ("3", "4", "5", "6"): + kwargs["float_precision"] = float_precision + else: # pragma: NO COVER + warnings.warn( + "The argument `float_precision` has been removed from Protobuf 7.x.", + DeprecationWarning, + stacklevel=3, + ) + + return map_fn(cls.pb(instance), **kwargs) + + __all__ = ("Message",) diff --git a/tests/test_json.py b/tests/test_json.py index ae3cf59e..c2468383 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -253,13 +253,33 @@ class Squid(proto.Message): assert re.search(r"massKg.*name", j) -# TODO: https://github.com/googleapis/proto-plus-python/issues/390 def test_json_float_precision(): + if int(proto.message._PROTOBUF_MAJOR_VERSION) >= 7: + pytest.skip("float_precision was removed in protobuf 7.x") + 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) + s = Squid(name="Steve", mass_kg=3.141592) j = Squid.to_json(s, float_precision=3, indent=None) assert j == '{"name": "Steve", "massKg": 3.14}' + + +def test_json_float_precision_7_plus(): + if int(proto.message._PROTOBUF_MAJOR_VERSION) < 7: + pytest.skip("unsupported protobuf version for test") + + 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.141592) + with pytest.warns(DeprecationWarning) as warnings: + j = Squid.to_json(s, float_precision=3, indent=None) + + assert j == '{"name": "Steve", "massKg": 3.141592}' + + assert len(warnings) == 1 + assert "`float_precision` has been removed" in warnings[0].message.args[0] diff --git a/tests/test_message.py b/tests/test_message.py index 720995a9..36a0655e 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -326,17 +326,36 @@ class Color(proto.Enum): ) -# TODO: https://github.com/googleapis/proto-plus-python/issues/390 def test_serialize_to_dict_float_precision(): + if int(proto.message._PROTOBUF_MAJOR_VERSION) >= 7: + pytest.skip("float_precision was removed in protobuf 7.x") + class Squid(proto.Message): mass_kg = proto.Field(proto.FLOAT, number=1) - s = Squid(mass_kg=3.14159265) + s = Squid(mass_kg=3.141592) s_dict = Squid.to_dict(s, float_precision=3) assert s_dict["mass_kg"] == 3.14 +def test_serialize_to_dict_float_precision_7_plus(): + if int(proto.message._PROTOBUF_MAJOR_VERSION) < 7: + pytest.skip("unsupported protobuf version for test") + + class Squid(proto.Message): + mass_kg = proto.Field(proto.FLOAT, number=1) + + s = Squid(mass_kg=3.141592) + + with pytest.warns(DeprecationWarning) as warnings: + s_dict = Squid.to_dict(s, float_precision=3) + assert s_dict["mass_kg"] == pytest.approx(3.141592) + + assert len(warnings) == 1 + assert "`float_precision` has been removed" in warnings[0].message.args[0] + + 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