Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 49 additions & 59 deletions proto/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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",)
24 changes: 22 additions & 2 deletions tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
23 changes: 21 additions & 2 deletions tests/test_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading