Git Product home page Git Product logo

protobuf_to_pydantic's Introduction

protobuf_to_pydantic

Generate Pydantic Model or source code with parameter verification function based on Protobuf file (Proto3).

中文文档

Feature

Feature:

  • Generate source code through Protobuf plugin。
  • Generate Pydantic Model or source code by parsing Protobuf Message in Python runtime.
  • Compatible with V1 and V2 versions of Pydantic
  • Supports multiple verification rules and is compatible with proto-gen-validate (subsequent versions will support the rules of proto-gen-validate 1.0)。
  • Support custom functionality through templates。
  • Supports protovalidate verification rules(proto-gen-validate version >= 1.0)

The following is a functional overview diagram of protobuf-to-pydantic. In the picture P2P represents protobuf-to-pydantic, Protoc represents the command for Protobuf to generate code, and plugin represents Plugin for Protoc: protobuf-to-pydantic

Installation

By default, protobuf-to-pydantic can be installed directly via the following command:

pip install protobuf_to_pydantic

If want to use the full functionality of protobuf-to-pydantic, can install protobuf-to-pydantic with the following command:.

pip install protobuf_to_pydantic[all]

Usage

1.code generation

protobuf-to-pydantic currently has two methods to generate Pydantic Model objects based on Protobuf files.:

  • 1: Use the Protoc plug-in to generate the corresponding Python code file through the Protobuf file。
  • 2: Generate the corresponding Pydantic Model object through the Message object in Python runtime。

1.1.Directly generate Pydantic Model code files through plug-ins

1.1.0.Install dependencies

The protobuf-to-pydantic plug-in depends on mypy-protobuf, need to install mypy-protobuf through the following command first:

python -m pip install protobuf-to-pydantic[mypy-protobuf]

or

poetry add protobuf-to-pydantic -E mypy-protobuf

1.1.1.Use plugins

Plug-in is the Pydantic Model source code generation method recommended by protobuf-to-pydantic. It supports the most complete functions and is also very simple to use.

Assume that it is usually generated through the following command Code corresponding to Protobuf file:

python -m grpc_tools.protoc -I. example.proto

After installing protobuf-to-pydantic,can use the protobuf-to-pydantic plugin with the --protobuf-to-pydantic_out option with the following command:

python -m grpc_tools.protoc -I. --protobuf-to-pydantic_out=. example.proto

In this command, --protobuf-to-pydantic_out=. means using the prorobuf-to-pydantic plug-in, And it is declared that the output location of the protobuf-to-pydantic plug-in is .

. indicates the output path used by grpc_tools.proto.

After running the command, the protobuf-to-pydantic plugin writes the generated source code to a file with the filename suffix p2p.py, e.g., example.proto generates a file with the name example_p2p.py.

1.1.2.Plug-in configuration

The protobuf-to-pydantic plugin supports loading configuration by reading a Python file。

In order to ensure that the variables of the configuration file can be introduced normally, the configuration file must be stored in the current path of the running command.。

An example configuration that can be read by protobuf-to-pydantic is as follows:

import logging
from typing import List, Type

from google.protobuf.any_pb2 import Any  # type: ignore
from pydantic import confloat, conint
from pydantic.fields import FieldInfo

from protobuf_to_pydantic.desc_template import DescTemplate

# Configure the log output format and log level of the plugin, which is very useful when debugging
logging.basicConfig(format="[%(asctime)s %(levelname)s] %(message)s", datefmt="%y-%m-%d %H:%M:%S", level=logging.DEBUG)


class CustomerField(FieldInfo):
    pass


def customer_any() -> Any:
    return Any  # type: ignore


# For the configuration of the local template, see the use of the local template for details
local_dict = {
    "CustomerField": CustomerField,
    "confloat": confloat,
    "conint": conint,
    "customer_any": customer_any,
}
# Specifies the start of key comments
comment_prefix = "p2p"
# Specify the class of the template, can extend the template by inheriting this class, see the chapter on custom templates for details
desc_template: Type[DescTemplate] = DescTemplate
# Specify the protobuf files of which packages to ignore, and the messages of the ignored packages will not be parsed
ignore_pkg_list: List[str] = ["validate", "p2p_validate"]
# Specifies the generated file name suffix (without .py)
file_name_suffix = "_p2p"

Next, in order to be able to read this file, need to change the --protobuf-to-pydantic_out=. to --protobuf-to-pydantic_out=config_path=plugin_config.py:.. where the left side of : indicates that the configuration file path to be read is plugin_config.py, and the right side of : declares that the output location of the protobuf-to-pydantic plugin is . The final complete command is as follows:

python -m grpc_tools.protoc -I. --protobuf-to-pydantic_out=config_path=plugin_config.py:. example.proto

Through this command, can load the corresponding configuration and run the protobuf-to-pydantic plug-in。

In addition to the configuration options in the example configuration file, the protobuf-to-pydantic plug-in also supports other configuration options. The specific configuration instructions are as follows:

Configuration name Functional module Type Hidden meaning
local_dict Template dict Holds variables for the local template
desc_template Template protobuf_to_pydantic.desc_template.DescTemplate Implementation of the template class
comment_prefix Template str Comment prefix.Only strings with a fixed prefix will be used by the template
customer_import_set Code generation Set[str] A collection of custom import statements, such as from typing import Setor import typing, that will write data in order to the source code file
customer_deque Code generation deque[str] Custom source file content, used to add custom content
module_path str str Used to define the root path of the project or module, which helps protobuf-to-pydanticto better automatically generate module import statements
pyproject_file_path Code generation str Define the pyproject file path, which defaults to the current project path
code_indent Code generation int Defines the number of indentation Spaces in the code; the default is 4
ignore_pkg_list Code generation(Limit plug-ins only) list[str] Definition ignores parsing of the specified package file
base_model_class Model Code generation, Code generation Type[BaseModel] Define the parent class of the generated Model
file_name_suffix Code generation str Define the generated file suffix, default _p2p.py
file_descriptor_proto_to_code Code generation(Limit plug-ins only) Type[FileDescriptorProtoToCode] Define the FileDescriptorProtoToCode to use

1.1.3.buf-cli

If you are using buf-cli to manage Protobuf files, then you can also use protobuf-to-pydantic in buf-cli, See How to use protobuf-to-pydantic in buf-cli

1.2.Generate a Pydantic Model object in Python runtime

protobuf_to_pydantic can generate the corresponding PydanticModel object based on the Message object at runtime。

For example, the UserMessage in the following Protobuf file named demo.proto:

// path: ./demo.proto
syntax = "proto3";
package user;

enum SexType {
  man = 0;
  women = 1;
}

message UserMessage {
  string uid=1;
  int32 age=2;
  float height=3;
  SexType sex=4;
  bool is_adult=5;
  string user_name=6;
}

grpc_tools.protoc can be used to generate the Python code file corresponding to the Protobuf file (the file name is demo_pb2.py), and the code related to the UserMessage is stored in the code file.

At Python runtime, The func msg_to_pydantic_model can be called to read the UserMessage object from the demo_pb2 module and generate the corresponding Pydantic Model object as follows:

