4 Commits

Author SHA1 Message Date
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
8 changed files with 115 additions and 31 deletions

36
CHANGELOG.md Normal file
View File

@@ -0,0 +1,36 @@
# 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.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.0...HEAD
[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 = "*" jinja2 = "*"
grpclib = "*" grpclib = "*"
stringcase = "*" stringcase = "*"
black = "*"
[requires] [requires]
python_version = "3.7" python_version = "3.7"
@@ -23,3 +24,6 @@ python_version = "3.7"
plugin = "protoc --plugin=protoc-gen-custom=betterproto/plugin.py --custom_out=output" plugin = "protoc --plugin=protoc-gen-custom=betterproto/plugin.py --custom_out=output"
generate = "python betterproto/tests/generate.py" generate = "python betterproto/tests/generate.py"
test = "pytest ./betterproto/tests" test = "pytest ./betterproto/tests"
[pipenv]
allow_prereleases = true

64
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "28c38cd6c4eafb0b9ac9a64cf623145868fdee163111d3b941b34d23011db6ca" "sha256": "c7b72ed87dc3d70566c53d7ec8a636c8d4854aa30aa97a9116c0734cd5266f33"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -16,12 +16,41 @@
] ]
}, },
"default": { "default": {
"grpclib": { "appdirs": {
"hashes": [ "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", "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": { "h2": {
"hashes": [ "hashes": [
@@ -154,6 +183,13 @@
], ],
"index": "pypi", "index": "pypi",
"version": "==1.2.0" "version": "==1.2.0"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
} }
}, },
"develop": { "develop": {
@@ -180,11 +216,11 @@
}, },
"flake8": { "flake8": {
"hashes": [ "hashes": [
"sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
"sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.7.8" "version": "==3.7.9"
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
@@ -287,11 +323,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", "sha256:27abc3fef618a01bebb1f0d6d303d2816a99aa87a5968ebc32fe971be91eb1e6",
"sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0" "sha256:58cee9e09242937e136dbb3dab466116ba20d6b7828c7620f23947f37eb4dae4"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.2.1" "version": "==5.2.2"
}, },
"rope": { "rope": {
"hashes": [ "hashes": [
@@ -336,11 +372,11 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2",
"sha256:b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d",
"sha256:d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed" "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"
], ],
"version": "==3.7.4" "version": "==3.7.4.1"
}, },
"wcwidth": { "wcwidth": {
"hashes": [ "hashes": [

View File

@@ -281,7 +281,7 @@ You can do stuff like:
```py ```py
>>> t = Test().from_dict({"maybe": True, "ts": "2019-01-01T12:00:00Z", "duration": "1.200s"}) >>> t = Test().from_dict({"maybe": True, "ts": "2019-01-01T12:00:00Z", "duration": "1.200s"})
>>> t >>> 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 >>> t.ts - t.duration
datetime.datetime(2019, 1, 1, 11, 59, 58, 800000, tzinfo=datetime.timezone.utc) datetime.datetime(2019, 1, 1, 11, 59, 58, 800000, tzinfo=datetime.timezone.utc)

View File

@@ -21,6 +21,7 @@ from typing import (
TypeVar, TypeVar,
Union, Union,
get_type_hints, get_type_hints,
TYPE_CHECKING,
) )
import grpclib.client import grpclib.client
@@ -29,6 +30,9 @@ import stringcase
from .casing import safe_snake_case from .casing import safe_snake_case
if TYPE_CHECKING:
from grpclib._protocols import IProtoMessage
# Proto 3 data types # Proto 3 data types
TYPE_ENUM = "enum" TYPE_ENUM = "enum"
TYPE_BOOL = "bool" TYPE_BOOL = "bool"
@@ -420,6 +424,7 @@ class Message(ABC):
register the message fields which get used by the serializers and parsers register the message fields which get used by the serializers and parsers
to go between Python, binary and JSON protobuf message representations. to go between Python, binary and JSON protobuf message representations.
""" """
_serialized_on_wire: bool _serialized_on_wire: bool
_unknown_fields: bytes _unknown_fields: bytes
_group_map: Dict[str, dict] _group_map: Dict[str, dict]
@@ -705,7 +710,7 @@ class Message(ABC):
for field in dataclasses.fields(self): for field in dataclasses.fields(self):
meta = FieldMetadata.get(field) meta = FieldMetadata.get(field)
v = getattr(self, field.name) 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 meta.proto_type == "message":
if isinstance(v, datetime): if isinstance(v, datetime):
if v != DATETIME_ZERO: if v != DATETIME_ZERO:
@@ -741,7 +746,7 @@ class Message(ABC):
else: else:
output[cased_name] = b64encode(v).decode("utf8") output[cased_name] = b64encode(v).decode("utf8")
elif meta.proto_type == TYPE_ENUM: 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): if isinstance(v, list):
output[cased_name] = [enum_values[e].name for e in v] output[cased_name] = [enum_values[e].name for e in v]
else: else:
@@ -902,6 +907,7 @@ class _WrappedMessage(Message):
Google protobuf wrapper types base class. JSON representation is just the Google protobuf wrapper types base class. JSON representation is just the
value itself. value itself.
""" """
value: Any value: Any
def to_dict(self, casing: Casing = Casing.CAMEL) -> Any: def to_dict(self, casing: Casing = Casing.CAMEL) -> Any:
@@ -982,11 +988,11 @@ class ServiceStub(ABC):
self.channel = channel self.channel = channel
async def _unary_unary( 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: ) -> T:
"""Make a unary request and return the response.""" """Make a unary request and return the response."""
async with self.channel.request( 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: ) as stream:
await stream.send_message(request, end=True) await stream.send_message(request, end=True)
response = await stream.recv_message() response = await stream.recv_message()
@@ -994,11 +1000,11 @@ class ServiceStub(ABC):
return response return response
async def _unary_stream( 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]: ) -> AsyncGenerator[T, None]:
"""Make a unary request and return the stream response iterator.""" """Make a unary request and return the stream response iterator."""
async with self.channel.request( 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: ) as stream:
await stream.send_message(request, end=True) await stream.send_message(request, end=True)
async for message in stream: async for message in stream:

View File

@@ -9,13 +9,14 @@ import textwrap
from typing import Any, List, Tuple from typing import Any, List, Tuple
try: try:
import jinja2 import black
except ImportError: except ImportError:
print( 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) raise SystemExit(1)
import jinja2
import stringcase import stringcase
from google.protobuf.compiler import plugin_pb2 as plugin from google.protobuf.compiler import plugin_pb2 as plugin
@@ -398,8 +399,11 @@ def generate_code(request, response):
# print(filename, file=sys.stderr) # print(filename, file=sys.stderr)
f.name = filename.replace(".", os.path.sep) + ".py" f.name = filename.replace(".", os.path.sep) + ".py"
# f.content = json.dumps(output, indent=2) # Render and then format the output file.
f.content = template.render(description=output).rstrip("\n") + "\n" f.content = black.format_str(
template.render(description=output),
mode=black.FileMode(target_versions=set([black.TargetVersion.PY37])),
)
inits = set([""]) inits = set([""])
for f in response.file: for f in response.file:

View File

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

View File

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