12 Commits

Author SHA1 Message Date
Daniel G. Taylor
559b8833d8 Bump version to 1.2.2 2020-01-09 16:47:25 -08:00
Daniel G. Taylor
7ccef16579 Mention no proto 2, fixes #6 2020-01-09 16:43:45 -08:00
Daniel G. Taylor
d8785b4622 Merge pull request #10 from qix/master
Fix serialization of dataclass constructor parameters
2020-01-09 16:35:06 -08:00
Daniel G. Taylor
45e7a30300 Merge pull request #7 from ulasozguler/master
Fix - propagate `casing` param of `to_dict` function recursively
2020-01-09 16:32:29 -08:00
Josh Yudaken
d7559c22f8 Fix serialization of dataclass constructor parameters 2020-01-08 11:29:45 -05:00
ulas
f9c351a98d propagate casing param recursively. 2019-12-04 19:28:53 +03:00
Daniel G. Taylor
feea790116 Bump library version 2019-10-29 22:00:27 -07:00
Daniel G. Taylor
33f74f6a45 Fix comment indent bug; bump version 2019-10-29 21:59:23 -07:00
Daniel G. Taylor
3d5c12c532 Add changelog, version bump 2019-10-28 21:13:25 -07:00
Daniel G. Taylor
706bd5a475 Slightly simplify gRPC helper functions 2019-10-28 20:58:33 -07:00
Daniel G. Taylor
52beeb0d73 Fix typo in example 2019-10-28 20:44:57 -07:00
Daniel G. Taylor
7e2dc595db Autoformat files after rendering 2019-10-28 20:44:50 -07:00
9 changed files with 161 additions and 45 deletions

48
CHANGELOG.md Normal file
View File