from typing import Type
from protobuf_to_pydantic import msg_to_pydantic_model
from pydantic import BaseModel

# import protobuf gen python obj
from . import demo_pb2

UserModel: Type[BaseModel] = msg_to_pydantic_model(demo_pb2.UserMessage)
print(
    {
        k: v.field_info
        for k, v in UserModel.__fields__.items()
    }
)

# output
# {
#   `uid`: FieldInfo(default=``, extra={}),
#   `age`: FieldInfo(default=0, extra={}),
#   `height`: FieldInfo(default=0.0, extra={}),
#   `sex`: FieldInfo(default=0, extra={}),
#   `is_adult`: FieldInfo(default=False, extra={}),
#   `user_name`: FieldInfo(default=``, extra={})
#  }

Through the output results, it can be found that the generated pydantic.BaseModel object also contains uid, age, height, sex, is adult and user name fields, and the default property matches the zero value of the Protobuf type。

The msg_to_pydantic_model func is customizable just like plugins, with the following extension parameters:

Fields Meaning
default_field Generate a Field for each field in the Pydantic Model
comment_prefix The prefix of a comment that can be parsed
parse_msg_desc_method Parsing rules to use
local_dict Variables used by the local template
pydantic_base Generates the parent class of the Pydantic Model object
pydantic_module Generate the Module of the Pydantic Model object
desc_template Template class to use
message_type_dict_by_type_name Protobuf type mapping to Python type
message_default_factory_dict_by_type_name Protobuf type mapping to the Python type factory

In addition to generating the corresponding Pydantic Model object at runtime, protobuf-to-pydantic also supports converting Pydantic Model objects to Python code text at runtime (only compatible with Pydantic Model objects generated by protobuf-to-pydantic). The pydantic_model_to_py_code func is used to generate the source code, and the pydantic_model_to_py_file func is used to generate the code file. The example code of the pydantic_model_to_py_file func is as follows:

from protobuf_to_pydantic import msg_to_pydantic_model, pydantic_model_to_py_file

# import protobuf gen python obj
from example.example_proto_python_code.example_proto.demo import demo_pb2

pydantic_model_to_py_file(
    "./demo_gen_code.py",
    msg_to_pydantic_model(demo_pb2.NestedMessage),
)

When the code runs, it convertsdemo_pb2.NestedMessageto a Pydantic Model object and passes it to the pydantic_model_to_py_file. pydantic_model_to_py_file generates the source code and writes it to a demo_gen_code.py file.

2.Parameter validation

In the previous section, the Pydantic Model object generated by the Protobuf file is very simple because the Protobuf file does not have enough parameters to verify the relevant information. In order for each field in the generated Pydantic Model object to have parameter validation capabilities, the corresponding parameter checking rules for the field need to be refined in the Protobuf file.

Currently, protobuf-to-pydantic supports three validation rules:

  • 1.Text annotations
  • 2.PGV(protoc-geb-validate)
  • 3.P2P

With these rules, the Pydantic Model object generated by protobuf-to-pydantic will have parameter validation feature. Among them, text annotations and P2P rules are consistent, they both support most of the parameters in Pydantic Field, some of the variations and new parameters are seen 2.4.P2P and text annotation rule other parameter support

NOTE:

  • 1.Text annotation rules are not the focus of subsequent functional iterative development, and it is recommended to use P2P verification rules.
  • 2.Protoc Plug-in only support PGV and P2P rule.

2.1.Text annotations

In the Protobuf file, can write annotations for each field that meet the requirements of protobuf-to-pydantic, so that protobuf-to-pydantic can obtain the validation information of the parameters when parsing the Protobuf file, such as the following example

syntax = "proto3";
package user;

enum SexType {
  man = 0;
  women = 1;
}

// user info
message UserMessage {
  // p2p: {"required": true, "example": "10086"}
  // p2p: {"title": "UID"}
  string uid=1; // p2p: {"description": "user union id"}
  // p2p: {"example": 18, "title": "use age", "ge": 0}
  int32 age=2;
  // p2p: {"ge": 0, "le": 2.5}
  float height=3;
  SexType sex=4;
  bool is_adult=5;
  // p2p: {"description": "user name"}
  // p2p: {"default": "", "min_length": 1, "max_length": "10", "example": "so1n"}
  string user_name=6;
}

In this example, each annotation that can be used by protobuf_to_pydantic starts with p2p: (supports customization) and is followed by a complete Json string. If are familiar with the usage of pydantic, can find This Json string contains the verification information corresponding to pydantic.Field. For example, the uid field in UserMessage contains a total of 4 pieces of information as follows:

Column Meaning
required Indicates that the generated field does not have a default value
example An example value representing the generated field is 10086
title Indicates that the schema name of the field is UID
description The schema documentation for the representation field is described as user_union_id

Note:

  • 1.Currently only single-line comments are supported and comments must be a complete Json data (no line breaks).
  • 2.multi line comments are not supported。

When these annotations are written, protobuf_to_pydantic will bring the corresponding information for each field when converting the Message into the corresponding Pydantic.BaseModel object, as follows:

# pydantic version V1
from typing import Type
from protobuf_to_pydantic import msg_to_pydantic_model
from pydantic import BaseModel

# import protobuf gen python obj
from example.example_proto_python_code.example_proto.demo import demo_pb2

UserModel: Type[BaseModel] = msg_to_pydantic_model(demo_pb2.UserMessage, parse_msg_desc_method=demo_pb2)
print(
    {
        k: v.field_info
        for k, v in UserModel.__fields__.items()
    }
)
# output
# {
#   `uid`: FieldInfo(default=PydanticUndefined, title=`UID`, description=`user union id`, extra={`example`: `10086`}),
#   `age`: FieldInfo(default=0, title=`use age`, ge=0, extra={`example`: 18}),
#   `height`: FieldInfo(default=0.0, ge=0, le=2, extra={}),
#   `sex`: FieldInfo(default=0, extra={}),
#   `is_adult`: FieldInfo(default=False, extra={}),
#   `user_name`: FieldInfo(default=``, description=`user name`, min_length=1, max_length=10, extra={`example`: `so1n`})
# }

It can be seen that the output fields carry the corresponding information, which is consistent with the comments of the Protobuf file.

In addition, this code differs from the previous section in that the msg_to_pydantic_model function has a keyword argument named parse_msg_desc_method and its value is the 'demo_pb2' module. This parameter enables protobuf-to-pydantic to obtain additional information about each field in the Message object through comments in the .pyi file of the demo_pb2 module.

