跳转到主要内容

更好的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
    • 支持时区的 datetimetimedelta 对象
    • 相对导入
    • Mypy 类型检查

本项目深受以下项目启发,并借鉴了其功能:

动机

本项目存在是因为我对官方 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'}

开发

  • 加入我们吧!Slack
  • 了解如何帮助 → 贡献

要求

  • 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 中失败。

测试

有两种类型的测试

  1. 标准测试
  2. 自定义测试

标准测试

添加标准测试用例很简单。

  • 创建一个新的目录 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

http://dgt.mit-license.org/

项目详情


下载文件

下载适用于您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源分发

avast.betterproto-0.3.1.tar.gz (63.7 kB 查看哈希值)

上传时间

构建分发

avast.betterproto-0.3.1-py3-none-any.whl (61.7 kB 查看哈希值)

上传时间 Python 3

由以下组织支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面