Support JSON base64 bytes and enums as strings

This commit is contained in:
Daniel G. Taylor 2019-10-19 12:31:22 -07:00
parent 7fe64ad8fe
commit b5c1f1aa7c
No known key found for this signature in database
GPG Key ID: 7BD6DC99C9A87E22
8 changed files with 81 additions and 13 deletions

View File

@ -169,10 +169,10 @@ Sometimes it is useful to be able to determine whether a message has been sent o
Use `Message().serialized_on_wire` to determine if it was sent. This is a little bit different from the official Google generated Python code: Use `Message().serialized_on_wire` to determine if it was sent. This is a little bit different from the official Google generated Python code:
```py ```py
# Old way # Old way (official Google Protobuf package)
>>> mymessage.HasField('myfield') >>> mymessage.HasField('myfield')
# New way # New way (this project)
>>> mymessage.myfield.serialized_on_wire >>> mymessage.myfield.serialized_on_wire
``` ```
@ -226,8 +226,9 @@ $ pipenv run tests
- [x] 64-bit ints as strings - [x] 64-bit ints as strings
- [x] Maps - [x] Maps
- [x] Lists - [x] Lists
- [ ] Bytes as base64 - [x] Bytes as base64
- [ ] Any support - [ ] Any support
- [x] Enum strings
- [ ] Well known types support (timestamp, duration, wrappers) - [ ] Well known types support (timestamp, duration, wrappers)
- [ ] Async service stubs - [ ] Async service stubs
- [x] Unary-unary - [x] Unary-unary

View File

@ -1,8 +1,10 @@
import dataclasses import dataclasses
import enum
import inspect import inspect
import json import json
import struct import struct
from abc import ABC from abc import ABC
from base64 import b64encode, b64decode
from typing import ( from typing import (
Any, Any,
AsyncGenerator, AsyncGenerator,
@ -222,6 +224,18 @@ def map_field(number: int, key_type: str, value_type: str) -> Any:
return dataclass_field(number, TYPE_MAP, map_types=(key_type, value_type)) return dataclass_field(number, TYPE_MAP, map_types=(key_type, value_type))
class Enum(int, enum.Enum):
"""Protocol buffers enumeration base class. Acts like `enum.IntEnum`."""
@classmethod
def from_string(cls, name: str) -> int:
"""Return the value which corresponds to the string name."""
try:
return cls.__members__[name]
except KeyError as e:
raise ValueError(f"Unknown value {name} for enum {cls.__name__}") from e
def _pack_fmt(proto_type: str) -> str: def _pack_fmt(proto_type: str) -> str:
"""Returns a little-endian format string for reading/writing binary.""" """Returns a little-endian format string for reading/writing binary."""
return { return {
@ -596,6 +610,17 @@ class Message(ABC):
output[field.name] = [str(n) for n in v] output[field.name] = [str(n) for n in v]
else: else:
output[field.name] = str(v) output[field.name] = str(v)
elif meta.proto_type == TYPE_BYTES:
if isinstance(v, list):
output[field.name] = [b64encode(b).decode("utf8") for b in v]
else:
output[field.name] = b64encode(v).decode("utf8")
elif meta.proto_type == TYPE_ENUM:
enum_values = list(self._cls_for(field))
if isinstance(v, list):
output[field.name] = [enum_values[e].name for e in v]
else:
output[field.name] = enum_values[v].name
else: else:
output[field.name] = v output[field.name] = v
return output return output
@ -630,6 +655,19 @@ class Message(ABC):
v = [int(n) for n in value[field.name]] v = [int(n) for n in value[field.name]]
else: else:
v = int(value[field.name]) v = int(value[field.name])
elif meta.proto_type == TYPE_BYTES:
if isinstance(value[field.name], list):
v = [b64decode(n) for n in value[field.name]]
else:
v = b64decode(value[field.name])
elif meta.proto_type == TYPE_ENUM:
enum_cls = self._cls_for(field)
if isinstance(v, list):
v = [enum_cls.from_string(e) for e in v]
elif isinstance(v, str):
v = enum_cls.from_string(v)
if v is not None:
setattr(self, field.name, v) setattr(self, field.name, v)
return self return self

View File

@ -1,8 +1,6 @@
# Generated by the protocol buffer compiler. DO NOT EDIT! # Generated by the protocol buffer compiler. DO NOT EDIT!
# sources: {{ ', '.join(description.files) }} # sources: {{ ', '.join(description.files) }}
# plugin: python-betterproto # plugin: python-betterproto
{% if description.enums %}import enum
{% endif %}
from dataclasses import dataclass from dataclasses import dataclass
{% if description.typing_imports %} {% if description.typing_imports %}
from typing import {% for i in description.typing_imports %}{{ i }}{% if not loop.last %}, {% endif %}{% endfor %} from typing import {% for i in description.typing_imports %}{{ i }}{% if not loop.last %}, {% endif %}{% endfor %}
@ -20,7 +18,7 @@ import grpclib
{% if description.enums %}{% for enum in description.enums %} {% if description.enums %}{% for enum in description.enums %}
class {{ enum.name }}(enum.IntEnum): class {{ enum.name }}(betterproto.Enum):
{% if enum.comment %} {% if enum.comment %}
{{ enum.comment }} {{ enum.comment }}

View File

@ -0,0 +1,3 @@
{
"data": "SGVsbG8sIFdvcmxkIQ=="
}

View File

@ -0,0 +1,5 @@
syntax = "proto3";
message Test {
bytes data = 1;
}

View File

@ -1,3 +1,3 @@
{ {
"greeting": 1 "greeting": "HEY"
} }

View File

@ -69,10 +69,15 @@ if __name__ == "__main__":
print(f"Using {parts[0]}_pb2 to generate {os.path.basename(out)}") print(f"Using {parts[0]}_pb2 to generate {os.path.basename(out)}")
imported = importlib.import_module(f"{parts[0]}_pb2") imported = importlib.import_module(f"{parts[0]}_pb2")
parsed = Parse(open(filename).read(), imported.Test()) input_json = open(filename).read()
parsed = Parse(input_json, imported.Test())
serialized = parsed.SerializeToString() serialized = parsed.SerializeToString()
serialized_json = MessageToJson( serialized_json = MessageToJson(parsed, preserving_proto_field_name=True)
parsed, preserving_proto_field_name=True, use_integers_for_enums=True
) s_loaded = json.loads(serialized_json)
assert json.loads(serialized_json) == json.load(open(filename)) in_loaded = json.loads(input_json)
if s_loaded != in_loaded:
raise AssertionError("Expected JSON to be equal:", s_loaded, in_loaded)
open(out, "wb").write(serialized) open(out, "wb").write(serialized)

View File

@ -30,3 +30,21 @@ def test_has_field():
# Can manually set it but defaults to false # Can manually set it but defaults to false
foo.bar = Bar() foo.bar = Bar()
assert foo.bar.serialized_on_wire == False assert foo.bar.serialized_on_wire == False
def test_enum_as_int_json():
class TestEnum(betterproto.Enum):
ZERO = 0
ONE = 1
@dataclass
class Foo(betterproto.Message):
bar: TestEnum = betterproto.enum_field(1)
# JSON strings are supported, but ints should still be supported too.
foo = Foo().from_dict({"bar": 1})
assert foo.bar == TestEnum.ONE
# Plain-ol'-ints should serialize properly too.
foo.bar = 1
assert foo.to_dict() == {"bar": "ONE"}