Note: This function needs to use the mypy-protobuf plugin when generating the corresponding Python code through the Protobuf file, and the specified pyi file output path is the same as the generated Pythoncode path to take effect. And need to installprotobuf-to-pydanticvia thepython -m pip install protobuf-to-pydantic[mypy-protobuf]` command

In addition to getting comments from the .pyi file, protobuf-to-pydantic also supports getting comment information for each field through comments on the Protobuf file to which the Message object belongs. Using this feature is as simple as setting the value of parse_msg_desc_method to the root directory path specified when the Message object was generated.

When using this method, make sure to install protobuf-to-pydantic via python -m pip install protobuf-to-pydantic[lark], and also make sure that the Protobuf file exists in the project.

For example, the project structure of the protobuf-to-pydantic sample code is as follows:

./protobuf_to_pydantic/
├── example/
│ ├── python_example_proto_code/
│ └── example_proto/
├── protobuf_to_pydantic/
└── /

The Protobuf file is stored in the example/example_proto folder, and then run the following command in the example directory to generate the Python code file corresponding to Protobuf:

cd example

python -m grpc_tools.protoc
  --python_out=./python_example_proto_code \
  --grpc_python_out=./python_example_proto_code \
  -I. \

Then the path that needs to be filled in for parse_msg_desc_method at this time is ./protobuf_to_pydantic/example. The following sample code:

# pydantic Version v1
from typing import Type
from protobuf_to_pydantic import msg_to_pydantic_model
from pydantic import BaseModel

# import protobuf gen python obj
from example.proto_3_20_pydanticv1.example.example_proto.demo import demo_pb2

UserModel: Type[BaseModel] = msg_to_pydantic_model(
    demo_pb2.UserMessage, parse_msg_desc_method="./protobuf_to_pydantic/example"
)
print(
    {
        k: v.field_info
        for k, v in UserModel.__fields__.items()
    }
)
# output
# {
#   `uid`: FieldInfo(default=PydanticUndefined, title=`UID`, description=`user union id`, extra={`example`: `10086`}),
#   `age`: FieldInfo(default=0, title=`use age`, ge=0, extra={`example`: 18}),
#   `height`: FieldInfo(default=0.0, ge=0, le=2, extra={}),
#   `sex`: FieldInfo(default=0, extra={}),
#   `is_adult`: FieldInfo(default=False, extra={}),
#   `user_name`: FieldInfo(default=``, description=`user name`, min_length=1, max_length=10, extra={`example`: `so1n`})
# }

As you can see, the only difference in this code is the value of the parse_msg_desc_method, but through the output result, you can see that the field carries the same information as the result obtained through the module.

2.2.PGV(protoc-gen-validate)

At present, the commonly used parameter verification project in the Protobuf ecosystem is protoc-gen-validate, It has become a common standard in Protobuf because it supports multiple languages and requires only one writing of PGV rules to make the generated Message object support the corresponding validation rules.

Currently protobuf-to-pydantic only supports rules that protoc-gen-validate is less than version 1.0.0

protobuf-to-pydantic supports parsing of PGV validation rules and generates Pydantic Model objects with validation logic functions. Using PGV checksum rules in protobuf-to-pydantic is very simple, just write the corresponding PGV rules in the Protobuf file first, and then specify the value of parse_msg_desc_method to be PGV when calling msg_to_pydantic_model as the code below:

# pydantic version V1
from typing import Type
from protobuf_to_pydantic import msg_to_pydantic_model
from pydantic import BaseModel

# import protobuf gen python obj
from example.proto_3_20_pydanticv1.example.example_proto.validate import demo_pb2

UserModel: Type[BaseModel] = msg_to_pydantic_model(
    demo_pb2.FloatTest, parse_msg_desc_method="PGV"
)
print(
    {
        k: v.field_info
        for k, v in UserModel.__fields__.items()
    }
)
# output
# {
#   `const_test`: FieldInfo(default=1.0, const=True, extra={}),
#   `range_e_test`: FieldInfo(default=0.0, ge=1, le=10, extra={}),
#   `range_test`: FieldInfo(default=0.0, gt=1, lt=10, extra={}),
#   `in_test`: FieldInfo(default=0.0, extra={`in`: [1.0, 2.0, 3.0]}),
#   `not_in_test`: FieldInfo(default=0.0, extra={`not_in`: [1.0, 2.0, 3.0]}),
#   `ignore_test`: FieldInfo(default=0.0, extra={})
# }

Note:

  • 1.For the usage of PGV, see: protoc-gen-validate doc
  • 2.Need to install PGV through pip install protoc_gen_validate or download validate.proto to the protobuf directory in the project to write pgv rules in the Protobuf file.

2.3.P2P

The PGV verification rules are written in the Option attribute of each field of Message and have a better code specification, so Protobuf that use PGV checksum rules will be more readable than Protobuf that use annotation .

At the same time, when writing PGV rules, can also experience the convenience of the IDE's auto-completion and the security of checksumming when generating the corresponding language objects from Protobuf files, but it only supports checksumming-related logic, which is not as rich as the file annotation mode.

The P2P verification rule that comes with protobuf-to-pydantic expands on the PGV verification rule by incorporating some of the functionality of the text annotation verification rule, which satisfies most of the customization of the properties of each Field in the Pydantic Model, such as the following Protobuf file.

syntax = "proto3";
package p2p_validate_test;

import "example_proto/common/p2p_validate.proto";


message FloatTest {
  float const_test = 1 [(p2p_validate.rules).float.const = 1];
  float range_e_test = 2 [(p2p_validate.rules).float = {ge: 1, le: 10}];
  float range_test = 3[(p2p_validate.rules).float = {gt: 1, lt: 10}];
  float in_test = 4[(p2p_validate.rules).float = {in: [1,2,3]}];
  float not_in_test = 5[(p2p_validate.rules).float = {not_in: [1,2,3]}];
  float default_test = 6[(p2p_validate.rules).float.default = 1.0];
  float not_enable_test = 7[(p2p_validate.rules).float.enable = false];
  float default_factory_test = 8[(p2p_validate.rules).float.default_factory = "p2p@builtin|float"];
  float miss_default_test = 9[(p2p_validate.rules).float.miss_default = true];
  float alias_test = 10 [(p2p_validate.rules).float.alias = "alias"];
  float desc_test = 11 [(p2p_validate.rules).float.description = "test desc"];
  float multiple_of_test = 12 [(p2p_validate.rules).float.multiple_of = 3.0];
  float example_test = 13 [(p2p_validate.rules).float.example = 1.0];
  float example_factory = 14 [(p2p_validate.rules).float.example_factory = "p2p@builtin|float"];
  float field_test = 15[(p2p_validate.rules).float.field = "p2p@local|CustomerField"];
  float type_test = 16[(p2p_validate.rules).float.type = "p2p@local|confloat"];
  float title_test = 17 [(p2p_validate.rules).float.title = "title_test"];
}

protobuf-to-pydantic can read the generated Message object at runtime and generate a Pydantic Model object with the corresponding information:

# pydantic version V1
from typing import Type
from protobuf_to_pydantic import msg_to_pydantic_model
from pydantic import BaseModel, confloat
from pydantic.fields import FieldInfo

# import protobuf gen python obj
from example.proto_3_20_pydanticv1.example.example_proto.p2p_validate import demo_pb2


class CustomerField(FieldInfo):
    pass


DemoModel: Type[BaseModel] = msg_to_pydantic_model(
    demo_pb2.FloatTest,
    local_dict={"CustomerField": CustomerField, "confloat": confloat},
)
print(
    {
        k: v.field_info
        for k, v in DemoModel.__fields__.items()
    }
)
# output:
# {
#   'const_test': FieldInfo(default=1.0, const=True, extra={}),
#   'range_e_test': FieldInfo(default=0.0, ge=1, le=10, extra={}),
#   'range_test': FieldInfo(default=0.0, gt=1, lt=10, extra={}),
#   'in_test': FieldInfo(default=0.0, extra={'in': [1.0, 2.0, 3.0]}),
#   'not_in_test': FieldInfo(default=0.0, extra={'not_in': [1.0, 2.0, 3.0]}),
#   'default_test': FieldInfo(default=1.0, extra={}),
#   'default_factory_test': FieldInfo(default=PydanticUndefined, default_factory=<class 'float'>, extra={}),
#   'miss_default_test': FieldInfo(extra={}),
#   'alias_test': FieldInfo(default=0.0, alias='alias', alias_priority=2, extra={}),
#   'desc_test': FieldInfo(default=0.0, description='test desc', extra={}),
#   'multiple_of_test': FieldInfo(default=0.0, multiple_of=3, extra={}),
#   'example_test': FieldInfo(default=0.0, extra={'example': 1.0}),
#   'example_factory': FieldInfo(default=0.0, extra={'example': <class 'float'>}),
#   'field_test': CustomerField(default=0.0, extra={}),
#   'type_test': FieldInfo(default=0.0, extra={}),
#   'title_test': FieldInfo(default=0.0, title='title_test', extra={})
#   }

Note:

  • 1.See the 2.5.template for the usage of local_dict
  • 2.If the reference to the Proto file fails, need to download p2p_validate.proto in the project and use it in the Protobuf file。

2.4.P2P and text annotation rule other parameter support

The protobuf-to-pydantic text annotation rules and the P2P rules support most of the parameters in FieldInfo, as described in the Pydantic Field doc

The new parameters added to Pydantic V2 will be supported in version 2.1, for now P2P rule naming is still written on the basis of Pydantic V1, but automatic mapping to Pydantic V2 naming is supported.

Other partial changes in meaning and new parameters are described as follows:

Parameter Default value Illustrate
required False By default, the default value of each field in the generated Pydantic Model object is the same as the zero value of its corresponding type. When required is True, no more default values are generated for the fields.
enable True By default, protobuf-to-pydantic generates all fields for Message, if don't want the generated Message to have this field, can set enable to False.
const None Used to specify a constant value for a field, though different Pydantic versions behave differently
For Pydantic V1, the value of default in Field is set to the value specified by const, and const in Field is set to True.Note: Pydantic Model's const only supports bool variables, when const is True, the accepted value can only be the value set by default, and the default value carried by the message generated by protobuf is the zero value of the corresponding type does not match with Pydantic Model, so protobuf-to-pydantic makes some changes to the input of this value.
For Pydantic V2, the value of default in Field remains the same, but the type annotation changes to typing_extensions.Literal[xxx]
type None By default, the default type of a field is the same as Protobuf's, but use the 2.5.template function to modify the type of a field.
extra None The extra parameter accepted by Pydantic is of type Python Dict, which is not supported by Protobuf, and requires the use of either 2.5.Templates or the corresponding Json structure protobuf-to-pydantic in the Protobuf file to parse it properly.
field None By default, the Field of the parameter is Pydantic FieldInfo, although it can be customized using the 2.5.Templates function
default_template None Similar to default, default values can be customized in fields that are not of string type using the 2.5.Templates feature.

In addition to the above parameters, also support for fast import of Pydantic type for string types. For example, if want to add a check for card numbers via the pydantic.types.PaymentCardNumber type, can specify the type of the pydantic_type parameter field to be PaymentCardNumber, which is similar to the use of template imports in the type rule, as follows:

  • Text annotation rules:
    syntax = "proto3";
    package common_validate_test;
    
    // common example
    message UserPayMessage {
      string bank_number=1; // p2p: {"pydantic_type": "PaymentCardNumber"}
      string other_bank_number=2; // p2p: {"type": "p2p@import|pydantic.types|PaymentCardNumber"}
    }
  • P2P rules:
    syntax = "proto3";
    package p2p_validate_test;
    
    import "example_proto/common/p2p_validate.proto";
    // p2p example
    message UserPayMessage {
      string bank_number=1[(p2p_validate.rules).string.pydantic_type = "PaymentCardNumber"];
      string other_bank_number=2[(p2p_validate.rules).string.type = "p2p@import|pydantic.types|PaymentCardNumber"];
    }

See Extra Types Overview for supported `Pydantic Types'.

