protoc-gen-python-betterproto
v1.0.0
Published
Zero-dependency proto3 parser and betterproto-style Python dataclass code generator — messages, enums, service stubs, async gRPC client methods
Maintainers
Readme
protoc-gen-python-betterproto
Zero-dependency proto3 parser and betterproto-style Python dataclass code generator.
Parse .proto files into an AST, then generate idiomatic Python with @dataclass messages, betterproto.Enum enums, and fully-typed async service stubs — all from Node.js, no native dependencies, no protoc binary required.
npm install protoc-gen-python-betterprotoQuick start
import { parseProto, generatePython } from 'protoc-gen-python-betterproto';
const source = `
syntax = "proto3";
package acme.v1;
enum UserRole {
USER_ROLE_UNKNOWN = 0;
USER_ROLE_ADMIN = 1;
USER_ROLE_VIEWER = 2;
}
message User {
string id = 1;
string name = 2;
int32 age = 3;
UserRole role = 4;
repeated string tags = 5;
}
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
rpc CreateUser (stream UserChunk) returns (User);
}
`;
const ast = parseProto(source);
console.log(generatePython(ast));Output:
# Generated by protoc-gen-python-betterproto. DO NOT EDIT.
from __future__ import annotations
import dataclasses
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional
import betterproto
import grpclib.client
class UserRole(betterproto.Enum):
USER_ROLE_UNKNOWN = 0
USER_ROLE_ADMIN = 1
USER_ROLE_VIEWER = 2
@dataclasses.dataclass
class User(betterproto.Message):
id: str = betterproto.string_field(1)
name: str = betterproto.string_field(2)
age: int = betterproto.int32_field(3)
role: Optional["UserRole"] = betterproto.enum_field(4)
tags: List[str] = betterproto.list_field(5, wraps=betterproto.TYPE_STRING)
class UserServiceStub(betterproto.ServiceStub):
async def get_user(
self,
*,
request: "GetUserRequest",
) -> "User":
return await self._unary_unary(
"/acme.v1.UserService/GetUser", request, User
)
async def list_users(
self,
*,
request: "ListUsersRequest",
) -> AsyncIterator["User"]:
async for response in self._unary_stream(
"/acme.v1.UserService/ListUsers", request, User
):
yield response
async def create_user(
self,
request_iterator: AsyncIterator["UserChunk"],
) -> "User":
return await self._stream_unary(
"/acme.v1.UserService/CreateUser", request_iterator, User
)generatePython(ast, opts?)
import { generatePython } from 'protoc-gen-python-betterproto';
const py = generatePython(ast, {
useOptional: true, // wrap message-type fields in Optional (default true)
generateStubs: true, // emit ServiceStub classes (default true)
banner: '# Generated by protoc-gen-python-betterproto. DO NOT EDIT.',
});Streaming patterns
| Proto RPC | Python stub method |
|---|---|
| rpc M (A) returns (B) | async def m(self, *, request: A) -> B |
| rpc M (A) returns (stream B) | async def m(self, *, request: A) -> AsyncIterator[B] |
| rpc M (stream A) returns (B) | async def m(self, request_iterator: AsyncIterator[A]) -> B |
| rpc M (stream A) returns (stream B) | async def m(self, request_iterator: AsyncIterator[A]) -> AsyncIterator[B] |
parseProto(source)
Parse a proto3 source string into a typed AST:
import { parseProto } from 'protoc-gen-python-betterproto';
const ast = parseProto(source);
// {
// syntax: 'proto3',
// package: 'acme.v1',
// imports: [...],
// messages: [...],
// enums: [...],
// services: [...],
// }Supports: message, enum, service/rpc, oneof, map<K,V>, repeated, nested types, options, reserved, line/block comments.
Type mapping
Proto scalars → Python
| Proto | Python |
|---|---|
| string | str |
| bool | bool |
| int32, uint32, int64, uint64, sint32… | int |
| float, double | float |
| bytes | bytes |
Well-known types
| Proto | Python |
|---|---|
| google.protobuf.Timestamp | datetime |
| google.protobuf.Duration | timedelta |
| google.protobuf.Struct | Dict[str, Any] |
| google.protobuf.Any | Any |
| google.protobuf.Empty | "betterproto.Empty" |
| google.protobuf.StringValue | Optional[str] |
| google.protobuf.BoolValue | Optional[bool] |
import { protoTypeToPy, protoScalarToPy } from 'protoc-gen-python-betterproto';
protoTypeToPy('string') // 'str'
protoTypeToPy('int64') // 'int'
protoTypeToPy('map<string,int32>') // 'Dict[str, int]'
protoTypeToPy('google.protobuf.Timestamp') // 'datetime'
protoTypeToPy('MyMessage') // '"MyMessage"'
protoTypeToPy('MyMessage', { optional: true }) // 'Optional["MyMessage"]'
protoScalarToPy('float') // 'float'
protoScalarToPy('MyMsg') // nullbetterproto field descriptors
import { betterprotoField } from 'protoc-gen-python-betterproto';
betterprotoField({ type: 'string', label: '', number: 1 })
// 'betterproto.string_field(1)'
betterprotoField({ type: 'MyMessage', label: '', number: 2 })
// 'betterproto.message_field(2)'
betterprotoField({ type: 'string', label: 'repeated', number: 3 })
// 'betterproto.list_field(3, wraps=betterproto.TYPE_STRING)'
betterprotoField({ type: 'map<string,int32>', label: 'map', number: 4 })
// 'betterproto.map_field(4, betterproto.TYPE_STRING, betterproto.TYPE_INT32)'Name helpers
import { toSnakeCase, toPascalCase, toPythonIdent } from 'protoc-gen-python-betterproto';
toSnakeCase('GetUserById') // 'get_user_by_id'
toPascalCase('get_user_by_id') // 'GetUserById'
toPythonIdent('from') // 'from_' (reserved word mangling)
toPythonIdent('user_name') // 'user_name'AST traversal
import { collectMessages, collectEnums, collectPythonImports } from 'protoc-gen-python-betterproto';
// Flat list including nested types, each with fullName
const messages = collectMessages(ast);
// [{ name: 'User', fullName: 'User', ... }, { name: 'Address', fullName: 'User.Address', ... }]
const enums = collectEnums(ast);
// Determine required Python imports
const imports = collectPythonImports(ast);
// { stdlib: ['from datetime import datetime'], typing: ['Optional', 'List', 'Dict'], extras: ['import grpclib.client'] }buildProto(descriptor)
Generate .proto source from a plain JavaScript object:
import { buildProto } from 'protoc-gen-python-betterproto';
const proto = buildProto({
syntax: 'proto3',
pkg: 'acme.v1',
messages: [
{ name: 'User', fields: [
{ type: 'string', name: 'id', number: 1 },
{ type: 'string', name: 'name', number: 2 },
]},
],
services: [
{ name: 'UserService', rpcs: [
{ name: 'GetUser', inputType: 'GetUserRequest', outputType: 'User' },
]},
],
});CommonJS
const { parseProto, generatePython, protoTypeToPy } = require('protoc-gen-python-betterproto');License
MIT