@@ -0,0 +1,48 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.2.2] - 2020-01-09
- Mention lack of Proto 2 support in README.
- Fix serialization of constructor parameters [#10](https://github.com/danielgtaylor/python-betterproto/pull/10)
- Fix `casing` parameter propagation [#7](https://github.com/danielgtaylor/python-betterproto/pull/7)
## [1.2.1] - 2019-10-29
- Fix comment indentation bug in rendered gRPC methods.
## [1.2.0] - 2019-10-28
- Generated code output auto-formatting via [Black](https://github.com/psf/black)
- Simplified gRPC helper functions
## [1.1.0] - 2019-10-27
- Better JSON casing support
- Handle field names which clash with Python reserved words
- Better handling of default values from type introspection
- Support for Google Duration & Timestamp types
- Support for Google wrapper types
- Documentation updates
## [1.0.1] - 2019-10-22
- README to the PyPI details page
## [1.0.0] - 2019-10-22
- Initial release
[unreleased]: https://github.com/danielgtaylor/python-betterproto/compare/v1.2.2...HEAD
[1.2.2]: https://github.com/danielgtaylor/python-betterproto/compare/v1.2.1...v1.2.2
[1.2.1]: https://github.com/danielgtaylor/python-betterproto/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/danielgtaylor/python-betterproto/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/danielgtaylor/python-betterproto/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/danielgtaylor/python-betterproto/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/danielgtaylor/python-betterproto/releases/tag/v1.0.0

View File

@@ -15,6 +15,7 @@ protobuf = "*"
jinja2 = "*"
grpclib = "*"
stringcase = "*"
black = "*"
[requires]
python_version = "3.7"
@@ -23,3 +24,6 @@ python_version = "3.7"
plugin = "protoc --plugin=protoc-gen-custom=betterproto/plugin.py --custom_out=output"
generate = "python betterproto/tests/generate.py"
test = "pytest ./betterproto/tests"
[pipenv]
allow_prereleases = true

64
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "28c38cd6c4eafb0b9ac9a64cf623145868fdee163111d3b941b34d23011db6ca"
"sha256": "c7b72ed87dc3d70566c53d7ec8a636c8d4854aa30aa97a9116c0734cd5266f33"
},
"pipfile-spec": 6,
"requires": {
@@ -16,12 +16,41 @@
]
},
"default": {
"grpclib": {
"appdirs": {
"hashes": [
"sha256:d19e2ea87cb073e5b0825dfee15336fd2b1c09278d271816e04c90faddc107ea"
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"black": {
"hashes": [
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
],
"index": "pypi",
"version": "==0.3.0"
"version": "==19.3b0"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"grpclib": {
"hashes": [
"sha256:2d63cee35f764e40a7ea196f27354d2f4ab936401c40b14128bbb4fec06f51d4"
],
"index": "pypi",
"version": "==0.3.1rc2"
},
"h2": {
"hashes": [
@@ -154,6 +183,13 @@
],
"index": "pypi",
"version": "==1.2.0"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
}
},
"develop": {
@@ -180,11 +216,11 @@
},
"flake8": {
"hashes": [
"sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548",
"sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
"sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
],
"index": "pypi",
"version": "==3.7.8"
"version": "==3.7.9"
},
"importlib-metadata": {
"hashes": [
@@ -287,11 +323,11 @@
},
"pytest": {
"hashes": [
"sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8",
"sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"
"sha256:27abc3fef618a01bebb1f0d6d303d2816a99aa87a5968ebc32fe971be91eb1e6",
"sha256:58cee9e09242937e136dbb3dab466116ba20d6b7828c7620f23947f37eb4dae4"
],
"index": "pypi",
"version": "==5.2.1"
"version": "==5.2.2"
},
"rope": {
"hashes": [
@@ -336,11 +372,11 @@
},
"typing-extensions": {
"hashes": [
"sha256:2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95",
"sha256:b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87",
"sha256:d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"
"sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2",
"sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d",
"sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"
],
"version": "==3.7.4"
"version": "==3.7.4.1"
},
"wcwidth": {
"hashes": [

View File

@@ -2,7 +2,7 @@
![](https://github.com/danielgtaylor/python-betterproto/workflows/CI/badge.svg)
This project aims to provide an improved experience when using Protobuf / gRPC in a modern Python environment by making use of modern language features and generating readable, understandable, idiomatic Python code. It will not support legacy features or environments. The following are supported:
This project aims to provide an improved experience when using Protobuf / gRPC in a modern Python environment by making use of modern language features and generating readable, understandable, idiomatic Python code. It will not support legacy features or environments (e.g. Protobuf 2). The following are supported:
- Protobuf 3 & gRPC code generation
- Both binary & JSON serialization is built-in
@@ -281,7 +281,7 @@ You can do stuff like:
```py
>>> t = Test().from_dict({"maybe": True, "ts": "2019-01-01T12:00:00Z", "duration": "1.200s"})
>>> t
st(maybe=True, ts=datetime.datetime(2019, 1, 1, 12, 0, tzinfo=datetime.timezone.utc), duration=datetime.timedelta(seconds=1, microseconds=200000))
Test(maybe=True, ts=datetime.datetime(2019, 1, 1, 12, 0, tzinfo=datetime.timezone.utc), duration=datetime.timedelta(seconds=1, microseconds=200000))
>>> t.ts - t.duration
datetime.datetime(2019, 1, 1, 11, 59, 58, 800000, tzinfo=datetime.timezone.utc)

View File

@@ -21,6 +21,7 @@ from typing import (
TypeVar,
Union,
get_type_hints,
TYPE_CHECKING,
)
import grpclib.client
@@ -29,6 +30,9 @@ import stringcase
from .casing import safe_snake_case
if TYPE_CHECKING:
from grpclib._protocols import IProtoMessage
# Proto 3 data types
TYPE_ENUM = "enum"
TYPE_BOOL = "bool"
@@ -420,11 +424,15 @@ class Message(ABC):
register the message fields which get used by the serializers and parsers
to go between Python, binary and JSON protobuf message representations.
"""
_serialized_on_wire: bool
_unknown_fields: bytes
_group_map: Dict[str, dict]
def __post_init__(self) -> None:
# Keep track of whether every field was default
all_sentinel = True
# Set a default value for each field in the class after `__init__` has
# already been run.
group_map: Dict[str, dict] = {"fields": {}, "groups": {}}
@@ -441,6 +449,7 @@ class Message(ABC):
if getattr(self, field.name) != PLACEHOLDER:
# Skip anything not set to the sentinel value
all_sentinel = False
if meta.group:
# This was set, so make it the selected value of the one-of.
@@ -451,7 +460,7 @@ class Message(ABC):
setattr(self, field.name, self._get_field_default(field, meta))
# Now that all the defaults are set, reset it!
self.__dict__["_serialized_on_wire"] = False
self.__dict__["_serialized_on_wire"] = not all_sentinel
self.__dict__["_unknown_fields"] = b""
self.__dict__["_group_map"] = group_map
@@ -705,7 +714,7 @@ class Message(ABC):
for field in dataclasses.fields(self):
meta = FieldMetadata.get(field)
v = getattr(self, field.name)
cased_name = casing(field.name).rstrip("_") # type: ignore
cased_name = casing(field.name).rstrip("_") # type: ignore
if meta.proto_type == "message":
if isinstance(v, datetime):
if v != DATETIME_ZERO:
@@ -718,14 +727,14 @@ class Message(ABC):
output[cased_name] = v
elif isinstance(v, list):
# Convert each item.
v = [i.to_dict() for i in v]
v = [i.to_dict(casing) for i in v]
output[cased_name] = v
elif v._serialized_on_wire:
output[cased_name] = v.to_dict()
output[cased_name] = v.to_dict(casing)
elif meta.proto_type == "map":
for k in v:
if hasattr(v[k], "to_dict"):
v[k] = v[k].to_dict()
v[k] = v[k].to_dict(casing)
if v:
output[cased_name] = v
@@ -741,7 +750,7 @@ class Message(ABC):
else:
output[cased_name] = b64encode(v).decode("utf8")
elif meta.proto_type == TYPE_ENUM:
enum_values = list(self._cls_for(field)) # type: ignore
enum_values = list(self._cls_for(field)) # type: ignore
if isinstance(v, list):
output[cased_name] = [enum_values[e].name for e in v]
else:
@@ -902,6 +911,7 @@ class _WrappedMessage(Message):
Google protobuf wrapper types base class. JSON representation is just the
value itself.
"""
value: Any
def to_dict(self, casing: Casing = Casing.CAMEL) -> Any:
@@ -982,11 +992,11 @@ class ServiceStub(ABC):
self.channel = channel
async def _unary_unary(
self, route: str, request_type: Type, response_type: Type[T], request: Any
self, route: str, request: "IProtoMessage", response_type: Type[T]
) -> T:
"""Make a unary request and return the response."""
async with self.channel.request(
route, grpclib.const.Cardinality.UNARY_UNARY, request_type, response_type
route, grpclib.const.Cardinality.UNARY_UNARY, type(request), response_type
) as stream:
await stream.send_message(request, end=True)
response = await stream.recv_message()
@@ -994,11 +1004,11 @@ class ServiceStub(ABC):
return response
async def _unary_stream(
self, route: str, request_type: Type, response_type: Type[T], request: Any
self, route: str, request: "IProtoMessage", response_type: Type[T]
) -> AsyncGenerator[T, None]:
"""Make a unary request and return the stream response iterator."""
async with self.channel.request(
route, grpclib.const.Cardinality.UNARY_STREAM, request_type, response_type
route, grpclib.const.Cardinality.UNARY_STREAM, type(request), response_type
) as stream:
await stream.send_message(request, end=True)
async for message in stream:

View File

@@ -9,13 +9,14 @@ import textwrap
from typing import Any, List, Tuple
try:
import jinja2
import black
except ImportError:
print(
"Unable to import `jinja2`. Did you install the compiler feature with `pip install betterproto[compiler]`?"
"Unable to import `black` formatter. Did you install the compiler feature with `pip install betterproto[compiler]`?"
)
raise SystemExit(1)
import jinja2
import stringcase
from google.protobuf.compiler import plugin_pb2 as plugin
@@ -141,25 +142,26 @@ def traverse(proto_file):
)
def get_comment(proto_file, path: List[int]) -> str:
def get_comment(proto_file, path: List[int], indent: int = 4) -> str:
pad = " " * indent
for sci in proto_file.source_code_info.location:
# print(list(sci.path), path, file=sys.stderr)
if list(sci.path) == path and sci.leading_comments:
lines = textwrap.wrap(
sci.leading_comments.strip().replace("\n", ""), width=75
sci.leading_comments.strip().replace("\n", ""), width=79 - indent
)
if path[-2] == 2 and path[-4] != 6:
# This is a field
return " # " + "\n # ".join(lines)
return f"{pad}# " + f"\n{pad}# ".join(lines)
else:
# This is a message, enum, service, or method
if len(lines) == 1 and len(lines[0]) < 70:
if len(lines) == 1 and len(lines[0]) < 79 - indent - 6:
lines[0] = lines[0].strip('"')
return f' """{lines[0]}"""'
return f'{pad}"""{lines[0]}"""'
else:
joined = "\n ".join(lines)
return f' """\n {joined}\n """'
joined = f"\n{pad}".join(lines)
return f'{pad}"""\n{pad}{joined}\n{pad}"""'
return ""
@@ -370,7 +372,7 @@ def generate_code(request, response):
{
"name": method.name,
"py_name": stringcase.snakecase(method.name),
"comment": get_comment(proto_file, [6, i, 2, j]),
"comment": get_comment(proto_file, [6, i, 2, j], indent=8),
"route": f"/{package}.{service.name}/{method.name}",
"input": get_ref_type(
package, output["imports"], method.input_type
@@ -398,8 +400,11 @@ def generate_code(request, response):
# print(filename, file=sys.stderr)
f.name = filename.replace(".", os.path.sep) + ".py"
# f.content = json.dumps(output, indent=2)
f.content = template.render(description=output).rstrip("\n") + "\n"
# Render and then format the output file.
f.content = black.format_str(
template.render(description=output),
mode=black.FileMode(target_versions=set([black.TargetVersion.PY37])),
)
inits = set([""])
for f in response.file:

View File

@@ -15,8 +15,8 @@ import betterproto
{% if description.services %}
import grpclib
{% endif %}
{% for i in description.imports %}
{% for i in description.imports %}
{{ i }}
{% endfor %}
@@ -81,17 +81,15 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub):
{% if method.server_streaming %}
async for response in self._unary_stream(
"{{ method.route }}",
{{ method.input }},
{{ method.output }},
request,
{{ method.output }},
):
yield response
{% else %}
return await self._unary_unary(
"{{ method.route }}",
{{ method.input }},
{{ method.output }},
request,
{{ method.output }},
)
{% endif %}

View File

@@ -33,6 +33,21 @@ def test_has_field():
assert betterproto.serialized_on_wire(foo.bar) == False
def test_class_init():
@dataclass
class Bar(betterproto.Message):
name: str = betterproto.string_field(1)
@dataclass
class Foo(betterproto.Message):
name: str = betterproto.string_field(1)
child: Bar = betterproto.message_field(2)
foo = Foo(name="foo", child=Bar(name="bar"))
assert foo.to_dict() == {"name": "foo", "child": {"name": "bar"}}
def test_enum_as_int_json():
class TestEnum(betterproto.Enum):
ZERO = 0

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="betterproto",
version="1.1.0",
version="1.2.2",
description="A better Protobuf / gRPC generator & library",
long_description=open("README.md", "r").read(),
long_description_content_type="text/markdown",
@@ -19,6 +19,6 @@ setup(
package_data={"betterproto": ["py.typed", "templates/template.py"]},
python_requires=">=3.7",
install_requires=["grpclib", "stringcase"],
extras_require={"compiler": ["jinja2", "protobuf"]},
extras_require={"compiler": ["black", "jinja2", "protobuf"]},
zip_safe=False,
)