2.5.Template

When working with definition fields, will find that some fields are filled with values that are methods or functions of one of the libraries in Python (e.g., the values of the type parameter and the default_factory parameter), which can't be accomplished with the Json syntax. At this point, templates can be used to solve the corresponding problem, and currently protobuf-to-pydantic supports a variety of template functi

Note: The p2p string at the beginning of a template can be defined via the comment_prefix variable

2.5.1.p2p@importTemplate

The p2p@import template is used to represent variables in other modules that need to be introduced before they can be used, as follows.

  • Examples of text annotation rules:

    syntax = "proto3";
    package comment_validate_test;
    
    // comment example
    message UserPayMessage {
      string bank_number=1; // p2p: {"type": "p2p@import|pydantic.types|PaymentCardNumber"}
    }
  • Examples of P2P rules (1):

    syntax = "proto3";
    package p2p_validate_test;
    import "example_proto/common/p2p_validate.proto";
    
    message UserPayMessage {
      string bank_number=1[(p2p_validate.rules).string.type = "p2p@import|pydantic.types|PaymentCardNumber"];
    }
  • Examples of P2P rules (2):

    syntax = "proto3";
    package p2p_other_validate_test;
    import "example_proto/common/p2p_validate.proto";
    // p2p other example
    message UserPayMessage {
      string bank_number=1[(p2p_validate.rules).string.pydantic_type = "PaymentCardNumber"];
    }

The example Protobuf file uses a syntax in the format p2p@{methods of the template}|{modules to be imported:A}|{variables in modules:B}, indicating that a B object needs to be imported by from A import B and used by the corresponding rule. With the definition of the template, protobuf-to-pydantic converts the corresponding Message into a Pydantic Model, as follows:

from pydantic import BaseModel
from pydantic.fields import FieldInfo
# p2p@import|pydantic.types|PaymentCardNumber
from pydantic.types import PaymentCardNumber

class UserPayMessage(BaseModel):
    bank_number: PaymentCardNumber = FieldInfo(default="", extra={})

2.5.2.p2p@import_instance Template

The p2p@import_instance template introduces the class of a library and then instantiates it in combination with the specified parameters before it is used by the corresponding rule, which is used as follows:

syntax = "proto3";
package p2p_validate_test;
import "google/protobuf/any.proto";
import "example_proto/common/p2p_validate.proto";
// p2p example
message AnyTest {
  google.protobuf.Any default_test = 23 [
    (p2p_validate.rules).any.default = 'p2p@import_instance|google.protobuf.any_pb2|Any|{"type_url": "type.googleapis.com/google.protobuf.Duration"}'
  ];
}

The syntax used here is p2p@{methods of the template}|{modules to be introduced}|{classes to be introduced}|{initialization parameters}, and the definition of protobuf-to-pydantic through the template will turn the corresponding Message into the following Pydantic Model object:

from google.protobuf.any_pb2 import Any as AnyMessage
from pydantic import BaseModel
from pydantic.fields import FieldInfo


class AnyTest(BaseModel):
    default_test: AnyMessage = FieldInfo(
        default=AnyMessage(type_url="type.googleapis.com/google.protobuf.Duration")
    )

2.5.3.p2p@local Template

This template is used to introduce user-defined variables, using a syntax in the format {method of the template}|{local variable to be used}, as follows:

  • Example of text annotation:
    syntax = "proto3";
    package comment_validate_test;
    import "google/protobuf/timestamp.proto";
    import "example_proto/common/p2p_validate.proto";
    // comment example
    message UserPayMessage {
      google.protobuf.Timestamp exp=1; // p2p: {"default_factory": "p2p@local|exp_time"}
    }
  • Examples of P2P rules:
    syntax = "proto3";
    package p2p_validate_test;
    import "google/protobuf/timestamp.proto";
    import "example_proto/common/p2p_validate.proto";
    // p2p example
    message UserPayMessage {
      google.protobuf.Timestamp exp=1[(p2p_validate.rules).timestamp.default_factory= "p2p@local|exp_time"];
    }

However, the msg_to_pydantic_model func needs to be called with the parameter local_dict to register the corresponding value, the pseudo-code is as follows:

# a.py
import time

from example.proto_3_20_pydanticv1.example.example_proto.p2p_validate import demo_pb2
from protobuf_to_pydantic import msg_to_pydantic_model


def exp_time() -> float:
  return time.time()

msg_to_pydantic_model(
    demo_pb2.NestedMessage,
    local_dict={"exp_time": exp_time},  # <----  use local_dict
)

In this way, protobuf-to-pydantic generates a conforming Pydantic Model object:

# b.py
from datetime import datetime
from pydantic import BaseModel
from pydantic.fields import FieldInfo

from a import exp_time  # <-- exp_time in a.py

class UserPayMessage(BaseModel):
    exp: datetime = FieldInfo(default_factory=exp_time, extra={})

2.5.4.p2p@builtin Template

This template (which can be thought of as a simplified version of the p2p@local template) can be used directly when the variables to be used come from Python built-in functions,the syntax is used as follows:

  • Examples of text annotation rules:
    syntax = "proto3";
    package comment_validate_test;
    import "google/protobuf/timestamp.proto";
    import "example_proto/common/p2p_validate.proto";
    // comment example
    message UserPayMessage {
      google.protobuf.Timestamp exp=1; // p2p: {"type": "p2p@builtin|float"}
    }
  • Examples of P2P rules:
    syntax = "proto3";
    package p2p_validate_test;
    import "google/protobuf/timestamp.proto";
    import "example_proto/common/p2p_validate.proto";
    // p2p example
    message UserPayMessage {
      google.protobuf.Timestamp exp=1[(p2p_validate.rules).timestamp.type= "p2p@builtin|float"];
    }

Then can directly generate a conforming Pydantic Model object by calling the msg_to_pydantic_model function, as follows:

from pydantic import BaseModel
from pydantic.fields import FieldInfo


class UserPayMessage(BaseModel):
    exp: float = FieldInfo()

2.5.5.Customized templates

Currently protobuf-to-pydantic only supports a few simple templates, if have more template needs, can extend the templates by inheriting the DescTemplate class.

For example, there is an odd feature that requires the default value of a field to be the timestamp of the time when the Pydantic Model object was generated, but the timestamps used are available in lengths of 10 and 13, so the following Protobuf file needs to be written to support defining the length of the timestamps:

syntax = "proto3";
package p2p_validate_test;
import "google/protobuf/timestamp.proto";
import "example_proto/common/p2p_validate.proto";

message TimestampTest{
  int32 timestamp_10 = 1[(p2p_validate.rules).int32.default_template = "p2p@timestamp|10"];
  int32 timestamp_13 = 2[(p2p_validate.rules).int32.default_template = "p2p@timestamp|13"];
}

As you can see, the Protobuf file customizes the syntax of p2p@timestamp|{x}, where x has only two values, 10 and 13. The next step is to write code based on this template behavior, which looks like this.

import time
from protobuf_to_pydantic.gen_model import DescTemplate

class CustomDescTemplate(DescTemplate):
    def template_timestamp(self, length_str: str) -> int:
        timestamp: float = time.time()
        if length_str == "10":
            return int(timestamp)
        elif length_str == "13":
            return int(timestamp * 100)
        else:
            raise KeyError(f"timestamp template not support value:{length_str}")


from .demo_pb2 import TimestampTest # fake code
from protobuf_to_pydantic import msg_to_pydantic_model

msg_to_pydantic_model(
    TimestampTest,
    desc_template=CustomDescTemplate   # <-- Use a custom template class
)

This code first creates a class CustomDescTemplate that inherits from DescTemplate. DescTemplate will forwards to the corresponding template_{template name} method based on the naming of the template, so this class needs to define the template_timestamp method to implement the p2p@timestamp template functionality. In addition, the length_str variable received in this method is either 10 in p2p@timestamp|10 or 13 in p2p@timestamp|13.

Then load the CustomDescTemplate through the msg_to_pydantic_model function, then the following code will be generated (assuming that the code is generated at a timestamp of 1600000000):

from pydantic import BaseModel
from pydantic.fields import FieldInfo

class TimestampTest(BaseModel):
    timestamp_10: int = FieldInfo(default=1600000000)
    timestamp_13: int = FieldInfo(default=1600000000000)

3.Code format

The code generated directly through protobuf-to-pydantic is not perfect, but it is possible to indirectly generate code that conforms to the Python specification through different formatting tools. Currently, protobuf-to-pydantic supports formatting tools such as autoflake, black and isort. If the corresponding formatting tool is installed in the current Python environment, then protobuf-to-pydantic will call the tool to format the generated code before outputting it to a file.

In addition, the decision to enable or disable a formatting tool can be made through the pyproject.toml configuration file, the pyproject.toml example of which reads as follows:

# Controls which formatting tools protobuf-to-pydantic uses,
# if false then no formatting tools are used (default is true)
[tool.protobuf-to-pydantic.format]
black = true
isort = true
autoflake = true

# black docc:https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-format
[tool.black]
line-length = 120
target-version = ['py37']

# isort doc:https://pycqa.github.io/isort/docs/configuration/config_files.html#pyprojecttoml-preferred-format
[tool.isort]
profile = "black"
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
line_length = 120

# autoflake doc:https://github.com/PyCQA/autoflake#configuration
[tool.autoflake]
in-place = true
remove-all-unused-imports = true
remove-unused-variables = true

4.example

protobuf-to-pydantic provides some simple example code for reference only.

4.1.Generate code directly

Protobuf file: demo/demo.proto

Generate Pydantic Model(Pydantic V1): proto_pydanticv1/demo_gen_code.py

Generate Pydantic Model(Pydantic V2): proto_pydanticv2/demo_gen_code.py

4.2.Text annotation

Protobuf File: demo/demo.proto

Pydantic Model generated based on pyi file(Pydantic V1): proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py

Pydantic Model generated based on pyi file(Pydantic V2): proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py

