更好的Protobuf / gRPC生成器和库
项目描述
Avast分支
这是我们为了加快发布速度而使用的Avast分支,以便我们可以在项目中使用它。
我们的包是从分支 avast/release
发布的。此分支包含我们发布所需的更改。通常,它将基于 upstream
远程(原始仓库)的 master
分支。如果我们需要发布一些自定义更改,那些更改将在另一个分支中完成,并将 avast/release
重基到它上。这需要大量的强制推送 - 如果你想与 avast/release
一起工作,那么你需要自己处理。
如何与这个仓库一起工作
此分支是按照 本指南 设置的。
开始工作之前
# This will synchronize upstream/master (original repo) and origin/master (our fork)
git fetch --all --prune
git checkout master
git merge upstream/master --ff-only
git push origin master
如果你想向上游提出更改
git checkout -b fix/small-bug
# Commit some work
git push -u origin fix/small-bug
# Create a Pull Request
如果你想发布包含更改的我们的包
git checkout avast/release
git rebase fix/small-bug
# Raise version in pyproject.toml
git push -f # This will run testing Github actions
git tag -a v0.1.0 # Add any comments into the annotation
git push --tags # This will trigger the release to PyPI
更好的Python Protobuf / gRPC支持
:octocat: 如果你在这份GitHub阅读,请注意这可能会提及未发布的特性!请查看PyPI上的最新发布的README。
此项目旨在通过利用现代语言特性来提供在现代化Python环境中使用Protobuf / gRPC时的改进体验,生成可读、可理解、符合Python语法的代码。它将不支持旧版特性或环境(例如,Protobuf 2)。以下特性受到支持
- Protobuf 3 与 gRPC 代码生成
- 内置二进制和 JSON 序列化
- 支持 Python 3.6+
- 枚举类型
- 数据类
async
/await
- 支持时区的
datetime
和timedelta
对象 - 相对导入
- Mypy 类型检查
本项目深受以下项目启发,并借鉴了其功能:
- https://github.com/protocolbuffers/protobuf/tree/master/python
- https://github.com/eigenein/protobuf/
- https://github.com/vmagamedov/grpclib
动机
本项目存在是因为我对官方 Google protoc 插件的 Python 版本状态不满意。
- 没有
async
支持(需要额外的grpclib
插件) - 没有类型支持或代码补全/智能提示(需要额外的
mypy
插件) - 不会生成
__init__.py
模块文件 - 输出不可导入
- 除非你修改
sys.path
,否则 Python 3 中导入路径会中断
- 除非你修改
- 名称冲突时的错误(例如
codecs
包) - 生成的代码不符合惯例
- 运行时代码生成完全不可读
- 许多代码看起来像是将 C++ 或 Java 1:1 转换到 Python
- 使用大写函数名称,如
HasField()
和SerializeToString()
- 使用
SerializeToString()
而不是内置的__bytes__()
- 特殊包装类型不使用 Python 的
None
- 时间戳/持续时间类型不使用 Python 的内置
datetime
模块。本项目从头开始重新实现,专注于符合现代 Python 惯例,以帮助解决上述问题。虽然由于方法名称和调用模式的变化,它可能不是完全的 1:1 替换,但网络格式是相同的。
安装
首先,安装包。请注意,[compiler]
功能标志告诉它只安装 protoc
插件所需的额外依赖项
# Install both the library and compiler
pip install "betterproto[compiler]"
# Install just the library (to use the generated code output)
pip install betterproto
Betterproto 正在积极开发。要安装最新测试版,请使用 pip install --pre betterproto
。
入门
编译 proto 文件
现在,假设你已经安装了编译器并且有一个 proto 文件,例如 example.proto
syntax = "proto3";
package hello;
// Greeting represents a message you can tell a user.
message Greeting {
string message = 1;
}
你可以运行以下命令直接调用 protoc
mkdir lib
protoc -I . --python_betterproto_out=lib example.proto
或者运行以下命令通过 grpcio-tools 调用 protoc
pip install grpcio-tools
python -m grpc_tools.protoc -I . --python_betterproto_out=lib example.proto
这将生成 lib/hello/__init__.py
,看起来像这样
# Generated by the protocol buffer compiler. DO NOT EDIT!
# sources: example.proto
# plugin: python-betterproto
from dataclasses import dataclass
import betterproto
@dataclass
class Greeting(betterproto.Message):
"""Greeting represents a message you can tell a user."""
message: str = betterproto.string_field(1)
现在你可以使用它了!
>>> from lib.hello import Greeting
>>> test = Greeting()
>>> test
Greeting(message='')
>>> test.message = "Hey!"
>>> test
Greeting(message="Hey!")
>>> serialized = bytes(test)
>>> serialized
b'\n\x04Hey!'
>>> another = Greeting().parse(serialized)
>>> another
Greeting(message="Hey!")
>>> another.to_dict()
{"message": "Hey!"}
>>> another.to_json(indent=2)
'{\n "message": "Hey!"\n}'
异步 gRPC 支持
生成的 Protobuf Message
类与 grpclib 兼容,因此如果你喜欢,你可以自由使用它。话虽如此,本项目还包括异步 gRPC 代理生成支持,具有更好的静态类型检查和代码补全支持。默认启用。
给定一个示例服务定义
syntax = "proto3";
package echo;
message EchoRequest {
string value = 1;
// Number of extra times to echo
uint32 extra_times = 2;
}
message EchoResponse {
repeated string values = 1;
}
message EchoStreamResponse {
string value = 1;
}
service Echo {
rpc Echo(EchoRequest) returns (EchoResponse);
rpc EchoStream(EchoRequest) returns (stream EchoStreamResponse);
}
生成 echo proto 文件
python -m grpc_tools.protoc -I . --python_betterproto_out=. echo.proto
客户端可以如下实现
import asyncio
import echo
from grpclib.client import Channel
async def main():
channel = Channel(host="127.0.0.1", port=50051)
service = echo.EchoStub(channel)
response = await service.echo(value="hello", extra_times=1)
print(response)
async for response in service.echo_stream(value="hello", extra_times=1):
print(response)
# don't forget to close the channel when done!
channel.close()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
这将输出
EchoResponse(values=['hello', 'hello'])
EchoStreamResponse(value='hello')
EchoStreamResponse(value='hello')
本项目还生成用于实现 Python gRPC 服务器的一端代理。要使用它们,只需在生成的文件中扩展基本类并覆盖服务方法即可
import asyncio
from echo import EchoBase, EchoResponse, EchoStreamResponse
from grpclib.server import Server
from typing import AsyncIterator
class EchoService(EchoBase):
async def echo(self, value: str, extra_times: int) -> "EchoResponse":
return EchoResponse([value for _ in range(extra_times)])
async def echo_stream(self, value: str, extra_times: int) -> AsyncIterator["EchoStreamResponse"]:
for _ in range(extra_times):
yield EchoStreamResponse(value)
async def main():
server = Server([EchoService()])
await server.start("127.0.0.1", 50051)
await server.wait_closed()
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
JSON
支持使用以下方法将消息序列化和反序列化为 JSON 和 Python 字典
- 字典:
Message().to_dict()
,Message().from_dict(...)
- JSON:
Message().to_json()
,Message().from_json(...)
为了兼容性,默认将字段名称转换为 camelCase
。你可以通过传递一个 casing 值来控制此行为,例如
MyMessage().to_dict(casing=betterproto.Casing.SNAKE)
确定消息是否已发送
有时确定消息是否已通过网络发送很有用。这是 Google 包装类型如何让你知道值是否未设置、默认(零值)或设置为其他值的示例。
使用 betterproto.serialized_on_wire(message)
来确定是否已发送。这比官方Google生成的Python代码略有不同,并且它位于生成的 Message
类外部,以防止名称冲突。请注意,它 仅 支持 Proto 3,因此只能用于检查 Message
字段是否已设置。您无法检查标量是否已通过线路发送。
# Old way (official Google Protobuf package)
>>> mymessage.HasField('myfield')
# New way (this project)
>>> betterproto.serialized_on_wire(mymessage.myfield)
单选支持
Protobuf 支持在 oneof
子句中对字段进行分组。在给定时间内,组中只能设置一个字段。例如,给定以下 proto
syntax = "proto3";
message Test {
oneof foo {
bool on = 1;
int32 count = 2;
string name = 3;
}
}
您可以使用 betterproto.which_one_of(message, group_name)
来确定哪个字段已设置。它返回一个包含字段名和值的元组,如果未设置,则返回一个空字符串和 None
。
>>> test = Test()
>>> betterproto.which_one_of(test, "foo")
["", None]
>>> test.on = True
>>> betterproto.which_one_of(test, "foo")
["on", True]
# Setting one member of the group resets the others.
>>> test.count = 57
>>> betterproto.which_one_of(test, "foo")
["count", 57]
>>> test.on
False
# Default (zero) values also work.
>>> test.name = ""
>>> betterproto.which_one_of(test, "foo")
["name", ""]
>>> test.count
0
>>> test.on
False
这又与官方Google代码生成器略有不同
# Old way (official Google protobuf package)
>>> message.WhichOneof("group")
"foo"
# New way (this project)
>>> betterproto.which_one_of(message, "group")
["foo", "foo's value"]
知名Google类型
Google 提供了几个知名的消息类型,如时间戳、持续时间和几个包装器,用于提供可选零值支持。每个类型都有特殊的 JSON 表示,并且与正常消息的处理方式略有不同。Python 映射如下所示
Google 消息 | Python 类型 | 默认值 |
---|---|---|
google.protobuf.duration |
datetime.timedelta |
0 |
google.protobuf.timestamp |
带时区的 datetime.datetime |
1970-01-01T00:00:00Z |
google.protobuf.*Value |
Optional[...] |
None |
google.protobuf.* |
betterproto.lib.google.protobuf.* |
None |
对于包装类型,Python 类型对应于包装类型,例如 google.protobuf.BoolValue
变为 Optional[bool]
,而 google.protobuf.Int32Value
变为 Optional[int]
。所有可选值默认为 None
,因此请记住检查该可能的状态。给定
syntax = "proto3";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
message Test {
google.protobuf.BoolValue maybe = 1;
google.protobuf.Timestamp ts = 2;
google.protobuf.Duration duration = 3;
}
您可以执行类似以下操作
>>> t = Test().from_dict({"maybe": True, "ts": "2019-01-01T12:00:00Z", "duration": "1.200s"})
>>> t
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)
>>> t.ts.isoformat()
'2019-01-01T12:00:00+00:00'
>>> t.maybe = None
>>> t.to_dict()
{'ts': '2019-01-01T12:00:00Z', 'duration': '1.200s'}
开发
要求
-
Python (3.6 或更高版本)
-
poetry 需要用于在虚拟环境中安装依赖项
-
poethepoet 用于执行 pyproject.toml 中定义的开发任务
- 可以通过
pip install poethepoet
安装到您的宿主环境,然后作为简单的poe
执行 - 或者从 poetry venv 中运行
poetry run poe
- 可以通过
设置
# Get set up with the virtual env & dependencies
poetry run pip install --upgrade pip
poetry install
# Activate the poetry environment
poetry shell
代码风格
该项目强制执行 black Python 代码格式化。
在提交更改之前运行
poe format
为了避免以后的合并冲突,非 black 格式的 Python 代码将在 CI 中失败。
测试
有两种类型的测试
- 标准测试
- 自定义测试
标准测试
添加标准测试用例很简单。
- 创建一个新的目录
betterproto/tests/inputs/<name>
- 添加
<name>.proto
,其中包含名为Test
的消息 - 添加
<name>.json
,其中包含一些测试数据(可选)
- 添加
当您运行测试时,它将自动被选中。
- 另请参阅: 标准测试开发指南
自定义测试
自定义测试位于 tests/test_*.py
中,并使用 pytest 运行。
运行
以下是运行测试的方法。
# Generate assets from sample .proto files required by the tests
poe generate
# Run the tests
poe test
要像 CI 中运行测试那样运行测试(使用 tox),请运行
poe full-test
重新编译 Google 知名类型
Betterproto 在 betterproto/lib/google 中包含了 Google 知名类型的编译版本。在修改插件输出格式时,请务必重新生成这些文件,并通过运行测试进行验证。
通常,插件不会编译任何对 google.protobuf
的引用,因为它们是预编译的。要强制编译 google.protobuf
,请使用选项 --custom_opt=INCLUDE_GOOGLE
。
假设您的 google.protobuf
源文件(包含在所有版本的 protoc
中)位于 /usr/local/include
,您可以按照以下方式重新生成它们:
protoc \
--plugin=protoc-gen-custom=src/betterproto/plugin/main.py \
--custom_opt=INCLUDE_GOOGLE \
--custom_out=src/betterproto/lib \
-I /usr/local/include/ \
/usr/local/include/google/protobuf/*.proto
待办事项
- 固定长度字段
- 打包固定长度
- Zig-zag 有符号字段(sint32,sint64)
- 不要为嵌套类型编码零值
- 枚举类型
- 重复消息字段
- 映射
- 消息字段的映射
- 支持未知字段的透传
- 嵌套类型的引用
- proto文件中的导入
- 知名Google类型
- 支持作为请求输入
- 支持作为响应输出
- 自动包装/解包响应
- OneOf支持
- 有线基本支持
- 检查哪个组已设置
- 设置一个将取消设置其他所有设置
- 并非完全天真的JSON。
- 64位整数为字符串
- 映射
- 列表
- 字节为base64
- Any支持
- 枚举字符串
- 知名类型支持(时间戳、持续时间、包装器)
- 支持不同的命名方式(原始名称与驼峰命名等)
- 异步服务存根
- 一元-一元
- 服务器流式响应
- 客户端流式请求
- 重命名消息和字段以符合Python命名标准
- 重命名与语言关键字冲突
- Python包
- 自动运行测试
- 清理!
社区
加入我们吧!Slack!
许可协议
版权©2019 Daniel G. Taylor
项目详情
下载文件
下载适用于您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。
源分发
构建分发
avast.betterproto-0.3.1.tar.gz的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 33d5dbddb191807ed9a86d2b7854ae830139afe4e7eed83aa169db173e56e876 |
|
MD5 | 4a42194a7c44cf1241cbc20312e95f74 |
|
BLAKE2b-256 | d5489703113df4b0f4bcb00e0ad63b2a64b161165a8557c226092333342d05f2 |
avast.betterproto-0.3.1-py3-none-any.whl的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | fd54fede9c761dc5acba8541bade30ccd7488517482062f1cf2e38e5b262b083 |
|
MD5 | b120e784e582e4f1cc3ebc01d96ac0a8 |
|
BLAKE2b-256 | f3271df73a3138beea0076d92777c8d79a8bdfd46d6ef5726b62daad7b64cbdb |