├── betterproto2 ├── tests │ ├── __init__.py │ ├── grpc │ │ ├── __init__.py │ │ ├── test_message_enum_descriptors.py │ │ ├── test_stream_stream.py │ │ └── thing_service.py │ ├── streams │ │ ├── load_varint_cutoff.in │ │ ├── delimited_messages.in │ │ ├── dump_varint_negative.expected │ │ ├── dump_varint_positive.expected │ │ ├── message_dump_file_single.expected │ │ ├── message_dump_file_multiple.expected │ │ └── java │ │ │ ├── .gitignore │ │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── betterproto │ │ │ └── CompatibilityTest.java │ ├── inputs │ │ ├── oneof_empty │ │ │ ├── test_oneof_empty.py │ │ │ ├── oneof_empty.json │ │ │ ├── oneof_empty_maybe1.json │ │ │ └── oneof_empty_maybe2.json │ │ ├── bool │ │ │ ├── bool.json │ │ │ └── test_bool.py │ │ ├── googletypes │ │ │ ├── googletypes-missing.json │ │ │ └── googletypes.json │ │ ├── double │ │ │ ├── double.json │ │ │ └── double-negative.json │ │ ├── oneof │ │ │ ├── oneof.json │ │ │ ├── oneof-name.json │ │ │ ├── oneof_name.json │ │ │ └── test_oneof.py │ │ ├── bytes │ │ │ └── bytes.json │ │ ├── proto3_field_presence │ │ │ ├── proto3_field_presence_default.json │ │ │ ├── proto3_field_presence_missing.json │ │ │ ├── proto3_field_presence.json │ │ │ └── test_proto3_field_presence.py │ │ ├── oneof_enum │ │ │ ├── oneof_enum-enum-0.json │ │ │ ├── oneof_enum-enum-1.json │ │ │ ├── oneof_enum.json │ │ │ └── test_oneof_enum.py │ │ ├── casing │ │ │ ├── casing.json │ │ │ └── test_casing.py │ │ ├── int32 │ │ │ └── int32.json │ │ ├── repeated │ │ │ └── repeated.json │ │ ├── ref │ │ │ └── ref.json │ │ ├── proto3_field_presence_oneof │ │ │ ├── proto3_field_presence_oneof.json │ │ │ └── test_proto3_field_presence_oneof.py │ │ ├── googletypes_struct │ │ │ └── googletypes_struct.json │ │ ├── timestamp_dict_encode │ │ │ ├── timestamp_dict_encode.json │ │ │ └── test_timestamp_dict_encode.py │ │ ├── deprecated │ │ │ └── deprecated.json │ │ ├── map │ │ │ └── map.json │ │ ├── nested │ │ │ └── nested.json │ │ ├── signed │ │ │ └── signed.json │ │ ├── fixed │ │ │ └── fixed.json │ │ ├── repeatedpacked │ │ │ └── repeatedpacked.json │ │ ├── enum │ │ │ ├── enum.json │ │ │ └── test_enum.py │ │ ├── mapmessage │ │ │ └── mapmessage.json │ │ ├── repeatedmessage │ │ │ └── repeatedmessage.json │ │ ├── field_name_identical_to_type │ │ │ └── field_name_identical_to_type.json │ │ ├── repeated_duration_timestamp │ │ │ ├── repeated_duration_timestamp.json │ │ │ └── test_repeated_duration_timestamp.py │ │ ├── googletypes_value │ │ │ └── googletypes_value.json │ │ ├── float │ │ │ └── float.json │ │ ├── recursivemessage │ │ │ └── recursivemessage.json │ │ ├── nestedtwice │ │ │ ├── nestedtwice.json │ │ │ └── test_nestedtwice.py │ │ ├── service_uppercase │ │ │ └── test_service.py │ │ ├── regression_387 │ │ │ └── test_regression_387.py │ │ ├── regression_414 │ │ │ └── test_regression_414.py │ │ ├── casing_message_field_uppercase │ │ │ └── casing_message_field_uppercase.py │ │ ├── namespace_builtin_types │ │ │ └── namespace_builtin_types.json │ │ ├── casing_inner_class │ │ │ └── test_casing_inner_class.py │ │ ├── invalid_field │ │ │ └── test_invalid_field.py │ │ ├── namespace_keywords │ │ │ └── namespace_keywords.json │ │ ├── rpc_empty_input_message │ │ │ └── test_rpc_empty_input_message.py │ │ ├── googletypes_response_embedded │ │ │ └── test_googletypes_response_embedded.py │ │ ├── googletypes_request │ │ │ └── test_googletypes_request.py │ │ ├── import_service_input_message │ │ │ └── test_import_service_input_message.py │ │ ├── oneof_default_value_serialization │ │ │ └── test_oneof_default_value_serialization.py │ │ ├── googletypes_response │ │ │ └── test_googletypes_response.py │ │ ├── example_service │ │ │ └── test_example_service.py │ │ └── google_impl_behavior_equivalence │ │ │ └── test_google_impl_behavior_equivalence.py │ ├── test_unwrap.py │ ├── test_message_wraping.py │ ├── test_oneof_pattern_matching.py │ ├── test_map.py │ ├── test_mapmessage.py │ ├── test_nested.py │ ├── test_manual_validation.py │ ├── test_version_check.py │ ├── test_encoding_decoding.py │ ├── test_all_definition.py │ ├── oneof_pattern_matching.py │ ├── mocks.py │ ├── test_timestamp.py │ ├── test_any.py │ ├── test_struct.py │ ├── test_documentation.py │ ├── test_deprecated.py │ ├── README.md │ ├── test_sync_client.py │ ├── test_enum.py │ ├── test_pickling.py │ └── util.py ├── src │ └── betterproto2 │ │ ├── py.typed │ │ ├── grpclib │ │ ├── __init__.py │ │ └── grpclib_server.py │ │ ├── validators │ │ ├── __init__.py │ │ └── proto_types.py │ │ ├── _types.py │ │ ├── message_pool.py │ │ ├── utils.py │ │ ├── _version.py │ │ └── enum_.py ├── docs │ ├── api.md │ ├── index.md │ ├── development.md │ ├── tutorial │ │ └── clients.md │ └── descriptors.md └── mkdocs.yml ├── betterproto2_compiler ├── tests │ ├── __init__.py │ ├── inputs │ │ ├── nested2 │ │ │ ├── package.proto │ │ │ └── nested2.proto │ │ ├── bool │ │ │ └── bool.proto │ │ ├── bytes │ │ │ └── bytes.proto │ │ ├── double │ │ │ └── double.proto │ │ ├── map │ │ │ └── map.proto │ │ ├── import_root_sibling │ │ │ ├── sibling.proto │ │ │ └── import_root_sibling.proto │ │ ├── invalid_field │ │ │ └── invalid_field.proto │ │ ├── repeated │ │ │ └── repeated.proto │ │ ├── import_packages_same_name │ │ │ ├── posts_v1.proto │ │ │ ├── users_v1.proto │ │ │ └── import_packages_same_name.proto │ │ ├── import_circular_dependency │ │ │ ├── root.proto │ │ │ ├── other.proto │ │ │ └── import_circular_dependency.proto │ │ ├── import_root_package_from_child │ │ │ ├── root.proto │ │ │ └── child.proto │ │ ├── stream_stream │ │ │ └── stream_stream.proto │ │ ├── import_capitalized_package │ │ │ ├── capitalized.proto │ │ │ └── test.proto │ │ ├── manual_validation │ │ │ └── manual_validation.proto │ │ ├── empty_service │ │ │ └── empty_service.proto │ │ ├── import_child_package_from_root │ │ │ ├── child.proto │ │ │ └── import_child_package_from_root.proto │ │ ├── import_child_scoping_rules │ │ │ ├── child.proto │ │ │ ├── import_child_scoping_rules.proto │ │ │ └── package.proto │ │ ├── import_cousin_package │ │ │ ├── cousin.proto │ │ │ └── test.proto │ │ ├── import_cousin_package_same_name │ │ │ ├── cousin.proto │ │ │ └── test.proto │ │ ├── import_child_package_from_package │ │ │ ├── child.proto │ │ │ ├── package_message.proto │ │ │ └── import_child_package_from_package.proto │ │ ├── import_parent_package_from_child │ │ │ ├── parent_package_message.proto │ │ │ └── import_parent_package_from_child.proto │ │ ├── ref │ │ │ ├── ref.proto │ │ │ └── repeatedmessage.proto │ │ ├── import_nested_child_package_from_root │ │ │ ├── child.proto │ │ │ └── import_nested_child_package_from_root.proto │ │ ├── import_service_input_message │ │ │ ├── request_message.proto │ │ │ ├── child_package_request_message.proto │ │ │ └── import_service_input_message.proto │ │ ├── unwrap │ │ │ └── unwrap.proto │ │ ├── any │ │ │ └── any.proto │ │ ├── fixed │ │ │ └── fixed.proto │ │ ├── mapmessage │ │ │ └── mapmessage.proto │ │ ├── regression_414 │ │ │ └── regression_414.proto │ │ ├── repeatedmessage │ │ │ └── repeatedmessage.proto │ │ ├── googletypes_struct │ │ │ └── googletypes_struct.proto │ │ ├── repeatedpacked │ │ │ └── repeatedpacked.proto │ │ ├── timestamp_dict_encode │ │ │ └── timestamp_dict_encode.proto │ │ ├── regression_387 │ │ │ └── regression_387.proto │ │ ├── int32 │ │ │ └── int32.proto │ │ ├── casing_message_field_uppercase │ │ │ └── casing_message_field_uppercase.proto │ │ ├── rpc_empty_input_message │ │ │ └── rpc_empty_input_message.proto │ │ ├── casing_inner_class │ │ │ └── casing_inner_class.proto │ │ ├── recursivemessage │ │ │ └── recursivemessage.proto │ │ ├── encoding_decoding │ │ │ └── encoding_decoding.proto │ │ ├── googletypes_service_returns_empty │ │ │ └── googletypes_service_returns_empty.proto │ │ ├── oneof_enum │ │ │ └── oneof_enum.proto │ │ ├── oneof_empty │ │ │ └── oneof_empty.proto │ │ ├── message_wrapping │ │ │ └── message_wrapping.proto │ │ ├── casing │ │ │ └── casing.proto │ │ ├── float │ │ │ └── float.proto │ │ ├── service_uppercase │ │ │ └── service.proto │ │ ├── repeated_duration_timestamp │ │ │ └── repeated_duration_timestamp.proto │ │ ├── field_name_identical_to_type │ │ │ └── field_name_identical_to_type.proto │ │ ├── oneof │ │ │ └── oneof.proto │ │ ├── signed │ │ │ └── signed.proto │ │ ├── proto3_field_presence_oneof │ │ │ └── proto3_field_presence_oneof.proto │ │ ├── googletypes_value │ │ │ └── googletypes_value.proto │ │ ├── google │ │ │ └── google.proto │ │ ├── googletypes │ │ │ └── googletypes.proto │ │ ├── simple_service │ │ │ └── simple_service.proto │ │ ├── validation │ │ │ └── validation.proto │ │ ├── google_impl_behavior_equivalence │ │ │ └── google_impl_behavior_equivalence.proto │ │ ├── compiler_lib │ │ │ └── protobuf.proto │ │ ├── nested │ │ │ └── nested.proto │ │ ├── deprecated │ │ │ └── deprecated.proto │ │ ├── service_separate_packages │ │ │ ├── service.proto │ │ │ └── messages.proto │ │ ├── proto3_field_presence │ │ │ └── proto3_field_presence.proto │ │ ├── googletypes_service_returns_googletype │ │ │ └── googletypes_service_returns_googletype.proto │ │ ├── entry │ │ │ └── entry.proto │ │ ├── example_service │ │ │ └── example_service.proto │ │ ├── grpclib_reflection │ │ │ └── example_service.proto │ │ ├── googletypes_response_embedded │ │ │ └── googletypes_response_embedded.proto │ │ ├── pickling │ │ │ └── pickling.proto │ │ ├── service │ │ │ └── service.proto │ │ ├── googletypes_response │ │ │ └── googletypes_response.proto │ │ ├── oneof_default_value_serialization │ │ │ └── oneof_default_value_serialization.proto │ │ ├── nestedtwice │ │ │ └── nestedtwice.proto │ │ ├── googletypes_request │ │ │ └── googletypes_request.proto │ │ ├── namespace_keywords │ │ │ └── namespace_keywords.proto │ │ ├── namespace_builtin_types │ │ │ └── namespace_builtin_types.proto │ │ ├── documentation │ │ │ └── documentation.proto │ │ ├── enum │ │ │ └── enum.proto │ │ └── features │ │ │ └── features.proto │ ├── test_casing.py │ ├── util.py │ └── test_module_validation.py └── src │ └── betterproto2_compiler │ ├── py.typed │ ├── __init__.py │ ├── lib │ ├── py.typed │ ├── google │ │ └── __init__.py │ ├── message_pool.py │ └── __init__.py │ ├── compile │ ├── __init__.py │ └── naming.py │ ├── plugin │ ├── __main__.py │ ├── plugin.bat │ ├── __init__.py │ ├── main.py │ └── compiler.py │ ├── templates │ ├── service_stub.py.j2 │ ├── header.py.j2 │ ├── service_stub_sync.py.j2 │ └── service_stub_async.py.j2 │ ├── settings.py │ ├── known_types │ ├── duration.py │ └── any.py │ └── casing.py ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── documentation.yml │ ├── code-quality.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.md └── README.md /betterproto2/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /betterproto2/src/betterproto2/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /betterproto2/tests/grpc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /betterproto2/tests/streams/load_varint_cutoff.in: -------------------------------------------------------------------------------- 1 | ȁ -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof_empty/test_oneof_empty.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/lib/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/compile/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/bool/bool.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": true 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/googletypes/googletypes-missing.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/lib/google/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/double/double.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 123.45 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof/oneof.json: -------------------------------------------------------------------------------- 1 | { 2 | "pitied": 100 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof/oneof-name.json: -------------------------------------------------------------------------------- 1 | { 2 | "pitier": "Mr. T" 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof/oneof_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "pitier": "Mr. T" 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/double/double-negative.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": -123.45 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof_empty/oneof_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "nothing": {} 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/bytes/bytes.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": "SGVsbG8sIFdvcmxkIQ==" 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof_empty/oneof_empty_maybe1.json: -------------------------------------------------------------------------------- 1 | { 2 | "maybe1": {} 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/proto3_field_presence/proto3_field_presence_default.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof_enum/oneof_enum-enum-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": "PASS" 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof_enum/oneof_enum-enum-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": "RESIGN" 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/casing/casing.json: -------------------------------------------------------------------------------- 1 | { 2 | "camelCase": 1, 3 | "snakeCase": "ONE" 4 | } 5 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/int32/int32.json: -------------------------------------------------------------------------------- 1 | { 2 | "positive": 150, 3 | "negative": -150 4 | } 5 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/repeated/repeated.json: -------------------------------------------------------------------------------- 1 | { 2 | "names": ["one", "two", "three"] 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/ref/ref.json: -------------------------------------------------------------------------------- 1 | { 2 | "greeting": { 3 | "greeting": "hello" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/plugin/__main__.py: -------------------------------------------------------------------------------- 1 | from .main import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/proto3_field_presence_oneof/proto3_field_presence_oneof.json: -------------------------------------------------------------------------------- 1 | { 2 | "nested": {} 3 | } 4 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/plugin/plugin.bat: -------------------------------------------------------------------------------- 1 | @SET plugin_dir=%~dp0 2 | @python -m %plugin_dir% %* -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof_enum/oneof_enum.json: -------------------------------------------------------------------------------- 1 | { 2 | "move": { 3 | "x": 2, 4 | "y": 3 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/plugin/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["main"] 2 | 3 | from .main import main 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/googletypes_struct/googletypes_struct.json: -------------------------------------------------------------------------------- 1 | { 2 | "struct": { 3 | "key": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof_empty/oneof_empty_maybe2.json: -------------------------------------------------------------------------------- 1 | { 2 | "maybe2": { 3 | "sometimes": "now" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/timestamp_dict_encode/timestamp_dict_encode.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts" : "2023-03-15T22:35:51.253277Z" 3 | } -------------------------------------------------------------------------------- /betterproto2/tests/inputs/deprecated/deprecated.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": { 3 | "value": "hello" 4 | }, 5 | "value": 10 6 | } 7 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/map/map.json: -------------------------------------------------------------------------------- 1 | { 2 | "counts": { 3 | "item1": 1, 4 | "item2": 2, 5 | "item3": 3 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/nested2/package.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package nested2.equipment; 4 | 5 | message Weapon {} 6 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/nested/nested.json: -------------------------------------------------------------------------------- 1 | { 2 | "nested": { 3 | "count": 150 4 | }, 5 | "sibling": {}, 6 | "msg": "THIS" 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/bool/bool.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package bool; 4 | 5 | message Test { 6 | bool value = 1; 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/lib/message_pool.py: -------------------------------------------------------------------------------- 1 | import betterproto2 2 | 3 | default_message_pool = betterproto2.MessagePool() 4 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/bytes/bytes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package bytes; 4 | 5 | message Test { 6 | bytes data = 1; 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/signed/signed.json: -------------------------------------------------------------------------------- 1 | { 2 | "signed32": 150, 3 | "negative32": -150, 4 | "string64": "150", 5 | "negative64": "-150" 6 | } 7 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/double/double.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package double; 4 | 5 | message Test { 6 | double count = 1; 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2/tests/streams/delimited_messages.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betterproto/python-betterproto2/HEAD/betterproto2/tests/streams/delimited_messages.in -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/map/map.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package map; 4 | 5 | message Test { 6 | map counts = 1; 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2/src/betterproto2/grpclib/__init__.py: -------------------------------------------------------------------------------- 1 | from .grpclib_client import ServiceStub as ServiceStub 2 | from .grpclib_server import ServiceBase as ServiceBase 3 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_root_sibling/sibling.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_root_sibling; 4 | 5 | message SiblingMessage {} 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/invalid_field/invalid_field.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package invalid_field; 4 | 5 | message Test { 6 | int32 x = 1; 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/repeated/repeated.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package repeated; 4 | 5 | message Test { 6 | repeated string names = 1; 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2/src/betterproto2/validators/__init__.py: -------------------------------------------------------------------------------- 1 | from .proto_types import validate_float32, validate_string 2 | 3 | __all__ = ["validate_float32", "validate_string"] 4 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/fixed/fixed.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": 4294967295, 3 | "bar": -2147483648, 4 | "baz": "18446744073709551615", 5 | "qux": "-9223372036854775808" 6 | } 7 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/repeatedpacked/repeatedpacked.json: -------------------------------------------------------------------------------- 1 | { 2 | "counts": [1, 2, -1, -2], 3 | "signed": ["1", "2", "-1", "-2"], 4 | "fixed": [1.0, 2.7, 3.4] 5 | } 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_packages_same_name/posts_v1.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_packages_same_name.posts.v1; 4 | 5 | message Post {} 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_packages_same_name/users_v1.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_packages_same_name.users.v1; 4 | 5 | message User {} 6 | -------------------------------------------------------------------------------- /betterproto2/tests/streams/dump_varint_negative.expected: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betterproto/python-betterproto2/HEAD/betterproto2/tests/streams/dump_varint_negative.expected -------------------------------------------------------------------------------- /betterproto2/tests/streams/dump_varint_positive.expected: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betterproto/python-betterproto2/HEAD/betterproto2/tests/streams/dump_varint_positive.expected -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_circular_dependency/root.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_circular_dependency; 4 | 5 | message RootPackageMessage {} 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_root_package_from_child/root.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_root_package_from_child; 4 | 5 | message RootMessage {} 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/stream_stream/stream_stream.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package stream_stream; 4 | 5 | message Message { 6 | string body = 1; 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2/tests/streams/message_dump_file_single.expected: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betterproto/python-betterproto2/HEAD/betterproto2/tests/streams/message_dump_file_single.expected -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_capitalized_package/capitalized.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_capitalized_package.Capitalized; 4 | 5 | message Message {} 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/manual_validation/manual_validation.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package manual_validation; 4 | 5 | message Msg { 6 | uint32 x = 1; 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2/tests/streams/message_dump_file_multiple.expected: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betterproto/python-betterproto2/HEAD/betterproto2/tests/streams/message_dump_file_multiple.expected -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/empty_service/empty_service.proto: -------------------------------------------------------------------------------- 1 | /* Empty service without comments */ 2 | syntax = "proto3"; 3 | 4 | package empty_service; 5 | 6 | service Test {} 7 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_child_package_from_root/child.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_child_package_from_root.childpackage; 4 | 5 | message Message {} 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_child_scoping_rules/child.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_child_scoping_rules.aaa.bbb.ccc.ddd; 4 | 5 | message ChildMessage {} 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_cousin_package/cousin.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_cousin_package.cousin.cousin_subpackage; 4 | 5 | message CousinMessage {} 6 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/enum/enum.json: -------------------------------------------------------------------------------- 1 | { 2 | "choice": "FOUR", 3 | "choices": [ 4 | "ZERO", 5 | "ONE", 6 | "THREE", 7 | "FOUR", 8 | "MINUS_ONE" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/googletypes/googletypes.json: -------------------------------------------------------------------------------- 1 | { 2 | "maybe": false, 3 | "ts": "1972-01-01T10:00:20.021Z", 4 | "duration": "1.200s", 5 | "important": 10, 6 | "empty": {} 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/mapmessage/mapmessage.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": { 3 | "foo": { 4 | "count": 1 5 | }, 6 | "bar": { 7 | "count": 2 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_cousin_package_same_name/cousin.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_cousin_package_same_name.cousin.subpackage; 4 | 5 | message CousinMessage {} 6 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/repeatedmessage/repeatedmessage.json: -------------------------------------------------------------------------------- 1 | { 2 | "greetings": [ 3 | { 4 | "greeting": "hello" 5 | }, 6 | { 7 | "greeting": "hi" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_child_package_from_package/child.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_child_package_from_package.package.childpackage; 4 | 5 | message ChildMessage {} 6 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/field_name_identical_to_type/field_name_identical_to_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "int": 26, 3 | "float": 26.0, 4 | "str": "value-for-str", 5 | "bytes": "001a", 6 | "bool": true 7 | } -------------------------------------------------------------------------------- /betterproto2/tests/inputs/repeated_duration_timestamp/repeated_duration_timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "times": ["1972-01-01T10:00:20.021Z", "1972-01-01T10:00:20.021Z"], 3 | "durations": ["1.200s", "1.200s"] 4 | } 5 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_parent_package_from_child/parent_package_message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_parent_package_from_child.parent; 4 | 5 | message ParentPackageMessage {} 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/ref/ref.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ref; 4 | 5 | import "repeatedmessage.proto"; 6 | 7 | message Test { 8 | repeatedmessage.Sub greeting = 1; 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_nested_child_package_from_root/child.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_nested_child_package_from_root.package.child.otherchild; 4 | 5 | message ChildMessage {} 6 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_service_input_message/request_message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_service_input_message; 4 | 5 | message RequestMessage { 6 | int32 argument = 1; 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/unwrap/unwrap.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package unwrap; 4 | 5 | message NestedMessage { 6 | int32 x = 1; 7 | } 8 | 9 | message Message { 10 | NestedMessage x = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/any/any.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package any; 4 | 5 | import "google/protobuf/any.proto"; 6 | 7 | message Person { 8 | string first_name = 1; 9 | string last_name = 2; 10 | } 11 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/fixed/fixed.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package fixed; 4 | 5 | message Test { 6 | fixed32 foo = 1; 7 | sfixed32 bar = 2; 8 | fixed64 baz = 3; 9 | sfixed64 qux = 4; 10 | } 11 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/googletypes_value/googletypes_value.json: -------------------------------------------------------------------------------- 1 | { 2 | "value1": "hello world", 3 | "value2": true, 4 | "value3": 1, 5 | "value4": null, 6 | "value5": [ 7 | 1, 8 | 2, 9 | 3 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/proto3_field_presence/proto3_field_presence_missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "test1": 0, 3 | "test2": false, 4 | "test3": "", 5 | "test4": "", 6 | "test6": "A", 7 | "test7": "0", 8 | "test8": 0 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/mapmessage/mapmessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mapmessage; 4 | 5 | message Test { 6 | map items = 1; 7 | } 8 | 9 | message Nested { 10 | int32 count = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/regression_414/regression_414.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package regression_414; 4 | 5 | message Test { 6 | bytes body = 1; 7 | bytes auth = 2; 8 | repeated bytes signatures = 3; 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_service_input_message/child_package_request_message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_service_input_message.child; 4 | 5 | message ChildRequestMessage { 6 | int32 child_argument = 1; 7 | } 8 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/ref/repeatedmessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package repeatedmessage; 4 | 5 | message Test { 6 | repeated Sub greetings = 1; 7 | } 8 | 9 | message Sub { 10 | string greeting = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_child_scoping_rules/import_child_scoping_rules.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_child_scoping_rules; 4 | 5 | import "package.proto"; 6 | 7 | message Test { 8 | aaa.bbb.Msg msg = 1; 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/repeatedmessage/repeatedmessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package repeatedmessage; 4 | 5 | message Test { 6 | repeated Sub greetings = 1; 7 | } 8 | 9 | message Sub { 10 | string greeting = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/googletypes_struct/googletypes_struct.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package googletypes_struct; 4 | 5 | import "google/protobuf/struct.proto"; 6 | 7 | message Test { 8 | google.protobuf.Struct struct = 1; 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_circular_dependency/other.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package import_circular_dependency.other; 3 | 4 | import "root.proto"; 5 | 6 | message OtherPackageMessage { 7 | RootPackageMessage rootPackageMessage = 1; 8 | } 9 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/repeatedpacked/repeatedpacked.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package repeatedpacked; 4 | 5 | message Test { 6 | repeated int32 counts = 1; 7 | repeated sint64 signed = 2; 8 | repeated double fixed = 3; 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/float/float.json: -------------------------------------------------------------------------------- 1 | { 2 | "positive": "Infinity", 3 | "negative": "-Infinity", 4 | "nan": "NaN", 5 | "three": 3.0, 6 | "threePointOneFour": 3.14, 7 | "negThree": -3.0, 8 | "negThreePointOneFour": -3.14 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/timestamp_dict_encode/timestamp_dict_encode.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package timestamp_dict_encode; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | message Test { 8 | google.protobuf.Timestamp ts = 1; 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/recursivemessage/recursivemessage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Zues", 3 | "child": { 4 | "name": "Hercules" 5 | }, 6 | "intermediate": { 7 | "child": { 8 | "name": "Douglas Adams" 9 | }, 10 | "number": 42 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/regression_387/regression_387.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package regression_387; 4 | 5 | message Test { 6 | uint64 id = 1; 7 | } 8 | 9 | message ParentElement { 10 | string name = 1; 11 | repeated Test elems = 2; 12 | } 13 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/int32/int32.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package int32; 4 | 5 | // Some documentation about the Test message. 6 | message Test { 7 | // Some documentation about the count. 8 | int32 positive = 1; 9 | int32 negative = 2; 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | name: 2 | description: 3 | contact_links: 4 | - name: For questions about the library 5 | about: Support questions are better answered in our Slack group. 6 | url: https://join.slack.com/t/betterproto/shared_invite/zt-f0n0uolx-iN8gBNrkPxtKHTLpG3o1OQ 7 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_child_package_from_package/package_message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_child_package_from_package.package; 4 | 5 | import "child.proto"; 6 | 7 | message PackageMessage { 8 | package.childpackage.ChildMessage c = 1; 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/nestedtwice/nestedtwice.json: -------------------------------------------------------------------------------- 1 | { 2 | "top": { 3 | "name": "double-nested", 4 | "middle": { 5 | "bottom": [{"foo": "hello"}], 6 | "enumBottom": ["A"], 7 | "topMiddleBottom": [{"a": "hello"}], 8 | "bar": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package casing_message_field_uppercase; 4 | 5 | message Test { 6 | int32 UPPERCASE = 1; 7 | int32 UPPERCASE_V2 = 2; 8 | int32 UPPER_CAMEL_CASE = 3; 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_nested_child_package_from_root/import_nested_child_package_from_root.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_nested_child_package_from_root; 4 | 5 | import "child.proto"; 6 | 7 | message Test { 8 | package.child.otherchild.ChildMessage child = 1; 9 | } 10 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/rpc_empty_input_message/rpc_empty_input_message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package rpc_empty_input_message; 4 | 5 | message Test {} 6 | 7 | message Response { 8 | int32 v = 1; 9 | } 10 | 11 | service Service { 12 | rpc read(Test) returns (Response); 13 | } 14 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_root_package_from_child/child.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_root_package_from_child.child; 4 | 5 | import "root.proto"; 6 | 7 | // Verify that we can import root message from child package 8 | 9 | message Test { 10 | RootMessage message = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/casing_inner_class/casing_inner_class.proto: -------------------------------------------------------------------------------- 1 | // https://github.com/danielgtaylor/python-betterproto/issues/344 2 | syntax = "proto3"; 3 | 4 | package casing_inner_class; 5 | 6 | message Test { 7 | message inner_class { 8 | sint32 old_exp = 1; 9 | } 10 | inner_class inner = 2; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_cousin_package/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_cousin_package.test.subpackage; 4 | 5 | import "cousin.proto"; 6 | 7 | // Verify that we can import message unrelated to us 8 | 9 | message Test { 10 | cousin.cousin_subpackage.CousinMessage message = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/recursivemessage/recursivemessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package recursivemessage; 4 | 5 | message Test { 6 | string name = 1; 7 | Test child = 2; 8 | Intermediate intermediate = 3; 9 | } 10 | 11 | message Intermediate { 12 | int32 number = 1; 13 | Test child = 2; 14 | } 15 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/proto3_field_presence/proto3_field_presence.json: -------------------------------------------------------------------------------- 1 | { 2 | "test1": 128, 3 | "test2": true, 4 | "test3": "A value", 5 | "test4": "aGVsbG8=", 6 | "test5": { 7 | "test": "Hello" 8 | }, 9 | "test6": "B", 10 | "test7": "8589934592", 11 | "test8": 2.5, 12 | "test9": "2022-01-24T12:12:42Z" 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .DS_Store 3 | .env 4 | .vscode/settings.json 5 | .mypy_cache 6 | .pytest_cache 7 | .python-version 8 | build/ 9 | */tests/output_* 10 | */tests/outputs/* 11 | **/__pycache__ 12 | dist 13 | **/*.egg-info 14 | output 15 | .idea 16 | .DS_Store 17 | .tox 18 | .venv 19 | .asv 20 | venv 21 | .devcontainer 22 | .ruff_cache 23 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/encoding_decoding/encoding_decoding.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package encoding_decoding; 4 | 5 | message Overflow32 { 6 | uint32 uint = 1; 7 | int32 int = 2; 8 | sint32 sint = 3; 9 | } 10 | 11 | message Overflow64 { 12 | uint64 uint = 1; 13 | int64 int = 2; 14 | sint64 sint = 3; 15 | } 16 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_root_sibling/import_root_sibling.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_root_sibling; 4 | 5 | import "sibling.proto"; 6 | 7 | // Tests generated imports when a message in the root package refers to another message in the root package 8 | 9 | message Test { 10 | SiblingMessage sibling = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/googletypes_service_returns_empty/googletypes_service_returns_empty.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package googletypes_service_returns_empty; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service Test { 8 | rpc Send(RequestMessage) returns (google.protobuf.Empty) {} 9 | } 10 | 11 | message RequestMessage {} 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_capitalized_package/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_capitalized_package; 4 | 5 | import "capitalized.proto"; 6 | 7 | // Tests that we can import from a package with a capital name, that looks like a nested type, but isn't. 8 | 9 | message Test { 10 | Capitalized.Message message = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2/docs/api.md: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | The following document outlines betterproto's api. These classes should not be extended manually. 5 | 6 | 7 | ## Message 8 | 9 | ::: betterproto2.Message 10 | 11 | ::: betterproto2.which_one_of 12 | 13 | 14 | ## Enumerations 15 | 16 | ::: betterproto2.Enum 17 | 18 | ::: betterproto2.Casing 19 | -------------------------------------------------------------------------------- /betterproto2/tests/test_unwrap.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_unwrap() -> None: 5 | from betterproto2 import unwrap 6 | from tests.outputs.unwrap.unwrap import Message, NestedMessage 7 | 8 | with pytest.raises(ValueError): 9 | unwrap(Message().x) 10 | 11 | msg = Message(x=NestedMessage()) 12 | assert msg.x == unwrap(msg.x) 13 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/oneof_enum/oneof_enum.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package oneof_enum; 4 | 5 | message Test { 6 | oneof action { 7 | Signal signal = 1; 8 | Move move = 2; 9 | } 10 | } 11 | 12 | enum Signal { 13 | PASS = 0; 14 | RESIGN = 1; 15 | } 16 | 17 | message Move { 18 | int32 x = 1; 19 | int32 y = 2; 20 | } 21 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/oneof_empty/oneof_empty.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package oneof_empty; 4 | 5 | message Nothing {} 6 | 7 | message MaybeNothing { 8 | string sometimes = 42; 9 | } 10 | 11 | message Test { 12 | oneof empty { 13 | Nothing nothing = 1; 14 | MaybeNothing maybe1 = 2; 15 | MaybeNothing maybe2 = 3; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_child_package_from_root/import_child_package_from_root.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_child_package_from_root; 4 | 5 | import "child.proto"; 6 | 7 | // Tests generated imports when a message in root refers to a message in a child package. 8 | 9 | message Test { 10 | childpackage.Message child = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/message_wrapping/message_wrapping.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package message_wrapping; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/wrappers.proto"; 7 | 8 | message MapMessage { 9 | map map1 = 1; 10 | map map2 = 2; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # sources: protobuf.proto 3 | # plugin: python-betterproto2 4 | # This file has been @generated 5 | 6 | __all__ = () 7 | 8 | 9 | import betterproto2 10 | 11 | _COMPILER_VERSION = "0.9.0" 12 | betterproto2.check_compiler_version(_COMPILER_VERSION) 13 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_cousin_package_same_name/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_cousin_package_same_name.test.subpackage; 4 | 5 | import "cousin.proto"; 6 | 7 | // Verify that we can import a message unrelated to us, in a subpackage with the same name as us. 8 | 9 | message Test { 10 | cousin.subpackage.CousinMessage message = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2/tests/test_message_wraping.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | def test_message_wrapping_map(): 5 | from tests.outputs.message_wrapping.message_wrapping import MapMessage 6 | 7 | msg = MapMessage(map1={"key": 12.0}, map2={"key": datetime.timedelta(seconds=1)}) 8 | 9 | bytes(msg) 10 | 11 | assert msg.to_dict() == {"map1": {"key": 12.0}, "map2": {"key": "1s"}} 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/casing/casing.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package casing; 4 | 5 | enum my_enum { 6 | ZERO = 0; 7 | ONE = 1; 8 | TWO = 2; 9 | } 10 | 11 | message Test { 12 | int32 camelCase = 1; 13 | my_enum snake_case = 2; 14 | snake_case_message snake_case_message = 3; 15 | int32 UPPERCASE = 4; 16 | } 17 | 18 | message snake_case_message {} 19 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/service_uppercase/test_service.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from tests.util import requires_grpclib # noqa: F401 4 | 5 | 6 | def test_parameters(requires_grpclib): 7 | from tests.outputs.service_uppercase.service_uppercase import TestStub 8 | 9 | sig = inspect.signature(TestStub.do_thing) 10 | assert len(sig.parameters) == 5, "Expected 5 parameters" 11 | -------------------------------------------------------------------------------- /betterproto2/tests/test_oneof_pattern_matching.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.skipif( 7 | sys.version_info < (3, 10), 8 | reason="pattern matching is only supported in python3.10+", 9 | ) 10 | def test_oneof_pattern_matching(): 11 | from tests.oneof_pattern_matching import test_oneof_pattern_matching 12 | 13 | test_oneof_pattern_matching() 14 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/float/float.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package float; 4 | 5 | // Some documentation about the Test message. 6 | message Test { 7 | double positive = 1; 8 | double negative = 2; 9 | double nan = 3; 10 | double three = 4; 11 | double three_point_one_four = 5; 12 | double neg_three = 6; 13 | double neg_three_point_one_four = 7; 14 | } 15 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/service_uppercase/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package service_uppercase; 4 | 5 | message DoTHINGRequest { 6 | string name = 1; 7 | repeated string comments = 2; 8 | } 9 | 10 | message DoTHINGResponse { 11 | repeated string names = 1; 12 | } 13 | 14 | service Test { 15 | rpc DoThing(DoTHINGRequest) returns (DoTHINGResponse); 16 | } 17 | -------------------------------------------------------------------------------- /betterproto2/src/betterproto2/_types.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, TypeVar 2 | 3 | if TYPE_CHECKING: 4 | from grpclib._typing import IProtoMessage # type: ignore[reportPrivateImportUsage] 5 | 6 | from . import Message 7 | 8 | # Bound type variable to allow methods to return `self` of subclasses 9 | T = TypeVar("T", bound="Message") 10 | ST = TypeVar("ST", bound="IProtoMessage") 11 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/repeated_duration_timestamp/test_repeated_duration_timestamp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from tests.outputs.repeated_duration_timestamp.repeated_duration_timestamp import Test 4 | 5 | 6 | def test_roundtrip(): 7 | message = Test() 8 | message.times = [datetime.now(), datetime.now()] 9 | message.durations = [timedelta(), timedelta()] 10 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/repeated_duration_timestamp/repeated_duration_timestamp.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package repeated_duration_timestamp; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/timestamp.proto"; 7 | 8 | message Test { 9 | repeated google.protobuf.Timestamp times = 1; 10 | repeated google.protobuf.Duration durations = 2; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_child_package_from_package/import_child_package_from_package.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_child_package_from_package; 4 | 5 | import "package_message.proto"; 6 | 7 | // Tests generated imports when a message in a package refers to a message in a nested child package. 8 | 9 | message Test { 10 | package.PackageMessage message = 1; 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_packages_same_name/import_packages_same_name.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_packages_same_name; 4 | 5 | import "posts_v1.proto"; 6 | import "users_v1.proto"; 7 | 8 | // Tests generated message can correctly reference two packages with the same leaf-name 9 | 10 | message Test { 11 | users.v1.User user = 1; 12 | posts.v1.Post post = 2; 13 | } 14 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/regression_387/test_regression_387.py: -------------------------------------------------------------------------------- 1 | from tests.outputs.regression_387.regression_387 import ParentElement, Test 2 | 3 | 4 | def test_regression_387(): 5 | el = ParentElement(name="test", elems=[Test(id=0), Test(id=42)]) 6 | binary = bytes(el) 7 | decoded = ParentElement.parse(binary) 8 | assert decoded == el 9 | assert decoded.elems == [Test(id=0), Test(id=42)] 10 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/field_name_identical_to_type/field_name_identical_to_type.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package field_name_identical_to_type; 4 | 5 | // Tests that messages may contain fields with names that are identical to their python types (PR #294) 6 | 7 | message Test { 8 | int32 int = 1; 9 | float float = 2; 10 | string str = 3; 11 | bytes bytes = 4; 12 | bool bool = 5; 13 | } 14 | -------------------------------------------------------------------------------- /betterproto2/tests/test_map.py: -------------------------------------------------------------------------------- 1 | # from tests.output_betterproto.map import Enum, Test 2 | 3 | 4 | def test_map(): 5 | # msg = Test(counts={"a": 1, "b": 2}) 6 | # assert msg == Test.parse(bytes(msg)) 7 | # assert msg == Test.from_dict(msg.to_dict()) 8 | 9 | pass 10 | # msg = Test(map_enum={1: Enum.ONE, 2: Enum.TWO}) 11 | # assert msg == Test.parse(bytes(msg)) 12 | # assert msg == Test.from_dict(msg.to_dict()) 13 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/nested2/nested2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package nested2; 4 | 5 | import "package.proto"; 6 | 7 | message Game { 8 | message Player { 9 | enum Race { 10 | human = 0; 11 | orc = 1; 12 | } 13 | } 14 | } 15 | 16 | message Test { 17 | Game game = 1; 18 | Game.Player GamePlayer = 2; 19 | Game.Player.Race GamePlayerRace = 3; 20 | equipment.Weapon Weapon = 4; 21 | } 22 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/oneof/oneof.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package oneof; 4 | 5 | message MixedDrink { 6 | int32 shots = 1; 7 | } 8 | 9 | message Test { 10 | oneof foo { 11 | int32 pitied = 1; 12 | string pitier = 2; 13 | } 14 | 15 | int32 just_a_regular_field = 3; 16 | 17 | oneof bar { 18 | int32 drinks = 11; 19 | string bar_name = 12; 20 | MixedDrink mixed_drink = 13; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/signed/signed.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package signed; 4 | 5 | message Test { 6 | // todo: rename fields after fixing bug where 'signed_32_positive' will map to 'signed_32Positive' as output json 7 | sint32 signed32 = 1; // signed_32_positive 8 | sint32 negative32 = 2; // signed_32_negative 9 | sint64 string64 = 3; // signed_64_positive 10 | sint64 negative64 = 4; // signed_64_negative 11 | } 12 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_child_scoping_rules/package.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_child_scoping_rules.aaa.bbb; 4 | 5 | import "child.proto"; 6 | 7 | message Msg { 8 | .import_child_scoping_rules.aaa.bbb.ccc.ddd.ChildMessage a = 1; 9 | import_child_scoping_rules.aaa.bbb.ccc.ddd.ChildMessage b = 2; 10 | aaa.bbb.ccc.ddd.ChildMessage c = 3; 11 | bbb.ccc.ddd.ChildMessage d = 4; 12 | ccc.ddd.ChildMessage e = 5; 13 | } 14 | -------------------------------------------------------------------------------- /betterproto2/tests/test_mapmessage.py: -------------------------------------------------------------------------------- 1 | from tests.outputs.mapmessage.mapmessage import ( 2 | Nested, 3 | Test, 4 | ) 5 | 6 | 7 | def test_mapmessage_to_dict_preserves_message(): 8 | message = Test( 9 | items={ 10 | "test": Nested( 11 | count=1, 12 | ) 13 | } 14 | ) 15 | 16 | message.to_dict() 17 | 18 | assert isinstance(message.items["test"], Nested), "Wrong nested type after to_dict" 19 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_parent_package_from_child/import_parent_package_from_child.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_parent_package_from_child.parent.child; 4 | 5 | import "parent_package_message.proto"; 6 | 7 | // Tests generated imports when a message refers to a message defined in its parent package 8 | 9 | message Test { 10 | ParentPackageMessage message_implicit = 1; 11 | parent.ParentPackageMessage message_explicit = 2; 12 | } 13 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/proto3_field_presence_oneof/proto3_field_presence_oneof.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto3_field_presence_oneof; 4 | 5 | message Test { 6 | oneof kind { 7 | Nested nested = 1; 8 | WithOptional with_optional = 2; 9 | } 10 | } 11 | 12 | message InnerNested { 13 | optional bool a = 1; 14 | } 15 | 16 | message Nested { 17 | InnerNested inner = 1; 18 | } 19 | 20 | message WithOptional { 21 | optional bool b = 2; 22 | } 23 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/regression_414/test_regression_414.py: -------------------------------------------------------------------------------- 1 | from tests.outputs.regression_414.regression_414 import Test 2 | 3 | 4 | def test_full_cycle(): 5 | body = bytes([0, 1]) 6 | auth = bytes([2, 3]) 7 | sig = [b""] 8 | 9 | obj = Test(body=body, auth=auth, signatures=sig) 10 | 11 | decoded = Test.parse(bytes(obj)) 12 | assert decoded == obj 13 | assert decoded.body == body 14 | assert decoded.auth == auth 15 | assert decoded.signatures == sig 16 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/googletypes_value/googletypes_value.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package googletypes_value; 4 | 5 | import "google/protobuf/struct.proto"; 6 | 7 | // Tests that fields of type google.protobuf.Value can contain arbitrary JSON-values. 8 | 9 | message Test { 10 | google.protobuf.Value value1 = 1; 11 | google.protobuf.Value value2 = 2; 12 | google.protobuf.Value value3 = 3; 13 | google.protobuf.Value value4 = 4; 14 | google.protobuf.Value value5 = 5; 15 | } 16 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/google/google.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/any.proto"; 4 | import "google/protobuf/api.proto"; 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/empty.proto"; 7 | import "google/protobuf/field_mask.proto"; 8 | import "google/protobuf/source_context.proto"; 9 | import "google/protobuf/struct.proto"; 10 | import "google/protobuf/timestamp.proto"; 11 | import "google/protobuf/type.proto"; 12 | import "google/protobuf/wrappers.proto"; 13 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/googletypes/googletypes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package googletypes; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/empty.proto"; 7 | import "google/protobuf/timestamp.proto"; 8 | import "google/protobuf/wrappers.proto"; 9 | 10 | message Test { 11 | google.protobuf.BoolValue maybe = 1; 12 | google.protobuf.Timestamp ts = 2; 13 | google.protobuf.Duration duration = 3; 14 | google.protobuf.Int32Value important = 4; 15 | google.protobuf.Empty empty = 5; 16 | } 17 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/simple_service/simple_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package simple_service; 4 | 5 | message Request { 6 | int32 value = 1; 7 | } 8 | 9 | message Response { 10 | string message = 1; 11 | } 12 | 13 | service SimpleService { 14 | rpc GetUnaryUnary(Request) returns (Response); 15 | 16 | rpc GetUnaryStream(Request) returns (stream Response); 17 | 18 | rpc GetStreamUnary(stream Request) returns (Response); 19 | 20 | rpc GetStreamStream(stream Request) returns (stream Response); 21 | } 22 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/validation/validation.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package validation; 4 | 5 | message Message { 6 | int32 int32_value = 1; 7 | int64 int64_value = 2; 8 | uint32 uint32_value = 3; 9 | uint64 uint64_value = 4; 10 | sint32 sint32_value = 5; 11 | sint64 sint64_value = 6; 12 | fixed32 fixed32_value = 7; 13 | fixed64 fixed64_value = 8; 14 | sfixed32 sfixed32_value = 9; 15 | sfixed64 sfixed64_value = 10; 16 | 17 | float float_value = 11; 18 | 19 | string string_value = 12; 20 | } 21 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.py: -------------------------------------------------------------------------------- 1 | from tests.output_betterproto.casing_message_field_uppercase import Test 2 | 3 | 4 | def test_message_casing(): 5 | message = Test() 6 | assert hasattr(message, "uppercase"), "UPPERCASE attribute is converted to 'uppercase' in python" 7 | assert hasattr(message, "uppercase_v2"), "UPPERCASE_V2 attribute is converted to 'uppercase_v2' in python" 8 | assert hasattr(message, "upper_camel_case"), "UPPER_CAMEL_CASE attribute is converted to upper_camel_case in python" 9 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/namespace_builtin_types/namespace_builtin_types.json: -------------------------------------------------------------------------------- 1 | { 2 | "int": "value-for-int", 3 | "float": "value-for-float", 4 | "complex": "value-for-complex", 5 | "list": "value-for-list", 6 | "tuple": "value-for-tuple", 7 | "range": "value-for-range", 8 | "str": "value-for-str", 9 | "bytearray": "value-for-bytearray", 10 | "bytes": "value-for-bytes", 11 | "memoryview": "value-for-memoryview", 12 | "set": "value-for-set", 13 | "frozenset": "value-for-frozenset", 14 | "map": "value-for-map", 15 | "bool": "value-for-bool" 16 | } -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/google_impl_behavior_equivalence/google_impl_behavior_equivalence.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package google_impl_behavior_equivalence; 3 | 4 | import "google/protobuf/timestamp.proto"; 5 | 6 | message Foo { 7 | int64 bar = 1; 8 | } 9 | 10 | message Test { 11 | oneof group { 12 | string string = 1; 13 | int64 integer = 2; 14 | Foo foo = 3; 15 | } 16 | } 17 | 18 | message Spam { 19 | google.protobuf.Timestamp ts = 1; 20 | } 21 | 22 | message Request { 23 | Empty foo = 1; 24 | } 25 | 26 | message Empty {} 27 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/compiler_lib/protobuf.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/any.proto"; 4 | import "google/protobuf/api.proto"; 5 | import "google/protobuf/descriptor.proto"; 6 | import "google/protobuf/duration.proto"; 7 | import "google/protobuf/empty.proto"; 8 | import "google/protobuf/field_mask.proto"; 9 | import "google/protobuf/source_context.proto"; 10 | import "google/protobuf/struct.proto"; 11 | import "google/protobuf/timestamp.proto"; 12 | import "google/protobuf/type.proto"; 13 | import "google/protobuf/wrappers.proto"; 14 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/casing_inner_class/test_casing_inner_class.py: -------------------------------------------------------------------------------- 1 | import tests.outputs.casing_inner_class.casing_inner_class as casing_inner_class 2 | 3 | 4 | def test_message_casing_inner_class_name(): 5 | assert hasattr(casing_inner_class, "TestInnerClass"), "Inline defined Message is correctly converted to CamelCase" 6 | 7 | 8 | def test_message_casing_inner_class_attributes(): 9 | message = casing_inner_class.Test(inner=casing_inner_class.TestInnerClass()) 10 | assert hasattr(message.inner, "old_exp"), "Inline defined Message attribute is snake_case" 11 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/nested/nested.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package nested; 4 | 5 | // A test message with a nested message inside of it. 6 | message Test { 7 | // This is the nested type. 8 | message Nested { 9 | // Stores a simple counter. 10 | int32 count = 1; 11 | } 12 | // This is the nested enum. 13 | enum Msg { 14 | NONE = 0; 15 | THIS = 1; 16 | } 17 | 18 | Nested nested = 1; 19 | Sibling sibling = 2; 20 | Sibling sibling2 = 3; 21 | Msg msg = 4; 22 | } 23 | 24 | message Sibling { 25 | int32 foo = 1; 26 | } 27 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/invalid_field/test_invalid_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.util import requires_pydantic # noqa: F401 4 | 5 | 6 | def test_invalid_field(): 7 | from tests.outputs.invalid_field.invalid_field import Test 8 | 9 | with pytest.raises(TypeError): 10 | Test(unknown_field=12) 11 | 12 | 13 | def test_invalid_field_pydantic(requires_pydantic): 14 | from pydantic import ValidationError 15 | 16 | from tests.outputs.invalid_field_pydantic.invalid_field import Test 17 | 18 | with pytest.raises(ValidationError): 19 | Test(unknown_field=12) 20 | -------------------------------------------------------------------------------- /betterproto2/tests/test_nested.py: -------------------------------------------------------------------------------- 1 | def test_nested_from_dict(): 2 | """ 3 | Make sure that from_dict() arguments are passed recursively 4 | """ 5 | from tests.outputs.nested.nested import Test 6 | 7 | data = { 8 | "nested": {"count": 1}, 9 | "sibling": {"foo": 2}, 10 | } 11 | Test.from_dict(data) 12 | 13 | data["bar"] = 3 14 | Test.from_dict(data, ignore_unknown_fields=True) 15 | 16 | data["nested"]["bar"] = 3 17 | Test.from_dict(data, ignore_unknown_fields=True) 18 | 19 | data["sibling"]["bar"] = 4 20 | Test.from_dict(data, ignore_unknown_fields=True) 21 | -------------------------------------------------------------------------------- /betterproto2/docs/index.md: -------------------------------------------------------------------------------- 1 | Home 2 | ==== 3 | 4 | Welcome to betterproto2's documentation! 5 | 6 | betterproto is a protobuf compiler and interpreter. It improves the experience of using 7 | Protobuf and gRPC in Python, by generating readable, understandable, and idiomatic 8 | Python code, using modern language features. 9 | 10 | 11 | ## Features 12 | 13 | - Generated messages are both binary & JSON serializable 14 | - Messages use relevant python types, e.g. ``Enum``, ``datetime`` and ``timedelta`` objects 15 | - ``async``/``await`` support for gRPC Clients and Servers 16 | - Generates modern, readable, idiomatic python code 17 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/deprecated/deprecated.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package deprecated; 4 | 5 | // Some documentation about the Test message. 6 | message Test { 7 | Message message = 1 [deprecated = true]; 8 | int32 value = 2; 9 | message Nested { 10 | int32 nested_value = 1 [deprecated = true]; 11 | } 12 | } 13 | 14 | message Message { 15 | option deprecated = true; 16 | string value = 1; 17 | } 18 | 19 | message Empty {} 20 | 21 | service TestService { 22 | rpc func(Empty) returns (Empty); 23 | rpc deprecated_func(Empty) returns (Empty) { 24 | option deprecated = true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/service_separate_packages/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package service_separate_packages.things.service; 4 | 5 | import "messages.proto"; 6 | 7 | service Test { 8 | rpc DoThing(things.messages.DoThingRequest) returns (things.messages.DoThingResponse); 9 | rpc DoManyThings(stream things.messages.DoThingRequest) returns (things.messages.DoThingResponse); 10 | rpc GetThingVersions(things.messages.GetThingRequest) returns (stream things.messages.GetThingResponse); 11 | rpc GetDifferentThings(stream things.messages.GetThingRequest) returns (stream things.messages.GetThingResponse); 12 | } 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_prs: false 3 | 4 | repos: 5 | - repo: https://github.com/astral-sh/ruff-pre-commit 6 | rev: v0.14.2 7 | hooks: 8 | - id: ruff-format 9 | args: ["--diff", "betterproto2/src", "betterproto2/tests", "betterproto2_compiler/src", "betterproto2_compiler/tests"] 10 | - id: ruff 11 | args: ["betterproto2/src", "betterproto2/tests", "betterproto2_compiler/src", "betterproto2_compiler/tests"] 12 | 13 | - repo: https://github.com/bufbuild/buf 14 | rev: v1.57.2 15 | hooks: 16 | - id: buf-format 17 | files: ^betterproto2_compiler/tests/inputs/.*\.proto$ 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | 5 | ## Checklist 6 | 7 | 8 | 9 | - [ ] If code changes were made then they have been tested. 10 | - [ ] I have updated the documentation to reflect the changes. 11 | - [ ] This PR fixes an issue. 12 | - [ ] This PR adds something new (e.g. new method or parameters). 13 | - [ ] This change has an associated test. 14 | - [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed) 15 | - [ ] This PR is **not** a code change (e.g. documentation, README, ...) 16 | 17 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/proto3_field_presence/proto3_field_presence.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto3_field_presence; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | message InnerTest { 8 | string test = 1; 9 | } 10 | 11 | message Test { 12 | optional uint32 test1 = 1; 13 | optional bool test2 = 2; 14 | optional string test3 = 3; 15 | optional bytes test4 = 4; 16 | optional InnerTest test5 = 5; 17 | optional TestEnum test6 = 6; 18 | optional uint64 test7 = 7; 19 | optional float test8 = 8; 20 | optional google.protobuf.Timestamp test9 = 9; 21 | } 22 | 23 | enum TestEnum { 24 | A = 0; 25 | B = 1; 26 | } 27 | -------------------------------------------------------------------------------- /betterproto2/tests/streams/java/.gitignore: -------------------------------------------------------------------------------- 1 | ### Output ### 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | dependency-reduced-pom.xml 7 | MANIFEST.MF 8 | 9 | ### IntelliJ IDEA ### 10 | .idea/ 11 | *.iws 12 | *.iml 13 | *.ipr 14 | 15 | ### Eclipse ### 16 | .apt_generated 17 | .classpath 18 | .factorypath 19 | .project 20 | .settings 21 | .springBeans 22 | .sts4-cache 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | !**/src/main/**/build/ 32 | !**/src/test/**/build/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | 37 | ### Mac OS ### 38 | .DS_Store -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/compile/naming.py: -------------------------------------------------------------------------------- 1 | from betterproto2_compiler import casing 2 | 3 | 4 | def pythonize_class_name(name: str) -> str: 5 | return casing.pascal_case(name) 6 | 7 | 8 | def pythonize_field_name(name: str) -> str: 9 | return casing.safe_snake_case(name) 10 | 11 | 12 | def pythonize_method_name(name: str) -> str: 13 | return casing.safe_snake_case(name) 14 | 15 | 16 | def pythonize_enum_member_name(name: str, enum_name: str) -> str: 17 | enum_name = casing.snake_case(enum_name).upper() 18 | find = name.find(enum_name) 19 | if find != -1: 20 | name = name[find + len(enum_name) :].strip("_") 21 | return casing.sanitize_name(name) 22 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/googletypes_service_returns_googletype/googletypes_service_returns_googletype.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package googletypes_service_returns_googletype; 4 | 5 | import "google/protobuf/empty.proto"; 6 | import "google/protobuf/struct.proto"; 7 | 8 | // Tests that imports are generated correctly when returning Google well-known types 9 | 10 | service Test { 11 | rpc GetEmpty(RequestMessage) returns (google.protobuf.Empty); 12 | rpc GetStruct(RequestMessage) returns (google.protobuf.Struct); 13 | rpc GetListValue(RequestMessage) returns (google.protobuf.ListValue); 14 | rpc GetValue(RequestMessage) returns (google.protobuf.Value); 15 | } 16 | 17 | message RequestMessage {} 18 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/namespace_keywords/namespace_keywords.json: -------------------------------------------------------------------------------- 1 | { 2 | "False": 1, 3 | "None": 2, 4 | "True": 3, 5 | "and": 4, 6 | "as": 5, 7 | "assert": 6, 8 | "async": 7, 9 | "await": 8, 10 | "break": 9, 11 | "class": 10, 12 | "continue": 11, 13 | "def": 12, 14 | "del": 13, 15 | "elif": 14, 16 | "else": 15, 17 | "except": 16, 18 | "finally": 17, 19 | "for": 18, 20 | "from": 19, 21 | "global": 20, 22 | "if": 21, 23 | "import": 22, 24 | "in": 23, 25 | "is": 24, 26 | "lambda": 25, 27 | "nonlocal": 26, 28 | "not": 27, 29 | "or": 28, 30 | "pass": 29, 31 | "raise": 30, 32 | "return": 31, 33 | "try": 32, 34 | "while": 33, 35 | "with": 34, 36 | "yield": 35 37 | } 38 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_service_input_message/import_service_input_message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_service_input_message; 4 | 5 | import "child_package_request_message.proto"; 6 | import "request_message.proto"; 7 | 8 | // Tests generated service correctly imports the RequestMessage 9 | 10 | service Test { 11 | rpc DoThing(RequestMessage) returns (RequestResponse); 12 | rpc DoThing2(child.ChildRequestMessage) returns (RequestResponse); 13 | rpc DoThing3(Nested.RequestMessage) returns (RequestResponse); 14 | } 15 | 16 | message RequestResponse { 17 | int32 value = 1; 18 | } 19 | 20 | message Nested { 21 | message RequestMessage { 22 | int32 nestedArgument = 1; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/entry/entry.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package entry; 4 | 5 | // This is a minimal example of a repeated message field that caused issues when 6 | // checking whether a message is a map. 7 | // 8 | // During the check wheter a field is a "map", the string "entry" is added to 9 | // the field name, checked against the type name and then further checks are 10 | // made against the nested type of a parent message. In this edge-case, the 11 | // first check would pass even though it shouldn't and that would cause an 12 | // error because the parent type does not have a "nested_type" attribute. 13 | 14 | message Test { 15 | repeated ExportEntry export = 1; 16 | } 17 | 18 | message ExportEntry { 19 | string name = 1; 20 | } 21 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/casing/test_casing.py: -------------------------------------------------------------------------------- 1 | import tests.outputs.casing.casing as casing 2 | from tests.outputs.casing.casing import Test 3 | 4 | 5 | def test_message_attributes(): 6 | message = Test() 7 | assert hasattr(message, "snake_case_message"), "snake_case field name is same in python" 8 | assert hasattr(message, "camel_case"), "CamelCase field is snake_case in python" 9 | assert hasattr(message, "uppercase"), "UPPERCASE field is lowercase in python" 10 | 11 | 12 | def test_message_casing(): 13 | assert hasattr(casing, "SnakeCaseMessage"), "snake_case Message name is converted to CamelCase in python" 14 | 15 | 16 | def test_enum_casing(): 17 | assert hasattr(casing, "MyEnum"), "snake_case Enum name is converted to CamelCase in python" 18 | -------------------------------------------------------------------------------- /betterproto2/tests/test_manual_validation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.util import requires_pydantic # noqa: F401 4 | 5 | 6 | def test_manual_validation(requires_pydantic): 7 | import pydantic 8 | 9 | from tests.outputs.manual_validation_pydantic.manual_validation import Msg 10 | 11 | msg = Msg() 12 | 13 | msg.x = 12 14 | msg._validate() 15 | 16 | msg.x = 2**50 # This is an invalid int32 value 17 | with pytest.raises(pydantic.ValidationError): 18 | msg._validate() 19 | 20 | 21 | def test_manual_validation_non_pydantic(): 22 | from tests.outputs.manual_validation.manual_validation import Msg 23 | 24 | # Validation is not available for non-pydantic messages 25 | with pytest.raises(TypeError): 26 | Msg()._validate() 27 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/nestedtwice/test_nestedtwice.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.outputs.nestedtwice.nestedtwice import ( 4 | Test, 5 | TestTop, 6 | TestTopMiddle, 7 | TestTopMiddleBottom, 8 | TestTopMiddleEnumBottom, 9 | TestTopMiddleTopMiddleBottom, 10 | ) 11 | 12 | 13 | @pytest.mark.parametrize( 14 | ("cls", "expected_comment"), 15 | [ 16 | (Test, "Test doc."), 17 | (TestTopMiddleEnumBottom, "EnumBottom doc."), 18 | (TestTop, "Top doc."), 19 | (TestTopMiddle, "Middle doc."), 20 | (TestTopMiddleTopMiddleBottom, "TopMiddleBottom doc."), 21 | (TestTopMiddleBottom, "Bottom doc."), 22 | ], 23 | ) 24 | def test_comment(cls, expected_comment): 25 | assert cls.__doc__.strip() == expected_comment 26 | -------------------------------------------------------------------------------- /betterproto2/tests/test_version_check.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_check_compiler_version(): 5 | from betterproto2 import __version__, check_compiler_version 6 | 7 | x, y, z = (int(x) for x in __version__.split(".")) 8 | 9 | check_compiler_version(__version__) 10 | check_compiler_version(f"{x}.{y}.{z - 1}") 11 | check_compiler_version(f"{x}.{y}.{z + 1}") 12 | 13 | with pytest.raises(ImportError): 14 | check_compiler_version(f"{x}.{y - 1}.{z}") 15 | 16 | with pytest.raises(ImportError): 17 | check_compiler_version(f"{x}.{y + 1}.{z}") 18 | 19 | with pytest.raises(ImportError): 20 | check_compiler_version(f"{x + 1}.{y}.{z}") 21 | 22 | with pytest.raises(ImportError): 23 | check_compiler_version(f"{x - 1}.{y}.{z}") 24 | -------------------------------------------------------------------------------- /betterproto2/tests/test_encoding_decoding.py: -------------------------------------------------------------------------------- 1 | def test_int_overflow(): 2 | """Make sure that overflows in encoded values are handled correctly.""" 3 | from tests.outputs.encoding_decoding.encoding_decoding import Overflow32, Overflow64 4 | 5 | b = bytes(Overflow64(uint=2**50 + 42)) 6 | msg = Overflow32.parse(b) 7 | assert msg.uint == 42 8 | 9 | b = bytes(Overflow64(int=2**50 + 42)) 10 | msg = Overflow32.parse(b) 11 | assert msg.int == 42 12 | 13 | b = bytes(Overflow64(int=2**50 - 42)) 14 | msg = Overflow32.parse(b) 15 | assert msg.int == -42 16 | 17 | b = bytes(Overflow64(sint=2**50 + 42)) 18 | msg = Overflow32.parse(b) 19 | assert msg.sint == 42 20 | 21 | b = bytes(Overflow64(sint=-(2**50) - 42)) 22 | msg = Overflow32.parse(b) 23 | assert msg.sint == -42 24 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/example_service/example_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package example_service; 4 | 5 | import "google/protobuf/struct.proto"; 6 | 7 | service Test { 8 | rpc ExampleUnaryUnary(ExampleRequest) returns (ExampleResponse); 9 | rpc ExampleUnaryStream(ExampleRequest) returns (stream ExampleResponse); 10 | rpc ExampleStreamUnary(stream ExampleRequest) returns (ExampleResponse); 11 | rpc ExampleStreamStream(stream ExampleRequest) returns (stream ExampleResponse); 12 | } 13 | 14 | message ExampleRequest { 15 | string example_string = 1; 16 | int64 example_integer = 2; 17 | google.protobuf.Struct example_struct = 3; 18 | } 19 | 20 | message ExampleResponse { 21 | string example_string = 1; 22 | int64 example_integer = 2; 23 | google.protobuf.Struct example_struct = 3; 24 | } 25 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/grpclib_reflection/example_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package example_service; 4 | 5 | import "google/protobuf/struct.proto"; 6 | 7 | service Test { 8 | rpc ExampleUnaryUnary(ExampleRequest) returns (ExampleResponse); 9 | rpc ExampleUnaryStream(ExampleRequest) returns (stream ExampleResponse); 10 | rpc ExampleStreamUnary(stream ExampleRequest) returns (ExampleResponse); 11 | rpc ExampleStreamStream(stream ExampleRequest) returns (stream ExampleResponse); 12 | } 13 | 14 | message ExampleRequest { 15 | string example_string = 1; 16 | int64 example_integer = 2; 17 | google.protobuf.Struct example_struct = 3; 18 | } 19 | 20 | message ExampleResponse { 21 | string example_string = 1; 22 | int64 example_integer = 2; 23 | google.protobuf.Struct example_struct = 3; 24 | } 25 | -------------------------------------------------------------------------------- /betterproto2/docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | This page targets the betterproto maintainers. 4 | 5 | ## Recompiling the lib proto files 6 | 7 | After some updates in the compiler, it might be useful to recompile the standard Google proto files used by the 8 | compiler. The output of the `compiler_lib` test should be used. 9 | 10 | !!! warning 11 | These proto files are written with the `proto2` syntax, which is not supported by betterproto. For the compiler to 12 | work, you need to manually patch the generated file to mark the field `oneof_index` in `Field` and 13 | `FieldDescriptorProto` optional. 14 | 15 | In the compiler, you also need to compile the [plugin.proto](https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/compiler/plugin.proto) 16 | file in `src/betterproto2_compiler/lib/google.protobug/compiler/__init__.py`. 17 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/googletypes_response_embedded/googletypes_response_embedded.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package googletypes_response_embedded; 4 | 5 | import "google/protobuf/wrappers.proto"; 6 | 7 | // Tests that wrapped values are supported as part of output message 8 | service Test { 9 | rpc getOutput(Input) returns (Output); 10 | } 11 | 12 | message Input {} 13 | 14 | message Output { 15 | google.protobuf.DoubleValue double_value = 1; 16 | google.protobuf.FloatValue float_value = 2; 17 | google.protobuf.Int64Value int64_value = 3; 18 | google.protobuf.UInt64Value uint64_value = 4; 19 | google.protobuf.Int32Value int32_value = 5; 20 | google.protobuf.UInt32Value uint32_value = 6; 21 | google.protobuf.BoolValue bool_value = 7; 22 | google.protobuf.StringValue string_value = 8; 23 | google.protobuf.BytesValue bytes_value = 9; 24 | } 25 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/pickling/pickling.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pickling; 4 | 5 | import "google/protobuf/any.proto"; 6 | import "google/protobuf/struct.proto"; 7 | 8 | message Test {} 9 | 10 | message Fe { 11 | string abc = 1; 12 | } 13 | 14 | message Fi { 15 | string abc = 1; 16 | } 17 | 18 | message Fo { 19 | string abc = 1; 20 | } 21 | 22 | message NestedData { 23 | map struct_foo = 1; 24 | map map_str_any_bar = 2; 25 | } 26 | 27 | message Complex { 28 | string foo_str = 1; 29 | oneof grp { 30 | Fe fe = 3; 31 | Fi fi = 4; 32 | Fo fo = 5; 33 | } 34 | NestedData nested_data = 6; 35 | map mapping = 7; 36 | } 37 | 38 | message PickledMessage { 39 | bool foo = 1; 40 | int32 bar = 2; 41 | repeated string baz = 3; 42 | } 43 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/service/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package service; 4 | 5 | enum ThingType { 6 | UNKNOWN = 0; 7 | LIVING = 1; 8 | DEAD = 2; 9 | } 10 | 11 | message DoThingRequest { 12 | string name = 1; 13 | repeated string comments = 2; 14 | ThingType type = 3; 15 | } 16 | 17 | message DoThingResponse { 18 | repeated string names = 1; 19 | } 20 | 21 | message GetThingRequest { 22 | string name = 1; 23 | } 24 | 25 | message GetThingResponse { 26 | string name = 1; 27 | int32 version = 2; 28 | } 29 | 30 | service Test { 31 | rpc DoThing(DoThingRequest) returns (DoThingResponse); 32 | rpc DoManyThings(stream DoThingRequest) returns (DoThingResponse); 33 | rpc GetThingVersions(GetThingRequest) returns (stream GetThingResponse); 34 | rpc GetDifferentThings(stream GetThingRequest) returns (stream GetThingResponse); 35 | } 36 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/service_separate_packages/messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package service_separate_packages.things.messages; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/timestamp.proto"; 7 | 8 | message DoThingRequest { 9 | string name = 1; 10 | 11 | // use `repeated` so we can check if `List` is correctly imported 12 | repeated string comments = 2; 13 | 14 | // use google types `timestamp` and `duration` so we can check 15 | // if everything from `datetime` is correctly imported 16 | google.protobuf.Timestamp when = 3; 17 | google.protobuf.Duration duration = 4; 18 | } 19 | 20 | message DoThingResponse { 21 | repeated string names = 1; 22 | } 23 | 24 | message GetThingRequest { 25 | string name = 1; 26 | } 27 | 28 | message GetThingResponse { 29 | string name = 1; 30 | int32 version = 2; 31 | } 32 | -------------------------------------------------------------------------------- /betterproto2/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Betterproto2 2 | site_url: https://betterproto.github.io/ 3 | theme: 4 | name: material 5 | palette: 6 | primary: deep orange 7 | accent: deep orange 8 | 9 | nav: 10 | - index.md 11 | - Getting Started: getting-started.md 12 | - Tutorial: 13 | - Messages: tutorial/messages.md 14 | - Clients: tutorial/clients.md 15 | - API: api.md 16 | - Development: development.md 17 | - Protobuf Descriptors: descriptors.md 18 | 19 | 20 | plugins: 21 | - search 22 | - mkdocstrings: 23 | handlers: 24 | python: 25 | options: 26 | show_source: false 27 | 28 | markdown_extensions: 29 | - admonition 30 | - pymdownx.highlight: 31 | anchor_linenums: true 32 | line_spans: __span 33 | pygments_lang_class: true 34 | - pymdownx.inlinehilite 35 | - pymdownx.snippets 36 | - pymdownx.superfences -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/googletypes_response/googletypes_response.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package googletypes_response; 4 | 5 | import "google/protobuf/wrappers.proto"; 6 | 7 | // Tests that wrapped values can be used directly as return values 8 | 9 | service Test { 10 | rpc GetDouble(Input) returns (google.protobuf.DoubleValue); 11 | rpc GetFloat(Input) returns (google.protobuf.FloatValue); 12 | rpc GetInt64(Input) returns (google.protobuf.Int64Value); 13 | rpc GetUInt64(Input) returns (google.protobuf.UInt64Value); 14 | rpc GetInt32(Input) returns (google.protobuf.Int32Value); 15 | rpc GetUInt32(Input) returns (google.protobuf.UInt32Value); 16 | rpc GetBool(Input) returns (google.protobuf.BoolValue); 17 | rpc GetString(Input) returns (google.protobuf.StringValue); 18 | rpc GetBytes(Input) returns (google.protobuf.BytesValue); 19 | } 20 | 21 | message Input {} 22 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/oneof_default_value_serialization/oneof_default_value_serialization.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package oneof_default_value_serialization; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/timestamp.proto"; 7 | import "google/protobuf/wrappers.proto"; 8 | 9 | message Message { 10 | int64 value = 1; 11 | } 12 | 13 | message NestedMessage { 14 | int64 id = 1; 15 | oneof value_type { 16 | Message wrapped_message_value = 2; 17 | } 18 | } 19 | 20 | message Test { 21 | oneof value_type { 22 | bool bool_value = 1; 23 | int64 int64_value = 2; 24 | google.protobuf.Timestamp timestamp_value = 3; 25 | google.protobuf.Duration duration_value = 4; 26 | Message wrapped_message_value = 5; 27 | NestedMessage wrapped_nested_message_value = 6; 28 | google.protobuf.BoolValue wrapped_bool_value = 7; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/rpc_empty_input_message/test_rpc_empty_input_message.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.util import requires_grpclib # noqa: F401 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_rpc_input_message(requires_grpclib): 8 | from grpclib.testing import ChannelFor 9 | 10 | from tests.outputs.rpc_empty_input_message.rpc_empty_input_message import ( 11 | Response, 12 | ServiceBase, 13 | ServiceStub, 14 | Test, 15 | ) 16 | 17 | class Service(ServiceBase): 18 | async def read(self, test: "Test") -> "Response": 19 | return Response(v=42) 20 | 21 | async with ChannelFor([Service()]) as channel: 22 | client = ServiceStub(channel) 23 | 24 | assert (await client.read(Test())).v == 42 25 | 26 | # Check that we can call the method without providing the message 27 | assert (await client.read()).v == 42 28 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/bool/test_bool.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.util import requires_pydantic # noqa: F401 4 | 5 | 6 | def test_value(): 7 | from tests.outputs.bool.bool import Test 8 | 9 | message = Test() 10 | assert not message.value, "Boolean is False by default" 11 | 12 | 13 | def test_pydantic_no_value(requires_pydantic): 14 | from tests.outputs.bool_pydantic.bool import Test as TestPyd 15 | 16 | message = TestPyd() 17 | assert not message.value, "Boolean is False by default" 18 | 19 | 20 | def test_pydantic_value(requires_pydantic): 21 | from tests.outputs.bool_pydantic.bool import Test as TestPyd 22 | 23 | message = TestPyd(value=False) 24 | assert not message.value 25 | 26 | 27 | def test_pydantic_bad_value(requires_pydantic): 28 | from tests.outputs.bool_pydantic.bool import Test as TestPyd 29 | 30 | with pytest.raises(ValueError): 31 | TestPyd(value=123) 32 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | on: 3 | release: 4 | types: 5 | - published 6 | workflow_dispatch: 7 | permissions: 8 | contents: write 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Configure Git Credentials 15 | run: | 16 | git config user.name github-actions[bot] 17 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 18 | 19 | - name: Install uv 20 | uses: astral-sh/setup-uv@v5 21 | with: 22 | version: "0.7.5" 23 | python-version: "3.10" 24 | 25 | - name: Install betterproto2 26 | working-directory: ./betterproto2 27 | run: uv sync --locked --all-extras --all-groups 28 | 29 | - name: Build and deploy documentation 30 | working-directory: ./betterproto2 31 | run: uv run mkdocs gh-deploy --force -------------------------------------------------------------------------------- /betterproto2/docs/tutorial/clients.md: -------------------------------------------------------------------------------- 1 | # Clients 2 | 3 | !!! warning 4 | Make sure to enable client generation when compiling your code. See [TODO link] 5 | 6 | ## Synchronous clients 7 | 8 | Compile the following proto file in a directory called `example`, with the generation of synchronous clients activated. 9 | 10 | ```proto 11 | syntax = "proto3"; 12 | 13 | message Request {} 14 | message Response {} 15 | 16 | service MyService { 17 | rpc MyRPC(Request) returns (Response); 18 | } 19 | ``` 20 | 21 | The synchronous client can be used as follows: 22 | 23 | ```python 24 | import grpc 25 | 26 | from example import Request, MyServiceStub 27 | 28 | with grpc.insecure_channel("address:port") as channel: 29 | client = MyServiceStub(channel) 30 | 31 | response = client.my_rpc(Request()) 32 | ``` 33 | 34 | ## Asynchronous clients 35 | 36 | ### With grpcio 37 | 38 | !!! warning 39 | No yet supported 40 | 41 | ### With grpclib 42 | 43 | !!! warning 44 | Documentation not yet available 45 | -------------------------------------------------------------------------------- /betterproto2/docs/descriptors.md: -------------------------------------------------------------------------------- 1 | # Google Protobuf Descriptors 2 | 3 | Google's protoc plugin for Python generated DESCRIPTOR fields that enable reflection capabilities in many libraries (e.g. grpc, grpclib, mcap). 4 | 5 | By default, betterproto2 doesn't generate these as it introduces a dependency on `protobuf`. If you're okay with this dependency and want to generate DESCRIPTORs, use the compiler option `python_betterproto2_opt=google_protobuf_descriptors`. 6 | 7 | 8 | ## grpclib Reflection 9 | 10 | In order to properly use reflection right now, you will need to modify the `DescriptorPool` that is used by grpclib's `ServerReflection`. To do so, take a look at the use of `ServerReflection.extend` in the `test_grpclib_reflection` test in https://github.com/vmagamedov/grpclib/blob/master/tests/grpc/test_grpclib_reflection.py 11 | In the future, once https://github.com/vmagamedov/grpclib/pull/204 is merged, you will be able to pass the `default_google_proto_descriptor_pool` into the `ServerReflection.extend` class method. 12 | -------------------------------------------------------------------------------- /betterproto2/src/betterproto2/grpclib/grpclib_server.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from collections.abc import AsyncIterable, Callable 3 | from typing import ( 4 | Any, 5 | ) 6 | 7 | import grpclib 8 | import grpclib.server 9 | 10 | 11 | class ServiceBase(ABC): 12 | """ 13 | Base class for async gRPC servers. 14 | """ 15 | 16 | async def _call_rpc_handler_server_stream( 17 | self, 18 | handler: Callable, 19 | stream: grpclib.server.Stream, 20 | request: Any, 21 | ) -> None: 22 | response_iter = handler(request) 23 | # check if response is actually an AsyncIterator 24 | # this might be false if the method just returns without 25 | # yielding at least once 26 | # in that case, we just interpret it as an empty iterator 27 | if isinstance(response_iter, AsyncIterable): 28 | async for response_message in response_iter: 29 | await stream.send_message(response_message) 30 | else: 31 | response_iter.close() 32 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/nestedtwice/nestedtwice.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package nestedtwice; 4 | 5 | /* Test doc. */ 6 | message Test { 7 | /* Top doc. */ 8 | message Top { 9 | /* Middle doc. */ 10 | message Middle { 11 | /* TopMiddleBottom doc.*/ 12 | message TopMiddleBottom { 13 | // TopMiddleBottom.a doc. 14 | string a = 1; 15 | } 16 | /* EnumBottom doc. */ 17 | enum EnumBottom { 18 | /* EnumBottom.A doc. */ 19 | A = 0; 20 | B = 1; 21 | } 22 | /* Bottom doc. */ 23 | message Bottom { 24 | /* Bottom.foo doc. */ 25 | string foo = 1; 26 | } 27 | reserved 1; 28 | /* Middle.bottom doc. */ 29 | repeated Bottom bottom = 2; 30 | repeated EnumBottom enumBottom = 3; 31 | repeated TopMiddleBottom topMiddleBottom = 4; 32 | bool bar = 5; 33 | } 34 | /* Top.name doc. */ 35 | string name = 1; 36 | Middle middle = 2; 37 | } 38 | /* Test.top doc. */ 39 | Top top = 1; 40 | } 41 | -------------------------------------------------------------------------------- /betterproto2/src/betterproto2/validators/proto_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pydantic validators for Protocol Buffer standard types. 3 | 4 | This module provides validator functions that can be used with Pydantic 5 | to validate Protocol Buffer standard types (int32, int64, sfixed32, etc.) 6 | to ensure they conform to their respective constraints. 7 | 8 | These validators are designed to be used as "after validators", meaning the value 9 | will already be of the correct type and only bounds checking is needed. 10 | """ 11 | 12 | import struct 13 | 14 | 15 | def validate_float32(v: float) -> float: 16 | try: 17 | packed = struct.pack("!f", v) 18 | struct.unpack("!f", packed) 19 | except (struct.error, OverflowError): 20 | raise ValueError(f"Value cannot be encoded as a float: {v}") 21 | 22 | return v 23 | 24 | 25 | def validate_string(v: str) -> str: 26 | try: 27 | v.encode("utf-8").decode("utf-8") 28 | except UnicodeError: 29 | raise ValueError("String contains invalid UTF-8 characters") 30 | return v 31 | -------------------------------------------------------------------------------- /betterproto2/tests/test_all_definition.py: -------------------------------------------------------------------------------- 1 | from tests.util import requires_grpcio, requires_grpclib # noqa: F401 2 | 3 | 4 | def test_all_definition(requires_grpclib, requires_grpcio): 5 | """ 6 | Check that a compiled module defines __all__ with the right value. 7 | 8 | These modules have been chosen since they contain messages, services and enums. 9 | """ 10 | import tests.outputs.enum.enum as enum 11 | import tests.outputs.service.service as service 12 | 13 | assert service.__all__ == ( 14 | "DoThingRequest", 15 | "DoThingResponse", 16 | "GetThingRequest", 17 | "GetThingResponse", 18 | "TestBase", 19 | "TestStub", 20 | "TestSyncStub", 21 | "ThingType", 22 | ) 23 | assert enum.__all__ == ( 24 | "ArithmeticOperator", 25 | "Choice", 26 | "EnumMessage", 27 | "HttpCode", 28 | "NewVersion", 29 | "NewVersionMessage", 30 | "NoStriping", 31 | "OldVersion", 32 | "OldVersionMessage", 33 | "Test", 34 | ) 35 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/proto3_field_presence_oneof/test_proto3_field_presence_oneof.py: -------------------------------------------------------------------------------- 1 | from tests.outputs.proto3_field_presence_oneof.proto3_field_presence_oneof import Nested, Test, WithOptional 2 | 3 | 4 | def test_serialization(): 5 | """Ensure that serialization of fields unset but with explicit field 6 | presence do not bloat the serialized payload with length-delimited fields 7 | with length 0""" 8 | 9 | def test_empty_nested(message: Test) -> None: 10 | # '0a' => tag 1, length delimited 11 | # '00' => length: 0 12 | assert bytes(message) == bytearray.fromhex("0a 00") 13 | 14 | test_empty_nested(Test(nested=Nested())) 15 | test_empty_nested(Test(nested=Nested(inner=None))) 16 | 17 | def test_empty_with_optional(message: Test) -> None: 18 | # '12' => tag 2, length delimited 19 | # '00' => length: 0 20 | assert bytes(message) == bytearray.fromhex("12 00") 21 | 22 | test_empty_with_optional(Test(with_optional=WithOptional())) 23 | test_empty_with_optional(Test(with_optional=WithOptional(b=None))) 24 | -------------------------------------------------------------------------------- /betterproto2/tests/oneof_pattern_matching.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_oneof_pattern_matching(): 5 | from tests.outputs.features.features import IntMsg, OneofMsg 6 | 7 | msg = OneofMsg(y="test1", b="test2") 8 | 9 | match msg: 10 | case OneofMsg(x=int(_)): 11 | pytest.fail("Matched 'bar' instead of 'baz'") 12 | case OneofMsg(y=v): 13 | assert v == "test1" 14 | case _: 15 | pytest.fail("Matched neither 'bar' nor 'baz'") 16 | 17 | match msg: 18 | case OneofMsg(a=IntMsg(_)): 19 | pytest.fail("Matched 'sub' instead of 'abc'") 20 | case OneofMsg(b=v): 21 | assert v == "test2" 22 | case _: 23 | pytest.fail("Matched neither 'sub' nor 'abc'") 24 | 25 | msg.b = None 26 | msg.a = IntMsg(val=1) 27 | 28 | match msg: 29 | case OneofMsg(a=IntMsg(val=v)): 30 | assert v == 1 31 | case OneofMsg(b=str(v)): 32 | pytest.fail("Matched 'abc' instead of 'sub'") 33 | case _: 34 | pytest.fail("Matched neither 'sub' nor 'abc'") 35 | -------------------------------------------------------------------------------- /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | code-quality: 16 | name: Check code formatting and typechecking 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: pre-commit/action@v3.0.1 21 | 22 | - name: Install uv 23 | uses: astral-sh/setup-uv@v5 24 | with: 25 | version: "0.7.5" 26 | python-version: "3.10" 27 | 28 | - name: Install betterproto2 29 | working-directory: ./betterproto2 30 | run: uv sync --locked --all-extras --all-groups 31 | 32 | - name: Install betterproto2_compiler 33 | working-directory: ./betterproto2_compiler 34 | run: uv sync --locked --all-extras --all-groups 35 | 36 | - name: Pyright 37 | working-directory: ./betterproto2 38 | shell: bash 39 | run: uv run poe typecheck 40 | 41 | - name: Pyright 42 | working-directory: ./betterproto2_compiler 43 | shell: bash 44 | run: uv run poe typecheck 45 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/templates/service_stub.py.j2: -------------------------------------------------------------------------------- 1 | class {% filter add_to_all %}{% block class_name %}{% endblock %}{% endfilter %}({% block inherit_from %}{% endblock %}): 2 | {% block service_docstring scoped %} 3 | {% if service.comment %} 4 | """ 5 | {{ service.comment | indent(4) }} 6 | """ 7 | {% elif not service.methods %} 8 | pass 9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block class_content %}{% endblock %} 13 | 14 | {% for method in service.methods %} 15 | {% block method_definition scoped required %}{% endblock %} 16 | {% block method_docstring scoped %} 17 | {% if method.comment %} 18 | """ 19 | {{ method.comment | indent(8) }} 20 | """ 21 | {% endif %} 22 | {% endblock %} 23 | 24 | {% block deprecation_warning scoped %} 25 | {% if method.deprecated %} 26 | warnings.warn("{{ service.py_name }}.{{ method.py_name }} is deprecated", DeprecationWarning) 27 | {% endif %} 28 | {% endblock %} 29 | 30 | {% block method_body scoped required %}{% endblock %} 31 | 32 | {% endfor %} -------------------------------------------------------------------------------- /betterproto2/tests/mocks.py: -------------------------------------------------------------------------------- 1 | from grpclib.client import Channel 2 | 3 | 4 | class MockChannel(Channel): 5 | # noinspection PyMissingConstructor 6 | def __init__(self, responses=None) -> None: 7 | self.responses = responses or [] 8 | self.requests = [] 9 | self._loop = None 10 | 11 | def request(self, route, cardinality, request, response_type, **kwargs): 12 | self.requests.append( 13 | { 14 | "route": route, 15 | "cardinality": cardinality, 16 | "request": request, 17 | "response_type": response_type, 18 | } 19 | ) 20 | return MockStream(self.responses) 21 | 22 | 23 | class MockStream: 24 | def __init__(self, responses: list) -> None: 25 | super().__init__() 26 | self.responses = responses 27 | 28 | async def recv_message(self): 29 | return self.responses.pop(0) 30 | 31 | async def send_message(self, *args, **kwargs): 32 | pass 33 | 34 | async def __aexit__(self, exc_type, exc_val, exc_tb): 35 | return True 36 | 37 | async def __aenter__(self): 38 | return self 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Daniel G. Taylor 4 | Copyright (c) 2024 The betterproto contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /betterproto2/tests/test_timestamp.py: -------------------------------------------------------------------------------- 1 | from datetime import ( 2 | datetime, 3 | timezone, 4 | ) 5 | 6 | import pytest 7 | 8 | from tests.outputs.google.google.protobuf import Timestamp 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "dt", 13 | [ 14 | datetime(2023, 10, 11, 9, 41, 12, tzinfo=timezone.utc), 15 | datetime.now(timezone.utc), 16 | # potential issue with floating point precision: 17 | datetime(2242, 12, 31, 23, 0, 0, 1, tzinfo=timezone.utc), 18 | # potential issue with negative timestamps: 19 | datetime(1969, 12, 31, 23, 0, 0, 1, tzinfo=timezone.utc), 20 | ], 21 | ) 22 | def test_timestamp_to_datetime_and_back(dt: datetime): 23 | """ 24 | Make sure converting a datetime to a protobuf timestamp message 25 | and then back again ends up with the same datetime. 26 | """ 27 | assert Timestamp.from_datetime(dt).to_datetime() == dt 28 | 29 | 30 | def test_invalid_datetime(): 31 | """ 32 | Make sure that a ValueError is raised when trying to convert a naive datetime 33 | to a protobuf timestamp message. 34 | """ 35 | with pytest.raises(ValueError): 36 | Timestamp.from_datetime(datetime.now()) 37 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/googletypes_request/googletypes_request.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package googletypes_request; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/empty.proto"; 7 | import "google/protobuf/timestamp.proto"; 8 | import "google/protobuf/wrappers.proto"; 9 | 10 | // Tests that google types can be used as params 11 | 12 | service Test { 13 | rpc SendDouble(google.protobuf.DoubleValue) returns (Input); 14 | rpc SendFloat(google.protobuf.FloatValue) returns (Input); 15 | rpc SendInt64(google.protobuf.Int64Value) returns (Input); 16 | rpc SendUInt64(google.protobuf.UInt64Value) returns (Input); 17 | rpc SendInt32(google.protobuf.Int32Value) returns (Input); 18 | rpc SendUInt32(google.protobuf.UInt32Value) returns (Input); 19 | rpc SendBool(google.protobuf.BoolValue) returns (Input); 20 | rpc SendString(google.protobuf.StringValue) returns (Input); 21 | rpc SendBytes(google.protobuf.BytesValue) returns (Input); 22 | rpc SendDatetime(google.protobuf.Timestamp) returns (Input); 23 | rpc SendTimedelta(google.protobuf.Duration) returns (Input); 24 | rpc SendEmpty(google.protobuf.Empty) returns (Input); 25 | } 26 | 27 | message Input {} 28 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/namespace_keywords/namespace_keywords.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package namespace_keywords; 4 | 5 | // Tests that messages may contain fields that are Python keywords 6 | // 7 | // Generated with Python 3.7.6 8 | // print('\n'.join(f'string {k} = {i+1};' for i,k in enumerate(keyword.kwlist))) 9 | 10 | message Test { 11 | string False = 1; 12 | string None = 2; 13 | string True = 3; 14 | string and = 4; 15 | string as = 5; 16 | string assert = 6; 17 | string async = 7; 18 | string await = 8; 19 | string break = 9; 20 | string class = 10; 21 | string continue = 11; 22 | string def = 12; 23 | string del = 13; 24 | string elif = 14; 25 | string else = 15; 26 | string except = 16; 27 | string finally = 17; 28 | string for = 18; 29 | string from = 19; 30 | string global = 20; 31 | string if = 21; 32 | string import = 22; 33 | string in = 23; 34 | string is = 24; 35 | string lambda = 25; 36 | string nonlocal = 26; 37 | string not = 27; 38 | string or = 28; 39 | string pass = 29; 40 | string raise = 30; 41 | string return = 31; 42 | string try = 32; 43 | string while = 33; 44 | string with = 34; 45 | string yield = 35; 46 | } 47 | -------------------------------------------------------------------------------- /betterproto2/src/betterproto2/message_pool.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | if TYPE_CHECKING: 4 | from betterproto2 import Message 5 | 6 | 7 | def get_type_url(package_name: str, message_name: str) -> str: 8 | """ 9 | Returns the type URL associated to a protobuf message. 10 | """ 11 | return f"type.googleapis.com/{package_name}.{message_name}" 12 | 13 | 14 | class MessagePool: 15 | """ 16 | Keep track of all the messages that are registered in the application. 17 | 18 | This structure is needed for the `google.protobuf.Any` type to work. 19 | """ 20 | 21 | def __init__(self): 22 | self.url_to_type: dict[str, type[Message]] = {} 23 | self.type_to_url: dict[type[Message], str] = {} 24 | 25 | def register_message(self, package_name: str, message_name: str, message_type: "type[Message]") -> None: 26 | url = get_type_url(package_name, message_name) 27 | 28 | if url in self.url_to_type or message_type in self.type_to_url: 29 | raise RuntimeError(f"the message {package_name}.{message_name} is already registered in the message pool") 30 | 31 | self.url_to_type[url] = message_type 32 | self.type_to_url[message_type] = url 33 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof/test_oneof.py: -------------------------------------------------------------------------------- 1 | import betterproto2 2 | from tests.util import get_test_case_json_data, requires_pydantic # noqa: F401 3 | 4 | 5 | def test_which_count(): 6 | from tests.outputs.oneof.oneof import Test 7 | 8 | message = Test.from_json(get_test_case_json_data("oneof")[0].json) 9 | assert betterproto2.which_one_of(message, "foo") == ("pitied", 100) 10 | 11 | 12 | def test_which_name(): 13 | from tests.outputs.oneof.oneof import Test 14 | 15 | message = Test.from_json(get_test_case_json_data("oneof", "oneof_name.json")[0].json) 16 | assert betterproto2.which_one_of(message, "foo") == ("pitier", "Mr. T") 17 | 18 | 19 | def test_which_count_pyd(requires_pydantic): 20 | from tests.outputs.oneof_pydantic.oneof import Test 21 | 22 | message = Test(pitier="Mr. T", just_a_regular_field=2, bar_name="a_bar") 23 | assert betterproto2.which_one_of(message, "foo") == ("pitier", "Mr. T") 24 | 25 | 26 | def test_oneof_constructor_assign(): 27 | from tests.outputs.oneof.oneof import MixedDrink, Test 28 | 29 | message = Test(mixed_drink=MixedDrink(shots=42)) 30 | field, value = betterproto2.which_one_of(message, "bar") 31 | assert field == "mixed_drink" 32 | assert value.shots == 42 33 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/import_circular_dependency/import_circular_dependency.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package import_circular_dependency; 4 | 5 | import "other.proto"; 6 | import "root.proto"; 7 | 8 | // This test-case verifies support for circular dependencies in the generated python files. 9 | // 10 | // This is important because we generate 1 python file/module per package, rather than 1 file per proto file. 11 | // 12 | // Scenario: 13 | // 14 | // The proto messages depend on each other in a non-circular way: 15 | // 16 | // Test -------> RootPackageMessage <--------------. 17 | // `------------------------------------> OtherPackageMessage 18 | // 19 | // Test and RootPackageMessage are in different files, but belong to the same package (root): 20 | // 21 | // (Test -------> RootPackageMessage) <------------. 22 | // `------------------------------------> OtherPackageMessage 23 | // 24 | // After grouping the packages into single files or modules, a circular dependency is created: 25 | // 26 | // (root: Test & RootPackageMessage) <-------> (other: OtherPackageMessage) 27 | message Test { 28 | RootPackageMessage message = 1; 29 | other.OtherPackageMessage other_value = 2; 30 | } 31 | -------------------------------------------------------------------------------- /betterproto2/src/betterproto2/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Callable 4 | from typing import Any, Generic, TypeVar 5 | 6 | T_co = TypeVar("T_co") 7 | TT_co = TypeVar("TT_co", bound="type[Any]") 8 | 9 | 10 | class classproperty(Generic[TT_co, T_co]): 11 | def __init__(self, func: Callable[[TT_co], T_co]): 12 | self.__func__ = func 13 | 14 | def __get__(self, instance: Any, type: TT_co) -> T_co: 15 | return self.__func__(type) 16 | 17 | 18 | T = TypeVar("T") 19 | 20 | 21 | class staticproperty(Generic[T]): # Should be applied after @staticmethod 22 | def __init__(self, fget: Callable[[], T]) -> None: 23 | self.fget = fget 24 | 25 | def __get__(self, instance: Any, owner: type[Any]) -> T: 26 | return self.fget() 27 | 28 | 29 | def unwrap(x: T | None) -> T: 30 | """ 31 | Unwraps an optional value, returning the value if it exists, or raises a ValueError if the value is None. 32 | 33 | Args: 34 | value (Optional[T]): The optional value to unwrap. 35 | 36 | Returns: 37 | T: The unwrapped value if it exists. 38 | 39 | Raises: 40 | ValueError: If the value is None. 41 | """ 42 | if x is None: 43 | raise ValueError("Can't unwrap a None value") 44 | return x 45 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/namespace_builtin_types/namespace_builtin_types.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package namespace_builtin_types; 4 | 5 | // Tests that messages may contain fields with names that are python types 6 | 7 | message Test { 8 | // https://docs.python.org/2/library/stdtypes.html#numeric-types-int-float-long-complex 9 | string int = 1; 10 | string float = 2; 11 | string complex = 3; 12 | 13 | // https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range 14 | string list = 4; 15 | string tuple = 5; 16 | string range = 6; 17 | 18 | // https://docs.python.org/3/library/stdtypes.html#str 19 | string str = 7; 20 | 21 | // https://docs.python.org/3/library/stdtypes.html#bytearray-objects 22 | string bytearray = 8; 23 | 24 | // https://docs.python.org/3/library/stdtypes.html#bytes-and-bytearray-operations 25 | string bytes = 9; 26 | 27 | // https://docs.python.org/3/library/stdtypes.html#memory-views 28 | string memoryview = 10; 29 | 30 | // https://docs.python.org/3/library/stdtypes.html#set-types-set-frozenset 31 | string set = 11; 32 | string frozenset = 12; 33 | 34 | // https://docs.python.org/3/library/stdtypes.html#dict 35 | string map = 13; 36 | string dict = 14; 37 | 38 | // https://docs.python.org/3/library/stdtypes.html#boolean-values 39 | string bool = 15; 40 | } 41 | -------------------------------------------------------------------------------- /betterproto2/tests/grpc/test_message_enum_descriptors.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.util import requires_protobuf # noqa: F401 4 | 5 | 6 | def test_message_enum_descriptors(requires_protobuf): 7 | from tests.outputs.import_cousin_package_same_name.import_cousin_package_same_name.test.subpackage import Test 8 | 9 | # importing the cousin should cause no descriptor pool errors since the subpackage imports it once already 10 | from tests.outputs.import_cousin_package_same_name_descriptors.import_cousin_package_same_name.cousin.subpackage import ( # noqa: E501 11 | CousinMessage, 12 | ) 13 | from tests.outputs.import_cousin_package_same_name_descriptors.import_cousin_package_same_name.test.subpackage import ( # noqa: E501 14 | Test as TestWithDesc, 15 | ) 16 | 17 | # Normally descriptors are not available as they require protobuf support 18 | # to inteoperate with other libraries. 19 | with pytest.raises(AttributeError): 20 | Test.DESCRIPTOR.full_name 21 | 22 | # But the python_betterproto2_opt=google_protobuf_descriptors option 23 | # will add them in as long as protobuf is depended on. 24 | assert TestWithDesc.DESCRIPTOR.full_name == "import_cousin_package_same_name.test.subpackage.Test" 25 | assert CousinMessage.DESCRIPTOR.full_name == "import_cousin_package_same_name.cousin.subpackage.CousinMessage" 26 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof_enum/test_oneof_enum.py: -------------------------------------------------------------------------------- 1 | import betterproto2 2 | from tests.outputs.oneof_enum.oneof_enum import Move, Signal, Test 3 | from tests.util import get_test_case_json_data 4 | 5 | 6 | def test_which_one_of_returns_enum_with_default_value(): 7 | """ 8 | returns first field when it is enum and set with default value 9 | """ 10 | message = Test.from_json(get_test_case_json_data("oneof_enum", "oneof_enum-enum-0.json")[0].json) 11 | 12 | assert message.move is None 13 | assert message.signal == Signal.PASS 14 | assert betterproto2.which_one_of(message, "action") == ("signal", Signal.PASS) 15 | 16 | 17 | def test_which_one_of_returns_enum_with_non_default_value(): 18 | """ 19 | returns first field when it is enum and set with non default value 20 | """ 21 | message = Test.from_json(get_test_case_json_data("oneof_enum", "oneof_enum-enum-1.json")[0].json) 22 | 23 | assert message.move is None 24 | assert message.signal == Signal.RESIGN 25 | assert betterproto2.which_one_of(message, "action") == ("signal", Signal.RESIGN) 26 | 27 | 28 | def test_which_one_of_returns_second_field_when_set(): 29 | message = Test.from_json(get_test_case_json_data("oneof_enum")[0].json) 30 | assert message.move == Move(x=2, y=3) 31 | assert message.signal is None 32 | assert betterproto2.which_one_of(message, "action") == ("move", Move(x=2, y=3)) 33 | -------------------------------------------------------------------------------- /betterproto2/tests/test_any.py: -------------------------------------------------------------------------------- 1 | def test_any() -> None: 2 | from tests.outputs.any.any import Person 3 | from tests.outputs.any.google.protobuf import Any 4 | 5 | person = Person(first_name="John", last_name="Smith") 6 | 7 | any = Any.pack(person) 8 | 9 | new_any = Any.parse(bytes(any)) 10 | 11 | assert new_any.unpack() == person 12 | 13 | 14 | def test_any_to_dict() -> None: 15 | from tests.outputs.any.any import Person 16 | from tests.outputs.any.google.protobuf import Any 17 | 18 | person = Person(first_name="John", last_name="Smith") 19 | 20 | # TODO test with include defautl value 21 | assert Any().to_dict() == {"@type": ""} 22 | 23 | # Pack an object inside 24 | any = Any.pack(person) 25 | 26 | assert any.to_dict() == { 27 | "@type": "type.googleapis.com/any.Person", 28 | "firstName": "John", 29 | "lastName": "Smith", 30 | } 31 | 32 | assert Any.from_dict(any.to_dict()) == any 33 | assert Any.parse(bytes(any)) == any 34 | 35 | # Pack again in another Any 36 | any2 = Any.pack(any) 37 | 38 | assert any2.to_dict() == { 39 | "@type": "type.googleapis.com/google.protobuf.Any", 40 | "value": {"@type": "type.googleapis.com/any.Person", "firstName": "John", "lastName": "Smith"}, 41 | } 42 | 43 | assert Any.from_dict(any2.to_dict()) == any2 44 | assert Any.parse(bytes(any2)) == any2 45 | -------------------------------------------------------------------------------- /betterproto2/tests/streams/java/src/main/java/betterproto/CompatibilityTest.java: -------------------------------------------------------------------------------- 1 | package betterproto; 2 | 3 | import java.io.IOException; 4 | 5 | public class CompatibilityTest { 6 | public static void main(String[] args) throws IOException { 7 | if (args.length < 2) 8 | throw new RuntimeException("Attempted to run without the required arguments."); 9 | else if (args.length > 2) 10 | throw new RuntimeException( 11 | "Attempted to run with more than the expected number of arguments (>1)."); 12 | 13 | Tests tests = new Tests(args[1]); 14 | 15 | switch (args[0]) { 16 | case "single_varint": 17 | tests.testSingleVarint(); 18 | break; 19 | 20 | case "multiple_varints": 21 | tests.testMultipleVarints(); 22 | break; 23 | 24 | case "single_message": 25 | tests.testSingleMessage(); 26 | break; 27 | 28 | case "multiple_messages": 29 | tests.testMultipleMessages(); 30 | break; 31 | 32 | case "infinite_messages": 33 | tests.testInfiniteMessages(); 34 | break; 35 | 36 | default: 37 | throw new RuntimeException( 38 | "Attempted to run with unknown argument '" + args[0] + "'."); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a feature for this library 3 | labels: ["enhancement"] 4 | 5 | body: 6 | - type: input 7 | attributes: 8 | label: Summary 9 | description: > 10 | What problem is your feature trying to solve? What would become easier or possible if feature was implemented? 11 | validations: 12 | required: true 13 | 14 | - type: dropdown 15 | attributes: 16 | multiple: false 17 | label: What is the feature request for? 18 | options: 19 | - The core library 20 | - RPC handling 21 | - The documentation 22 | validations: 23 | required: true 24 | 25 | - type: textarea 26 | attributes: 27 | label: The Problem 28 | description: > 29 | What problem is your feature trying to solve? 30 | What would become easier or possible if feature was implemented? 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | attributes: 36 | label: The Ideal Solution 37 | description: > 38 | What is your ideal solution to the problem? 39 | What would you like this feature to do? 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | attributes: 45 | label: The Current Solution 46 | description: > 47 | What is the current solution to the problem, if any? 48 | validations: 49 | required: false 50 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/googletypes_response_embedded/test_googletypes_response_embedded.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.util import requires_grpclib # noqa: F401 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_service_passes_through_unwrapped_values_embedded_in_response(requires_grpclib): 8 | """ 9 | We do not not need to implement value unwrapping for embedded well-known types, 10 | as this is already handled by grpclib. This test merely shows that this is the case. 11 | """ 12 | from tests.mocks import MockChannel 13 | from tests.outputs.googletypes_response_embedded.googletypes_response_embedded import Input, Output, TestStub 14 | 15 | output = Output( 16 | double_value=10.0, 17 | float_value=12.0, 18 | int64_value=-13, 19 | uint64_value=14, 20 | int32_value=-15, 21 | uint32_value=16, 22 | bool_value=True, 23 | string_value="string", 24 | bytes_value=bytes(0xFF)[0:4], 25 | ) 26 | 27 | service = TestStub(MockChannel(responses=[output])) 28 | response = await service.get_output(Input()) 29 | 30 | assert response.double_value == 10.0 31 | assert response.float_value == 12.0 32 | assert response.int64_value == -13 33 | assert response.uint64_value == 14 34 | assert response.int32_value == -15 35 | assert response.uint32_value == 16 36 | assert response.bool_value 37 | assert response.string_value == "string" 38 | assert response.bytes_value == bytes(0xFF)[0:4] 39 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/plugin/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | from betterproto2_compiler.lib.google.protobuf.compiler import ( 7 | CodeGeneratorRequest, 8 | ) 9 | from betterproto2_compiler.plugin.parser import generate_code 10 | 11 | 12 | def main() -> None: 13 | """The plugin's main entry point.""" 14 | # Read request message from stdin 15 | data = sys.stdin.buffer.read() 16 | 17 | # Parse request 18 | request = CodeGeneratorRequest.parse(data) 19 | 20 | dump_file = os.getenv("BETTERPROTO_DUMP") 21 | if dump_file: 22 | dump_request(dump_file, request) 23 | 24 | # Generate code 25 | response = generate_code(request) 26 | 27 | # Serialise response message 28 | output = response.SerializeToString() 29 | 30 | # Write to stdout 31 | sys.stdout.buffer.write(output) 32 | 33 | 34 | def dump_request(dump_file: str, request: CodeGeneratorRequest) -> None: 35 | """ 36 | For developers: Supports running plugin.py standalone so its possible to debug it. 37 | Run protoc (or generate.py) with BETTERPROTO_DUMP="yourfile.bin" to write the request to a file. 38 | Then run plugin.py from your IDE in debugging mode, and redirect stdin to the file. 39 | """ 40 | with open(str(dump_file), "wb") as fh: 41 | sys.stderr.write(f"\033[31mWriting input from protoc to: {dump_file}\033[0m\n") 42 | fh.write(request.SerializeToString()) 43 | 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/test_casing.py: -------------------------------------------------------------------------------- 1 | def test_snake_case() -> None: 2 | from betterproto2_compiler.casing import snake_case 3 | 4 | # Simple renaming 5 | assert snake_case("methodName") == "method_name" 6 | assert snake_case("MethodName") == "method_name" 7 | 8 | # Don't break acronyms 9 | assert snake_case("HTTPRequest") == "http_request" 10 | assert snake_case("RequestHTTP") == "request_http" 11 | assert snake_case("HTTPRequest2") == "http_request_2" 12 | assert snake_case("RequestHTTP2") == "request_http_2" 13 | assert snake_case("GetAResponse") == "get_a_response" 14 | 15 | # Split digits 16 | assert snake_case("Get2025Results") == "get_2025_results" 17 | assert snake_case("Get10yResults") == "get_10y_results" 18 | 19 | # If the name already contains an underscore or is lowercase, don't change it at all. 20 | # There is a risk of breaking names otherwise. 21 | assert snake_case("aaa_123_bbb") == "aaa_123_bbb" 22 | assert snake_case("aaa_123bbb") == "aaa_123bbb" 23 | assert snake_case("aaa123_bbb") == "aaa123_bbb" 24 | assert snake_case("get_HTTP_response") == "get_HTTP_response" 25 | assert snake_case("_methodName") == "_methodName" 26 | assert snake_case("make_gRPC_request") == "make_gRPC_request" 27 | 28 | assert snake_case("value1") == "value1" 29 | assert snake_case("value1string") == "value1string" 30 | 31 | # It is difficult to cover all the cases with a simple algorithm... 32 | # "GetValueAsUInt32" -> "get_value_as_u_int_32" 33 | -------------------------------------------------------------------------------- /betterproto2/tests/test_struct.py: -------------------------------------------------------------------------------- 1 | def test_struct_to_dict(): 2 | from tests.outputs.google.google.protobuf import Struct 3 | 4 | struct = Struct.from_dict( 5 | { 6 | "null_field": None, 7 | "number_field": 12, 8 | "string_field": "test", 9 | "bool_field": True, 10 | "struct_field": {"x": "abc"}, 11 | "list_field": [42, False, None], 12 | } 13 | ) 14 | 15 | assert struct.to_dict() == { 16 | "null_field": None, 17 | "number_field": 12, 18 | "string_field": "test", 19 | "bool_field": True, 20 | "struct_field": {"x": "abc"}, 21 | "list_field": [42, False, None], 22 | } 23 | 24 | assert Struct.from_dict(struct.to_dict()) == struct 25 | 26 | 27 | def test_listvalue_to_dict(): 28 | from tests.outputs.google.google.protobuf import ListValue 29 | 30 | list_value = ListValue.from_dict([42, False, {}]) 31 | 32 | assert list_value.to_dict() == [42, False, {}] 33 | assert ListValue.from_dict(list_value.to_dict()) == list_value 34 | 35 | 36 | def test_nullvalue(): 37 | from tests.outputs.google.google.protobuf import NullValue, Value 38 | 39 | null_value = NullValue.NULL_VALUE 40 | 41 | assert bytes(Value(null_value=null_value)) == b"\x08\x00" 42 | 43 | 44 | def test_value_to_dict(): 45 | from tests.outputs.google.google.protobuf import Value 46 | 47 | value = Value.from_dict([1, 2, False]) 48 | 49 | assert value.to_dict() == [1, 2, False] 50 | assert Value.from_dict(value.to_dict()) == value 51 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/timestamp_dict_encode/test_timestamp_dict_encode.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | 3 | import pytest 4 | 5 | from tests.outputs.timestamp_dict_encode.timestamp_dict_encode import Test 6 | 7 | # Current World Timezone range (UTC-12 to UTC+14) 8 | MIN_UTC_OFFSET_MIN = -12 * 60 9 | MAX_UTC_OFFSET_MIN = 14 * 60 10 | 11 | # Generate all timezones in range in 15 min increments 12 | timezones = [timezone(timedelta(minutes=x)) for x in range(MIN_UTC_OFFSET_MIN, MAX_UTC_OFFSET_MIN + 1, 15)] 13 | 14 | 15 | @pytest.mark.parametrize("tz", timezones) 16 | def test_datetime_dict_encode(tz: timezone): 17 | original_time = datetime.now(tz=tz) 18 | original_message = Test() 19 | original_message.ts = original_time 20 | encoded = original_message.to_dict() 21 | decoded_message = Test.from_dict(encoded) 22 | 23 | # check that the timestamps are equal after decoding from dict 24 | assert original_message.ts.tzinfo is not None 25 | assert decoded_message.ts.tzinfo is not None 26 | assert original_message.ts == decoded_message.ts 27 | 28 | 29 | @pytest.mark.parametrize("tz", timezones) 30 | def test_json_serialize(tz: timezone): 31 | original_time = datetime.now(tz=tz) 32 | original_message = Test() 33 | original_message.ts = original_time 34 | json_serialized = original_message.to_json() 35 | decoded_message = Test.from_json(json_serialized) 36 | 37 | # check that the timestamps are equal after decoding from dict 38 | assert original_message.ts.tzinfo is not None 39 | assert decoded_message.ts.tzinfo is not None 40 | assert original_message.ts == decoded_message.ts 41 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/googletypes_request/test_googletypes_request.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | import pytest 4 | 5 | import tests.outputs.googletypes_request.google.protobuf as protobuf 6 | from tests.util import requires_grpclib # noqa: F401 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_channel_receives_wrapped_type(requires_grpclib): 11 | from tests.mocks import MockChannel 12 | from tests.outputs.googletypes_request.googletypes_request import Input, TestStub 13 | 14 | test_cases = [ 15 | (TestStub.send_double, protobuf.DoubleValue, 2.5), 16 | (TestStub.send_float, protobuf.FloatValue, 2.5), 17 | (TestStub.send_int_64, protobuf.Int64Value, -64), 18 | (TestStub.send_u_int_64, protobuf.UInt64Value, 64), 19 | (TestStub.send_int_32, protobuf.Int32Value, -32), 20 | (TestStub.send_u_int_32, protobuf.UInt32Value, 32), 21 | (TestStub.send_bool, protobuf.BoolValue, True), 22 | (TestStub.send_string, protobuf.StringValue, "string"), 23 | (TestStub.send_bytes, protobuf.BytesValue, bytes(0xFF)[0:4]), 24 | (TestStub.send_datetime, protobuf.Timestamp, datetime(2038, 1, 19, 3, 14, 8)), 25 | (TestStub.send_timedelta, protobuf.Duration, timedelta(seconds=123456)), 26 | ] 27 | 28 | for service_method, wrapper_class, value in test_cases: 29 | wrapped_value = wrapper_class() 30 | wrapped_value.value = value 31 | channel = MockChannel(responses=[Input()]) 32 | service = TestStub(channel) 33 | 34 | await service_method(service, wrapped_value) 35 | 36 | assert channel.requests[0]["request"] == type(wrapped_value) 37 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/documentation/documentation.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package documentation; 3 | 4 | // Documentation of message 1 5 | // other line 1 6 | 7 | // Documentation of message 2 8 | // other line 2 9 | message Test { // Documentation of message 3 10 | // Documentation of field 1 11 | // other line 1 12 | 13 | // Documentation of field 2 14 | // other line 2 15 | uint32 x = 1; // Documentation of field 3 16 | 17 | // Documentation of oneof 1 18 | // other line 1 19 | 20 | // Documentation of oneof 2 21 | // other line 2 22 | oneof oneof_example { // Documentation of oneof 3 23 | // Documentation of oneof field 1 24 | // other line 1 25 | 26 | // Documentation of oneof field 2 27 | // other line 2 28 | int32 a = 2; // Documentation of oneof field 3 29 | } 30 | } 31 | 32 | // Documentation of enum 1 33 | // other line 1 34 | 35 | // Documentation of enum 2 36 | // other line 2 37 | enum Enum { // Documentation of enum 3 38 | // Documentation of variant 1 39 | // other line 1 40 | 41 | // Documentation of variant 2 42 | // other line 2 43 | Enum_Variant = 0; // Documentation of variant 3 44 | } 45 | 46 | // Documentation of service 1 47 | // other line 1 48 | 49 | // Documentation of service 2 50 | // other line 2 51 | service Service { // Documentation of service 3 52 | // Documentation of method 1 53 | // other line 1 54 | 55 | // Documentation of method 2 56 | // other line 2 57 | rpc get(Test) returns (Test); // Documentation of method 3 58 | } 59 | 60 | // A comment with backslashes \ and triple quotes """ 61 | // Simple quotes are not escaped " 62 | message ComplexDocumentation {} 63 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/enum/enum.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package enum; 4 | 5 | // Tests that enums are correctly serialized and that it correctly handles skipped and out-of-order enum values 6 | message Test { 7 | Choice choice = 1; 8 | repeated Choice choices = 2; 9 | } 10 | 11 | enum Choice { 12 | ZERO = 0; 13 | ONE = 1; 14 | // TWO = 2; 15 | FOUR = 4; 16 | THREE = 3; 17 | MINUS_ONE = -1; 18 | } 19 | 20 | // A "C" like enum with the enum name prefixed onto members, these should be stripped 21 | enum ArithmeticOperator { 22 | ARITHMETIC_OPERATOR_NONE = 0; 23 | ARITHMETIC_OPERATOR_PLUS = 1; 24 | ARITHMETIC_OPERATOR_MINUS = 2; 25 | ARITHMETIC_OPERATOR_0_PREFIXED = 3; 26 | } 27 | 28 | // If not all the fields are prefixed, the prefix should not be stripped at all 29 | enum NoStriping { 30 | NO_STRIPING_NONE = 0; 31 | NO_STRIPING_A = 1; 32 | B = 2; 33 | } 34 | 35 | // Make sure that the prefix are removed even if it's difficult to infer the position 36 | // of underscores. 37 | enum HTTPCode { 38 | HTTP_CODE_UNSPECIFIED = 0; 39 | HTTP_CODE_OK = 200; 40 | HTTP_CODE_NOT_FOUND = 404; 41 | } 42 | 43 | message EnumMessage { 44 | ArithmeticOperator arithmetic_operator = 1; 45 | NoStriping no_striping = 2; 46 | } 47 | 48 | enum OldVersion { 49 | OLD_VERSION_UNSPECIFIED = 0; 50 | OLD_VERSION_V1 = 1; 51 | OLD_VERSION_V2 = 2; 52 | } 53 | 54 | message OldVersionMessage { 55 | OldVersion old_version = 1; 56 | } 57 | 58 | enum NewVersion { 59 | NEW_VERSION_UNSPECIFIED = 0; 60 | NEW_VERSION_V1 = 1; 61 | NEW_VERSION_V2 = 2; 62 | NEW_VERSION_V3 = 3; 63 | } 64 | 65 | message NewVersionMessage { 66 | NewVersion new_version = 1; 67 | } 68 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/proto3_field_presence/test_proto3_field_presence.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from tests.outputs.proto3_field_presence.proto3_field_presence import Test 4 | 5 | 6 | def test_null_fields_json(): 7 | """Ensure that using "null" in JSON is equivalent to not specifying a 8 | field, for fields with explicit presence""" 9 | 10 | def test_json(ref_json: str, obj_json: str) -> None: 11 | """`ref_json` and `obj_json` are JSON strings describing a `Test` object. 12 | Test that deserializing both leads to the same object, and that 13 | `ref_json` is the normalized format.""" 14 | ref_obj = Test().from_json(ref_json) 15 | obj = Test().from_json(obj_json) 16 | 17 | assert obj == ref_obj 18 | assert json.loads(obj.to_json(0)) == json.loads(ref_json) 19 | 20 | test_json("{}", '{ "test1": null, "test2": null, "test3": null }') 21 | test_json("{}", '{ "test4": null, "test5": null, "test6": null }') 22 | test_json("{}", '{ "test7": null, "test8": null }') 23 | test_json('{ "test5": {} }', '{ "test3": null, "test5": {} }') 24 | 25 | # Make sure that if include_default_values is set, None values are 26 | # exported. 27 | obj = Test() 28 | assert obj.to_dict() == {} 29 | assert obj.to_dict(include_default_values=True) == { 30 | "test1": None, 31 | "test2": None, 32 | "test3": None, 33 | "test4": None, 34 | "test5": None, 35 | "test6": None, 36 | "test7": None, 37 | "test8": None, 38 | "test9": None, 39 | } 40 | 41 | 42 | def test_unset_access(): # see #523 43 | assert Test().test1 is None 44 | assert Test(test1=None).test1 is None 45 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/util.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" 7 | 8 | 9 | async def protoc( 10 | path: str | Path, 11 | output_dir: str | Path, 12 | reference: bool = False, 13 | pydantic_dataclasses: bool = False, 14 | google_protobuf_descriptors: bool = False, 15 | client_generation: str = "async_sync", 16 | ): 17 | resolved_path: Path = Path(path).resolve() 18 | resolved_output_dir: Path = Path(output_dir).resolve() 19 | python_out_option: str = "python_out" if reference else "python_betterproto2_out" 20 | 21 | command = [ 22 | sys.executable, 23 | "-m", 24 | "grpc.tools.protoc", 25 | f"--proto_path={resolved_path.as_posix()}", 26 | f"--{python_out_option}={resolved_output_dir.as_posix()}", 27 | *[p.as_posix() for p in resolved_path.glob("*.proto")], 28 | ] 29 | 30 | if not reference: 31 | command.insert(3, "--python_betterproto2_opt=server_generation=async") 32 | command.insert(3, f"--python_betterproto2_opt=client_generation={client_generation}") 33 | 34 | if pydantic_dataclasses: 35 | command.insert(3, "--python_betterproto2_opt=pydantic_dataclasses") 36 | 37 | if google_protobuf_descriptors: 38 | command.insert(3, "--python_betterproto2_opt=google_protobuf_descriptors") 39 | 40 | proc = await asyncio.create_subprocess_exec( 41 | *command, 42 | stdout=asyncio.subprocess.PIPE, 43 | stderr=asyncio.subprocess.PIPE, 44 | ) 45 | stdout, stderr = await proc.communicate() 46 | return stdout, stderr, proc.returncode or 0 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '**' 9 | 10 | jobs: 11 | publish_betterproto2: 12 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/lib-v') 13 | name: Publish betterproto2 to PyPI 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | id-token: write 18 | 19 | environment: 20 | name: pypi 21 | url: https://pypi.org/p/betterproto2 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Install uv 27 | uses: astral-sh/setup-uv@v5 28 | with: 29 | version: "0.7.5" 30 | python-version: "3.10" 31 | 32 | - name: Build package 33 | working-directory: ./betterproto2 34 | run: uv build 35 | 36 | - name: Publish package distributions to PyPI 37 | uses: pypa/gh-action-pypi-publish@release/v1.12 38 | with: 39 | packages-dir: betterproto2/dist 40 | 41 | publish_betterproto2_compiler: 42 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/compiler-v') 43 | name: Publish betterproto2_compiler to PyPI 44 | runs-on: ubuntu-latest 45 | 46 | permissions: 47 | id-token: write 48 | 49 | environment: 50 | name: pypi 51 | url: https://pypi.org/p/betterproto2_compiler 52 | 53 | steps: 54 | - uses: actions/checkout@v4 55 | 56 | - name: Install uv 57 | uses: astral-sh/setup-uv@v5 58 | with: 59 | version: "0.7.5" 60 | python-version: "3.10" 61 | 62 | - name: Build package 63 | working-directory: ./betterproto2_compiler 64 | run: uv build 65 | 66 | - name: Publish package distributions to PyPI 67 | uses: pypa/gh-action-pypi-publish@release/v1.12 68 | with: 69 | packages-dir: betterproto2_compiler/dist 70 | -------------------------------------------------------------------------------- /betterproto2/tests/test_documentation.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import inspect 3 | import sys 4 | 5 | from tests.util import requires_grpclib # noqa: F401 6 | 7 | 8 | def check(generated_doc: str, type: str) -> None: 9 | assert f"Documentation of {type} 1" in generated_doc 10 | assert "other line 1" in generated_doc 11 | assert f"Documentation of {type} 2" in generated_doc 12 | assert "other line 2" in generated_doc 13 | assert f"Documentation of {type} 3" in generated_doc 14 | 15 | 16 | def test_documentation(requires_grpclib) -> None: 17 | from .outputs.documentation.documentation import ( 18 | Enum, 19 | ServiceBase, 20 | ServiceStub, 21 | Test, 22 | ) 23 | 24 | check(Test.__doc__, "message") 25 | check(Test.__doc__, "oneof") 26 | 27 | source = inspect.getsource(Test) 28 | tree = ast.parse(source) 29 | check(tree.body[0].body[2].value.value, "field") 30 | 31 | check(Enum.__doc__, "enum") 32 | 33 | source = inspect.getsource(Enum) 34 | tree = ast.parse(source) 35 | check(tree.body[0].body[2].value.value, "variant") 36 | 37 | check(ServiceBase.__doc__, "service") 38 | check(ServiceBase.get.__doc__, "method") 39 | 40 | check(ServiceStub.__doc__, "service") 41 | check(ServiceStub.get.__doc__, "method") 42 | 43 | 44 | def test_escaping(requires_grpclib) -> None: 45 | from .outputs.documentation.documentation import ComplexDocumentation 46 | 47 | if sys.version_info >= (3, 13): 48 | assert ( 49 | ComplexDocumentation.__doc__ 50 | == """ 51 | A comment with backslashes \\ and triple quotes \"\"\" 52 | Simple quotes are not escaped " 53 | """ 54 | ) 55 | else: 56 | assert ( 57 | ComplexDocumentation.__doc__ 58 | == """ 59 | A comment with backslashes \\ and triple quotes \"\"\" 60 | Simple quotes are not escaped " 61 | """ 62 | ) 63 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/import_service_input_message/test_import_service_input_message.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.util import requires_grpclib # noqa: F401 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_service_correctly_imports_reference_message(requires_grpclib): 8 | from tests.mocks import MockChannel 9 | from tests.outputs.import_service_input_message.import_service_input_message import ( 10 | RequestMessage, 11 | RequestResponse, 12 | TestStub, 13 | ) 14 | 15 | mock_response = RequestResponse(value=10) 16 | service = TestStub(MockChannel([mock_response])) 17 | response = await service.do_thing(RequestMessage(1)) 18 | assert mock_response == response 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_service_correctly_imports_reference_message_from_child_package(requires_grpclib): 23 | from tests.mocks import MockChannel 24 | from tests.outputs.import_service_input_message.import_service_input_message import ( 25 | RequestResponse, 26 | TestStub, 27 | ) 28 | from tests.outputs.import_service_input_message.import_service_input_message.child import ( 29 | ChildRequestMessage, 30 | ) 31 | 32 | mock_response = RequestResponse(value=10) 33 | service = TestStub(MockChannel([mock_response])) 34 | response = await service.do_thing_2(ChildRequestMessage(1)) 35 | assert mock_response == response 36 | 37 | 38 | @pytest.mark.asyncio 39 | async def test_service_correctly_imports_nested_reference(requires_grpclib): 40 | from tests.mocks import MockChannel 41 | from tests.outputs.import_service_input_message.import_service_input_message import ( 42 | NestedRequestMessage, 43 | RequestResponse, 44 | TestStub, 45 | ) 46 | 47 | mock_response = RequestResponse(value=10) 48 | service = TestStub(MockChannel([mock_response])) 49 | response = await service.do_thing_3(NestedRequestMessage(1)) 50 | assert mock_response == response 51 | -------------------------------------------------------------------------------- /betterproto2/src/betterproto2/_version.py: -------------------------------------------------------------------------------- 1 | from importlib import metadata 2 | 3 | __version__ = metadata.version("betterproto2") 4 | 5 | 6 | def check_compiler_version(compiler_version: str) -> None: 7 | """ 8 | Checks that the compiled files can be used with this version of the library. 9 | 10 | If the versions do not match, the user is suggested to update the library or the compiler. The version x.y.z of the 11 | library matches the version a.b.c of the compiler if and only if a=x and b=y. 12 | """ 13 | parsed_lib_version = tuple(int(x) for x in __version__.split(".")[:2]) 14 | parsed_comp_version = tuple(int(x) for x in compiler_version.split(".")[:2]) 15 | 16 | if parsed_lib_version != parsed_comp_version: 17 | error = ( 18 | f"Unsupported version. The proto files were compiled with a version of betterproto2_compiler which is not " 19 | "compatible with this version of betterproto2.\n" 20 | f" - betterproto2 version: {__version__}\n" 21 | f" - betterproto2_compiler version: {compiler_version}\n" 22 | "The version x.y.z of the library matches the version a.b.c of the compiler if and only if a=x and b=y.\n" 23 | ) 24 | 25 | if parsed_lib_version < parsed_comp_version: 26 | error += ( 27 | f"Please upgrade betterproto2 to {parsed_comp_version[0]}.{parsed_comp_version[1]}.x (recommended) " 28 | f"or downgrade betterproto2_compiler to {parsed_lib_version[0]}.{parsed_lib_version[1]}.x and " 29 | "recompile your proto files." 30 | ) 31 | else: 32 | error += ( 33 | f"Please upgrade betterproto2_compiler to {parsed_lib_version[0]}.{parsed_lib_version[1]}.x and " 34 | "recompile your proto files (recommended) or downgrade betterproto2 to " 35 | f"{parsed_comp_version[0]}.{parsed_comp_version[1]}.x." 36 | ) 37 | 38 | raise ImportError(error) 39 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/templates/header.py.j2: -------------------------------------------------------------------------------- 1 | {# All the imports needed for this file. The useless imports will be removed by Ruff. #} 2 | 3 | # Generated by the protocol buffer compiler. DO NOT EDIT! 4 | # sources: {{ ', '.join(output_file.input_filenames) }} 5 | # plugin: python-betterproto2 6 | # This file has been @generated 7 | 8 | __all__ = ( 9 | {%- for name in all -%} 10 | "{{ name }}", 11 | {%- endfor -%} 12 | ) 13 | 14 | import re 15 | import builtins 16 | import datetime 17 | import dateutil.parser 18 | import warnings 19 | from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator 20 | import typing 21 | from typing import TYPE_CHECKING 22 | from typing_extensions import Self 23 | 24 | {% if output_file.settings.pydantic_dataclasses %} 25 | import pydantic 26 | from pydantic.dataclasses import dataclass 27 | from pydantic import model_validator 28 | {%- else -%} 29 | from dataclasses import dataclass 30 | {% endif %} 31 | 32 | import betterproto2 33 | from betterproto2 import grpclib as betterproto2_grpclib 34 | import grpc 35 | import grpclib 36 | from google.protobuf.descriptor import Descriptor, EnumDescriptor 37 | 38 | {# Import the message pool of the generated code. #} 39 | {% if output_file.package %} 40 | from {{ "." * output_file.package.count(".") }}..message_pool import default_message_pool 41 | {% if output_file.settings.google_protobuf_descriptors %} 42 | from {{ "." * output_file.package.count(".") }}..google_proto_descriptor_pool import default_google_proto_descriptor_pool 43 | {% endif %} 44 | {% else %} 45 | from .message_pool import default_message_pool 46 | {% if output_file.settings.google_protobuf_descriptors %} 47 | from .google_proto_descriptor_pool import default_google_proto_descriptor_pool 48 | {% endif %} 49 | {% endif %} 50 | 51 | if TYPE_CHECKING: 52 | import grpclib.server 53 | from betterproto2.grpclib.grpclib_client import MetadataLike 54 | from grpclib.metadata import Deadline 55 | 56 | _COMPILER_VERSION="{{ version }}" 57 | betterproto2.check_compiler_version(_COMPILER_VERSION) 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report broken or incorrect behaviour 3 | labels: ["bug", "investigation needed"] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: > 9 | Thanks for taking the time to fill out a bug report! 10 | 11 | If you're not sure it's a bug and you just have a question, the [community Slack channel](https://join.slack.com/t/betterproto/shared_invite/zt-f0n0uolx-iN8gBNrkPxtKHTLpG3o1OQ) is a better place for general questions than a GitHub issue. 12 | 13 | - type: input 14 | attributes: 15 | label: Summary 16 | description: A simple summary of your bug report 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | attributes: 22 | label: Reproduction Steps 23 | description: > 24 | What you did to make it happen. 25 | Ideally there should be a short code snippet in this section to help reproduce the bug. 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | attributes: 31 | label: Expected Results 32 | description: > 33 | What did you expect to happen? 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | attributes: 39 | label: Actual Results 40 | description: > 41 | What actually happened? 42 | validations: 43 | required: true 44 | 45 | - type: textarea 46 | attributes: 47 | label: System Information 48 | description: > 49 | Paste the result of `protoc --version; python --version; pip show betterproto` below. 50 | validations: 51 | required: true 52 | 53 | - type: checkboxes 54 | attributes: 55 | label: Checklist 56 | options: 57 | - label: I have searched the issues for duplicates. 58 | required: true 59 | - label: I have shown the entire traceback, if possible. 60 | required: true 61 | - label: I have verified this issue occurs on the latest prelease of betterproto which can be installed using `pip install -U --pre betterproto`, if possible. 62 | required: true 63 | 64 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/inputs/features/features.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package features; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/timestamp.proto"; 7 | import "google/protobuf/wrappers.proto"; 8 | 9 | message Bar { 10 | string name = 1; 11 | } 12 | 13 | message Foo { 14 | string name = 1; 15 | Bar child = 2; 16 | } 17 | 18 | enum Enum { 19 | ZERO = 0; 20 | ONE = 1; 21 | } 22 | 23 | message EnumMsg { 24 | Enum enum = 1; 25 | } 26 | 27 | message Newer { 28 | bool x = 1; 29 | int32 y = 2; 30 | string z = 3; 31 | } 32 | 33 | message Older { 34 | bool x = 1; 35 | } 36 | 37 | message IntMsg { 38 | int32 val = 1; 39 | } 40 | 41 | message OneofMsg { 42 | oneof group1 { 43 | int32 x = 1; 44 | string y = 2; 45 | } 46 | oneof group2 { 47 | IntMsg a = 3; 48 | string b = 4; 49 | } 50 | } 51 | 52 | message JsonCasingMsg { 53 | int32 pascal_case = 1; 54 | int32 camel_case = 2; 55 | int32 snake_case = 3; 56 | int32 kabob_case = 4; 57 | } 58 | 59 | message OptionalBoolMsg { 60 | google.protobuf.BoolValue field = 1; 61 | } 62 | 63 | message OptionalDatetimeMsg { 64 | google.protobuf.Timestamp field = 1; 65 | } 66 | 67 | message Empty {} 68 | 69 | message TimeMsg { 70 | google.protobuf.Timestamp timestamp = 1; 71 | google.protobuf.Duration duration = 2; 72 | } 73 | 74 | message MsgA { 75 | int32 some_int = 1; 76 | double some_double = 2; 77 | string some_str = 3; 78 | bool some_bool = 4; 79 | } 80 | 81 | message MsgB { 82 | int32 some_int = 1; 83 | double some_double = 2; 84 | string some_str = 3; 85 | bool some_bool = 4; 86 | int32 some_default_int = 5; 87 | double some_default_double = 6; 88 | string some_default_str = 7; 89 | bool some_default_bool = 8; 90 | } 91 | 92 | message MsgC { 93 | oneof group1 { 94 | int32 int_field = 1; 95 | string string_field = 2; 96 | Empty empty_field = 3; 97 | } 98 | } 99 | 100 | message MsgD { 101 | repeated google.protobuf.Timestamp timestamps = 1; 102 | } 103 | 104 | message MsgE { 105 | bool bool_field = 1; 106 | optional int32 int_field = 2; 107 | repeated string str_field = 3; 108 | } 109 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/settings.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from dataclasses import dataclass 3 | 4 | if sys.version_info >= (3, 11): 5 | from enum import StrEnum 6 | else: 7 | from strenum import StrEnum 8 | 9 | 10 | class ClientGeneration(StrEnum): 11 | NONE = "none" 12 | """Clients are not generated.""" 13 | 14 | SYNC = "sync" 15 | """Only synchronous clients are generated.""" 16 | 17 | ASYNC = "async" 18 | """Only asynchronous clients are generated.""" 19 | 20 | SYNC_ASYNC = "sync_async" 21 | """Both synchronous and asynchronous clients are generated. 22 | 23 | Asynchronous clients are generated with the Async suffix.""" 24 | 25 | ASYNC_SYNC = "async_sync" 26 | """Both synchronous and asynchronous clients are generated. 27 | 28 | Synchronous clients are generated with the Sync suffix.""" 29 | 30 | SYNC_ASYNC_NO_DEFAULT = "sync_async_no_default" 31 | """Both synchronous and asynchronous clients are generated. 32 | 33 | Synchronous clients are generated with the Sync suffix, and asynchronous clients are generated with the Async 34 | suffix.""" 35 | 36 | @property 37 | def is_sync_generated(self) -> bool: 38 | return self in { 39 | ClientGeneration.SYNC, 40 | ClientGeneration.SYNC_ASYNC, 41 | ClientGeneration.ASYNC_SYNC, 42 | ClientGeneration.SYNC_ASYNC_NO_DEFAULT, 43 | } 44 | 45 | @property 46 | def is_async_generated(self) -> bool: 47 | return self in { 48 | ClientGeneration.ASYNC, 49 | ClientGeneration.SYNC_ASYNC, 50 | ClientGeneration.ASYNC_SYNC, 51 | ClientGeneration.SYNC_ASYNC_NO_DEFAULT, 52 | } 53 | 54 | @property 55 | def is_sync_prefixed(self) -> bool: 56 | return self in {ClientGeneration.ASYNC_SYNC, ClientGeneration.SYNC_ASYNC_NO_DEFAULT} 57 | 58 | @property 59 | def is_async_prefixed(self) -> bool: 60 | return self in {ClientGeneration.SYNC_ASYNC, ClientGeneration.SYNC_ASYNC_NO_DEFAULT} 61 | 62 | 63 | class ServerGeneration(StrEnum): 64 | NONE = "none" 65 | ASYNC = "async" 66 | 67 | 68 | @dataclass 69 | class Settings: 70 | pydantic_dataclasses: bool 71 | google_protobuf_descriptors: bool 72 | 73 | client_generation: ClientGeneration 74 | server_generation: ServerGeneration 75 | -------------------------------------------------------------------------------- /betterproto2/src/betterproto2/enum_.py: -------------------------------------------------------------------------------- 1 | from enum import EnumMeta, IntEnum 2 | 3 | from typing_extensions import Self 4 | 5 | 6 | class _EnumMeta(EnumMeta): 7 | def __new__(metacls, cls, bases, classdict): 8 | enum_class = super().__new__(metacls, cls, bases, classdict) 9 | proto_names = enum_class.betterproto_value_to_renamed_proto_names() # type: ignore[reportAttributeAccessIssue] 10 | 11 | # Attach extra info to each enum member 12 | for member in enum_class: 13 | value = member.value # type: ignore[reportAttributeAccessIssue] 14 | extra = proto_names.get(value) 15 | member._proto_name = extra # type: ignore[reportAttributeAccessIssue] 16 | 17 | return enum_class 18 | 19 | 20 | class Enum(IntEnum, metaclass=_EnumMeta): 21 | @property 22 | def proto_name(self) -> str | None: 23 | return self._proto_name # type: ignore[reportAttributeAccessIssue] 24 | 25 | @classmethod 26 | def betterproto_value_to_renamed_proto_names(cls) -> dict[int, str]: 27 | return {} 28 | 29 | @classmethod 30 | def betterproto_renamed_proto_names_to_value(cls) -> dict[str, int]: 31 | return {} 32 | 33 | @classmethod 34 | def _missing_(cls, value): 35 | # If the given value is not an integer, let the standard enum implementation raise an error 36 | if not isinstance(value, int): 37 | return 38 | 39 | # Create a new "unknown" instance with the given value. 40 | obj = int.__new__(cls, value) 41 | obj._value_ = value 42 | obj._name_ = "" 43 | return obj 44 | 45 | def __str__(self): 46 | if not self.name: 47 | return f"UNKNOWN({self.value})" 48 | return self.name 49 | 50 | def __repr__(self): 51 | if not self.name: 52 | return f"<{self.__class__.__name__}.~UNKNOWN: {self.value}>" 53 | return super().__repr__() 54 | 55 | @classmethod 56 | def from_string(cls, name: str) -> Self: 57 | """Return the value which corresponds to the string name. 58 | 59 | Parameters: 60 | name: The name of the enum member to get. 61 | 62 | Raises: 63 | ValueError: The member was not found in the Enum. 64 | 65 | Returns: 66 | The corresponding value 67 | """ 68 | try: 69 | return cls[name] 70 | except KeyError as e: 71 | raise ValueError(f"Unknown value {name} for enum {cls.__name__}") from e 72 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/oneof_default_value_serialization/test_oneof_default_value_serialization.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import betterproto2 4 | from tests.outputs.oneof_default_value_serialization.oneof_default_value_serialization import ( 5 | Message, 6 | NestedMessage, 7 | Test, 8 | ) 9 | 10 | 11 | def assert_round_trip_serialization_works(message: Test) -> None: 12 | assert betterproto2.which_one_of(message, "value_type") == betterproto2.which_one_of( 13 | Test().from_json(message.to_json()), "value_type" 14 | ) 15 | 16 | 17 | def test_oneof_default_value_serialization_works_for_all_values(): 18 | """ 19 | Serialization from message with oneof set to default -> JSON -> message should keep 20 | default value field intact. 21 | """ 22 | 23 | test_cases = [ 24 | Test(bool_value=False), 25 | Test(int64_value=0), 26 | Test( 27 | timestamp_value=datetime.datetime( 28 | year=1970, 29 | month=1, 30 | day=1, 31 | hour=0, 32 | minute=0, 33 | tzinfo=datetime.timezone.utc, 34 | ) 35 | ), 36 | Test(duration_value=datetime.timedelta(0)), 37 | Test(wrapped_message_value=Message(value=0)), 38 | # NOTE: Do NOT use betterproto.BoolValue here, it will cause JSON serialization 39 | # errors. 40 | # TODO: Do we want to allow use of BoolValue directly within a wrapped field or 41 | # should we simply hard fail here? 42 | Test(wrapped_bool_value=False), 43 | ] 44 | for message in test_cases: 45 | assert_round_trip_serialization_works(message) 46 | 47 | 48 | def test_oneof_no_default_values_passed(): 49 | message = Test() 50 | assert ( 51 | betterproto2.which_one_of(message, "value_type") 52 | == betterproto2.which_one_of(Test().from_json(message.to_json()), "value_type") 53 | == ("", None) 54 | ) 55 | 56 | 57 | def test_oneof_nested_oneof_messages_are_serialized_with_defaults(): 58 | """ 59 | Nested messages with oneofs should also be handled 60 | """ 61 | message = Test(wrapped_nested_message_value=NestedMessage(id=0, wrapped_message_value=Message(value=0))) 62 | assert ( 63 | betterproto2.which_one_of(message, "value_type") 64 | == betterproto2.which_one_of(Test().from_json(message.to_json()), "value_type") 65 | == ( 66 | "wrapped_nested_message_value", 67 | NestedMessage(id=0, wrapped_message_value=Message(value=0)), 68 | ) 69 | ) 70 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/plugin/compiler.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import subprocess 3 | import sys 4 | from importlib import metadata 5 | 6 | import jinja2 7 | 8 | from .models import OutputTemplate 9 | from .module_validation import ModuleValidator 10 | 11 | 12 | def outputfile_compiler(output_file: OutputTemplate) -> str: 13 | templates_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "templates")) 14 | 15 | version = metadata.version("betterproto2_compiler") 16 | 17 | env = jinja2.Environment( 18 | trim_blocks=True, 19 | lstrip_blocks=True, 20 | loader=jinja2.FileSystemLoader(templates_folder), 21 | undefined=jinja2.StrictUndefined, 22 | ) 23 | 24 | # List of the symbols that should appear in the `__all__` variable of the file 25 | all: list[str] = [] 26 | 27 | def add_to_all(name: str) -> str: 28 | all.append(name) 29 | return name 30 | 31 | env.filters["add_to_all"] = add_to_all 32 | 33 | body_template = env.get_template("template.py.j2") 34 | header_template = env.get_template("header.py.j2") 35 | 36 | # Load the body first do know the symbols defined in the file 37 | code = body_template.render(output_file=output_file) 38 | code = header_template.render(output_file=output_file, version=version, all=all) + "\n" + code 39 | 40 | try: 41 | # Sort imports, delete unused ones, sort __all__ 42 | code = subprocess.check_output( 43 | ["ruff", "check", "--select", "I,F401,TC005,RUF022", "--fix", "--silent", "-"], 44 | input=code, 45 | encoding="utf-8", 46 | ) 47 | 48 | # Format the code 49 | code = subprocess.check_output(["ruff", "format", "-"], input=code, encoding="utf-8") 50 | except subprocess.CalledProcessError: 51 | with open("invalid-generated-code.py", "w") as f: 52 | f.write(code) 53 | 54 | raise SyntaxError( 55 | f"Can't format the source code:\nThe invalid generated code has been written in `invalid-generated-code.py`" 56 | ) 57 | 58 | # Validate the generated code. 59 | validator = ModuleValidator(iter(code.splitlines())) 60 | if not validator.validate(): 61 | message_builder = ["[WARNING]: Generated code has collisions in the module:"] 62 | for collision, lines in validator.collisions.items(): 63 | message_builder.append(f' "{collision}" on lines:') 64 | for num, line in lines: 65 | message_builder.append(f" {num}:{line}") 66 | print("\n".join(message_builder), file=sys.stderr) 67 | return code 68 | -------------------------------------------------------------------------------- /betterproto2/tests/test_deprecated.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import pytest 4 | 5 | from tests.util import requires_grpclib # noqa: F401 6 | 7 | 8 | @pytest.fixture 9 | def message(): 10 | from tests.outputs.deprecated.deprecated import Message 11 | 12 | with warnings.catch_warnings(): 13 | warnings.filterwarnings("ignore", category=DeprecationWarning) 14 | return Message(value="hello") 15 | 16 | 17 | def test_deprecated_message(requires_grpclib): 18 | from tests.outputs.deprecated.deprecated import Message 19 | 20 | with pytest.warns(DeprecationWarning) as record: 21 | Message(value="hello") 22 | 23 | assert len(record) == 1 24 | assert str(record[0].message) == f"{Message.__name__} is deprecated" 25 | 26 | 27 | def test_deprecated_nested_message_field(requires_grpclib): 28 | from tests.outputs.deprecated.deprecated import TestNested 29 | 30 | with pytest.warns(DeprecationWarning) as record: 31 | TestNested(nested_value="hello") 32 | 33 | assert len(record) == 1 34 | assert str(record[0].message) == f"TestNested.nested_value is deprecated" 35 | 36 | 37 | def test_message_with_deprecated_field(requires_grpclib, message): 38 | from tests.outputs.deprecated.deprecated import Test 39 | 40 | with pytest.warns(DeprecationWarning) as record: 41 | Test(message=message, value=10) 42 | 43 | assert len(record) == 1 44 | assert str(record[0].message) == f"{Test.__name__}.message is deprecated" 45 | 46 | 47 | def test_message_with_deprecated_field_not_set(requires_grpclib, message): 48 | from tests.outputs.deprecated.deprecated import Test 49 | 50 | with warnings.catch_warnings(): 51 | warnings.simplefilter("error") 52 | Test(value=10) 53 | 54 | 55 | def test_message_with_deprecated_field_not_set_default(requires_grpclib, message): 56 | from tests.outputs.deprecated.deprecated import Test 57 | 58 | with warnings.catch_warnings(): 59 | warnings.simplefilter("error") 60 | _ = Test(value=10).message 61 | 62 | 63 | @pytest.mark.asyncio 64 | async def test_service_with_deprecated_method(requires_grpclib): 65 | from tests.mocks import MockChannel 66 | from tests.outputs.deprecated.deprecated import Empty, TestServiceStub 67 | 68 | stub = TestServiceStub(MockChannel([Empty(), Empty()])) 69 | 70 | with pytest.warns(DeprecationWarning) as record: 71 | await stub.deprecated_func(Empty()) 72 | 73 | assert len(record) == 1 74 | assert str(record[0].message) == f"TestService.deprecated_func is deprecated" 75 | 76 | with warnings.catch_warnings(): 77 | warnings.simplefilter("error") 78 | await stub.func(Empty()) 79 | -------------------------------------------------------------------------------- /betterproto2/tests/README.md: -------------------------------------------------------------------------------- 1 | # Standard Tests Development Guide 2 | 3 | Standard test cases are found in [betterproto/tests/inputs](inputs), where each subdirectory represents a testcase, that is verified in isolation. 4 | 5 | ``` 6 | inputs/ 7 | bool/ 8 | double/ 9 | int32/ 10 | ... 11 | ``` 12 | 13 | ## Test case directory structure 14 | 15 | Each testcase has a `.proto` file with a message called `Test`, and optionally a matching `.json` file and a custom test called `test_*.py`. 16 | 17 | ```bash 18 | bool/ 19 | bool.proto 20 | bool.json # optional 21 | test_bool.py # optional 22 | ``` 23 | 24 | ### proto 25 | 26 | `.proto` — *The protobuf message to test* 27 | 28 | ```protobuf 29 | syntax = "proto3"; 30 | 31 | message Test { 32 | bool value = 1; 33 | } 34 | ``` 35 | 36 | You can add multiple `.proto` files to the test case, as long as one file matches the directory name. 37 | 38 | ### json 39 | 40 | `.json` — *Test-data to validate the message with* 41 | 42 | ```json 43 | { 44 | "value": true 45 | } 46 | ``` 47 | 48 | ### pytest 49 | 50 | `test_.py` — *Custom test to validate specific aspects of the generated class* 51 | 52 | ```python 53 | from tests.output_betterproto.bool.bool import Test 54 | 55 | def test_value(): 56 | message = Test() 57 | assert not message.value, "Boolean is False by default" 58 | ``` 59 | 60 | ## Standard tests 61 | 62 | The following tests are automatically executed for all cases: 63 | 64 | - [x] Can the generated python code be imported? 65 | - [x] Can the generated message class be instantiated? 66 | - [x] Is the generated code compatible with the Google's `grpc_tools.protoc` implementation? 67 | - _when `.json` is present_ 68 | 69 | ## Running the tests 70 | 71 | - `pipenv run generate` 72 | This generates: 73 | - `betterproto/tests/output_betterproto` — *the plugin generated python classes* 74 | - `betterproto/tests/output_reference` — *reference implementation classes* 75 | - `pipenv run test` 76 | 77 | ## Intentionally Failing tests 78 | 79 | The standard test suite includes tests that fail by intention. These tests document known bugs and missing features that are intended to be corrected in the future. 80 | 81 | When running `pytest`, they show up as `x` or `X` in the test results. 82 | 83 | ``` 84 | betterproto/tests/test_inputs.py ..x...x..x...x.X........xx........x.....x.......x.xx....x...................... [ 84%] 85 | ``` 86 | 87 | - `.` — PASSED 88 | - `x` — XFAIL: expected failure 89 | - `X` — XPASS: expected failure, but still passed 90 | 91 | Test cases marked for expected failure are declared in [inputs/config.py](inputs/config.py) -------------------------------------------------------------------------------- /betterproto2/tests/inputs/googletypes_response/test_googletypes_response.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TYPE_CHECKING, Any 3 | 4 | import pytest 5 | 6 | from tests.util import requires_grpclib # noqa: F401 7 | 8 | if TYPE_CHECKING: 9 | from tests.outputs.googletypes_response.googletypes_response import Input, TestStub 10 | 11 | 12 | def get_test_cases() -> list[tuple[Callable[["TestStub", "Input"], Any], Callable, Any]]: 13 | import tests.outputs.googletypes_response.google.protobuf as protobuf 14 | from tests.outputs.googletypes_response.googletypes_response import TestStub 15 | 16 | return [ 17 | (TestStub.get_double, protobuf.DoubleValue, 2.5), 18 | (TestStub.get_float, protobuf.FloatValue, 2.5), 19 | (TestStub.get_int_64, protobuf.Int64Value, -64), 20 | (TestStub.get_u_int_64, protobuf.UInt64Value, 64), 21 | (TestStub.get_int_32, protobuf.Int32Value, -32), 22 | (TestStub.get_u_int_32, protobuf.UInt32Value, 32), 23 | (TestStub.get_bool, protobuf.BoolValue, True), 24 | (TestStub.get_string, protobuf.StringValue, "string"), 25 | (TestStub.get_bytes, protobuf.BytesValue, bytes(0xFF)[0:4]), 26 | ] 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_channel_receives_wrapped_type(requires_grpclib): 31 | from tests.mocks import MockChannel 32 | from tests.outputs.googletypes_response.googletypes_response import Input, TestStub 33 | 34 | for service_method, wrapper_class, value in get_test_cases(): 35 | wrapped_value = wrapper_class() 36 | wrapped_value.value = value 37 | channel = MockChannel(responses=[wrapped_value]) 38 | service = TestStub(channel) 39 | method_param = Input() 40 | 41 | await service_method(service, method_param) 42 | 43 | assert channel.requests[0]["response_type"] != type(value) | None 44 | assert channel.requests[0]["response_type"] == type(wrapped_value) 45 | 46 | 47 | @pytest.mark.asyncio 48 | @pytest.mark.xfail 49 | async def test_service_unwraps_response(requires_grpclib): 50 | """ 51 | grpclib does not unwrap wrapper values returned by services 52 | """ 53 | from tests.mocks import MockChannel 54 | from tests.outputs.googletypes_response.googletypes_response import Input, TestStub 55 | 56 | for service_method, wrapper_class, value in get_test_cases(): 57 | wrapped_value = wrapper_class() 58 | wrapped_value.value = value 59 | service = TestStub(MockChannel(responses=[wrapped_value])) 60 | method_param = Input() 61 | 62 | response_value = await service_method(service, method_param) 63 | 64 | assert response_value == value 65 | assert type(response_value) == type(value) 66 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/known_types/duration.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | import typing 4 | 5 | import betterproto2 6 | from typing_extensions import Self 7 | 8 | from betterproto2_compiler.lib.google.protobuf import Duration as VanillaDuration 9 | 10 | 11 | class Duration(VanillaDuration): 12 | @classmethod 13 | def from_timedelta( 14 | cls, delta: datetime.timedelta, *, _1_microsecond: datetime.timedelta = datetime.timedelta(microseconds=1) 15 | ) -> "Duration": 16 | total_ms = delta // _1_microsecond 17 | seconds = int(total_ms / 1e6) 18 | nanos = int((total_ms % 1e6) * 1e3) 19 | return cls(seconds, nanos) 20 | 21 | def to_timedelta(self) -> datetime.timedelta: 22 | return datetime.timedelta(seconds=self.seconds, microseconds=self.nanos / 1e3) 23 | 24 | @staticmethod 25 | def delta_to_json(delta: datetime.timedelta) -> str: 26 | parts = str(delta.total_seconds()).split(".") 27 | if len(parts) > 1: 28 | while len(parts[1]) not in (3, 6, 9): 29 | parts[1] = f"{parts[1]}0" 30 | return f"{'.'.join(parts)}s" 31 | 32 | # TODO typing 33 | @classmethod 34 | def from_dict(cls, value, *, ignore_unknown_fields: bool = False) -> Self: 35 | if isinstance(value, str): 36 | if not re.match(r"^\d+(\.\d+)?s$", value): 37 | raise ValueError(f"Invalid duration string: {value}") 38 | 39 | seconds = float(value[:-1]) 40 | return cls(seconds=int(seconds), nanos=int((seconds - int(seconds)) * 1e9)) 41 | 42 | return super().from_dict(value, ignore_unknown_fields=ignore_unknown_fields) 43 | 44 | # TODO typing 45 | def to_dict( 46 | self, 47 | *, 48 | output_format: betterproto2.OutputFormat = betterproto2.OutputFormat.PROTO_JSON, 49 | casing: betterproto2.Casing = betterproto2.Casing.CAMEL, 50 | include_default_values: bool = False, 51 | ) -> dict[str, typing.Any] | typing.Any: 52 | # If the output format is PYTHON, we should have kept the wrapped type without building the real class 53 | assert output_format == betterproto2.OutputFormat.PROTO_JSON 54 | 55 | assert 0 <= self.nanos < 1e9 56 | 57 | if self.nanos == 0: 58 | return f"{self.seconds}s" 59 | 60 | nanos = f"{self.nanos:09d}".rstrip("0") 61 | if len(nanos) < 3: 62 | nanos += "0" * (3 - len(nanos)) 63 | 64 | return f"{self.seconds}.{nanos}s" 65 | 66 | @staticmethod 67 | def from_wrapped(wrapped: datetime.timedelta) -> "Duration": 68 | return Duration.from_timedelta(wrapped) 69 | 70 | def to_wrapped(self) -> datetime.timedelta: 71 | return self.to_timedelta() 72 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/templates/service_stub_sync.py.j2: -------------------------------------------------------------------------------- 1 | {% extends "service_stub.py.j2" %} 2 | 3 | {# Class definition #} 4 | {% block class_name %}{{ service.py_name }}{% if output_file.settings.client_generation.is_sync_prefixed %}Sync{% endif %}Stub{% endblock %} 5 | 6 | {% block class_content %} 7 | {# TODO move to parent class #} 8 | def __init__(self, channel: grpc.Channel): 9 | self._channel = channel 10 | {% endblock %} 11 | 12 | {# Methods definition #} 13 | {% block method_definition %} 14 | def {{ method.py_name }}(self 15 | {%- if not method.client_streaming -%} 16 | , message: 17 | {%- if method.is_input_msg_empty -%} 18 | "{{ method.py_input_message_type }} | None" = None 19 | {%- else -%} 20 | "{{ method.py_input_message_type }}" 21 | {%- endif -%} 22 | {%- else -%} 23 | {# Client streaming: need a request iterator instead #} 24 | , messages: "Iterable[{{ method.py_input_message_type }}]" 25 | {%- endif -%} 26 | ) -> "{% if method.server_streaming %}Iterator[{{ method.py_output_message_type }}]{% else %}{{ method.py_output_message_type }}{% endif %}": 27 | {% endblock %} 28 | 29 | {% block method_body %} 30 | {% if method.server_streaming %} 31 | {% if method.client_streaming %} 32 | yield from self._channel.stream_stream( 33 | "{{ method.route }}", 34 | {{ method.py_input_message_type }}.SerializeToString, 35 | {{ method.py_output_message_type }}.FromString, 36 | )(iter(messages)) 37 | {% else %} 38 | {% if method.is_input_msg_empty %} 39 | if message is None: 40 | message = {{ method.py_input_message_type }}() 41 | 42 | {% endif %} 43 | yield from self._channel.unary_stream( 44 | "{{ method.route }}", 45 | {{ method.py_input_message_type }}.SerializeToString, 46 | {{ method.py_output_message_type }}.FromString, 47 | )(message) 48 | 49 | {% endif %} 50 | {% else %} 51 | {% if method.client_streaming %} 52 | return self._channel.stream_unary( 53 | "{{ method.route }}", 54 | {{ method.py_input_message_type }}.SerializeToString, 55 | {{ method.py_output_message_type }}.FromString, 56 | )(iter(messages)) 57 | {% else %} 58 | {% if method.is_input_msg_empty %} 59 | if message is None: 60 | message = {{ method.py_input_message_type }}() 61 | 62 | {% endif %} 63 | return self._channel.unary_unary( 64 | "{{ method.route }}", 65 | {{ method.py_input_message_type }}.SerializeToString, 66 | {{ method.py_output_message_type }}.FromString, 67 | )(message) 68 | {% endif %} 69 | {% endif %} 70 | {% endblock %} -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/known_types/any.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import betterproto2 4 | from typing_extensions import Self 5 | 6 | from betterproto2_compiler.lib.google.protobuf import Any as VanillaAny 7 | 8 | default_message_pool = betterproto2.MessagePool() # Only for typing purpose 9 | 10 | 11 | class Any(VanillaAny): 12 | @classmethod 13 | def pack(cls, message: betterproto2.Message, message_pool: "betterproto2.MessagePool | None" = None) -> "Any": 14 | """ 15 | Pack the given message in the `Any` object. 16 | 17 | The message type must be registered in the message pool, which is done automatically when the module defining 18 | the message type is imported. 19 | """ 20 | message_pool = message_pool or default_message_pool 21 | 22 | type_url = message_pool.type_to_url[type(message)] 23 | value = bytes(message) 24 | 25 | return cls(type_url=type_url, value=value) 26 | 27 | def unpack(self, message_pool: "betterproto2.MessagePool | None" = None) -> betterproto2.Message | None: 28 | """ 29 | Return the message packed inside the `Any` object. 30 | 31 | The target message type must be registered in the message pool, which is done automatically when the module 32 | defining the message type is imported. 33 | """ 34 | if not self.type_url: 35 | return None 36 | 37 | message_pool = message_pool or default_message_pool 38 | 39 | try: 40 | message_type = message_pool.url_to_type[self.type_url] 41 | except KeyError: 42 | raise TypeError(f"Can't unpack unregistered type: {self.type_url}") 43 | 44 | return message_type.parse(self.value) 45 | 46 | def to_dict(self, **kwargs) -> dict[str, typing.Any]: 47 | # TODO allow passing a message pool to `to_dict` 48 | output: dict[str, typing.Any] = {"@type": self.type_url} 49 | 50 | value = self.unpack() 51 | 52 | if value is None: 53 | return output 54 | 55 | if type(value).to_dict == betterproto2.Message.to_dict: 56 | output.update(value.to_dict(**kwargs)) 57 | else: 58 | output["value"] = value.to_dict(**kwargs) 59 | 60 | return output 61 | 62 | # TODO typing 63 | @classmethod 64 | def from_dict(cls, value, *, ignore_unknown_fields: bool = False) -> Self: 65 | value = dict(value) # Make a copy 66 | 67 | type_url = value.pop("@type", None) 68 | msg_cls = default_message_pool.url_to_type.get(type_url, None) 69 | 70 | if not msg_cls: 71 | raise TypeError(f"Can't unpack unregistered type: {type_url}") 72 | 73 | if not msg_cls.to_dict == betterproto2.Message.to_dict: 74 | value = value["value"] 75 | 76 | return cls( 77 | type_url=type_url, value=bytes(msg_cls.from_dict(value, ignore_unknown_fields=ignore_unknown_fields)) 78 | ) 79 | -------------------------------------------------------------------------------- /betterproto2/tests/grpc/test_stream_stream.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections.abc import AsyncIterator 3 | 4 | import pytest 5 | 6 | from tests.grpc.async_channel import AsyncChannel 7 | from tests.outputs.stream_stream.stream_stream import Message 8 | 9 | 10 | @pytest.fixture 11 | def expected_responses(): 12 | return [Message("Hello world 1"), Message("Hello world 2"), Message("Done")] 13 | 14 | 15 | class ClientStub: 16 | async def connect(self, requests: AsyncIterator): 17 | await asyncio.sleep(0.1) 18 | async for request in requests: 19 | await asyncio.sleep(0.1) 20 | yield request 21 | await asyncio.sleep(0.1) 22 | yield Message("Done") 23 | 24 | 25 | async def to_list(generator: AsyncIterator): 26 | return [value async for value in generator] 27 | 28 | 29 | @pytest.fixture 30 | def client(): 31 | # channel = Channel(host='127.0.0.1', port=50051) 32 | # return ClientStub(channel) 33 | return ClientStub() 34 | 35 | 36 | @pytest.mark.asyncio 37 | async def test_send_from_before_connect_and_close_automatically(client, expected_responses): 38 | requests = AsyncChannel() 39 | await requests.send_from([Message(body="Hello world 1"), Message(body="Hello world 2")], close=True) 40 | responses = client.connect(requests) 41 | 42 | assert await to_list(responses) == expected_responses 43 | 44 | 45 | @pytest.mark.asyncio 46 | async def test_send_from_after_connect_and_close_automatically(client, expected_responses): 47 | requests = AsyncChannel() 48 | responses = client.connect(requests) 49 | await requests.send_from([Message(body="Hello world 1"), Message(body="Hello world 2")], close=True) 50 | 51 | assert await to_list(responses) == expected_responses 52 | 53 | 54 | @pytest.mark.asyncio 55 | async def test_send_from_close_manually_immediately(client, expected_responses): 56 | requests = AsyncChannel() 57 | responses = client.connect(requests) 58 | await requests.send_from([Message(body="Hello world 1"), Message(body="Hello world 2")], close=False) 59 | requests.close() 60 | 61 | assert await to_list(responses) == expected_responses 62 | 63 | 64 | @pytest.mark.asyncio 65 | async def test_send_individually_and_close_before_connect(client, expected_responses): 66 | requests = AsyncChannel() 67 | await requests.send(Message(body="Hello world 1")) 68 | await requests.send(Message(body="Hello world 2")) 69 | requests.close() 70 | responses = client.connect(requests) 71 | 72 | assert await to_list(responses) == expected_responses 73 | 74 | 75 | @pytest.mark.asyncio 76 | async def test_send_individually_and_close_after_connect(client, expected_responses): 77 | requests = AsyncChannel() 78 | await requests.send(Message(body="Hello world 1")) 79 | await requests.send(Message(body="Hello world 2")) 80 | responses = client.connect(requests) 81 | requests.close() 82 | 83 | assert await to_list(responses) == expected_responses 84 | -------------------------------------------------------------------------------- /betterproto2/tests/grpc/thing_service.py: -------------------------------------------------------------------------------- 1 | import grpclib 2 | import grpclib.server 3 | 4 | from tests.outputs.service.service import ( 5 | DoThingRequest, 6 | DoThingResponse, 7 | GetThingRequest, 8 | GetThingResponse, 9 | ) 10 | 11 | 12 | class ThingService: 13 | def __init__(self, test_hook=None): 14 | # This lets us pass assertions to the servicer ;) 15 | self.test_hook = test_hook 16 | 17 | async def do_thing(self, stream: "grpclib.server.Stream[DoThingRequest, DoThingResponse]"): 18 | request = await stream.recv_message() 19 | if self.test_hook is not None: 20 | self.test_hook(stream) 21 | await stream.send_message(DoThingResponse([request.name])) 22 | 23 | async def do_many_things(self, stream: "grpclib.server.Stream[DoThingRequest, DoThingResponse]"): 24 | thing_names = [request.name async for request in stream] 25 | if self.test_hook is not None: 26 | self.test_hook(stream) 27 | await stream.send_message(DoThingResponse(thing_names)) 28 | 29 | async def get_thing_versions(self, stream: "grpclib.server.Stream[GetThingRequest, GetThingResponse]"): 30 | request = await stream.recv_message() 31 | if self.test_hook is not None: 32 | self.test_hook(stream) 33 | for version_num in range(1, 6): 34 | await stream.send_message(GetThingResponse(name=request.name, version=version_num)) 35 | 36 | async def get_different_things(self, stream: "grpclib.server.Stream[GetThingRequest, GetThingResponse]"): 37 | if self.test_hook is not None: 38 | self.test_hook(stream) 39 | # Respond to each input item immediately 40 | response_num = 0 41 | async for request in stream: 42 | response_num += 1 43 | await stream.send_message(GetThingResponse(name=request.name, version=response_num)) 44 | 45 | def __mapping__(self) -> dict[str, "grpclib.const.Handler"]: 46 | return { 47 | "/service.Test/DoThing": grpclib.const.Handler( 48 | self.do_thing, 49 | grpclib.const.Cardinality.UNARY_UNARY, 50 | DoThingRequest, 51 | DoThingResponse, 52 | ), 53 | "/service.Test/DoManyThings": grpclib.const.Handler( 54 | self.do_many_things, 55 | grpclib.const.Cardinality.STREAM_UNARY, 56 | DoThingRequest, 57 | DoThingResponse, 58 | ), 59 | "/service.Test/GetThingVersions": grpclib.const.Handler( 60 | self.get_thing_versions, 61 | grpclib.const.Cardinality.UNARY_STREAM, 62 | GetThingRequest, 63 | GetThingResponse, 64 | ), 65 | "/service.Test/GetDifferentThings": grpclib.const.Handler( 66 | self.get_different_things, 67 | grpclib.const.Cardinality.STREAM_STREAM, 68 | GetThingRequest, 69 | GetThingResponse, 70 | ), 71 | } 72 | -------------------------------------------------------------------------------- /betterproto2/tests/test_sync_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import threading 3 | from collections.abc import AsyncIterator 4 | 5 | import pytest 6 | 7 | from tests.util import requires_grpcio, requires_grpclib # noqa: F401 8 | 9 | 10 | @pytest.mark.asyncio 11 | async def test_sync_client(requires_grpcio, requires_grpclib): 12 | import grpc 13 | from grpclib.server import Server 14 | 15 | from tests.outputs.simple_service.simple_service import Request, Response, SimpleServiceBase, SimpleServiceSyncStub 16 | 17 | class SimpleService(SimpleServiceBase): 18 | async def get_unary_unary(self, message: "Request") -> "Response": 19 | return Response(message=f"Hello {message.value}") 20 | 21 | async def get_unary_stream(self, message: "Request") -> "AsyncIterator[Response]": 22 | for i in range(5): 23 | yield Response(message=f"Hello {message.value} {i}") 24 | 25 | async def get_stream_unary(self, messages: "AsyncIterator[Request]") -> "Response": 26 | s = 0 27 | async for m in messages: 28 | s += m.value 29 | return Response(message=f"Hello {s}") 30 | 31 | async def get_stream_stream(self, messages: "AsyncIterator[Request]") -> "AsyncIterator[Response]": 32 | async for message in messages: 33 | yield Response(message=f"Hello {message.value}") 34 | 35 | start_server_event = threading.Event() 36 | close_server_event = asyncio.Event() 37 | 38 | def start_server(): 39 | async def run_server(): 40 | server = Server([SimpleService()]) 41 | await server.start("127.0.0.1", 1234) 42 | start_server_event.set() 43 | 44 | await close_server_event.wait() 45 | server.close() 46 | 47 | loop = asyncio.new_event_loop() 48 | loop.run_until_complete(run_server()) 49 | loop.close() 50 | 51 | # We need to start the server in a new thread to avoid a deadlock 52 | server_thread = threading.Thread(target=start_server) 53 | server_thread.start() 54 | 55 | # Create a sync client 56 | start_server_event.wait() 57 | 58 | with grpc.insecure_channel("localhost:1234") as channel: 59 | client = SimpleServiceSyncStub(channel) 60 | 61 | response = client.get_unary_unary(Request(value=42)) 62 | assert response.message == "Hello 42" 63 | 64 | response = client.get_unary_stream(Request(value=42)) 65 | assert [r.message for r in response] == [f"Hello 42 {i}" for i in range(5)] 66 | 67 | response = client.get_stream_unary([Request(value=i) for i in range(5)]) 68 | assert response.message == "Hello 10" 69 | 70 | response = client.get_stream_stream([Request(value=i) for i in range(5)]) 71 | assert [r.message for r in response] == [f"Hello {i}" for i in range(5)] 72 | 73 | close_server_event.set() 74 | 75 | # Create an async client 76 | # client = SimpleServiceStub(Channel(host="127.0.0.1", port=1234)) 77 | # response = await client.get_unary_unary(Request(value=42)) 78 | # assert response.message == "Hello 42" 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better Protobuf / gRPC Support for Python 2 | 3 | ![](https://github.com/betterproto/python-betterproto2/actions/workflows/ci.yml/badge.svg) 4 | 5 | > :warning: `betterproto2` is a fork of the original [`betterproto`](https://github.com/danielgtaylor/python-betterproto) repository. It is a major redesign of the library, allowing to fix several bugs and to support new features. 6 | > 7 | > However, it is still in active developement. The documentation is not complete, there is still work to do and the project is still subject to breaking changes. 8 | 9 | This project aims to provide an improved experience when using Protobuf / gRPC in a modern Python environment by making use of modern language features and generating readable, understandable, idiomatic Python code. It will not support legacy features or environments (e.g. Protobuf 2). The following are supported: 10 | 11 | - Protobuf 3 & gRPC code generation 12 | - Both binary & JSON serialization is built-in 13 | - Python 3.7+ making use of: 14 | - Enums 15 | - Dataclasses 16 | - `async`/`await` 17 | - Timezone-aware `datetime` and `timedelta` objects 18 | - Relative imports 19 | - Mypy type checking 20 | - [Pydantic Models](https://docs.pydantic.dev/) generation 21 | 22 | 23 | ## Motivation 24 | 25 | This project exists because of the following limitations of the Google protoc plugin for Python. 26 | 27 | - No `async` support (requires additional `grpclib` plugin) 28 | - No typing support or code completion/intelligence (requires additional `mypy` plugin) 29 | - No `__init__.py` module files get generated 30 | - Output is not importable 31 | - Import paths break in Python 3 unless you mess with `sys.path` 32 | - Bugs when names clash (e.g. `codecs` package) 33 | - Generated code is not idiomatic 34 | - Completely unreadable runtime code-generation 35 | - Much code looks like C++ or Java ported 1:1 to Python 36 | - Capitalized function names like `HasField()` and `SerializeToString()` 37 | - Uses `SerializeToString()` rather than the built-in `__bytes__()` 38 | - Special wrapped types don't use Python's `None` 39 | - Timestamp/duration types don't use Python's built-in `datetime` module 40 | 41 | This project is a reimplementation from the ground up focused on idiomatic modern Python to help fix some of the above. While it may not be a 1:1 drop-in replacement due to changed method names and call patterns, the wire format is identical. 42 | 43 | ## Documentation 44 | 45 | The documentation of betterproto is available online: https://betterproto.github.io/python-betterproto2/ 46 | 47 | ## Development 48 | 49 | - _Join us on [Discord](https://discord.gg/DEVteTupPb)!_ 50 | 51 | ### Requirements 52 | 53 | - Python (3.10 or higher) 54 | 55 | - [poetry](https://python-poetry.org/docs/#installation) 56 | *Needed to install dependencies in a virtual environment* 57 | 58 | - [poethepoet](https://github.com/nat-n/poethepoet) for running development tasks as defined in pyproject.toml 59 | - Can be installed to your host environment via `pip install poethepoet` then executed as simple `poe` 60 | - or run from the poetry venv as `poetry run poe` 61 | 62 | ## License 63 | 64 | Copyright © 2019 Daniel G. Taylor 65 | 66 | Copyright © 2024 The betterproto contributors 67 | -------------------------------------------------------------------------------- /betterproto2/tests/test_enum.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import betterproto2 4 | 5 | 6 | class Colour(betterproto2.Enum): 7 | RED = 1 8 | GREEN = 2 9 | BLUE = 3 10 | 11 | 12 | PURPLE = Colour(4) 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "member, str_value", 17 | [ 18 | (Colour.RED, "RED"), 19 | (Colour.GREEN, "GREEN"), 20 | (Colour.BLUE, "BLUE"), 21 | (PURPLE, "UNKNOWN(4)"), 22 | ], 23 | ) 24 | def test_str(member: Colour, str_value: str) -> None: 25 | assert str(member) == str_value 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "member, repr_value", 30 | [ 31 | (Colour.RED, ""), 32 | (Colour.GREEN, ""), 33 | (Colour.BLUE, ""), 34 | (PURPLE, ""), 35 | ], 36 | ) 37 | def test_repr(member: Colour, repr_value: str) -> None: 38 | assert repr(member) == repr_value 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "member, values", 43 | [ 44 | (Colour.RED, ("RED", 1)), 45 | (Colour.GREEN, ("GREEN", 2)), 46 | (Colour.BLUE, ("BLUE", 3)), 47 | (PURPLE, ("", 4)), 48 | ], 49 | ) 50 | def test_name_values(member: Colour, values: tuple[str | None, int]) -> None: 51 | assert (member.name, member.value) == values 52 | 53 | 54 | @pytest.mark.parametrize( 55 | "member, input_str", 56 | [ 57 | (Colour.RED, "RED"), 58 | (Colour.GREEN, "GREEN"), 59 | (Colour.BLUE, "BLUE"), 60 | ], 61 | ) 62 | def test_from_string(member: Colour, input_str: str) -> None: 63 | assert Colour.from_string(input_str) == member 64 | 65 | 66 | @pytest.mark.parametrize( 67 | "member, input_int", 68 | [ 69 | (Colour.RED, 1), 70 | (Colour.GREEN, 2), 71 | (Colour.BLUE, 3), 72 | (PURPLE, 4), 73 | ], 74 | ) 75 | def test_construction(member: Colour, input_int: int) -> None: 76 | assert Colour(input_int) == member 77 | 78 | 79 | def test_enum_renaming() -> None: 80 | from tests.outputs.enum.enum import ArithmeticOperator, HttpCode, NoStriping 81 | 82 | assert set(ArithmeticOperator.__members__) == {"NONE", "PLUS", "MINUS", "_0_PREFIXED"} 83 | assert set(HttpCode.__members__) == {"UNSPECIFIED", "OK", "NOT_FOUND"} 84 | assert set(NoStriping.__members__) == {"NO_STRIPING_NONE", "NO_STRIPING_A", "B"} 85 | 86 | 87 | def test_enum_to_dict() -> None: 88 | from tests.outputs.enum.enum import ArithmeticOperator, EnumMessage, NoStriping 89 | 90 | msg = EnumMessage( 91 | arithmetic_operator=ArithmeticOperator.PLUS, 92 | no_striping=NoStriping.NO_STRIPING_A, 93 | ) 94 | 95 | assert msg.to_dict() == { 96 | "arithmeticOperator": "ARITHMETIC_OPERATOR_PLUS", # The original proto name must be preserved 97 | "noStriping": "NO_STRIPING_A", 98 | } 99 | 100 | assert EnumMessage.from_dict(msg.to_dict()) == msg 101 | 102 | 103 | def test_unknown_variant_to_dict() -> None: 104 | from tests.outputs.enum.enum import NewVersion, NewVersionMessage, OldVersionMessage 105 | 106 | serialized = bytes(NewVersionMessage(new_version=NewVersion.V3)) 107 | 108 | deserialized = OldVersionMessage.parse(serialized) 109 | 110 | assert deserialized.to_dict() == {"oldVersion": 3} 111 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/templates/service_stub_async.py.j2: -------------------------------------------------------------------------------- 1 | {% extends "service_stub.py.j2" %} 2 | 3 | {# Class definition #} 4 | {% block class_name %}{{ service.py_name }}{% if output_file.settings.client_generation.is_async_prefixed %}Async{% endif %}Stub{% endblock %} 5 | {% block inherit_from %}betterproto2_grpclib.ServiceStub{% endblock %} 6 | 7 | {# Methods definition #} 8 | {% block method_definition %} 9 | async def {{ method.py_name }}(self 10 | {%- if not method.client_streaming -%} 11 | , message: 12 | {%- if method.is_input_msg_empty -%} 13 | "{{ method.py_input_message_type }} | None" = None 14 | {%- else -%} 15 | "{{ method.py_input_message_type }}" 16 | {%- endif -%} 17 | {%- else -%} 18 | {# Client streaming: need a request iterator instead #} 19 | , messages: "AsyncIterable[{{ method.py_input_message_type }}] | Iterable[{{ method.py_input_message_type }}]" 20 | {%- endif -%} 21 | , 22 | * 23 | , timeout: "float | None" = None 24 | , deadline: "Deadline | None" = None 25 | , metadata: "MetadataLike | None" = None 26 | ) -> "{% if method.server_streaming %}AsyncIterator[{{ method.py_output_message_type }}]{% else %}{{ method.py_output_message_type }}{% endif %}": 27 | {% endblock %} 28 | 29 | {% block method_body %} 30 | {% if method.server_streaming %} 31 | {% if method.client_streaming %} 32 | async for response in self._stream_stream( 33 | "{{ method.route }}", 34 | messages, 35 | {{ method.py_input_message_type }}, 36 | {{ method.py_output_message_type }}, 37 | timeout=timeout, 38 | deadline=deadline, 39 | metadata=metadata, 40 | ): 41 | yield response 42 | {% else %} 43 | {% if method.is_input_msg_empty %} 44 | if message is None: 45 | message = {{ method.py_input_message_type }}() 46 | 47 | {% endif %} 48 | async for response in self._unary_stream( 49 | "{{ method.route }}", 50 | message, 51 | {{ method.py_output_message_type }}, 52 | timeout=timeout, 53 | deadline=deadline, 54 | metadata=metadata, 55 | ): 56 | yield response 57 | 58 | {% endif %} 59 | {% else %} 60 | {% if method.client_streaming %} 61 | return await self._stream_unary( 62 | "{{ method.route }}", 63 | messages, 64 | {{ method.py_input_message_type }}, 65 | {{ method.py_output_message_type }}, 66 | timeout=timeout, 67 | deadline=deadline, 68 | metadata=metadata, 69 | ) 70 | {% else %} 71 | {% if method.is_input_msg_empty %} 72 | if message is None: 73 | message = {{ method.py_input_message_type }}() 74 | 75 | {% endif %} 76 | return await self._unary_unary( 77 | "{{ method.route }}", 78 | message, 79 | {{ method.py_output_message_type }}, 80 | timeout=timeout, 81 | deadline=deadline, 82 | metadata=metadata, 83 | ) 84 | {% endif %} 85 | {% endif %} 86 | {% endblock %} -------------------------------------------------------------------------------- /betterproto2/tests/test_pickling.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from copy import copy, deepcopy 3 | 4 | import cachelib 5 | 6 | from tests.outputs.pickling.google import protobuf as google 7 | from tests.outputs.pickling.pickling import Complex, Fe, Fi, NestedData, PickledMessage 8 | 9 | 10 | def unpickled(message): 11 | return pickle.loads(pickle.dumps(message)) 12 | 13 | 14 | def complex_msg(): 15 | return Complex( 16 | foo_str="yep", 17 | fe=Fe(abc="1"), 18 | nested_data=NestedData( 19 | struct_foo={ 20 | "foo": google.Struct.from_dict( 21 | { 22 | "hello": [["world"]], 23 | } 24 | ), 25 | }, 26 | ), 27 | mapping={ 28 | "message": google.Any.pack(Fi(abc="hi")), 29 | }, 30 | ) 31 | 32 | 33 | def test_pickling_complex_message(): 34 | msg = complex_msg() 35 | deser = unpickled(msg) 36 | assert msg == deser 37 | assert msg.fe.abc == "1" 38 | assert msg.is_set("fi") is not True 39 | assert msg.mapping["message"] == google.Any.pack(Fi(abc="hi")) 40 | assert msg.nested_data.struct_foo["foo"].to_dict()["hello"][0][0] == "world" 41 | 42 | 43 | def test_recursive_message_defaults(): 44 | from tests.outputs.recursivemessage.recursivemessage import Intermediate, Test as RecursiveMessage 45 | 46 | msg = RecursiveMessage(name="bob", intermediate=Intermediate(42)) 47 | msg = unpickled(msg) 48 | 49 | assert msg == RecursiveMessage(name="bob", intermediate=Intermediate(42)) 50 | msg.child = RecursiveMessage(child=RecursiveMessage(name="jude")) 51 | assert msg == RecursiveMessage( 52 | name="bob", 53 | intermediate=Intermediate(42), 54 | child=RecursiveMessage(child=RecursiveMessage(name="jude")), 55 | ) 56 | 57 | 58 | def test_copyability(): 59 | msg = PickledMessage(bar=12, baz=["hello"]) 60 | msg = unpickled(msg) 61 | 62 | copied = copy(msg) 63 | assert msg == copied 64 | assert msg is not copied 65 | assert msg.baz is copied.baz 66 | 67 | deepcopied = deepcopy(msg) 68 | assert msg == deepcopied 69 | assert msg is not deepcopied 70 | assert msg.baz is not deepcopied.baz 71 | 72 | 73 | def test_message_can_be_cached(): 74 | """Cachelib uses pickling to cache values""" 75 | 76 | cache = cachelib.SimpleCache() 77 | 78 | def use_cache(): 79 | calls = getattr(use_cache, "calls", 0) 80 | result = cache.get("message") 81 | if result is not None: 82 | return result 83 | else: 84 | setattr(use_cache, "calls", calls + 1) 85 | result = complex_msg() 86 | cache.set("message", result) 87 | return result 88 | 89 | for n in range(10): 90 | if n == 0: 91 | assert not cache.has("message") 92 | else: 93 | assert cache.has("message") 94 | 95 | msg = use_cache() 96 | assert use_cache.calls == 1 # The message is only ever built once 97 | assert msg.fe.abc == "1" 98 | assert not msg.is_set("fi") 99 | assert msg.mapping["message"] == google.Any.pack(Fi(abc="hi")) 100 | assert msg.nested_data.struct_foo["foo"].to_dict()["hello"][0][0] == "world" 101 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/example_service/test_example_service.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncIterator 2 | 3 | import pytest 4 | 5 | from tests.util import requires_grpclib # noqa: F401 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_calls_with_different_cardinalities(requires_grpclib): 10 | from grpclib.testing import ChannelFor 11 | 12 | from tests.outputs.example_service.example_service import ( 13 | ExampleRequest, 14 | ExampleResponse, 15 | TestBase, 16 | TestStub, 17 | ) 18 | 19 | class ExampleService(TestBase): 20 | async def example_unary_unary(self, example_request: ExampleRequest) -> "ExampleResponse": 21 | return ExampleResponse( 22 | example_string=example_request.example_string, 23 | example_integer=example_request.example_integer, 24 | ) 25 | 26 | async def example_unary_stream(self, example_request: ExampleRequest) -> AsyncIterator["ExampleResponse"]: 27 | response = ExampleResponse( 28 | example_string=example_request.example_string, 29 | example_integer=example_request.example_integer, 30 | ) 31 | yield response 32 | yield response 33 | yield response 34 | 35 | async def example_stream_unary( 36 | self, example_request_iterator: AsyncIterator["ExampleRequest"] 37 | ) -> "ExampleResponse": 38 | async for example_request in example_request_iterator: 39 | return ExampleResponse( 40 | example_string=example_request.example_string, 41 | example_integer=example_request.example_integer, 42 | ) 43 | 44 | async def example_stream_stream( 45 | self, example_request_iterator: AsyncIterator["ExampleRequest"] 46 | ) -> AsyncIterator["ExampleResponse"]: 47 | async for example_request in example_request_iterator: 48 | yield ExampleResponse( 49 | example_string=example_request.example_string, 50 | example_integer=example_request.example_integer, 51 | ) 52 | 53 | example_request = ExampleRequest("test string", 42) 54 | 55 | async with ChannelFor([ExampleService()]) as channel: 56 | stub = TestStub(channel) 57 | 58 | # unary unary 59 | response = await stub.example_unary_unary(example_request) 60 | assert response.example_string == example_request.example_string 61 | assert response.example_integer == example_request.example_integer 62 | 63 | # unary stream 64 | async for response in stub.example_unary_stream(example_request): 65 | assert response.example_string == example_request.example_string 66 | assert response.example_integer == example_request.example_integer 67 | 68 | # stream unary 69 | async def request_iterator(): 70 | yield example_request 71 | yield example_request 72 | yield example_request 73 | 74 | response = await stub.example_stream_unary(request_iterator()) 75 | assert response.example_string == example_request.example_string 76 | assert response.example_integer == example_request.example_integer 77 | 78 | # stream stream 79 | async for response in stub.example_stream_stream(request_iterator()): 80 | assert response.example_string == example_request.example_string 81 | assert response.example_integer == example_request.example_integer 82 | -------------------------------------------------------------------------------- /betterproto2/tests/util.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | from collections.abc import Callable 4 | from dataclasses import dataclass 5 | from pathlib import Path 6 | from types import ModuleType 7 | 8 | import pytest 9 | 10 | os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" 11 | 12 | root_path = Path(__file__).resolve().parent 13 | inputs_path = root_path.joinpath("inputs") 14 | 15 | 16 | def get_directories(path): 17 | for root, directories, files in os.walk(path): 18 | yield from directories 19 | 20 | 21 | @dataclass 22 | class TestCaseJsonFile: 23 | json: str 24 | test_name: str 25 | file_name: str 26 | 27 | 28 | def get_test_case_json_data(test_case_name: str, *json_file_names: str) -> list[TestCaseJsonFile]: 29 | """ 30 | :return: 31 | A list of all files found in "{inputs_path}/test_case_name" with names matching 32 | f"{test_case_name}.json" or f"{test_case_name}_*.json", OR given by 33 | json_file_names 34 | """ 35 | test_case_dir = inputs_path.joinpath(test_case_name) 36 | possible_file_paths = [ 37 | *(test_case_dir.joinpath(json_file_name) for json_file_name in json_file_names), 38 | test_case_dir.joinpath(f"{test_case_name}.json"), 39 | *test_case_dir.glob(f"{test_case_name}_*.json"), 40 | ] 41 | 42 | result = [] 43 | for test_data_file_path in possible_file_paths: 44 | if not test_data_file_path.exists(): 45 | continue 46 | with test_data_file_path.open("r") as fh: 47 | result.append(TestCaseJsonFile(fh.read(), test_case_name, test_data_file_path.name.split(".")[0])) 48 | 49 | return result 50 | 51 | 52 | def find_module(module: ModuleType, predicate: Callable[[ModuleType], bool]) -> ModuleType | None: 53 | """ 54 | Recursively search module tree for a module that matches the search predicate. 55 | Assumes that the submodules are directories containing __init__.py. 56 | 57 | Example: 58 | 59 | # find module inside foo that contains Test 60 | import foo 61 | test_module = find_module(foo, lambda m: hasattr(m, 'Test')) 62 | """ 63 | if predicate(module): 64 | return module 65 | 66 | module_path = Path(*module.__path__) 67 | 68 | for sub in [sub.parent for sub in module_path.glob("**/__init__.py")]: 69 | if sub == module_path: 70 | continue 71 | sub_module_path = sub.relative_to(module_path) 72 | sub_module_name = ".".join(sub_module_path.parts) 73 | 74 | sub_module = importlib.import_module(f".{sub_module_name}", module.__name__) 75 | 76 | if predicate(sub_module): 77 | return sub_module 78 | 79 | return None 80 | 81 | 82 | @pytest.fixture 83 | def requires_pydantic(): 84 | try: 85 | import pydantic # noqa: F401 86 | except ImportError: 87 | pytest.skip("pydantic is not installed") 88 | 89 | 90 | @pytest.fixture 91 | def requires_grpclib(): 92 | try: 93 | import grpclib # noqa: F401 94 | except ImportError: 95 | pytest.skip("grpclib is not installed") 96 | 97 | 98 | @pytest.fixture 99 | def requires_grpcio(): 100 | try: 101 | import grpc # noqa: F401 102 | except ImportError: 103 | pytest.skip("grpcio is not installed") 104 | 105 | 106 | @pytest.fixture 107 | def requires_protobuf(): 108 | try: 109 | import google.protobuf # noqa: F401 110 | except ImportError: 111 | pytest.skip("protobuf is not installed") 112 | -------------------------------------------------------------------------------- /betterproto2_compiler/tests/test_module_validation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from betterproto2_compiler.plugin.module_validation import ModuleValidator 4 | 5 | 6 | @pytest.mark.parametrize( 7 | ["text", "expected_collisions"], 8 | [ 9 | pytest.param( 10 | ["import os"], 11 | None, 12 | id="single import", 13 | ), 14 | pytest.param( 15 | ["import os", "import sys"], 16 | None, 17 | id="multiple imports", 18 | ), 19 | pytest.param( 20 | ["import os", "import os"], 21 | {"os"}, 22 | id="duplicate imports", 23 | ), 24 | pytest.param( 25 | ["from os import path", "import os"], 26 | None, 27 | id="duplicate imports with alias", 28 | ), 29 | pytest.param( 30 | ["from os import path", "import os as os_alias"], 31 | None, 32 | id="duplicate imports with alias", 33 | ), 34 | pytest.param( 35 | ["from os import path", "import os as path"], 36 | {"path"}, 37 | id="duplicate imports with alias", 38 | ), 39 | pytest.param( 40 | ["import os", "class os:"], 41 | {"os"}, 42 | id="duplicate import with class", 43 | ), 44 | pytest.param( 45 | ["import os", "class os:", " pass", "import sys"], 46 | {"os"}, 47 | id="duplicate import with class and another", 48 | ), 49 | pytest.param( 50 | ["def test(): pass", "class test:"], 51 | {"test"}, 52 | id="duplicate class and function", 53 | ), 54 | pytest.param( 55 | ["def test(): pass", "def test(): pass"], 56 | {"test"}, 57 | id="duplicate functions", 58 | ), 59 | pytest.param( 60 | ["def test(): pass", "test = 100"], 61 | {"test"}, 62 | id="function and variable", 63 | ), 64 | pytest.param( 65 | ["def test():", " test = 3"], 66 | None, 67 | id="function and variable in function", 68 | ), 69 | pytest.param( 70 | [ 71 | "def test(): pass", 72 | "'''", 73 | "def test(): pass", 74 | "'''", 75 | "def test_2(): pass", 76 | ], 77 | None, 78 | id="duplicate functions with multiline string", 79 | ), 80 | pytest.param( 81 | ["def test(): pass", "# def test(): pass"], 82 | None, 83 | id="duplicate functions with comments", 84 | ), 85 | pytest.param( 86 | ["from test import (", " A", " B", " C", ")"], 87 | None, 88 | id="multiline import", 89 | ), 90 | pytest.param( 91 | ["from test import (", " A", " B", " C", ")", "from test import A"], 92 | {"A"}, 93 | id="multiline import with duplicate", 94 | ), 95 | ], 96 | ) 97 | def test_module_validator(text: list[str], expected_collisions: set[str] | None): 98 | line_iterator = iter(text) 99 | validator = ModuleValidator(line_iterator) 100 | valid = validator.validate() 101 | if expected_collisions is None: 102 | assert valid 103 | else: 104 | assert set(validator.collisions.keys()) == expected_collisions 105 | assert not valid 106 | -------------------------------------------------------------------------------- /betterproto2_compiler/src/betterproto2_compiler/casing.py: -------------------------------------------------------------------------------- 1 | import keyword 2 | import re 3 | 4 | # Word delimiters and symbols that will not be preserved when re-casing. 5 | # language=PythonRegExp 6 | SYMBOLS = "[^a-zA-Z0-9]*" 7 | 8 | # Optionally capitalized word. 9 | # language=PythonRegExp 10 | WORD = "[A-Z]*[a-z]*[0-9]*" 11 | 12 | # Uppercase word, not followed by lowercase letters. 13 | # language=PythonRegExp 14 | WORD_UPPER = "[A-Z]+(?![a-z])[0-9]*" 15 | 16 | 17 | def safe_snake_case(value: str) -> str: 18 | """Snake case a value taking into account Python keywords.""" 19 | value = snake_case(value) 20 | value = sanitize_name(value) 21 | return value 22 | 23 | 24 | def snake_case(name: str) -> str: 25 | """ 26 | Join words with an underscore into lowercase and remove symbols. 27 | """ 28 | 29 | # If there are already underscores in the name, don't break it 30 | if "_" in name or not any([c.isupper() for c in name]): 31 | return name 32 | 33 | # Add an underscore before capital letters 34 | name = re.sub(r"(?<=[a-z0-9])([A-Z])", r"_\1", name) 35 | 36 | # Add an underscore before capital letters following an acronym 37 | name = re.sub(r"(?<=[A-Z])([A-Z])(?=[a-z])", r"_\1", name) 38 | 39 | # Add an underscore before digits 40 | name = re.sub(r"(?<=[a-zA-Z])([0-9])", r"_\1", name) 41 | 42 | return name.lower() 43 | 44 | 45 | def pascal_case(value: str, strict: bool = True) -> str: 46 | """ 47 | Capitalize each word and remove symbols. 48 | 49 | Parameters 50 | ----------- 51 | value: :class:`str` 52 | The value to convert. 53 | strict: :class:`bool` 54 | Whether or not to output only alphanumeric characters. 55 | 56 | Returns 57 | -------- 58 | :class:`str` 59 | The value in PascalCase. 60 | """ 61 | 62 | def substitute_word(symbols, word): 63 | if strict: 64 | return word.capitalize() # Remove all delimiters 65 | 66 | if word.islower(): 67 | delimiter_length = len(symbols[:-1]) # Lose one delimiter 68 | else: 69 | delimiter_length = len(symbols) # Preserve all delimiters 70 | 71 | return ("_" * delimiter_length) + word.capitalize() 72 | 73 | return re.sub( 74 | f"({SYMBOLS})({WORD_UPPER}|{WORD})", 75 | lambda groups: substitute_word(groups[1], groups[2]), 76 | value, 77 | ) 78 | 79 | 80 | def camel_case(value: str, strict: bool = True) -> str: 81 | """ 82 | Capitalize all words except first and remove symbols. 83 | 84 | Parameters 85 | ----------- 86 | value: :class:`str` 87 | The value to convert. 88 | strict: :class:`bool` 89 | Whether or not to output only alphanumeric characters. 90 | 91 | Returns 92 | -------- 93 | :class:`str` 94 | The value in camelCase. 95 | """ 96 | return lowercase_first(pascal_case(value, strict=strict)) 97 | 98 | 99 | def lowercase_first(value: str) -> str: 100 | """ 101 | Lower cases the first character of the value. 102 | 103 | Parameters 104 | ---------- 105 | value: :class:`str` 106 | The value to lower case. 107 | 108 | Returns 109 | ------- 110 | :class:`str` 111 | The lower cased string. 112 | """ 113 | return value[0:1].lower() + value[1:] 114 | 115 | 116 | def sanitize_name(value: str) -> str: 117 | # https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles 118 | if keyword.iskeyword(value): 119 | return f"{value}_" 120 | if not value.isidentifier(): 121 | return f"_{value}" 122 | return value 123 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/enum/test_enum.py: -------------------------------------------------------------------------------- 1 | from tests.outputs.enum.enum import ( 2 | Choice, 3 | Test, 4 | ) 5 | 6 | 7 | def test_enum_set_and_get(): 8 | assert Test(choice=Choice.ZERO).choice == Choice.ZERO 9 | assert Test(choice=Choice.ONE).choice == Choice.ONE 10 | assert Test(choice=Choice.THREE).choice == Choice.THREE 11 | assert Test(choice=Choice.FOUR).choice == Choice.FOUR 12 | 13 | 14 | def test_enum_set_with_int(): 15 | assert Test(choice=0).choice == Choice.ZERO 16 | assert Test(choice=1).choice == Choice.ONE 17 | assert Test(choice=3).choice == Choice.THREE 18 | assert Test(choice=4).choice == Choice.FOUR 19 | 20 | 21 | def test_enum_is_comparable_with_int(): 22 | assert Test(choice=Choice.ZERO).choice == 0 23 | assert Test(choice=Choice.ONE).choice == 1 24 | assert Test(choice=Choice.THREE).choice == 3 25 | assert Test(choice=Choice.FOUR).choice == 4 26 | 27 | 28 | def test_enum_to_dict(): 29 | assert "choice" not in Test(choice=Choice.ZERO).to_dict(), "Default enum value is not serialized" 30 | assert Test(choice=Choice.ZERO).to_dict(include_default_values=True)["choice"] == "ZERO" 31 | assert Test(choice=Choice.ONE).to_dict()["choice"] == "ONE" 32 | assert Test(choice=Choice.THREE).to_dict()["choice"] == "THREE" 33 | assert Test(choice=Choice.FOUR).to_dict()["choice"] == "FOUR" 34 | 35 | 36 | def test_repeated_enum_is_comparable_with_int(): 37 | assert Test(choices=[Choice.ZERO]).choices == [0] 38 | assert Test(choices=[Choice.ONE]).choices == [1] 39 | assert Test(choices=[Choice.THREE]).choices == [3] 40 | assert Test(choices=[Choice.FOUR]).choices == [4] 41 | 42 | 43 | def test_repeated_enum_set_and_get(): 44 | assert Test(choices=[Choice.ZERO]).choices == [Choice.ZERO] 45 | assert Test(choices=[Choice.ONE]).choices == [Choice.ONE] 46 | assert Test(choices=[Choice.THREE]).choices == [Choice.THREE] 47 | assert Test(choices=[Choice.FOUR]).choices == [Choice.FOUR] 48 | 49 | 50 | def test_repeated_enum_to_dict(): 51 | assert Test(choices=[Choice.ZERO]).to_dict()["choices"] == ["ZERO"] 52 | assert Test(choices=[Choice.ONE]).to_dict()["choices"] == ["ONE"] 53 | assert Test(choices=[Choice.THREE]).to_dict()["choices"] == ["THREE"] 54 | assert Test(choices=[Choice.FOUR]).to_dict()["choices"] == ["FOUR"] 55 | 56 | all_enums_dict = Test(choices=[Choice.ZERO, Choice.ONE, Choice.THREE, Choice.FOUR]).to_dict() 57 | assert (all_enums_dict["choices"]) == ["ZERO", "ONE", "THREE", "FOUR"] 58 | 59 | 60 | def test_repeated_enum_with_non_list_iterables_to_dict(): 61 | assert Test(choices=(1, 3)).to_dict()["choices"] == ["ONE", "THREE"] 62 | assert Test(choices=(1, 3)).to_dict()["choices"] == ["ONE", "THREE"] 63 | assert Test(choices=(Choice.ONE, Choice.THREE)).to_dict()["choices"] == [ 64 | "ONE", 65 | "THREE", 66 | ] 67 | 68 | def enum_generator(): 69 | yield Choice.ONE 70 | yield Choice.THREE 71 | 72 | assert Test(choices=enum_generator()).to_dict()["choices"] == ["ONE", "THREE"] 73 | 74 | 75 | def test_enum_mapped_on_parse(): 76 | # test default value 77 | b = Test.parse(bytes(Test())) 78 | assert b.choice.name == Choice.ZERO.name 79 | assert b.choices == [] 80 | 81 | # test non default value 82 | a = Test.parse(bytes(Test(choice=Choice.ONE))) 83 | assert a.choice.name == Choice.ONE.name 84 | assert b.choices == [] 85 | 86 | # test repeated 87 | c = Test.parse(bytes(Test(choices=[Choice.THREE, Choice.FOUR]))) 88 | assert c.choices[0].name == Choice.THREE.name 89 | assert c.choices[1].name == Choice.FOUR.name 90 | 91 | # bonus: defaults after empty init are also mapped 92 | assert Test().choice.name == Choice.ZERO.name 93 | -------------------------------------------------------------------------------- /betterproto2/tests/inputs/google_impl_behavior_equivalence/test_google_impl_behavior_equivalence.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | import pytest 4 | 5 | from tests.outputs.google_impl_behavior_equivalence.google_impl_behavior_equivalence import ( 6 | Empty, 7 | Foo, 8 | Request, 9 | Spam, 10 | Test, 11 | ) 12 | from tests.util import requires_protobuf # noqa: F401 13 | 14 | 15 | def test_oneof_serializes_similar_to_google_oneof(requires_protobuf): 16 | from google.protobuf import json_format 17 | 18 | from tests.outputs.google_impl_behavior_equivalence_reference.google_impl_behavior_equivalence_pb2 import ( 19 | Foo as ReferenceFoo, 20 | Test as ReferenceTest, 21 | ) 22 | 23 | tests = [ 24 | (Test(string="abc"), ReferenceTest(string="abc")), 25 | (Test(integer=2), ReferenceTest(integer=2)), 26 | (Test(foo=Foo(bar=1)), ReferenceTest(foo=ReferenceFoo(bar=1))), 27 | # Default values should also behave the same within oneofs 28 | (Test(string=""), ReferenceTest(string="")), 29 | (Test(integer=0), ReferenceTest(integer=0)), 30 | (Test(foo=Foo(bar=0)), ReferenceTest(foo=ReferenceFoo(bar=0))), 31 | ] 32 | for message, message_reference in tests: 33 | # NOTE: As of July 2020, MessageToJson inserts newlines in the output string so, 34 | # just compare dicts 35 | assert message.to_dict() == json_format.MessageToDict(message_reference) 36 | 37 | 38 | def test_bytes_are_the_same_for_oneof(requires_protobuf): 39 | from tests.outputs.google_impl_behavior_equivalence_reference.google_impl_behavior_equivalence_pb2 import ( 40 | Foo as ReferenceFoo, 41 | Test as ReferenceTest, 42 | ) 43 | 44 | message = Test(string="") 45 | message_reference = ReferenceTest(string="") 46 | 47 | message_bytes = bytes(message) 48 | message_reference_bytes = message_reference.SerializeToString() 49 | 50 | assert message_bytes == message_reference_bytes 51 | 52 | message2 = Test.parse(message_reference_bytes) 53 | message_reference2 = ReferenceTest() 54 | message_reference2.ParseFromString(message_reference_bytes) 55 | 56 | assert message == message2 57 | assert message_reference == message_reference2 58 | 59 | # None of these fields were explicitly set BUT they should not actually be null 60 | # themselves 61 | assert message.foo is None 62 | assert message2.foo is None 63 | 64 | assert isinstance(message_reference.foo, ReferenceFoo) 65 | assert isinstance(message_reference2.foo, ReferenceFoo) 66 | 67 | 68 | @pytest.mark.parametrize("dt", (datetime.min.replace(tzinfo=timezone.utc),)) 69 | def test_datetime_clamping(dt, requires_protobuf): # see #407 70 | from google.protobuf.timestamp_pb2 import Timestamp 71 | 72 | from tests.outputs.google_impl_behavior_equivalence_reference.google_impl_behavior_equivalence_pb2 import ( 73 | Spam as ReferenceSpam, 74 | ) 75 | 76 | ts = Timestamp() 77 | ts.FromDatetime(dt) 78 | assert bytes(Spam(dt)) == ReferenceSpam(ts=ts).SerializeToString() 79 | message_bytes = bytes(Spam(dt)) 80 | 81 | assert Spam.parse(message_bytes).ts.timestamp() == ReferenceSpam.FromString(message_bytes).ts.seconds 82 | 83 | 84 | def test_empty_message_field(requires_protobuf): 85 | from tests.outputs.google_impl_behavior_equivalence_reference.google_impl_behavior_equivalence_pb2 import ( 86 | Empty as ReferenceEmpty, 87 | Request as ReferenceRequest, 88 | ) 89 | 90 | message = Request() 91 | reference_message = ReferenceRequest() 92 | 93 | message.foo = Empty() 94 | reference_message.foo.CopyFrom(ReferenceEmpty()) 95 | 96 | assert bytes(message) == reference_message.SerializeToString() 97 | --------------------------------------------------------------------------------