Pydantic Model generated based on protobuf file(Pydantic V1): proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py Pydantic Model generated based on protobuf file(Pydantic V2): proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py validate/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/validate/demo.proto)

Generate Pydantic Model(Pydantic V1): proto_pydanticv1/demo_gen_code_by_pgv.py

Generate Pydantic Model(Pydantic V2): proto_pydanticv2/demo_gen_code_by_pgv.py

4.4.P2P rule

Protobuf file: p2p_validate/demo.proto

Generate Pydantic Model(Pydantic V1): proto_pydanticv1/demo_gen_code_by_p2p.py

Generate Pydantic Model(Pydantic V2): proto_pydanticv2/demo_gen_code_by_p2p.py

4.5.Protoc Plugin-in

Protobuf field: demo/demo.protovalidate/demo.protop2p_validate/demo.proto

Note: The Protoc plugin only supports P2P and PGV rules

Pydantic Model generated via demo/demo.proto(Pydantic V1):example_proto/demo/demo_p2p.py

Pydantic Model generated via demo/demo.proto(Pydantic V2):example_proto/demo/demo_p2p.py

Pydantic Model generated via validate/demo.proto(Pydantic V1):example_proto/validate/demo_p2p.py

Pydantic Model generated via validate/demo.proto(Pydantic V1):example_proto/validate/demo_p2p.py

Pydantic Model generated via p2p_validate/demo.proto(Pydantic V1):example_proto/p2p_validate/demo_p2p.py

Pydantic Model generated via p2p_validate/demo.proto(Pydantic V1):example_proto/p2p_validate/demo_p2p.py

protobuf_to_pydantic's People

Contributors

jankatins avatar so1n avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

protobuf_to_pydantic's Issues

No module named 'mypy_protobuf'

python -m grpc_tools.protoc -I. --protobuf-to-pydantic_out=. proxy.proto

Traceback (most recent call last):
File "/Users/wangmingli/.local/share/virtualenvs/pythonProject3-USoLf3Xu/bin/protoc-gen-protobuf-to-pydantic", line 5, in
from protobuf_to_pydantic.plugin.main import main
File "/Users/wangmingli/.local/share/virtualenvs/pythonProject3-USoLf3Xu/lib/python3.8/site-packages/protobuf_to_pydantic/plugin/main.py", line 13, in
from mypy_protobuf.main import Descriptors, code_generation
ModuleNotFoundError: No module named 'mypy_protobuf'
--protobuf-to-pydantic_out: protoc-gen-protobuf-to-pydantic: Plugin failed with status code 1.
(pythonProject3) (base) wangmingli@wangminglidemac pythonProject3 % python -m grpc_tools.protoc -I. --protobuf-to-pydantic_out=. proxy.proto
Traceback (most recent call last):
File "/Users/wangmingli/.local/share/virtualenvs/pythonProject3-USoLf3Xu/bin/protoc-gen-protobuf-to-pydantic", line 5, in
from protobuf_to_pydantic.plugin.main import main
File "/Users/wangmingli/.local/share/virtualenvs/pythonProject3-USoLf3Xu/lib/python3.8/site-packages/protobuf_to_pydantic/plugin/main.py", line 13, in
from mypy_protobuf.main import Descriptors, code_generation
ModuleNotFoundError: No module named 'mypy_protobuf'
--protobuf-to-pydantic_out: protoc-gen-protobuf-to-pydantic: Plugin failed with status code 1.
(pythonProject3) (base) wangmingli@wangminglidemac pythonProject3 % python -m grpc_tools.protoc -I. --protobuf-to-pydantic_out=config_path=plugin_config.py:. proxy.proto
Traceback (most recent call last):
File "/Users/wangmingli/.local/share/virtualenvs/pythonProject3-USoLf3Xu/bin/protoc-gen-protobuf-to-pydantic", line 5, in
from protobuf_to_pydantic.plugin.main import main
File "/Users/wangmingli/.local/share/virtualenvs/pythonProject3-USoLf3Xu/lib/python3.8/site-packages/protobuf_to_pydantic/plugin/main.py", line 13, in
from mypy_protobuf.main import Descriptors, code_generation
ModuleNotFoundError: No module named 'mypy_protobuf'
--protobuf-to-pydantic_out: protoc-gen-protobuf-to-pydantic: Plugin failed with status code 1.

Integrate plugin with buf cli

Is your feature request related to a problem? Please describe.
Our team is using the buf cli to manage protobuf builds because
managing protoc calls can be clunky.

Describe the solution you'd like
I'd like this plugin integrated with the CodeGeneratorRequest so that a Dockerfile can be implemented that will allow a custom plugin to be implemented.

Additionally, this will facilitate adoption by Buf to make it a hosted plugin, which will likely increase adoption of this plugin.

Describe alternatives you've considered
The alternative is to use the CLI as documented. However, buf generate is a much friendlier interface to using this.

Additional context
bufbuild/plugins#589

ImportError: cannot import name 'PydanticGeneralMetadata' from 'pydantic._internal._fields' with latest pydantic

Describe the bug

After udating to the latest pydantic version, I get this:

cd ./build/proto && poetry run python -m grpc_tools.protoc -I. --protobuf-to-pydantic_out=. example_model/v1/classes.proto
/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/grpc_tools/protoc.py:21: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
  import pkg_resources
Traceback (most recent call last):
  File "/Users/jankatins/projects/project1/.venv/bin/protoc-gen-protobuf-to-pydantic", line 5, in <module>
    from protobuf_to_pydantic.plugin.main import main
  File "/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/protobuf_to_pydantic/__init__.py", line 2, in <module>
    from .gen_code import pydantic_model_to_py_code, pydantic_model_to_py_file
  File "/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/protobuf_to_pydantic/gen_code.py", line 33, in <module>
    from protobuf_to_pydantic import _pydantic_adapter, customer_validator, gen_model
  File "/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/protobuf_to_pydantic/gen_model.py", line 18, in <module>
    from protobuf_to_pydantic.get_desc import (
  File "/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/protobuf_to_pydantic/get_desc/__init__.py", line 1, in <module>
    from .from_pb_option import get_desc_from_p2p, get_desc_from_pgv
  File "/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/protobuf_to_pydantic/get_desc/from_pb_option/__init__.py", line 1, in <module>
    from .from_p2p import get_desc_from_p2p
  File "/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/protobuf_to_pydantic/get_desc/from_pb_option/from_p2p.py", line 5, in <module>
    from .base import DescFromOptionTypedDict, ParseFromPbOption
  File "/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/protobuf_to_pydantic/get_desc/from_pb_option/base.py", line 7, in <module>
    from protobuf_to_pydantic.customer_con_type import (
  File "/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/protobuf_to_pydantic/customer_con_type/__init__.py", line 6, in <module>
    from .v2 import *  # noqa
    ^^^^^^^^^^^^^^^^^
  File "/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/protobuf_to_pydantic/customer_con_type/v2.py", line 6, in <module>
    from pydantic._internal._fields import PydanticGeneralMetadata
ImportError: cannot import name 'PydanticGeneralMetadata' from 'pydantic._internal._fields' (/Users/jankatins/projects/project1/.venv/lib/python3.11/site-packages/pydantic/_internal/_fields.py)
--protobuf-to-pydantic_out: protoc-gen-protobuf-to-pydantic: Plugin failed with status code 1.

To Reproduce

Install the following versions:

λ  poetry show
...
grpcio               1.59.3    HTTP/2-based RPC framework
grpcio-tools         1.59.3    Protobuf code generator for gRPC
mypy                 1.7.1     Optional static typing for Python
mypy-extensions      1.0.0     Type system extensions for programs checked with the mypy type checker.
mypy-protobuf        3.5.0     Generate mypy stub files from protobuf specs
protobuf             4.25.1
protobuf-to-pydantic 0.2.1     Generate the `pydantic.BaseModel` class (and the corresponding source code) with parameter verification function through the Protobuf file
pydantic             2.5.2     Data validation using Python type hints
pydantic-core        2.14.5
types-protobuf       4.24.0.4  Typing stubs for protobuf

Expected behavior

This should work (and it did work with older versions) :-) -> will downgrade pydantic for now...

Oneof for non primitive types

Describe the bug
Unusable pydantic model is generated.

To Reproduce
Steps to reproduce the behavior:

syntax = "proto3";
message A {
  bool a = 1;
}
message B {
  bool b = 1;
}
message C {
  oneof z {
    A a = 1;
    B b = 2;
  }
  optional B c = 3;
}

generates this:

class A(BaseModel):
    a: bool = Field(default=False)


class B(BaseModel):
    b: bool = Field(default=False)


class C(BaseModel):
    _one_of_dict = {"C._c": {"fields": {"c"}}, "C.z": {"fields": {"a", "b"}}}
    one_of_validator = model_validator(mode="before")(check_one_of)

    a: A = Field()
    b: B = Field()
    c: typing.Optional[B] = Field(default=None)

Expected behavior

class A(BaseModel):
    a: bool = Field(default=False)


class B(BaseModel):
    b: bool = Field(default=False)


class C(BaseModel):
    _one_of_dict = {"C._c": {"fields": {"c"}}, "C.z": {"fields": {"a", "b"}}}
    one_of_validator = model_validator(mode="before")(check_one_of)

    a: typing.Optional[A] = Field(default=None)
    b: typing.Optional[B] = Field(default=None)
    c: typing.Optional[B] = Field(default=None)

RecursionError: maximum recursion depth exceeded in __instancecheck__

Describe the bug
After update proto in #2 , I tried to parse my proto, and I get this error

Error Log

maximum recursion depth exceeded in instancecheck
Traceback (most recent call last):
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/main.py", line 12, in main
CodeGen(ConfigModel)
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/code_gen.py", line 30, in init
self.generate_pydantic_model(Descriptors(request), response)
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/code_gen.py", line 82, in generate_pydantic_model
file.content = self.config.file_descriptor_proto_to_code(
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 57, in init
self._parse_field_descriptor()
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 405, in _parse_field_descriptor
self._content_deque.append(self._message(desc, [FileDescriptorProto.ENUM_TYPE_FIELD_NUMBER]))
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 321, in _message
_content_tuple: Optional[Tuple[str, str]] = self._message_field_handle(
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 168, in _message_field_handle
self._content_deque.append(self._message(message, [FileDescriptorProto.ENUM_TYPE_FIELD_NUMBER]))
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 321, in _message
_content_tuple: Optional[Tuple[str, str]] = self._message_field_handle(
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 168, in _message_field_handle
self._content_deque.append(self._message(message, [FileDescriptorProto.ENUM_TYPE_FIELD_NUMBER]))
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 321, in _message
_content_tuple: Optional[Tuple[str, str]] = self._message_field_handle(
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 168, in _message_field_handle
self._content_deque.append(self._message(message, [FileDescriptorProto.ENUM_TYPE_FIELD_NUMBER]))
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 321, in _message
_content_tuple: Optional[Tuple[str, str]] = self._message_field_handle(

......

File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 168, in _message_field_handle
self._content_deque.append(self._message(message, [FileDescriptorProto.ENUM_TYPE_FIELD_NUMBER]))
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 321, in _message
_content_tuple: Optional[Tuple[str, str]] = self._message_field_handle(
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 183, in _message_field_handle
protobuf_type_model = self._get_protobuf_type_model(field)
File "/usr/local/lib/python3.9/dist-packages/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py", line 359, in _get_protobuf_type_model
return ProtobufTypeModel(
File "pydantic/main.py", line 339, in pydantic.main.BaseModel.init
File "pydantic/main.py", line 1076, in pydantic.main.validate_model
File "pydantic/fields.py", line 884, in pydantic.fields.ModelField.validate
File "pydantic/fields.py", line 1101, in pydantic.fields.ModelField._validate_singleton
File "pydantic/fields.py", line 1151, in pydantic.fields.ModelField._apply_validators
File "pydantic/class_validators.py", line 337, in pydantic.class_validators._generic_validator_basic.lambda13
File "pydantic/validators.py", line 61, in pydantic.validators.str_validator
RecursionError: maximum recursion depth exceeded in instancecheck

Typo in readme.

Describe the bug
There is a typo in the readme. It says prorobuf_to_pydantic instead of protobuf-to-pydantic
To Reproduce
Steps to reproduce the behavior:

  1. Go to readme file
  2. Click on scroll down to 1.1.1 Use plugins section
  3. See grammatical error

Expected behavior
The text should read protobuf-to-pydantic

Screenshots
If applicable, add screenshots to help explain your problem.
image

Desktop (please complete the following information):

  • OS: [e.g. iOS] N/A
  • Browser [e.g. chrome, safari] Chrome
  • Version [e.g. 22]

Smartphone (please complete the following information):

  • Device: [e.g. iPhone6]
  • OS: [e.g. iOS8.1]
  • Browser [e.g. stock browser, safari] Chrome
  • Version [e.g. 22] Version 120.0.6099.131 (Official Build) (64-bit)

Additional context
N/A

Support `BaseModel` serialization to `Protobuf Message`

The current step of converting BaseModel to Protobuf Message is tedious and has poor performance. e.g:

from google.protobuf.json_format import ParseDict
from pydantic import BaseModel
from demo_pb2 import DemoMessage

class Demo(BaseModel):
    pass


ParseDict(Demo().dict(), DemoMessage())

Serialization can be optimized in the following ways:

  • 1.Convert directly to Message:

    from protobuf_to_pydantic.p2p_model import P2PBaseModel
    
    class Demo(P2PBaseModel):
        pass
    
    Demo().to_message()
  • 2.Serialized to dict, then converted to Message by the developer (pydantic/pydantic#1409 (comment))

    from google.protobuf.json_format import ParseDict
    from pydantic import BaseModel
    from demo_pb2 import DemoMessage
    
    class Demo(BaseModel):
        pass
    
    
    DemoMessage(**Demo().dict())

Typos in the word "pydantic" in the README.md file

Both files (README.md and README_ZH.md) contain a typo in some of the words "pydantic". In some places it says "pydanitc":

the command python -m pip install protobuf-to-pydanitc[mypy-protobuf].

indicates the use of the prorobuf-to-pydanitc plugin

Support of protobuf services

This library generating messages in good format. But no support of generating proto services to python and creating server and client

`protoc > 3.19` support

Is your feature request related to a problem? Please describe.
I got an error when using the plugin, I was on latest protoc at the time:

TypeError: Descriptors cannot not be created directly.
If this call came from a _pb2.py file, your generated code is out of date and must be regenerated with protoc >= 3.19.0.
If you cannot immediately regenerate your protos, some other possible workarounds are:
 1. Downgrade the protobuf package to 3.20.x or lower.
 2. Set PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python (but this will use pure-Python parsing and will be much slower).

Describe the solution you'd like
protoc > 3.19 support

Describe alternatives you've considered
Downgrade.

reorder generated classes to avoid unresolved references

Thanks for the nice module!

Is your feature request related to a problem? Please describe.
The order of generated pydantic classes (from python -m grpc_tools.protoc -I. --protobuf-to-pydantic_out=. example.proto) can cause unresolved references: a class definition comes after its references in other classes.

Describe the solution you'd like
I'm using the following script to reorder generated classes, but it may be better to integrate this step.

import ast

def reorder_classes(source):
    tree = ast.parse(source)

    # Extract the classes from the AST and sort them topologically
    class_nodes = [node for node in tree.body if isinstance(node, ast.ClassDef)]
    class_dependencies = {}
    for class_node in class_nodes:
        class_dependencies[class_node] = {
            dep.id for dep in ast.walk(class_node) if
            isinstance(dep, ast.Name) and dep.id in [cls.name for cls in class_nodes]
        }
    sorted_classes = []
    while class_dependencies:
        acyclic_nodes = [node for node, deps in class_dependencies.items() if not deps]
        if not acyclic_nodes:
            raise ValueError('Circular dependency found')
        for node in acyclic_nodes:
            del class_dependencies[node]
            sorted_classes.append(node)
            for deps in class_dependencies.values():
                deps.discard(node.name)

    # Rewrite the source code with the sorted classes
    sorted_source = '\n\n'.join([ast.unparse(node) for node in sorted_classes])
    return sorted_source

default_factory and default generated together

Describe the bug
Follow up on #22

For certain protobufs, this plugin generates a default and a default_factory in Pydantic which causes an error.

To Reproduce
Steps to reproduce the behavior:

syntax = "proto3";

message A {
    optional string a = 2;
    repeated string b = 3;
}

Output:

# This is an automatically generated file, please do not change
# gen by protobuf_to_pydantic[v0.2.0.1](https://github.com/so1n/protobuf_to_pydantic)
# Protobuf Version: 4.24.3
# Pydantic Version: 1.10.12
from google.protobuf.message import Message  # type: ignore
from pydantic import BaseModel
from pydantic import Field
import typing


class A(BaseModel):
    a: typing.Optional[str] = Field(default="")
    b: typing.Optional[typing.List[str]] = Field(default_factory=list, default=None)

Expected behavior
It should only generate the default factory list for repeated and not default=None.

Making a field required

GRPC proto have all fields optional in messages.

with p2p we can annotate defailt values, title, etc. however is there a way to have no default, i.e. making a field required?

add `pass` to generated class when message body is empty

Is your feature request related to a problem? Please describe.
When message is empty, the generated code is empty after class.

message Empty{}
message Something {
string s = 1;
}

Generated code is like

class Empty(BaseModel):


class Something(BaseModel):
    s : ...

Describe the solution you'd like
Add pass statement to generated class if the message body is empty

Optional fields do not generate correct defaults in Pydantic

Describe the bug
A clear and concise description of what the bug is.
Using proto v3

Marking a field as optional in Proto doesn't seem to change anything in the generated Pydantic model.

To Reproduce
.proto

syntax = "proto3";

message A {
    string a = 1;
}
message B {
    optional A a = 1;
}

Output:

class A(BaseModel):
    a: str = Field(default="")

class B(BaseModel):
    a: typing.Optional[A] = Field()

Expected behavior
This is the expected output:

class A(BaseModel):
    a: str = Field(default="")


class B(BaseModel):
    a: typing.Optional[A] = Field(default=None)

Note that without setting default = None in Pydantic, it's impossible to construct B() with no args. You have to construct it via B(a=None).

Missing typing import for optional field

Describe the bug

The following (shortend) proto file creates a non-valid pydantic python file:

syntax = "proto3";
package whatever.v1;

message Blub {
  int64 whatever = 1;
}

message CheckCreateCommitFromPreparationResponse {
  optional Blub blib = 1;
}

Converting with protobuf-to-pydantic gives this

# This is an automatically generated file, please do not change
# gen by protobuf_to_pydantic[v0.2.3](https://github.com/so1n/protobuf_to_pydantic)
# Protobuf Version: 4.25.2 
# Pydantic Version: 2.6.1 
from google.protobuf.message import Message  # type: ignore
from pydantic import BaseModel
from pydantic import Field


class Blub(BaseModel):

    whatever: int = Field(default=0) 


class CheckCreateCommitFromPreparationResponse(BaseModel):

    blib: typing.Optional[Blub] = Field(default=None) 

Which misses the import for typing and therefore the last lines errors on import.

To Reproduce
Steps to reproduce the behavior:

  1. Install the above versions (see python version)
  2. convert proto to pydantic
  3. try to import the pydantic file
  4. See error: NameError: name 'typing' is not defined. Did you forget to import 'typing'

Expected behavior
There is a typing import in the generated file and I can import the generated file without an error

Genereted code does not pass mypy

Describe the bug
The generated code does not pass mypy checks, as the field line is generated as

    id: int = FieldInfo(default=0)

instead of

    id: int = Field(default=0)
λ  mypy protobuf_p2p.py
protobuf_p2p.py:8: error: Incompatible types in assignment (expression has type "FieldInfo", variable has type "int")  [assignment]
protobuf_p2p.py:9: error: Incompatible types in assignment (expression has type "FieldInfo", variable has type "int")  [assignment]
Found 2 errors in 1 file (checked 1 source file)

Expected behavior

The resulting file is mypy clean

ideas

I tried outputting Fields instead of FieldInfo in the two lines below and for my usecase, that was fine.

field_name = "FieldInfo"
self._add_import_code("pydantic.fields", "FieldInfo")

But I guess there might be content which does not fit into a Field, but only into a FieldInfo?

The other idea how to fix this would be to add a # type: ignore[assignment] (specific, because otherwise ruff complains...) in the generated field code:

field_info_str: str = ", ".join([f"{k}={self._get_value_code(v)}" for k, v in field_info_dict.items()]) or ""
class_field_content: str = (
" " * (self.code_indent + indent) + f"{field.name}: {type_str} = {field_name}({field_info_str}) \n"
)

-            " " * (self.code_indent + indent) + f"{field.name}: {type_str} = {field_name}({field_info_str})\n"
+            " " * (self.code_indent + indent) + f"{field.name}: {type_str} = {field_name}({field_info_str}) # type: ignore[assignment]\n"

relevant mypy config

I use the pydantic mypy plugin:

[tool.mypy]
plugins = [
    # Better support for pydantic models
    "pydantic.mypy"
]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.