Compare commits
	
		
			12 Commits
		
	
	
		
			285-semant
			...
			changelog1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 3eaff291c4 | ||
|  | 9b5594adbe | ||
|  | d991040ff6 | ||
|  | d260f071e0 | ||
|  | 6dd7baa26c | ||
|  | 573c7292a6 | ||
|  | d77f44ebb7 | ||
|  | 671c0ff4ac | ||
|  | 9cecc8c3ff | ||
|  | bc3cfc5562 | ||
|  | b0a36d12e4 | ||
|  | a4d2d39546 | 
							
								
								
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -15,7 +15,7 @@ jobs: | |||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         os: [Ubuntu, MacOS, Windows] |         os: [Ubuntu, MacOS, Windows] | ||||||
|         python-version: [3.6, 3.7, 3.8, 3.9] |         python-version: ['3.6.7', '3.7', '3.8', '3.9', '3.10'] | ||||||
|         exclude: |         exclude: | ||||||
|           - os: Windows |           - os: Windows | ||||||
|             python-version: 3.6 |             python-version: 3.6 | ||||||
| @@ -66,4 +66,4 @@ jobs: | |||||||
|  |  | ||||||
|       - name: Execute test suite |       - name: Execute test suite | ||||||
|         shell: bash |         shell: bash | ||||||
|         run: poetry run pytest tests/ |         run: poetry run python -m pytest tests/ | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -17,3 +17,4 @@ output | |||||||
| .venv | .venv | ||||||
| .asv | .asv | ||||||
| venv | venv | ||||||
|  | .devcontainer | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||||||
|  |  | ||||||
| - Versions suffixed with `b*` are in `beta` and can be installed with `pip install --pre betterproto`. | - Versions suffixed with `b*` are in `beta` and can be installed with `pip install --pre betterproto`. | ||||||
|  |  | ||||||
|  | ## [Unreleased] | ||||||
|  |  | ||||||
|  | - fix: Format field comments also as docstrings (#304)  | ||||||
|  | - fix: Fix message text in NotImplementedError (#325) | ||||||
|  | - **Breaking**: Client and Service Stubs take 1 request parameter, not one for each field (#311)   | ||||||
|  |   Client and Service Stubs no longer pack and unpack the input message fields as parameters. | ||||||
|  |  | ||||||
|  |     Update your client calls and server handlers as follows: | ||||||
|  |  | ||||||
|  |     Clients before: | ||||||
|  |     ```py | ||||||
|  |     response = await service.echo(value="hello", extra_times=1) | ||||||
|  |     ``` | ||||||
|  |     Clients after: | ||||||
|  |     ```py | ||||||
|  |     response = await service.echo(EchoRequest(value="hello", extra_times=1)) | ||||||
|  |     ``` | ||||||
|  |     Servers before: | ||||||
|  |     ```py | ||||||
|  |     async def echo(self, value: str, extra_times: int) -> EchoResponse: | ||||||
|  |     ``` | ||||||
|  |     Servers after: | ||||||
|  |     ```py | ||||||
|  |     async def echo(self, echo_request: EchoRequest) -> EchoResponse: | ||||||
|  |         # Use echo_request.value | ||||||
|  |         # Use echo_request.extra_times | ||||||
|  |     ``` | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## [2.0.0b4] - 2022-01-03 | ||||||
|  |  | ||||||
|  | - **Breaking**: the minimum Python version has been bumped to `3.6.2` | ||||||
|  |  | ||||||
|  | - Always add `AsyncIterator` to imports if there are services [#264](https://github.com/danielgtaylor/python-betterproto/pull/264) | ||||||
|  | - Allow parsing of messages from `ByteStrings` [#266](https://github.com/danielgtaylor/python-betterproto/pull/266) | ||||||
|  | - Add support for proto3 optional [#281](https://github.com/danielgtaylor/python-betterproto/pull/281) | ||||||
|  |  | ||||||
|  | - Fix compilation of fields with names identical to builtin types [#294](https://github.com/danielgtaylor/python-betterproto/pull/294) | ||||||
|  | - Fix default values for enum service args [#299](https://github.com/danielgtaylor/python-betterproto/pull/299) | ||||||
|  |  | ||||||
| ## [2.0.0b3] - 2021-04-07 | ## [2.0.0b3] - 2021-04-07 | ||||||
|  |  | ||||||
| - Generate grpclib service stubs [#170](https://github.com/danielgtaylor/python-betterproto/pull/170) | - Generate grpclib service stubs [#170](https://github.com/danielgtaylor/python-betterproto/pull/170) | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							| @@ -177,10 +177,10 @@ from grpclib.client import Channel | |||||||
| async def main(): | async def main(): | ||||||
|     channel = Channel(host="127.0.0.1", port=50051) |     channel = Channel(host="127.0.0.1", port=50051) | ||||||
|     service = echo.EchoStub(channel) |     service = echo.EchoStub(channel) | ||||||
|     response = await service.echo(value="hello", extra_times=1) |     response = await service.echo(echo.EchoRequest(value="hello", extra_times=1)) | ||||||
|     print(response) |     print(response) | ||||||
|  |  | ||||||
|     async for response in service.echo_stream(value="hello", extra_times=1): |     async for response in service.echo_stream(echo.EchoRequest(value="hello", extra_times=1)): | ||||||
|         print(response) |         print(response) | ||||||
|  |  | ||||||
|     # don't forget to close the channel when done! |     # don't forget to close the channel when done! | ||||||
| @@ -206,18 +206,18 @@ service methods: | |||||||
|  |  | ||||||
| ```python | ```python | ||||||
| import asyncio | import asyncio | ||||||
| from echo import EchoBase, EchoResponse, EchoStreamResponse | from echo import EchoBase, EchoRequest, EchoResponse, EchoStreamResponse | ||||||
| from grpclib.server import Server | from grpclib.server import Server | ||||||
| from typing import AsyncIterator | from typing import AsyncIterator | ||||||
|  |  | ||||||
|  |  | ||||||
| class EchoService(EchoBase): | class EchoService(EchoBase): | ||||||
|     async def echo(self, value: str, extra_times: int) -> "EchoResponse": |     async def echo(self, echo_request: "EchoRequest") -> "EchoResponse": | ||||||
|         return EchoResponse([value for _ in range(extra_times)]) |         return EchoResponse([echo_request.value for _ in range(echo_request.extra_times)]) | ||||||
|  |  | ||||||
|     async def echo_stream(self, value: str, extra_times: int) -> AsyncIterator["EchoStreamResponse"]: |     async def echo_stream(self, echo_request: "EchoRequest") -> AsyncIterator["EchoStreamResponse"]: | ||||||
|         for _ in range(extra_times): |         for _ in range(echo_request.extra_times): | ||||||
|             yield EchoStreamResponse(value) |             yield EchoStreamResponse(echo_request.value) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def main(): | async def main(): | ||||||
|   | |||||||
							
								
								
									
										1330
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1330
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "betterproto" | name = "betterproto" | ||||||
| version = "2.0.0b3" | version = "2.0.0b4" | ||||||
| description = "A better Protobuf / gRPC generator & library" | description = "A better Protobuf / gRPC generator & library" | ||||||
| authors = ["Daniel G. Taylor <danielgtaylor@gmail.com>"] | authors = ["Daniel G. Taylor <danielgtaylor@gmail.com>"] | ||||||
| readme = "README.md" | readme = "README.md" | ||||||
| @@ -12,7 +12,7 @@ packages = [ | |||||||
| ] | ] | ||||||
|  |  | ||||||
| [tool.poetry.dependencies] | [tool.poetry.dependencies] | ||||||
| python = "^3.6" | python = ">=3.6.2,<4.0" | ||||||
| black = { version = ">=19.3b0", optional = true } | black = { version = ">=19.3b0", optional = true } | ||||||
| dataclasses = { version = "^0.7", python = ">=3.6, <3.7" } | dataclasses = { version = "^0.7", python = ">=3.6, <3.7" } | ||||||
| grpclib = "^0.4.1" | grpclib = "^0.4.1" | ||||||
| @@ -21,14 +21,14 @@ python-dateutil = "^2.8" | |||||||
|  |  | ||||||
| [tool.poetry.dev-dependencies] | [tool.poetry.dev-dependencies] | ||||||
| asv = "^0.4.2" | asv = "^0.4.2" | ||||||
| black = "^20.8b1" | black = "^21.11b0" | ||||||
| bpython = "^0.19" | bpython = "^0.19" | ||||||
| grpcio-tools = "^1.30.0" | grpcio-tools = "^1.40.0" | ||||||
| jinja2 = "^2.11.2" | jinja2 = "^2.11.2" | ||||||
| mypy = "^0.770" | mypy = "^0.930" | ||||||
| poethepoet = ">=0.9.0" | poethepoet = ">=0.9.0" | ||||||
| protobuf = "^3.12.2" | protobuf = "^3.12.2" | ||||||
| pytest = "^5.4.2" | pytest = "^6.2.5" | ||||||
| pytest-asyncio = "^0.12.0" | pytest-asyncio = "^0.12.0" | ||||||
| pytest-cov = "^2.9.0" | pytest-cov = "^2.9.0" | ||||||
| pytest-mock = "^3.1.1" | pytest-mock = "^3.1.1" | ||||||
| @@ -111,7 +111,7 @@ omit = ["betterproto/tests/*"] | |||||||
| legacy_tox_ini = """ | legacy_tox_ini = """ | ||||||
| [tox] | [tox] | ||||||
| isolated_build = true | isolated_build = true | ||||||
| envlist = py36, py37, py38 | envlist = py36, py37, py38, py310 | ||||||
|  |  | ||||||
| [testenv] | [testenv] | ||||||
| whitelist_externals = poetry | whitelist_externals = poetry | ||||||
|   | |||||||
| @@ -145,6 +145,8 @@ class FieldMetadata: | |||||||
|     group: Optional[str] = None |     group: Optional[str] = None | ||||||
|     # Describes the wrapped type (e.g. when using google.protobuf.BoolValue) |     # Describes the wrapped type (e.g. when using google.protobuf.BoolValue) | ||||||
|     wraps: Optional[str] = None |     wraps: Optional[str] = None | ||||||
|  |     # Is the field optional | ||||||
|  |     optional: Optional[bool] = False | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def get(field: dataclasses.Field) -> "FieldMetadata": |     def get(field: dataclasses.Field) -> "FieldMetadata": | ||||||
| @@ -159,12 +161,15 @@ def dataclass_field( | |||||||
|     map_types: Optional[Tuple[str, str]] = None, |     map_types: Optional[Tuple[str, str]] = None, | ||||||
|     group: Optional[str] = None, |     group: Optional[str] = None, | ||||||
|     wraps: Optional[str] = None, |     wraps: Optional[str] = None, | ||||||
|  |     optional: bool = False, | ||||||
| ) -> dataclasses.Field: | ) -> dataclasses.Field: | ||||||
|     """Creates a dataclass field with attached protobuf metadata.""" |     """Creates a dataclass field with attached protobuf metadata.""" | ||||||
|     return dataclasses.field( |     return dataclasses.field( | ||||||
|         default=PLACEHOLDER, |         default=None if optional else PLACEHOLDER, | ||||||
|         metadata={ |         metadata={ | ||||||
|             "betterproto": FieldMetadata(number, proto_type, map_types, group, wraps) |             "betterproto": FieldMetadata( | ||||||
|  |                 number, proto_type, map_types, group, wraps, optional | ||||||
|  |             ) | ||||||
|         }, |         }, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @@ -174,74 +179,107 @@ def dataclass_field( | |||||||
| # out at runtime. The generated dataclass variables are still typed correctly. | # out at runtime. The generated dataclass variables are still typed correctly. | ||||||
|  |  | ||||||
|  |  | ||||||
| def enum_field(number: int, group: Optional[str] = None) -> Any: | def enum_field(number: int, group: Optional[str] = None, optional: bool = False) -> Any: | ||||||
|     return dataclass_field(number, TYPE_ENUM, group=group) |     return dataclass_field(number, TYPE_ENUM, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def bool_field(number: int, group: Optional[str] = None) -> Any: | def bool_field(number: int, group: Optional[str] = None, optional: bool = False) -> Any: | ||||||
|     return dataclass_field(number, TYPE_BOOL, group=group) |     return dataclass_field(number, TYPE_BOOL, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def int32_field(number: int, group: Optional[str] = None) -> Any: | def int32_field( | ||||||
|     return dataclass_field(number, TYPE_INT32, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_INT32, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def int64_field(number: int, group: Optional[str] = None) -> Any: | def int64_field( | ||||||
|     return dataclass_field(number, TYPE_INT64, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_INT64, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def uint32_field(number: int, group: Optional[str] = None) -> Any: | def uint32_field( | ||||||
|     return dataclass_field(number, TYPE_UINT32, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_UINT32, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def uint64_field(number: int, group: Optional[str] = None) -> Any: | def uint64_field( | ||||||
|     return dataclass_field(number, TYPE_UINT64, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_UINT64, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def sint32_field(number: int, group: Optional[str] = None) -> Any: | def sint32_field( | ||||||
|     return dataclass_field(number, TYPE_SINT32, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_SINT32, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def sint64_field(number: int, group: Optional[str] = None) -> Any: | def sint64_field( | ||||||
|     return dataclass_field(number, TYPE_SINT64, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_SINT64, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def float_field(number: int, group: Optional[str] = None) -> Any: | def float_field( | ||||||
|     return dataclass_field(number, TYPE_FLOAT, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_FLOAT, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def double_field(number: int, group: Optional[str] = None) -> Any: | def double_field( | ||||||
|     return dataclass_field(number, TYPE_DOUBLE, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_DOUBLE, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def fixed32_field(number: int, group: Optional[str] = None) -> Any: | def fixed32_field( | ||||||
|     return dataclass_field(number, TYPE_FIXED32, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_FIXED32, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def fixed64_field(number: int, group: Optional[str] = None) -> Any: | def fixed64_field( | ||||||
|     return dataclass_field(number, TYPE_FIXED64, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_FIXED64, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def sfixed32_field(number: int, group: Optional[str] = None) -> Any: | def sfixed32_field( | ||||||
|     return dataclass_field(number, TYPE_SFIXED32, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_SFIXED32, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def sfixed64_field(number: int, group: Optional[str] = None) -> Any: | def sfixed64_field( | ||||||
|     return dataclass_field(number, TYPE_SFIXED64, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_SFIXED64, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def string_field(number: int, group: Optional[str] = None) -> Any: | def string_field( | ||||||
|     return dataclass_field(number, TYPE_STRING, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_STRING, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def bytes_field(number: int, group: Optional[str] = None) -> Any: | def bytes_field( | ||||||
|     return dataclass_field(number, TYPE_BYTES, group=group) |     number: int, group: Optional[str] = None, optional: bool = False | ||||||
|  | ) -> Any: | ||||||
|  |     return dataclass_field(number, TYPE_BYTES, group=group, optional=optional) | ||||||
|  |  | ||||||
|  |  | ||||||
| def message_field( | def message_field( | ||||||
|     number: int, group: Optional[str] = None, wraps: Optional[str] = None |     number: int, | ||||||
|  |     group: Optional[str] = None, | ||||||
|  |     wraps: Optional[str] = None, | ||||||
|  |     optional: bool = False, | ||||||
| ) -> Any: | ) -> Any: | ||||||
|     return dataclass_field(number, TYPE_MESSAGE, group=group, wraps=wraps) |     return dataclass_field( | ||||||
|  |         number, TYPE_MESSAGE, group=group, wraps=wraps, optional=optional | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def map_field( | def map_field( | ||||||
| @@ -586,7 +624,8 @@ class Message(ABC): | |||||||
|             if meta.group: |             if meta.group: | ||||||
|                 group_current.setdefault(meta.group) |                 group_current.setdefault(meta.group) | ||||||
|  |  | ||||||
|             if self.__raw_get(field_name) != PLACEHOLDER: |             value = self.__raw_get(field_name) | ||||||
|  |             if value != PLACEHOLDER and not (meta.optional and value is None): | ||||||
|                 # Found a non-sentinel value |                 # Found a non-sentinel value | ||||||
|                 all_sentinel = False |                 all_sentinel = False | ||||||
|  |  | ||||||
| @@ -701,12 +740,16 @@ class Message(ABC): | |||||||
|  |  | ||||||
|             if value is None: |             if value is None: | ||||||
|                 # Optional items should be skipped. This is used for the Google |                 # Optional items should be skipped. This is used for the Google | ||||||
|                 # wrapper types. |                 # wrapper types and proto3 field presence/optional fields. | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             # Being selected in a a group means this field is the one that is |             # Being selected in a a group means this field is the one that is | ||||||
|             # currently set in a `oneof` group, so it must be serialized even |             # currently set in a `oneof` group, so it must be serialized even | ||||||
|             # if the value is the default zero value. |             # if the value is the default zero value. | ||||||
|  |             # | ||||||
|  |             # Note that proto3 field presence/optional fields are put in a | ||||||
|  |             # synthetic single-item oneof by protoc, which helps us ensure we | ||||||
|  |             # send the value even if the value is the default zero value. | ||||||
|             selected_in_group = ( |             selected_in_group = ( | ||||||
|                 meta.group and self._group_current[meta.group] == field_name |                 meta.group and self._group_current[meta.group] == field_name | ||||||
|             ) |             ) | ||||||
| @@ -803,7 +846,7 @@ class Message(ABC): | |||||||
|     @classmethod |     @classmethod | ||||||
|     def _type_hints(cls) -> Dict[str, Type]: |     def _type_hints(cls) -> Dict[str, Type]: | ||||||
|         module = sys.modules[cls.__module__] |         module = sys.modules[cls.__module__] | ||||||
|         return get_type_hints(cls, vars(module)) |         return get_type_hints(cls, module.__dict__, {}) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _cls_for(cls, field: dataclasses.Field, index: int = 0) -> Type: |     def _cls_for(cls, field: dataclasses.Field, index: int = 0) -> Type: | ||||||
| @@ -829,8 +872,9 @@ class Message(ABC): | |||||||
|                 # This is some kind of list (repeated) field. |                 # This is some kind of list (repeated) field. | ||||||
|                 return list |                 return list | ||||||
|             elif t.__origin__ is Union and t.__args__[1] is type(None): |             elif t.__origin__ is Union and t.__args__[1] is type(None): | ||||||
|                 # This is an optional (wrapped) field. For setting the default we |                 # This is an optional field (either wrapped, or using proto3 | ||||||
|                 # really don't care what kind of field it is. |                 # field presence). For setting the default we really don't care | ||||||
|  |                 # what kind of field it is. | ||||||
|                 return type(None) |                 return type(None) | ||||||
|             else: |             else: | ||||||
|                 return t |                 return t | ||||||
| @@ -1041,6 +1085,9 @@ class Message(ABC): | |||||||
|                         ] |                         ] | ||||||
|                     if value or include_default_values: |                     if value or include_default_values: | ||||||
|                         output[cased_name] = value |                         output[cased_name] = value | ||||||
|  |                 elif value is None: | ||||||
|  |                     if include_default_values: | ||||||
|  |                         output[cased_name] = value | ||||||
|                 elif ( |                 elif ( | ||||||
|                     value._serialized_on_wire |                     value._serialized_on_wire | ||||||
|                     or include_default_values |                     or include_default_values | ||||||
| @@ -1066,6 +1113,9 @@ class Message(ABC): | |||||||
|                 if meta.proto_type in INT_64_TYPES: |                 if meta.proto_type in INT_64_TYPES: | ||||||
|                     if field_is_repeated: |                     if field_is_repeated: | ||||||
|                         output[cased_name] = [str(n) for n in value] |                         output[cased_name] = [str(n) for n in value] | ||||||
|  |                     elif value is None: | ||||||
|  |                         if include_default_values: | ||||||
|  |                             output[cased_name] = value | ||||||
|                     else: |                     else: | ||||||
|                         output[cased_name] = str(value) |                         output[cased_name] = str(value) | ||||||
|                 elif meta.proto_type == TYPE_BYTES: |                 elif meta.proto_type == TYPE_BYTES: | ||||||
| @@ -1073,6 +1123,8 @@ class Message(ABC): | |||||||
|                         output[cased_name] = [ |                         output[cased_name] = [ | ||||||
|                             b64encode(b).decode("utf8") for b in value |                             b64encode(b).decode("utf8") for b in value | ||||||
|                         ] |                         ] | ||||||
|  |                     elif value is None and include_default_values: | ||||||
|  |                         output[cased_name] = value | ||||||
|                     else: |                     else: | ||||||
|                         output[cased_name] = b64encode(value).decode("utf8") |                         output[cased_name] = b64encode(value).decode("utf8") | ||||||
|                 elif meta.proto_type == TYPE_ENUM: |                 elif meta.proto_type == TYPE_ENUM: | ||||||
| @@ -1085,6 +1137,12 @@ class Message(ABC): | |||||||
|                         else: |                         else: | ||||||
|                             # transparently upgrade single value to repeated |                             # transparently upgrade single value to repeated | ||||||
|                             output[cased_name] = [enum_class(value).name] |                             output[cased_name] = [enum_class(value).name] | ||||||
|  |                     elif value is None: | ||||||
|  |                         if include_default_values: | ||||||
|  |                             output[cased_name] = value | ||||||
|  |                     elif meta.optional: | ||||||
|  |                         enum_class = field_types[field_name].__args__[0] | ||||||
|  |                         output[cased_name] = enum_class(value).name | ||||||
|                     else: |                     else: | ||||||
|                         enum_class = field_types[field_name]  # noqa |                         enum_class = field_types[field_name]  # noqa | ||||||
|                         output[cased_name] = enum_class(value).name |                         output[cased_name] = enum_class(value).name | ||||||
| @@ -1141,6 +1199,9 @@ class Message(ABC): | |||||||
|                         setattr(self, field_name, v) |                         setattr(self, field_name, v) | ||||||
|                     elif meta.wraps: |                     elif meta.wraps: | ||||||
|                         setattr(self, field_name, value[key]) |                         setattr(self, field_name, value[key]) | ||||||
|  |                     elif v is None: | ||||||
|  |                         cls = self._betterproto.cls_by_field[field_name] | ||||||
|  |                         setattr(self, field_name, cls().from_dict(value[key])) | ||||||
|                     else: |                     else: | ||||||
|                         # NOTE: `from_dict` mutates the underlying message, so no |                         # NOTE: `from_dict` mutates the underlying message, so no | ||||||
|                         # assignment here is necessary. |                         # assignment here is necessary. | ||||||
|   | |||||||
| @@ -133,16 +133,6 @@ def lowercase_first(value: str) -> str: | |||||||
|     return value[0:1].lower() + value[1:] |     return value[0:1].lower() + value[1:] | ||||||
|  |  | ||||||
|  |  | ||||||
| def is_reserved_name(value: str) -> bool: |  | ||||||
|     if keyword.iskeyword(value): |  | ||||||
|         return True |  | ||||||
|  |  | ||||||
|     if value in ("bytes", "str"): |  | ||||||
|         return True |  | ||||||
|  |  | ||||||
|     return False |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def sanitize_name(value: str) -> str: | def sanitize_name(value: str) -> str: | ||||||
|     # https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles |     # https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles | ||||||
|     return f"{value}_" if is_reserved_name(value) else value |     return f"{value}_" if keyword.iskeyword(value) else value | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from abc import ABC | from abc import ABC | ||||||
| from collections.abc import AsyncIterable | from collections.abc import AsyncIterable | ||||||
| from typing import Callable, Any, Dict | from typing import Any, Callable, Dict | ||||||
|  |  | ||||||
| import grpclib | import grpclib | ||||||
| import grpclib.server | import grpclib.server | ||||||
| @@ -15,10 +15,10 @@ class ServiceBase(ABC): | |||||||
|         self, |         self, | ||||||
|         handler: Callable, |         handler: Callable, | ||||||
|         stream: grpclib.server.Stream, |         stream: grpclib.server.Stream, | ||||||
|         request_kwargs: Dict[str, Any], |         request: Any, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|  |  | ||||||
|         response_iter = handler(**request_kwargs) |         response_iter = handler(request) | ||||||
|         # check if response is actually an AsyncIterator |         # check if response is actually an AsyncIterator | ||||||
|         # this might be false if the method just returns without |         # this might be false if the method just returns without | ||||||
|         # yielding at least once |         # yielding at least once | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ from dataclasses import dataclass | |||||||
| from typing import Dict, List | from typing import Dict, List | ||||||
|  |  | ||||||
| import betterproto | import betterproto | ||||||
|  | from betterproto.grpc.grpclib_server import ServiceBase | ||||||
|  |  | ||||||
|  |  | ||||||
| class Syntax(betterproto.Enum): | class Syntax(betterproto.Enum): | ||||||
| @@ -46,17 +47,6 @@ class FieldCardinality(betterproto.Enum): | |||||||
|     CARDINALITY_REPEATED = 3 |     CARDINALITY_REPEATED = 3 | ||||||
|  |  | ||||||
|  |  | ||||||
| class NullValue(betterproto.Enum): |  | ||||||
|     """ |  | ||||||
|     `NullValue` is a singleton enumeration to represent the null value for the |  | ||||||
|     `Value` type union.  The JSON representation for `NullValue` is JSON |  | ||||||
|     `null`. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # Null value. |  | ||||||
|     NULL_VALUE = 0 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class FieldDescriptorProtoType(betterproto.Enum): | class FieldDescriptorProtoType(betterproto.Enum): | ||||||
|     TYPE_DOUBLE = 1 |     TYPE_DOUBLE = 1 | ||||||
|     TYPE_FLOAT = 2 |     TYPE_FLOAT = 2 | ||||||
| @@ -108,165 +98,15 @@ class MethodOptionsIdempotencyLevel(betterproto.Enum): | |||||||
|     IDEMPOTENT = 2 |     IDEMPOTENT = 2 | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) | class NullValue(betterproto.Enum): | ||||||
| class Timestamp(betterproto.Message): |  | ||||||
|     """ |     """ | ||||||
|     A Timestamp represents a point in time independent of any time zone or |     `NullValue` is a singleton enumeration to represent the null value for the | ||||||
|     local calendar, encoded as a count of seconds and fractions of seconds at |     `Value` type union.  The JSON representation for `NullValue` is JSON | ||||||
|     nanosecond resolution. The count is relative to an epoch at UTC midnight on |     `null`. | ||||||
|     January 1, 1970, in the proleptic Gregorian calendar which extends the |  | ||||||
|     Gregorian calendar backwards to year one. All minutes are 60 seconds long. |  | ||||||
|     Leap seconds are "smeared" so that no leap second table is needed for |  | ||||||
|     interpretation, using a [24-hour linear |  | ||||||
|     smear](https://developers.google.com/time/smear). The range is from |  | ||||||
|     0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By restricting to |  | ||||||
|     that range, we ensure that we can convert to and from [RFC |  | ||||||
|     3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. # Examples |  | ||||||
|     Example 1: Compute Timestamp from POSIX `time()`.     Timestamp timestamp; |  | ||||||
|     timestamp.set_seconds(time(NULL));     timestamp.set_nanos(0); Example 2: |  | ||||||
|     Compute Timestamp from POSIX `gettimeofday()`.     struct timeval tv; |  | ||||||
|     gettimeofday(&tv, NULL);     Timestamp timestamp; |  | ||||||
|     timestamp.set_seconds(tv.tv_sec);     timestamp.set_nanos(tv.tv_usec * |  | ||||||
|     1000); Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. |  | ||||||
|     FILETIME ft;     GetSystemTimeAsFileTime(&ft);     UINT64 ticks = |  | ||||||
|     (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;     // A Windows |  | ||||||
|     tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z     // is |  | ||||||
|     11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.     Timestamp |  | ||||||
|     timestamp;     timestamp.set_seconds((INT64) ((ticks / 10000000) - |  | ||||||
|     11644473600LL));     timestamp.set_nanos((INT32) ((ticks % 10000000) * |  | ||||||
|     100)); Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. |  | ||||||
|     long millis = System.currentTimeMillis();     Timestamp timestamp = |  | ||||||
|     Timestamp.newBuilder().setSeconds(millis / 1000)         .setNanos((int) |  | ||||||
|     ((millis % 1000) * 1000000)).build(); Example 5: Compute Timestamp from |  | ||||||
|     current time in Python.     timestamp = Timestamp() |  | ||||||
|     timestamp.GetCurrentTime() # JSON Mapping In JSON format, the Timestamp |  | ||||||
|     type is encoded as a string in the [RFC |  | ||||||
|     3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the format is |  | ||||||
|     "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" where {year} is |  | ||||||
|     always expressed using four digits while {month}, {day}, {hour}, {min}, and |  | ||||||
|     {sec} are zero-padded to two digits each. The fractional seconds, which can |  | ||||||
|     go up to 9 digits (i.e. up to 1 nanosecond resolution), are optional. The |  | ||||||
|     "Z" suffix indicates the timezone ("UTC"); the timezone is required. A |  | ||||||
|     proto3 JSON serializer should always use UTC (as indicated by "Z") when |  | ||||||
|     printing the Timestamp type and a proto3 JSON parser should be able to |  | ||||||
|     accept both UTC and other timezones (as indicated by an offset). For |  | ||||||
|     example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past 01:30 UTC on |  | ||||||
|     January 15, 2017. In JavaScript, one can convert a Date object to this |  | ||||||
|     format using the standard [toISOString()](https://developer.mozilla.org/en- |  | ||||||
|     US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) method. |  | ||||||
|     In Python, a standard `datetime.datetime` object can be converted to this |  | ||||||
|     format using |  | ||||||
|     [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) |  | ||||||
|     with the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one |  | ||||||
|     can use the Joda Time's [`ISODateTimeFormat.dateTime()`]( |  | ||||||
|     http://www.joda.org/joda- |  | ||||||
|     time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D ) |  | ||||||
|     to obtain a formatter capable of generating timestamps in this format. |  | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     # Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must |     # Null value. | ||||||
|     # be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive. |     NULL_VALUE = 0 | ||||||
|     seconds: int = betterproto.int64_field(1) |  | ||||||
|     # Non-negative fractions of a second at nanosecond resolution. Negative |  | ||||||
|     # second values with fractions must still have non-negative nanos values that |  | ||||||
|     # count forward in time. Must be from 0 to 999,999,999 inclusive. |  | ||||||
|     nanos: int = betterproto.int32_field(2) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class FieldMask(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     `FieldMask` represents a set of symbolic field paths, for example: |  | ||||||
|     paths: "f.a"     paths: "f.b.d" Here `f` represents a field in some root |  | ||||||
|     message, `a` and `b` fields in the message found in `f`, and `d` a field |  | ||||||
|     found in the message in `f.b`. Field masks are used to specify a subset of |  | ||||||
|     fields that should be returned by a get operation or modified by an update |  | ||||||
|     operation. Field masks also have a custom JSON encoding (see below). # |  | ||||||
|     Field Masks in Projections When used in the context of a projection, a |  | ||||||
|     response message or sub-message is filtered by the API to only contain |  | ||||||
|     those fields as specified in the mask. For example, if the mask in the |  | ||||||
|     previous example is applied to a response message as follows:     f { |  | ||||||
|     a : 22       b {         d : 1         x : 2       }       y : 13     } |  | ||||||
|     z: 8 The result will not contain specific values for fields x,y and z |  | ||||||
|     (their value will be set to the default, and omitted in proto text output): |  | ||||||
|     f {       a : 22       b {         d : 1       }     } A repeated field is |  | ||||||
|     not allowed except at the last position of a paths string. If a FieldMask |  | ||||||
|     object is not present in a get operation, the operation applies to all |  | ||||||
|     fields (as if a FieldMask of all fields had been specified). Note that a |  | ||||||
|     field mask does not necessarily apply to the top-level response message. In |  | ||||||
|     case of a REST get operation, the field mask applies directly to the |  | ||||||
|     response, but in case of a REST list operation, the mask instead applies to |  | ||||||
|     each individual message in the returned resource list. In case of a REST |  | ||||||
|     custom method, other definitions may be used. Where the mask applies will |  | ||||||
|     be clearly documented together with its declaration in the API.  In any |  | ||||||
|     case, the effect on the returned resource/resources is required behavior |  | ||||||
|     for APIs. # Field Masks in Update Operations A field mask in update |  | ||||||
|     operations specifies which fields of the targeted resource are going to be |  | ||||||
|     updated. The API is required to only change the values of the fields as |  | ||||||
|     specified in the mask and leave the others untouched. If a resource is |  | ||||||
|     passed in to describe the updated values, the API ignores the values of all |  | ||||||
|     fields not covered by the mask. If a repeated field is specified for an |  | ||||||
|     update operation, new values will be appended to the existing repeated |  | ||||||
|     field in the target resource. Note that a repeated field is only allowed in |  | ||||||
|     the last position of a `paths` string. If a sub-message is specified in the |  | ||||||
|     last position of the field mask for an update operation, then new value |  | ||||||
|     will be merged into the existing sub-message in the target resource. For |  | ||||||
|     example, given the target message:     f {       b {         d: 1 |  | ||||||
|     x: 2       }       c: [1]     } And an update message:     f {       b { |  | ||||||
|     d: 10       }       c: [2]     } then if the field mask is:  paths: ["f.b", |  | ||||||
|     "f.c"] then the result will be:     f {       b {         d: 10         x: |  | ||||||
|     2       }       c: [1, 2]     } An implementation may provide options to |  | ||||||
|     override this default behavior for repeated and message fields. In order to |  | ||||||
|     reset a field's value to the default, the field must be in the mask and set |  | ||||||
|     to the default value in the provided resource. Hence, in order to reset all |  | ||||||
|     fields of a resource, provide a default instance of the resource and set |  | ||||||
|     all fields in the mask, or do not provide a mask as described below. If a |  | ||||||
|     field mask is not present on update, the operation applies to all fields |  | ||||||
|     (as if a field mask of all fields has been specified). Note that in the |  | ||||||
|     presence of schema evolution, this may mean that fields the client does not |  | ||||||
|     know and has therefore not filled into the request will be reset to their |  | ||||||
|     default. If this is unwanted behavior, a specific service may require a |  | ||||||
|     client to always specify a field mask, producing an error if not. As with |  | ||||||
|     get operations, the location of the resource which describes the updated |  | ||||||
|     values in the request message depends on the operation kind. In any case, |  | ||||||
|     the effect of the field mask is required to be honored by the API. ## |  | ||||||
|     Considerations for HTTP REST The HTTP kind of an update operation which |  | ||||||
|     uses a field mask must be set to PATCH instead of PUT in order to satisfy |  | ||||||
|     HTTP semantics (PUT must only be used for full updates). # JSON Encoding of |  | ||||||
|     Field Masks In JSON, a field mask is encoded as a single string where paths |  | ||||||
|     are separated by a comma. Fields name in each path are converted to/from |  | ||||||
|     lower-camel naming conventions. As an example, consider the following |  | ||||||
|     message declarations:     message Profile {       User user = 1; |  | ||||||
|     Photo photo = 2;     }     message User {       string display_name = 1; |  | ||||||
|     string address = 2;     } In proto a field mask for `Profile` may look as |  | ||||||
|     such:     mask {       paths: "user.display_name"       paths: "photo" |  | ||||||
|     } In JSON, the same mask is represented as below:     {       mask: |  | ||||||
|     "user.displayName,photo"     } # Field Masks and Oneof Fields Field masks |  | ||||||
|     treat fields in oneofs just as regular fields. Consider the following |  | ||||||
|     message:     message SampleMessage {       oneof test_oneof { |  | ||||||
|     string name = 4;         SubMessage sub_message = 9;       }     } The |  | ||||||
|     field mask can be:     mask {       paths: "name"     } Or:     mask { |  | ||||||
|     paths: "sub_message"     } Note that oneof type names ("test_oneof" in this |  | ||||||
|     case) cannot be used in paths. ## Field Mask Verification The |  | ||||||
|     implementation of any API method which has a FieldMask type field in the |  | ||||||
|     request should verify the included field paths, and return an |  | ||||||
|     `INVALID_ARGUMENT` error if any path is unmappable. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The set of field mask paths. |  | ||||||
|     paths: List[str] = betterproto.string_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class SourceContext(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     `SourceContext` represents information about the source of a protobuf |  | ||||||
|     element, like the file in which it is defined. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The path-qualified name of the .proto file that contained the associated |  | ||||||
|     # protobuf element.  For example: `"google/protobuf/source_context.proto"`. |  | ||||||
|     file_name: str = betterproto.string_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) | @dataclass(eq=False, repr=False) | ||||||
| @@ -283,24 +123,25 @@ class Any(betterproto.Message): | |||||||
|     Example 3: Pack and unpack a message in Python.     foo = Foo(...)     any |     Example 3: Pack and unpack a message in Python.     foo = Foo(...)     any | ||||||
|     = Any()     any.Pack(foo)     ...     if any.Is(Foo.DESCRIPTOR): |     = Any()     any.Pack(foo)     ...     if any.Is(Foo.DESCRIPTOR): | ||||||
|     any.Unpack(foo)       ...  Example 4: Pack and unpack a message in Go |     any.Unpack(foo)       ...  Example 4: Pack and unpack a message in Go | ||||||
|     foo := &pb.Foo{...}      any, err := ptypes.MarshalAny(foo)      ... |     foo := &pb.Foo{...}      any, err := anypb.New(foo)      if err != nil { | ||||||
|     foo := &pb.Foo{}      if err := ptypes.UnmarshalAny(any, foo); err != nil { |     ...      }      ...      foo := &pb.Foo{}      if err := | ||||||
|     ...      } The pack methods provided by protobuf library will by default |     any.UnmarshalTo(foo); err != nil {        ...      } The pack methods | ||||||
|     use 'type.googleapis.com/full.type.name' as the type URL and the unpack |     provided by protobuf library will by default use | ||||||
|     methods only use the fully qualified type name after the last '/' in the |     'type.googleapis.com/full.type.name' as the type URL and the unpack methods | ||||||
|     type URL, for example "foo.bar.com/x/y.z" will yield type name "y.z". JSON |     only use the fully qualified type name after the last '/' in the type URL, | ||||||
|     ==== The JSON representation of an `Any` value uses the regular |     for example "foo.bar.com/x/y.z" will yield type name "y.z". JSON ==== The | ||||||
|     representation of the deserialized, embedded message, with an additional |     JSON representation of an `Any` value uses the regular representation of | ||||||
|     field `@type` which contains the type URL. Example:     package |     the deserialized, embedded message, with an additional field `@type` which | ||||||
|     google.profile;     message Person {       string first_name = 1; |     contains the type URL. Example:     package google.profile;     message | ||||||
|     string last_name = 2;     }     {       "@type": |     Person {       string first_name = 1;       string last_name = 2;     } | ||||||
|     "type.googleapis.com/google.profile.Person",       "firstName": <string>, |     {       "@type": "type.googleapis.com/google.profile.Person", | ||||||
|     "lastName": <string>     } If the embedded message type is well-known and |     "firstName": <string>,       "lastName": <string>     } If the embedded | ||||||
|     has a custom JSON representation, that representation will be embedded |     message type is well-known and has a custom JSON representation, that | ||||||
|     adding a field `value` which holds the custom JSON in addition to the |     representation will be embedded adding a field `value` which holds the | ||||||
|     `@type` field. Example (for message [google.protobuf.Duration][]):     { |     custom JSON in addition to the `@type` field. Example (for message | ||||||
|     "@type": "type.googleapis.com/google.protobuf.Duration",       "value": |     [google.protobuf.Duration][]):     {       "@type": | ||||||
|     "1.212s"     } |     "type.googleapis.com/google.protobuf.Duration",       "value": "1.212s" | ||||||
|  |     } | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     # A URL/resource name that uniquely identifies the type of the serialized |     # A URL/resource name that uniquely identifies the type of the serialized | ||||||
| @@ -327,6 +168,18 @@ class Any(betterproto.Message): | |||||||
|     value: bytes = betterproto.bytes_field(2) |     value: bytes = betterproto.bytes_field(2) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class SourceContext(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     `SourceContext` represents information about the source of a protobuf | ||||||
|  |     element, like the file in which it is defined. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The path-qualified name of the .proto file that contained the associated | ||||||
|  |     # protobuf element.  For example: `"google/protobuf/source_context.proto"`. | ||||||
|  |     file_name: str = betterproto.string_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) | @dataclass(eq=False, repr=False) | ||||||
| class Type(betterproto.Message): | class Type(betterproto.Message): | ||||||
|     """A protocol buffer message type.""" |     """A protocol buffer message type.""" | ||||||
| @@ -510,7 +363,7 @@ class Mixin(betterproto.Message): | |||||||
|     implies that all methods in `AccessControl` are also declared with same |     implies that all methods in `AccessControl` are also declared with same | ||||||
|     name and request/response types in `Storage`. A documentation generator or |     name and request/response types in `Storage`. A documentation generator or | ||||||
|     annotation processor will see the effective `Storage.GetAcl` method after |     annotation processor will see the effective `Storage.GetAcl` method after | ||||||
|     inherting documentation and annotations as follows:     service Storage { |     inheriting documentation and annotations as follows:     service Storage { | ||||||
|     // Get the underlying ACL object.       rpc GetAcl(GetAclRequest) returns |     // Get the underlying ACL object.       rpc GetAcl(GetAclRequest) returns | ||||||
|     (Acl) {         option (google.api.http).get = "/v2/{resource=**}:getAcl"; |     (Acl) {         option (google.api.http).get = "/v2/{resource=**}:getAcl"; | ||||||
|     }       ...     } Note how the version in the path pattern changed from |     }       ...     } Note how the version in the path pattern changed from | ||||||
| @@ -530,215 +383,6 @@ class Mixin(betterproto.Message): | |||||||
|     root: str = betterproto.string_field(2) |     root: str = betterproto.string_field(2) | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class Duration(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     A Duration represents a signed, fixed-length span of time represented as a |  | ||||||
|     count of seconds and fractions of seconds at nanosecond resolution. It is |  | ||||||
|     independent of any calendar and concepts like "day" or "month". It is |  | ||||||
|     related to Timestamp in that the difference between two Timestamp values is |  | ||||||
|     a Duration and it can be added or subtracted from a Timestamp. Range is |  | ||||||
|     approximately +-10,000 years. # Examples Example 1: Compute Duration from |  | ||||||
|     two Timestamps in pseudo code.     Timestamp start = ...;     Timestamp end |  | ||||||
|     = ...;     Duration duration = ...;     duration.seconds = end.seconds - |  | ||||||
|     start.seconds;     duration.nanos = end.nanos - start.nanos;     if |  | ||||||
|     (duration.seconds < 0 && duration.nanos > 0) {       duration.seconds += 1; |  | ||||||
|     duration.nanos -= 1000000000;     } else if (duration.seconds > 0 && |  | ||||||
|     duration.nanos < 0) {       duration.seconds -= 1;       duration.nanos += |  | ||||||
|     1000000000;     } Example 2: Compute Timestamp from Timestamp + Duration in |  | ||||||
|     pseudo code.     Timestamp start = ...;     Duration duration = ...; |  | ||||||
|     Timestamp end = ...;     end.seconds = start.seconds + duration.seconds; |  | ||||||
|     end.nanos = start.nanos + duration.nanos;     if (end.nanos < 0) { |  | ||||||
|     end.seconds -= 1;       end.nanos += 1000000000;     } else if (end.nanos |  | ||||||
|     >= 1000000000) {       end.seconds += 1;       end.nanos -= 1000000000; |  | ||||||
|     } Example 3: Compute Duration from datetime.timedelta in Python.     td = |  | ||||||
|     datetime.timedelta(days=3, minutes=10)     duration = Duration() |  | ||||||
|     duration.FromTimedelta(td) # JSON Mapping In JSON format, the Duration type |  | ||||||
|     is encoded as a string rather than an object, where the string ends in the |  | ||||||
|     suffix "s" (indicating seconds) and is preceded by the number of seconds, |  | ||||||
|     with nanoseconds expressed as fractional seconds. For example, 3 seconds |  | ||||||
|     with 0 nanoseconds should be encoded in JSON format as "3s", while 3 |  | ||||||
|     seconds and 1 nanosecond should be expressed in JSON format as |  | ||||||
|     "3.000000001s", and 3 seconds and 1 microsecond should be expressed in JSON |  | ||||||
|     format as "3.000001s". |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # Signed seconds of the span of time. Must be from -315,576,000,000 to |  | ||||||
|     # +315,576,000,000 inclusive. Note: these bounds are computed from: 60 |  | ||||||
|     # sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years |  | ||||||
|     seconds: int = betterproto.int64_field(1) |  | ||||||
|     # Signed fractions of a second at nanosecond resolution of the span of time. |  | ||||||
|     # Durations less than one second are represented with a 0 `seconds` field and |  | ||||||
|     # a positive or negative `nanos` field. For durations of one second or more, |  | ||||||
|     # a non-zero value for the `nanos` field must be of the same sign as the |  | ||||||
|     # `seconds` field. Must be from -999,999,999 to +999,999,999 inclusive. |  | ||||||
|     nanos: int = betterproto.int32_field(2) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class Struct(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     `Struct` represents a structured data value, consisting of fields which map |  | ||||||
|     to dynamically typed values. In some languages, `Struct` might be supported |  | ||||||
|     by a native representation. For example, in scripting languages like JS a |  | ||||||
|     struct is represented as an object. The details of that representation are |  | ||||||
|     described together with the proto support for the language. The JSON |  | ||||||
|     representation for `Struct` is JSON object. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # Unordered map of dynamically typed values. |  | ||||||
|     fields: Dict[str, "Value"] = betterproto.map_field( |  | ||||||
|         1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class Value(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     `Value` represents a dynamically typed value which can be either null, a |  | ||||||
|     number, a string, a boolean, a recursive struct value, or a list of values. |  | ||||||
|     A producer of value is expected to set one of that variants, absence of any |  | ||||||
|     variant indicates an error. The JSON representation for `Value` is JSON |  | ||||||
|     value. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # Represents a null value. |  | ||||||
|     null_value: "NullValue" = betterproto.enum_field(1, group="kind") |  | ||||||
|     # Represents a double value. |  | ||||||
|     number_value: float = betterproto.double_field(2, group="kind") |  | ||||||
|     # Represents a string value. |  | ||||||
|     string_value: str = betterproto.string_field(3, group="kind") |  | ||||||
|     # Represents a boolean value. |  | ||||||
|     bool_value: bool = betterproto.bool_field(4, group="kind") |  | ||||||
|     # Represents a structured value. |  | ||||||
|     struct_value: "Struct" = betterproto.message_field(5, group="kind") |  | ||||||
|     # Represents a repeated `Value`. |  | ||||||
|     list_value: "ListValue" = betterproto.message_field(6, group="kind") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class ListValue(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     `ListValue` is a wrapper around a repeated field of values. The JSON |  | ||||||
|     representation for `ListValue` is JSON array. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # Repeated field of dynamically typed values. |  | ||||||
|     values: List["Value"] = betterproto.message_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class DoubleValue(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     Wrapper message for `double`. The JSON representation for `DoubleValue` is |  | ||||||
|     JSON number. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The double value. |  | ||||||
|     value: float = betterproto.double_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class FloatValue(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     Wrapper message for `float`. The JSON representation for `FloatValue` is |  | ||||||
|     JSON number. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The float value. |  | ||||||
|     value: float = betterproto.float_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class Int64Value(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     Wrapper message for `int64`. The JSON representation for `Int64Value` is |  | ||||||
|     JSON string. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The int64 value. |  | ||||||
|     value: int = betterproto.int64_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class UInt64Value(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     Wrapper message for `uint64`. The JSON representation for `UInt64Value` is |  | ||||||
|     JSON string. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The uint64 value. |  | ||||||
|     value: int = betterproto.uint64_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class Int32Value(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     Wrapper message for `int32`. The JSON representation for `Int32Value` is |  | ||||||
|     JSON number. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The int32 value. |  | ||||||
|     value: int = betterproto.int32_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class UInt32Value(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     Wrapper message for `uint32`. The JSON representation for `UInt32Value` is |  | ||||||
|     JSON number. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The uint32 value. |  | ||||||
|     value: int = betterproto.uint32_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class BoolValue(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     Wrapper message for `bool`. The JSON representation for `BoolValue` is JSON |  | ||||||
|     `true` and `false`. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The bool value. |  | ||||||
|     value: bool = betterproto.bool_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class StringValue(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     Wrapper message for `string`. The JSON representation for `StringValue` is |  | ||||||
|     JSON string. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The string value. |  | ||||||
|     value: str = betterproto.string_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class BytesValue(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     Wrapper message for `bytes`. The JSON representation for `BytesValue` is |  | ||||||
|     JSON string. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # The bytes value. |  | ||||||
|     value: bytes = betterproto.bytes_field(1) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) |  | ||||||
| class Empty(betterproto.Message): |  | ||||||
|     """ |  | ||||||
|     A generic empty message that you can re-use to avoid defining duplicated |  | ||||||
|     empty messages in your APIs. A typical example is to use it as the request |  | ||||||
|     or the response type of an API method. For instance:     service Foo { |  | ||||||
|     rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);     } The |  | ||||||
|     JSON representation for `Empty` is empty JSON object `{}`. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) | @dataclass(eq=False, repr=False) | ||||||
| class FileDescriptorSet(betterproto.Message): | class FileDescriptorSet(betterproto.Message): | ||||||
|     """ |     """ | ||||||
| @@ -855,6 +499,23 @@ class FieldDescriptorProto(betterproto.Message): | |||||||
|     # camelCase. |     # camelCase. | ||||||
|     json_name: str = betterproto.string_field(10) |     json_name: str = betterproto.string_field(10) | ||||||
|     options: "FieldOptions" = betterproto.message_field(8) |     options: "FieldOptions" = betterproto.message_field(8) | ||||||
|  |     # If true, this is a proto3 "optional". When a proto3 field is optional, it | ||||||
|  |     # tracks presence regardless of field type. When proto3_optional is true, | ||||||
|  |     # this field must be belong to a oneof to signal to old proto3 clients that | ||||||
|  |     # presence is tracked for this field. This oneof is known as a "synthetic" | ||||||
|  |     # oneof, and this field must be its sole member (each proto3 optional field | ||||||
|  |     # gets its own synthetic oneof). Synthetic oneofs exist in the descriptor | ||||||
|  |     # only, and do not generate any API. Synthetic oneofs must be ordered after | ||||||
|  |     # all "real" oneofs. For message fields, proto3_optional doesn't create any | ||||||
|  |     # semantic change, since non-repeated message fields always track presence. | ||||||
|  |     # However it still indicates the semantic detail of whether the user wrote | ||||||
|  |     # "optional" or not. This can be useful for round-tripping the .proto file. | ||||||
|  |     # For consistency we give message fields a synthetic oneof also, even though | ||||||
|  |     # it is not required to track presence. This is especially important because | ||||||
|  |     # the parser can't tell if a field is a message or an enum, so it must always | ||||||
|  |     # create a synthetic oneof. Proto2 optional fields do not set this flag, | ||||||
|  |     # because they already indicate optional with `LABEL_OPTIONAL`. | ||||||
|  |     proto3_optional: bool = betterproto.bool_field(17) | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) | @dataclass(eq=False, repr=False) | ||||||
| @@ -937,17 +598,18 @@ class FileOptions(betterproto.Message): | |||||||
|     # inappropriate because proto packages do not normally start with backwards |     # inappropriate because proto packages do not normally start with backwards | ||||||
|     # domain names. |     # domain names. | ||||||
|     java_package: str = betterproto.string_field(1) |     java_package: str = betterproto.string_field(1) | ||||||
|     # If set, all the classes from the .proto file are wrapped in a single outer |     # Controls the name of the wrapper Java class generated for the .proto file. | ||||||
|     # class with the given name.  This applies to both Proto1 (equivalent to the |     # That class will always contain the .proto file's getDescriptor() method as | ||||||
|     # old "--one_java_file" option) and Proto2 (where a .proto always translates |     # well as any top-level extensions defined in the .proto file. If | ||||||
|     # to a single class, but you may want to explicitly choose the class name). |     # java_multiple_files is disabled, then all the other classes from the .proto | ||||||
|  |     # file will be nested inside the single wrapper outer class. | ||||||
|     java_outer_classname: str = betterproto.string_field(8) |     java_outer_classname: str = betterproto.string_field(8) | ||||||
|     # If set true, then the Java code generator will generate a separate .java |     # If enabled, then the Java code generator will generate a separate .java | ||||||
|     # file for each top-level message, enum, and service defined in the .proto |     # file for each top-level message, enum, and service defined in the .proto | ||||||
|     # file.  Thus, these types will *not* be nested inside the outer class named |     # file.  Thus, these types will *not* be nested inside the wrapper class | ||||||
|     # by java_outer_classname.  However, the outer class will still be generated |     # named by java_outer_classname.  However, the wrapper class will still be | ||||||
|     # to contain the file's getDescriptor() method as well as any top-level |     # generated to contain the file's getDescriptor() method as well as any top- | ||||||
|     # extensions defined in the file. |     # level extensions defined in the file. | ||||||
|     java_multiple_files: bool = betterproto.bool_field(10) |     java_multiple_files: bool = betterproto.bool_field(10) | ||||||
|     # This option does nothing. |     # This option does nothing. | ||||||
|     java_generate_equals_and_hash: bool = betterproto.bool_field(20) |     java_generate_equals_and_hash: bool = betterproto.bool_field(20) | ||||||
| @@ -1315,3 +977,363 @@ class GeneratedCodeInfoAnnotation(betterproto.Message): | |||||||
|     # the identified offset. The end offset should be one past the last relevant |     # the identified offset. The end offset should be one past the last relevant | ||||||
|     # byte (so the length of the text = end - begin). |     # byte (so the length of the text = end - begin). | ||||||
|     end: int = betterproto.int32_field(4) |     end: int = betterproto.int32_field(4) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class Duration(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     A Duration represents a signed, fixed-length span of time represented as a | ||||||
|  |     count of seconds and fractions of seconds at nanosecond resolution. It is | ||||||
|  |     independent of any calendar and concepts like "day" or "month". It is | ||||||
|  |     related to Timestamp in that the difference between two Timestamp values is | ||||||
|  |     a Duration and it can be added or subtracted from a Timestamp. Range is | ||||||
|  |     approximately +-10,000 years. # Examples Example 1: Compute Duration from | ||||||
|  |     two Timestamps in pseudo code.     Timestamp start = ...;     Timestamp end | ||||||
|  |     = ...;     Duration duration = ...;     duration.seconds = end.seconds - | ||||||
|  |     start.seconds;     duration.nanos = end.nanos - start.nanos;     if | ||||||
|  |     (duration.seconds < 0 && duration.nanos > 0) {       duration.seconds += 1; | ||||||
|  |     duration.nanos -= 1000000000;     } else if (duration.seconds > 0 && | ||||||
|  |     duration.nanos < 0) {       duration.seconds -= 1;       duration.nanos += | ||||||
|  |     1000000000;     } Example 2: Compute Timestamp from Timestamp + Duration in | ||||||
|  |     pseudo code.     Timestamp start = ...;     Duration duration = ...; | ||||||
|  |     Timestamp end = ...;     end.seconds = start.seconds + duration.seconds; | ||||||
|  |     end.nanos = start.nanos + duration.nanos;     if (end.nanos < 0) { | ||||||
|  |     end.seconds -= 1;       end.nanos += 1000000000;     } else if (end.nanos | ||||||
|  |     >= 1000000000) {       end.seconds += 1;       end.nanos -= 1000000000; | ||||||
|  |     } Example 3: Compute Duration from datetime.timedelta in Python.     td = | ||||||
|  |     datetime.timedelta(days=3, minutes=10)     duration = Duration() | ||||||
|  |     duration.FromTimedelta(td) # JSON Mapping In JSON format, the Duration type | ||||||
|  |     is encoded as a string rather than an object, where the string ends in the | ||||||
|  |     suffix "s" (indicating seconds) and is preceded by the number of seconds, | ||||||
|  |     with nanoseconds expressed as fractional seconds. For example, 3 seconds | ||||||
|  |     with 0 nanoseconds should be encoded in JSON format as "3s", while 3 | ||||||
|  |     seconds and 1 nanosecond should be expressed in JSON format as | ||||||
|  |     "3.000000001s", and 3 seconds and 1 microsecond should be expressed in JSON | ||||||
|  |     format as "3.000001s". | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # Signed seconds of the span of time. Must be from -315,576,000,000 to | ||||||
|  |     # +315,576,000,000 inclusive. Note: these bounds are computed from: 60 | ||||||
|  |     # sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years | ||||||
|  |     seconds: int = betterproto.int64_field(1) | ||||||
|  |     # Signed fractions of a second at nanosecond resolution of the span of time. | ||||||
|  |     # Durations less than one second are represented with a 0 `seconds` field and | ||||||
|  |     # a positive or negative `nanos` field. For durations of one second or more, | ||||||
|  |     # a non-zero value for the `nanos` field must be of the same sign as the | ||||||
|  |     # `seconds` field. Must be from -999,999,999 to +999,999,999 inclusive. | ||||||
|  |     nanos: int = betterproto.int32_field(2) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class Empty(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     A generic empty message that you can re-use to avoid defining duplicated | ||||||
|  |     empty messages in your APIs. A typical example is to use it as the request | ||||||
|  |     or the response type of an API method. For instance:     service Foo { | ||||||
|  |     rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);     } The | ||||||
|  |     JSON representation for `Empty` is empty JSON object `{}`. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class FieldMask(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     `FieldMask` represents a set of symbolic field paths, for example: | ||||||
|  |     paths: "f.a"     paths: "f.b.d" Here `f` represents a field in some root | ||||||
|  |     message, `a` and `b` fields in the message found in `f`, and `d` a field | ||||||
|  |     found in the message in `f.b`. Field masks are used to specify a subset of | ||||||
|  |     fields that should be returned by a get operation or modified by an update | ||||||
|  |     operation. Field masks also have a custom JSON encoding (see below). # | ||||||
|  |     Field Masks in Projections When used in the context of a projection, a | ||||||
|  |     response message or sub-message is filtered by the API to only contain | ||||||
|  |     those fields as specified in the mask. For example, if the mask in the | ||||||
|  |     previous example is applied to a response message as follows:     f { | ||||||
|  |     a : 22       b {         d : 1         x : 2       }       y : 13     } | ||||||
|  |     z: 8 The result will not contain specific values for fields x,y and z | ||||||
|  |     (their value will be set to the default, and omitted in proto text output): | ||||||
|  |     f {       a : 22       b {         d : 1       }     } A repeated field is | ||||||
|  |     not allowed except at the last position of a paths string. If a FieldMask | ||||||
|  |     object is not present in a get operation, the operation applies to all | ||||||
|  |     fields (as if a FieldMask of all fields had been specified). Note that a | ||||||
|  |     field mask does not necessarily apply to the top-level response message. In | ||||||
|  |     case of a REST get operation, the field mask applies directly to the | ||||||
|  |     response, but in case of a REST list operation, the mask instead applies to | ||||||
|  |     each individual message in the returned resource list. In case of a REST | ||||||
|  |     custom method, other definitions may be used. Where the mask applies will | ||||||
|  |     be clearly documented together with its declaration in the API.  In any | ||||||
|  |     case, the effect on the returned resource/resources is required behavior | ||||||
|  |     for APIs. # Field Masks in Update Operations A field mask in update | ||||||
|  |     operations specifies which fields of the targeted resource are going to be | ||||||
|  |     updated. The API is required to only change the values of the fields as | ||||||
|  |     specified in the mask and leave the others untouched. If a resource is | ||||||
|  |     passed in to describe the updated values, the API ignores the values of all | ||||||
|  |     fields not covered by the mask. If a repeated field is specified for an | ||||||
|  |     update operation, new values will be appended to the existing repeated | ||||||
|  |     field in the target resource. Note that a repeated field is only allowed in | ||||||
|  |     the last position of a `paths` string. If a sub-message is specified in the | ||||||
|  |     last position of the field mask for an update operation, then new value | ||||||
|  |     will be merged into the existing sub-message in the target resource. For | ||||||
|  |     example, given the target message:     f {       b {         d: 1 | ||||||
|  |     x: 2       }       c: [1]     } And an update message:     f {       b { | ||||||
|  |     d: 10       }       c: [2]     } then if the field mask is:  paths: ["f.b", | ||||||
|  |     "f.c"] then the result will be:     f {       b {         d: 10         x: | ||||||
|  |     2       }       c: [1, 2]     } An implementation may provide options to | ||||||
|  |     override this default behavior for repeated and message fields. In order to | ||||||
|  |     reset a field's value to the default, the field must be in the mask and set | ||||||
|  |     to the default value in the provided resource. Hence, in order to reset all | ||||||
|  |     fields of a resource, provide a default instance of the resource and set | ||||||
|  |     all fields in the mask, or do not provide a mask as described below. If a | ||||||
|  |     field mask is not present on update, the operation applies to all fields | ||||||
|  |     (as if a field mask of all fields has been specified). Note that in the | ||||||
|  |     presence of schema evolution, this may mean that fields the client does not | ||||||
|  |     know and has therefore not filled into the request will be reset to their | ||||||
|  |     default. If this is unwanted behavior, a specific service may require a | ||||||
|  |     client to always specify a field mask, producing an error if not. As with | ||||||
|  |     get operations, the location of the resource which describes the updated | ||||||
|  |     values in the request message depends on the operation kind. In any case, | ||||||
|  |     the effect of the field mask is required to be honored by the API. ## | ||||||
|  |     Considerations for HTTP REST The HTTP kind of an update operation which | ||||||
|  |     uses a field mask must be set to PATCH instead of PUT in order to satisfy | ||||||
|  |     HTTP semantics (PUT must only be used for full updates). # JSON Encoding of | ||||||
|  |     Field Masks In JSON, a field mask is encoded as a single string where paths | ||||||
|  |     are separated by a comma. Fields name in each path are converted to/from | ||||||
|  |     lower-camel naming conventions. As an example, consider the following | ||||||
|  |     message declarations:     message Profile {       User user = 1; | ||||||
|  |     Photo photo = 2;     }     message User {       string display_name = 1; | ||||||
|  |     string address = 2;     } In proto a field mask for `Profile` may look as | ||||||
|  |     such:     mask {       paths: "user.display_name"       paths: "photo" | ||||||
|  |     } In JSON, the same mask is represented as below:     {       mask: | ||||||
|  |     "user.displayName,photo"     } # Field Masks and Oneof Fields Field masks | ||||||
|  |     treat fields in oneofs just as regular fields. Consider the following | ||||||
|  |     message:     message SampleMessage {       oneof test_oneof { | ||||||
|  |     string name = 4;         SubMessage sub_message = 9;       }     } The | ||||||
|  |     field mask can be:     mask {       paths: "name"     } Or:     mask { | ||||||
|  |     paths: "sub_message"     } Note that oneof type names ("test_oneof" in this | ||||||
|  |     case) cannot be used in paths. ## Field Mask Verification The | ||||||
|  |     implementation of any API method which has a FieldMask type field in the | ||||||
|  |     request should verify the included field paths, and return an | ||||||
|  |     `INVALID_ARGUMENT` error if any path is unmappable. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The set of field mask paths. | ||||||
|  |     paths: List[str] = betterproto.string_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class Struct(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     `Struct` represents a structured data value, consisting of fields which map | ||||||
|  |     to dynamically typed values. In some languages, `Struct` might be supported | ||||||
|  |     by a native representation. For example, in scripting languages like JS a | ||||||
|  |     struct is represented as an object. The details of that representation are | ||||||
|  |     described together with the proto support for the language. The JSON | ||||||
|  |     representation for `Struct` is JSON object. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # Unordered map of dynamically typed values. | ||||||
|  |     fields: Dict[str, "Value"] = betterproto.map_field( | ||||||
|  |         1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class Value(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     `Value` represents a dynamically typed value which can be either null, a | ||||||
|  |     number, a string, a boolean, a recursive struct value, or a list of values. | ||||||
|  |     A producer of value is expected to set one of that variants, absence of any | ||||||
|  |     variant indicates an error. The JSON representation for `Value` is JSON | ||||||
|  |     value. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # Represents a null value. | ||||||
|  |     null_value: "NullValue" = betterproto.enum_field(1, group="kind") | ||||||
|  |     # Represents a double value. | ||||||
|  |     number_value: float = betterproto.double_field(2, group="kind") | ||||||
|  |     # Represents a string value. | ||||||
|  |     string_value: str = betterproto.string_field(3, group="kind") | ||||||
|  |     # Represents a boolean value. | ||||||
|  |     bool_value: bool = betterproto.bool_field(4, group="kind") | ||||||
|  |     # Represents a structured value. | ||||||
|  |     struct_value: "Struct" = betterproto.message_field(5, group="kind") | ||||||
|  |     # Represents a repeated `Value`. | ||||||
|  |     list_value: "ListValue" = betterproto.message_field(6, group="kind") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class ListValue(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     `ListValue` is a wrapper around a repeated field of values. The JSON | ||||||
|  |     representation for `ListValue` is JSON array. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # Repeated field of dynamically typed values. | ||||||
|  |     values: List["Value"] = betterproto.message_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class Timestamp(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     A Timestamp represents a point in time independent of any time zone or | ||||||
|  |     local calendar, encoded as a count of seconds and fractions of seconds at | ||||||
|  |     nanosecond resolution. The count is relative to an epoch at UTC midnight on | ||||||
|  |     January 1, 1970, in the proleptic Gregorian calendar which extends the | ||||||
|  |     Gregorian calendar backwards to year one. All minutes are 60 seconds long. | ||||||
|  |     Leap seconds are "smeared" so that no leap second table is needed for | ||||||
|  |     interpretation, using a [24-hour linear | ||||||
|  |     smear](https://developers.google.com/time/smear). The range is from | ||||||
|  |     0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By restricting to | ||||||
|  |     that range, we ensure that we can convert to and from [RFC | ||||||
|  |     3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. # Examples | ||||||
|  |     Example 1: Compute Timestamp from POSIX `time()`.     Timestamp timestamp; | ||||||
|  |     timestamp.set_seconds(time(NULL));     timestamp.set_nanos(0); Example 2: | ||||||
|  |     Compute Timestamp from POSIX `gettimeofday()`.     struct timeval tv; | ||||||
|  |     gettimeofday(&tv, NULL);     Timestamp timestamp; | ||||||
|  |     timestamp.set_seconds(tv.tv_sec);     timestamp.set_nanos(tv.tv_usec * | ||||||
|  |     1000); Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. | ||||||
|  |     FILETIME ft;     GetSystemTimeAsFileTime(&ft);     UINT64 ticks = | ||||||
|  |     (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;     // A Windows | ||||||
|  |     tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z     // is | ||||||
|  |     11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.     Timestamp | ||||||
|  |     timestamp;     timestamp.set_seconds((INT64) ((ticks / 10000000) - | ||||||
|  |     11644473600LL));     timestamp.set_nanos((INT32) ((ticks % 10000000) * | ||||||
|  |     100)); Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. | ||||||
|  |     long millis = System.currentTimeMillis();     Timestamp timestamp = | ||||||
|  |     Timestamp.newBuilder().setSeconds(millis / 1000)         .setNanos((int) | ||||||
|  |     ((millis % 1000) * 1000000)).build(); Example 5: Compute Timestamp from | ||||||
|  |     Java `Instant.now()`.     Instant now = Instant.now();     Timestamp | ||||||
|  |     timestamp =         Timestamp.newBuilder().setSeconds(now.getEpochSecond()) | ||||||
|  |     .setNanos(now.getNano()).build(); Example 6: Compute Timestamp from current | ||||||
|  |     time in Python.     timestamp = Timestamp()     timestamp.GetCurrentTime() | ||||||
|  |     # JSON Mapping In JSON format, the Timestamp type is encoded as a string in | ||||||
|  |     the [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the | ||||||
|  |     format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" where | ||||||
|  |     {year} is always expressed using four digits while {month}, {day}, {hour}, | ||||||
|  |     {min}, and {sec} are zero-padded to two digits each. The fractional | ||||||
|  |     seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), | ||||||
|  |     are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone | ||||||
|  |     is required. A proto3 JSON serializer should always use UTC (as indicated | ||||||
|  |     by "Z") when printing the Timestamp type and a proto3 JSON parser should be | ||||||
|  |     able to accept both UTC and other timezones (as indicated by an offset). | ||||||
|  |     For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past 01:30 UTC | ||||||
|  |     on January 15, 2017. In JavaScript, one can convert a Date object to this | ||||||
|  |     format using the standard [toISOString()](https://developer.mozilla.org/en- | ||||||
|  |     US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) method. | ||||||
|  |     In Python, a standard `datetime.datetime` object can be converted to this | ||||||
|  |     format using | ||||||
|  |     [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) | ||||||
|  |     with the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one | ||||||
|  |     can use the Joda Time's [`ISODateTimeFormat.dateTime()`]( | ||||||
|  |     http://www.joda.org/joda- | ||||||
|  |     time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D ) | ||||||
|  |     to obtain a formatter capable of generating timestamps in this format. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must | ||||||
|  |     # be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive. | ||||||
|  |     seconds: int = betterproto.int64_field(1) | ||||||
|  |     # Non-negative fractions of a second at nanosecond resolution. Negative | ||||||
|  |     # second values with fractions must still have non-negative nanos values that | ||||||
|  |     # count forward in time. Must be from 0 to 999,999,999 inclusive. | ||||||
|  |     nanos: int = betterproto.int32_field(2) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class DoubleValue(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     Wrapper message for `double`. The JSON representation for `DoubleValue` is | ||||||
|  |     JSON number. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The double value. | ||||||
|  |     value: float = betterproto.double_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class FloatValue(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     Wrapper message for `float`. The JSON representation for `FloatValue` is | ||||||
|  |     JSON number. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The float value. | ||||||
|  |     value: float = betterproto.float_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class Int64Value(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     Wrapper message for `int64`. The JSON representation for `Int64Value` is | ||||||
|  |     JSON string. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The int64 value. | ||||||
|  |     value: int = betterproto.int64_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class UInt64Value(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     Wrapper message for `uint64`. The JSON representation for `UInt64Value` is | ||||||
|  |     JSON string. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The uint64 value. | ||||||
|  |     value: int = betterproto.uint64_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class Int32Value(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     Wrapper message for `int32`. The JSON representation for `Int32Value` is | ||||||
|  |     JSON number. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The int32 value. | ||||||
|  |     value: int = betterproto.int32_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class UInt32Value(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     Wrapper message for `uint32`. The JSON representation for `UInt32Value` is | ||||||
|  |     JSON number. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The uint32 value. | ||||||
|  |     value: int = betterproto.uint32_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class BoolValue(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     Wrapper message for `bool`. The JSON representation for `BoolValue` is JSON | ||||||
|  |     `true` and `false`. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The bool value. | ||||||
|  |     value: bool = betterproto.bool_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class StringValue(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     Wrapper message for `string`. The JSON representation for `StringValue` is | ||||||
|  |     JSON string. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The string value. | ||||||
|  |     value: str = betterproto.string_field(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass(eq=False, repr=False) | ||||||
|  | class BytesValue(betterproto.Message): | ||||||
|  |     """ | ||||||
|  |     Wrapper message for `bytes`. The JSON representation for `BytesValue` is | ||||||
|  |     JSON string. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # The bytes value. | ||||||
|  |     value: bytes = betterproto.bytes_field(1) | ||||||
|   | |||||||
| @@ -5,6 +5,12 @@ from dataclasses import dataclass | |||||||
| from typing import List | from typing import List | ||||||
|  |  | ||||||
| import betterproto | import betterproto | ||||||
|  | from betterproto.grpc.grpclib_server import ServiceBase | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CodeGeneratorResponseFeature(betterproto.Enum): | ||||||
|  |     FEATURE_NONE = 0 | ||||||
|  |     FEATURE_PROTO3_OPTIONAL = 1 | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(eq=False, repr=False) | @dataclass(eq=False, repr=False) | ||||||
| @@ -59,6 +65,9 @@ class CodeGeneratorResponse(betterproto.Message): | |||||||
|     # unparseable -- should be reported by writing a message to stderr and |     # unparseable -- should be reported by writing a message to stderr and | ||||||
|     # exiting with a non-zero status code. |     # exiting with a non-zero status code. | ||||||
|     error: str = betterproto.string_field(1) |     error: str = betterproto.string_field(1) | ||||||
|  |     # A bitmask of supported features that the code generator supports. This is a | ||||||
|  |     # bitwise "or" of values from the Feature enum. | ||||||
|  |     supported_features: int = betterproto.uint64_field(2) | ||||||
|     file: List["CodeGeneratorResponseFile"] = betterproto.message_field(15) |     file: List["CodeGeneratorResponseFile"] = betterproto.message_field(15) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -108,6 +117,12 @@ class CodeGeneratorResponseFile(betterproto.Message): | |||||||
|     insertion_point: str = betterproto.string_field(2) |     insertion_point: str = betterproto.string_field(2) | ||||||
|     # The file contents. |     # The file contents. | ||||||
|     content: str = betterproto.string_field(15) |     content: str = betterproto.string_field(15) | ||||||
|  |     # Information describing the file content being inserted. If an insertion | ||||||
|  |     # point is used, this information will be appropriately offset and inserted | ||||||
|  |     # into the code generation metadata for the generated files. | ||||||
|  |     generated_code_info: "betterproto_lib_google_protobuf.GeneratedCodeInfo" = ( | ||||||
|  |         betterproto.message_field(16) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| import betterproto.lib.google.protobuf as betterproto_lib_google_protobuf | import betterproto.lib.google.protobuf as betterproto_lib_google_protobuf | ||||||
|   | |||||||
| @@ -33,5 +33,5 @@ def outputfile_compiler(output_file: OutputTemplate) -> str: | |||||||
|  |  | ||||||
|     return black.format_str( |     return black.format_str( | ||||||
|         template.render(output_file=output_file), |         template.render(output_file=output_file), | ||||||
|         mode=black.FileMode(target_versions={black.TargetVersion.PY37}), |         mode=black.Mode(), | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -28,11 +28,8 @@ def main() -> None: | |||||||
|     if dump_file: |     if dump_file: | ||||||
|         dump_request(dump_file, request) |         dump_request(dump_file, request) | ||||||
|  |  | ||||||
|     # Create response |  | ||||||
|     response = CodeGeneratorResponse() |  | ||||||
|  |  | ||||||
|     # Generate code |     # Generate code | ||||||
|     generate_code(request, response) |     response = generate_code(request) | ||||||
|  |  | ||||||
|     # Serialise response message |     # Serialise response message | ||||||
|     output = response.SerializeToString() |     output = response.SerializeToString() | ||||||
|   | |||||||
| @@ -30,13 +30,16 @@ reference to `A` to `B`'s `fields` attribute. | |||||||
| """ | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import builtins | ||||||
|  | import re | ||||||
|  | import textwrap | ||||||
|  | from dataclasses import dataclass, field | ||||||
|  | from typing import Dict, Iterable, Iterator, List, Optional, Set, Type, Union | ||||||
|  |  | ||||||
| import betterproto | import betterproto | ||||||
| from betterproto import which_one_of | from betterproto import which_one_of | ||||||
| from betterproto.casing import sanitize_name | from betterproto.casing import sanitize_name | ||||||
| from betterproto.compile.importing import ( | from betterproto.compile.importing import get_type_reference, parse_source_type_name | ||||||
|     get_type_reference, |  | ||||||
|     parse_source_type_name, |  | ||||||
| ) |  | ||||||
| from betterproto.compile.naming import ( | from betterproto.compile.naming import ( | ||||||
|     pythonize_class_name, |     pythonize_class_name, | ||||||
|     pythonize_field_name, |     pythonize_field_name, | ||||||
| @@ -45,22 +48,15 @@ from betterproto.compile.naming import ( | |||||||
| from betterproto.lib.google.protobuf import ( | from betterproto.lib.google.protobuf import ( | ||||||
|     DescriptorProto, |     DescriptorProto, | ||||||
|     EnumDescriptorProto, |     EnumDescriptorProto, | ||||||
|     FileDescriptorProto, |  | ||||||
|     MethodDescriptorProto, |  | ||||||
|     Field, |     Field, | ||||||
|     FieldDescriptorProto, |     FieldDescriptorProto, | ||||||
|     FieldDescriptorProtoType, |  | ||||||
|     FieldDescriptorProtoLabel, |     FieldDescriptorProtoLabel, | ||||||
|  |     FieldDescriptorProtoType, | ||||||
|  |     FileDescriptorProto, | ||||||
|  |     MethodDescriptorProto, | ||||||
| ) | ) | ||||||
| from betterproto.lib.google.protobuf.compiler import CodeGeneratorRequest | from betterproto.lib.google.protobuf.compiler import CodeGeneratorRequest | ||||||
|  |  | ||||||
|  |  | ||||||
| import re |  | ||||||
| import textwrap |  | ||||||
| from dataclasses import dataclass, field |  | ||||||
| from typing import Dict, Iterable, Iterator, List, Optional, Set, Text, Type, Union |  | ||||||
| import sys |  | ||||||
|  |  | ||||||
| from ..casing import sanitize_name | from ..casing import sanitize_name | ||||||
| from ..compile.importing import get_type_reference, parse_source_type_name | from ..compile.importing import get_type_reference, parse_source_type_name | ||||||
| from ..compile.naming import ( | from ..compile.naming import ( | ||||||
| @@ -69,7 +65,6 @@ from ..compile.naming import ( | |||||||
|     pythonize_method_name, |     pythonize_method_name, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| # Create a unique placeholder to deal with | # Create a unique placeholder to deal with | ||||||
| # https://stackoverflow.com/questions/51575931/class-inheritance-in-python-3-7-dataclasses | # https://stackoverflow.com/questions/51575931/class-inheritance-in-python-3-7-dataclasses | ||||||
| PLACEHOLDER = object() | PLACEHOLDER = object() | ||||||
| @@ -147,11 +142,7 @@ def get_comment( | |||||||
|                 sci_loc.leading_comments.strip().replace("\n", ""), width=79 - indent |                 sci_loc.leading_comments.strip().replace("\n", ""), width=79 - indent | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             if path[-2] == 2 and path[-4] != 6: |             # This is a field, message, enum, service, or method | ||||||
|                 # This is a field |  | ||||||
|                 return f"{pad}# " + f"\n{pad}# ".join(lines) |  | ||||||
|             else: |  | ||||||
|                 # This is a message, enum, service, or method |  | ||||||
|             if len(lines) == 1 and len(lines[0]) < 79 - indent - 6: |             if len(lines) == 1 and len(lines[0]) < 79 - indent - 6: | ||||||
|                 lines[0] = lines[0].strip('"') |                 lines[0] = lines[0].strip('"') | ||||||
|                 return f'{pad}"""{lines[0]}"""' |                 return f'{pad}"""{lines[0]}"""' | ||||||
| @@ -237,6 +228,7 @@ class OutputTemplate: | |||||||
|     imports: Set[str] = field(default_factory=set) |     imports: Set[str] = field(default_factory=set) | ||||||
|     datetime_imports: Set[str] = field(default_factory=set) |     datetime_imports: Set[str] = field(default_factory=set) | ||||||
|     typing_imports: Set[str] = field(default_factory=set) |     typing_imports: Set[str] = field(default_factory=set) | ||||||
|  |     builtins_import: bool = False | ||||||
|     messages: List["MessageCompiler"] = field(default_factory=list) |     messages: List["MessageCompiler"] = field(default_factory=list) | ||||||
|     enums: List["EnumDefinitionCompiler"] = field(default_factory=list) |     enums: List["EnumDefinitionCompiler"] = field(default_factory=list) | ||||||
|     services: List["ServiceCompiler"] = field(default_factory=list) |     services: List["ServiceCompiler"] = field(default_factory=list) | ||||||
| @@ -268,6 +260,8 @@ class OutputTemplate: | |||||||
|         imports = set() |         imports = set() | ||||||
|         if any(x for x in self.messages if any(x.deprecated_fields)): |         if any(x for x in self.messages if any(x.deprecated_fields)): | ||||||
|             imports.add("warnings") |             imports.add("warnings") | ||||||
|  |         if self.builtins_import: | ||||||
|  |             imports.add("builtins") | ||||||
|         return imports |         return imports | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -283,6 +277,7 @@ class MessageCompiler(ProtoContentBase): | |||||||
|         default_factory=list |         default_factory=list | ||||||
|     ) |     ) | ||||||
|     deprecated: bool = field(default=False, init=False) |     deprecated: bool = field(default=False, init=False) | ||||||
|  |     builtins_types: Set[str] = field(default_factory=set) | ||||||
|  |  | ||||||
|     def __post_init__(self) -> None: |     def __post_init__(self) -> None: | ||||||
|         # Add message to output file |         # Add message to output file | ||||||
| @@ -376,6 +371,8 @@ class FieldCompiler(MessageCompiler): | |||||||
|         betterproto_field_type = ( |         betterproto_field_type = ( | ||||||
|             f"betterproto.{self.field_type}_field({self.proto_obj.number}{field_args})" |             f"betterproto.{self.field_type}_field({self.proto_obj.number}{field_args})" | ||||||
|         ) |         ) | ||||||
|  |         if self.py_name in dir(builtins): | ||||||
|  |             self.parent.builtins_types.add(self.py_name) | ||||||
|         return f"{name}{annotations} = {betterproto_field_type}" |         return f"{name}{annotations} = {betterproto_field_type}" | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @@ -383,6 +380,8 @@ class FieldCompiler(MessageCompiler): | |||||||
|         args = [] |         args = [] | ||||||
|         if self.field_wraps: |         if self.field_wraps: | ||||||
|             args.append(f"wraps={self.field_wraps}") |             args.append(f"wraps={self.field_wraps}") | ||||||
|  |         if self.optional: | ||||||
|  |             args.append(f"optional=True") | ||||||
|         return args |         return args | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @@ -408,9 +407,16 @@ class FieldCompiler(MessageCompiler): | |||||||
|             imports.add("Dict") |             imports.add("Dict") | ||||||
|         return imports |         return imports | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def use_builtins(self) -> bool: | ||||||
|  |         return self.py_type in self.parent.builtins_types or ( | ||||||
|  |             self.py_type == self.py_name and self.py_name in dir(builtins) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def add_imports_to(self, output_file: OutputTemplate) -> None: |     def add_imports_to(self, output_file: OutputTemplate) -> None: | ||||||
|         output_file.datetime_imports.update(self.datetime_imports) |         output_file.datetime_imports.update(self.datetime_imports) | ||||||
|         output_file.typing_imports.update(self.typing_imports) |         output_file.typing_imports.update(self.typing_imports) | ||||||
|  |         output_file.builtins_import = output_file.builtins_import or self.use_builtins | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def field_wraps(self) -> Optional[str]: |     def field_wraps(self) -> Optional[str]: | ||||||
| @@ -431,6 +437,10 @@ class FieldCompiler(MessageCompiler): | |||||||
|             and not is_map(self.proto_obj, self.parent) |             and not is_map(self.proto_obj, self.parent) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def optional(self) -> bool: | ||||||
|  |         return self.proto_obj.proto3_optional | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def mutable(self) -> bool: |     def mutable(self) -> bool: | ||||||
|         """True if the field is a mutable type, otherwise False.""" |         """True if the field is a mutable type, otherwise False.""" | ||||||
| @@ -446,10 +456,12 @@ class FieldCompiler(MessageCompiler): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def default_value_string(self) -> Union[Text, None, float, int]: |     def default_value_string(self) -> str: | ||||||
|         """Python representation of the default proto value.""" |         """Python representation of the default proto value.""" | ||||||
|         if self.repeated: |         if self.repeated: | ||||||
|             return "[]" |             return "[]" | ||||||
|  |         if self.optional: | ||||||
|  |             return "None" | ||||||
|         if self.py_type == "int": |         if self.py_type == "int": | ||||||
|             return "0" |             return "0" | ||||||
|         if self.py_type == "float": |         if self.py_type == "float": | ||||||
| @@ -460,6 +472,14 @@ class FieldCompiler(MessageCompiler): | |||||||
|             return '""' |             return '""' | ||||||
|         elif self.py_type == "bytes": |         elif self.py_type == "bytes": | ||||||
|             return 'b""' |             return 'b""' | ||||||
|  |         elif self.field_type == "enum": | ||||||
|  |             enum_proto_obj_name = self.proto_obj.type_name.split(".").pop() | ||||||
|  |             enum = next( | ||||||
|  |                 e | ||||||
|  |                 for e in self.output_file.enums | ||||||
|  |                 if e.proto_obj.name == enum_proto_obj_name | ||||||
|  |             ) | ||||||
|  |             return enum.default_value_string | ||||||
|         else: |         else: | ||||||
|             # Message type |             # Message type | ||||||
|             return "None" |             return "None" | ||||||
| @@ -500,13 +520,18 @@ class FieldCompiler(MessageCompiler): | |||||||
|                 source_type=self.proto_obj.type_name, |                 source_type=self.proto_obj.type_name, | ||||||
|             ) |             ) | ||||||
|         else: |         else: | ||||||
|             raise NotImplementedError(f"Unknown type {field.type}") |             raise NotImplementedError(f"Unknown type {self.proto_obj.type}") | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def annotation(self) -> str: |     def annotation(self) -> str: | ||||||
|  |         py_type = self.py_type | ||||||
|  |         if self.use_builtins: | ||||||
|  |             py_type = f"builtins.{py_type}" | ||||||
|         if self.repeated: |         if self.repeated: | ||||||
|             return f"List[{self.py_type}]" |             return f"List[{py_type}]" | ||||||
|         return self.py_type |         if self.optional: | ||||||
|  |             return f"Optional[{py_type}]" | ||||||
|  |         return py_type | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
| @@ -641,12 +666,8 @@ class ServiceMethodCompiler(ProtoContentBase): | |||||||
|         self.parent.methods.append(self) |         self.parent.methods.append(self) | ||||||
|  |  | ||||||
|         # Check for imports |         # Check for imports | ||||||
|         if self.py_input_message: |  | ||||||
|             for f in self.py_input_message.fields: |  | ||||||
|                 f.add_imports_to(self.output_file) |  | ||||||
|         if "Optional" in self.py_output_message_type: |         if "Optional" in self.py_output_message_type: | ||||||
|             self.output_file.typing_imports.add("Optional") |             self.output_file.typing_imports.add("Optional") | ||||||
|         self.mutable_default_args  # ensure this is called before rendering |  | ||||||
|  |  | ||||||
|         # Check for Async imports |         # Check for Async imports | ||||||
|         if self.client_streaming: |         if self.client_streaming: | ||||||
| @@ -660,37 +681,6 @@ class ServiceMethodCompiler(ProtoContentBase): | |||||||
|  |  | ||||||
|         super().__post_init__()  # check for unset fields |         super().__post_init__()  # check for unset fields | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def mutable_default_args(self) -> Dict[str, str]: |  | ||||||
|         """Handle mutable default arguments. |  | ||||||
|  |  | ||||||
|         Returns a list of tuples containing the name and default value |  | ||||||
|         for arguments to this message who's default value is mutable. |  | ||||||
|         The defaults are swapped out for None and replaced back inside |  | ||||||
|         the method's body. |  | ||||||
|         Reference: |  | ||||||
|         https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments |  | ||||||
|  |  | ||||||
|         Returns |  | ||||||
|         ------- |  | ||||||
|         Dict[str, str] |  | ||||||
|             Name and actual default value (as a string) |  | ||||||
|             for each argument with mutable default values. |  | ||||||
|         """ |  | ||||||
|         mutable_default_args = {} |  | ||||||
|  |  | ||||||
|         if self.py_input_message: |  | ||||||
|             for f in self.py_input_message.fields: |  | ||||||
|                 if ( |  | ||||||
|                     not self.client_streaming |  | ||||||
|                     and f.default_value_string != "None" |  | ||||||
|                     and f.mutable |  | ||||||
|                 ): |  | ||||||
|                     mutable_default_args[f.py_name] = f.default_value_string |  | ||||||
|                     self.output_file.typing_imports.add("Optional") |  | ||||||
|  |  | ||||||
|         return mutable_default_args |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def py_name(self) -> str: |     def py_name(self) -> str: | ||||||
|         """Pythonized method name.""" |         """Pythonized method name.""" | ||||||
| @@ -748,6 +738,17 @@ class ServiceMethodCompiler(ProtoContentBase): | |||||||
|             source_type=self.proto_obj.input_type, |             source_type=self.proto_obj.input_type, | ||||||
|         ).strip('"') |         ).strip('"') | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def py_input_message_param(self) -> str: | ||||||
|  |         """Param name corresponding to py_input_message_type. | ||||||
|  |  | ||||||
|  |         Returns | ||||||
|  |         ------- | ||||||
|  |         str | ||||||
|  |             Param name corresponding to py_input_message_type. | ||||||
|  |         """ | ||||||
|  |         return pythonize_field_name(self.py_input_message_type) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def py_output_message_type(self) -> str: |     def py_output_message_type(self) -> str: | ||||||
|         """String representation of the Python type corresponding to the |         """String representation of the Python type corresponding to the | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ from betterproto.lib.google.protobuf import ( | |||||||
| from betterproto.lib.google.protobuf.compiler import ( | from betterproto.lib.google.protobuf.compiler import ( | ||||||
|     CodeGeneratorRequest, |     CodeGeneratorRequest, | ||||||
|     CodeGeneratorResponse, |     CodeGeneratorResponse, | ||||||
|  |     CodeGeneratorResponseFeature, | ||||||
|     CodeGeneratorResponseFile, |     CodeGeneratorResponseFile, | ||||||
| ) | ) | ||||||
| import itertools | import itertools | ||||||
| @@ -60,10 +61,11 @@ def traverse( | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def generate_code( | def generate_code(request: CodeGeneratorRequest) -> CodeGeneratorResponse: | ||||||
|     request: CodeGeneratorRequest, response: CodeGeneratorResponse |     response = CodeGeneratorResponse() | ||||||
| ) -> None: |  | ||||||
|     plugin_options = request.parameter.split(",") if request.parameter else [] |     plugin_options = request.parameter.split(",") if request.parameter else [] | ||||||
|  |     response.supported_features = CodeGeneratorResponseFeature.FEATURE_PROTO3_OPTIONAL | ||||||
|  |  | ||||||
|     request_data = PluginRequestCompiler(plugin_request_obj=request) |     request_data = PluginRequestCompiler(plugin_request_obj=request) | ||||||
|     # Gather output packages |     # Gather output packages | ||||||
| @@ -133,6 +135,8 @@ def generate_code( | |||||||
|     for output_package_name in sorted(output_paths.union(init_files)): |     for output_package_name in sorted(output_paths.union(init_files)): | ||||||
|         print(f"Writing {output_package_name}", file=sys.stderr) |         print(f"Writing {output_package_name}", file=sys.stderr) | ||||||
|  |  | ||||||
|  |     return response | ||||||
|  |  | ||||||
|  |  | ||||||
| def read_protobuf_type( | def read_protobuf_type( | ||||||
|     item: DescriptorProto, |     item: DescriptorProto, | ||||||
|   | |||||||
| @@ -28,10 +28,11 @@ class {{ enum.py_name }}(betterproto.Enum): | |||||||
|  |  | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     {% for entry in enum.entries %} |     {% for entry in enum.entries %} | ||||||
|  |     {{ entry.name }} = {{ entry.value }} | ||||||
|         {% if entry.comment %} |         {% if entry.comment %} | ||||||
| {{ entry.comment }} | {{ entry.comment }} | ||||||
|  |  | ||||||
|         {% endif %} |         {% endif %} | ||||||
|     {{ entry.name }} = {{ entry.value }} |  | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -45,10 +46,11 @@ class {{ message.py_name }}(betterproto.Message): | |||||||
|  |  | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     {% for field in message.fields %} |     {% for field in message.fields %} | ||||||
|  |     {{ field.get_field_string() }} | ||||||
|         {% if field.comment %} |         {% if field.comment %} | ||||||
| {{ field.comment }} | {{ field.comment }} | ||||||
|  |  | ||||||
|         {% endif %} |         {% endif %} | ||||||
|     {{ field.get_field_string() }} |  | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|     {% if not message.fields %} |     {% if not message.fields %} | ||||||
|     pass |     pass | ||||||
| @@ -79,51 +81,21 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): | |||||||
|     {% for method in service.methods %} |     {% for method in service.methods %} | ||||||
|     async def {{ method.py_name }}(self |     async def {{ method.py_name }}(self | ||||||
|         {%- if not method.client_streaming -%} |         {%- if not method.client_streaming -%} | ||||||
|             {%- if method.py_input_message and method.py_input_message.fields -%}, *, |             {%- if method.py_input_message -%}, {{ method.py_input_message_param }}: "{{ method.py_input_message_type }}"{%- endif -%} | ||||||
|                 {%- for field in method.py_input_message.fields -%} |  | ||||||
|                     {{ field.py_name }}: {% if field.py_name in method.mutable_default_args and not field.annotation.startswith("Optional[") -%} |  | ||||||
|                                             Optional[{{ field.annotation }}] |  | ||||||
|                                          {%- else -%} |  | ||||||
|                                             {{ field.annotation }} |  | ||||||
|                                          {%- endif -%} = |  | ||||||
|                                             {%- if field.py_name not in method.mutable_default_args -%} |  | ||||||
|                                                 {{ field.default_value_string }} |  | ||||||
|                                             {%- else -%} |  | ||||||
|                                                 None |  | ||||||
|                                             {% endif -%} |  | ||||||
|                     {%- if not loop.last %}, {% endif -%} |  | ||||||
|                 {%- endfor -%} |  | ||||||
|             {%- endif -%} |  | ||||||
|         {%- else -%} |         {%- else -%} | ||||||
|             {# Client streaming: need a request iterator instead #} |             {# Client streaming: need a request iterator instead #} | ||||||
|             , request_iterator: Union[AsyncIterable["{{ method.py_input_message_type }}"], Iterable["{{ method.py_input_message_type }}"]] |             , {{ method.py_input_message_param }}_iterator: Union[AsyncIterable["{{ method.py_input_message_type }}"], Iterable["{{ method.py_input_message_type }}"]] | ||||||
|         {%- endif -%} |         {%- endif -%} | ||||||
|             ) -> {% if method.server_streaming %}AsyncIterator["{{ method.py_output_message_type }}"]{% else %}"{{ method.py_output_message_type }}"{% endif %}: |             ) -> {% if method.server_streaming %}AsyncIterator["{{ method.py_output_message_type }}"]{% else %}"{{ method.py_output_message_type }}"{% endif %}: | ||||||
|         {% if method.comment %} |         {% if method.comment %} | ||||||
| {{ method.comment }} | {{ method.comment }} | ||||||
|  |  | ||||||
|         {% endif %} |         {% endif %} | ||||||
|         {%- for py_name, zero in method.mutable_default_args.items() %} |  | ||||||
|         {{ py_name }} = {{ py_name }} or {{ zero }} |  | ||||||
|         {% endfor %} |  | ||||||
|  |  | ||||||
|         {% if not method.client_streaming %} |  | ||||||
|         request = {{ method.py_input_message_type }}() |  | ||||||
|         {% for field in method.py_input_message.fields %} |  | ||||||
|             {% if field.field_type == 'message' %} |  | ||||||
|         if {{ field.py_name }} is not None: |  | ||||||
|             request.{{ field.py_name }} = {{ field.py_name }} |  | ||||||
|             {% else %} |  | ||||||
|         request.{{ field.py_name }} = {{ field.py_name }} |  | ||||||
|             {% endif %} |  | ||||||
|         {% endfor %} |  | ||||||
|         {% endif %} |  | ||||||
|  |  | ||||||
|         {% if method.server_streaming %} |         {% if method.server_streaming %} | ||||||
|             {% if method.client_streaming %} |             {% if method.client_streaming %} | ||||||
|         async for response in self._stream_stream( |         async for response in self._stream_stream( | ||||||
|             "{{ method.route }}", |             "{{ method.route }}", | ||||||
|             request_iterator, |             {{ method.py_input_message_param }}_iterator, | ||||||
|             {{ method.py_input_message_type }}, |             {{ method.py_input_message_type }}, | ||||||
|             {{ method.py_output_message_type.strip('"') }}, |             {{ method.py_output_message_type.strip('"') }}, | ||||||
|         ): |         ): | ||||||
| @@ -131,7 +103,7 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): | |||||||
|             {% else %}{# i.e. not client streaming #} |             {% else %}{# i.e. not client streaming #} | ||||||
|         async for response in self._unary_stream( |         async for response in self._unary_stream( | ||||||
|             "{{ method.route }}", |             "{{ method.route }}", | ||||||
|             request, |             {{ method.py_input_message_param }}, | ||||||
|             {{ method.py_output_message_type.strip('"') }}, |             {{ method.py_output_message_type.strip('"') }}, | ||||||
|         ): |         ): | ||||||
|             yield response |             yield response | ||||||
| @@ -141,14 +113,14 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): | |||||||
|             {% if method.client_streaming %} |             {% if method.client_streaming %} | ||||||
|         return await self._stream_unary( |         return await self._stream_unary( | ||||||
|             "{{ method.route }}", |             "{{ method.route }}", | ||||||
|             request_iterator, |             {{ method.py_input_message_param }}_iterator, | ||||||
|             {{ method.py_input_message_type }}, |             {{ method.py_input_message_type }}, | ||||||
|             {{ method.py_output_message_type.strip('"') }} |             {{ method.py_output_message_type.strip('"') }} | ||||||
|         ) |         ) | ||||||
|             {% else %}{# i.e. not client streaming #} |             {% else %}{# i.e. not client streaming #} | ||||||
|         return await self._unary_unary( |         return await self._unary_unary( | ||||||
|             "{{ method.route }}", |             "{{ method.route }}", | ||||||
|             request, |             {{ method.py_input_message_param }}, | ||||||
|             {{ method.py_output_message_type.strip('"') }} |             {{ method.py_output_message_type.strip('"') }} | ||||||
|         ) |         ) | ||||||
|             {% endif %}{# client streaming #} |             {% endif %}{# client streaming #} | ||||||
| @@ -167,19 +139,10 @@ class {{ service.py_name }}Base(ServiceBase): | |||||||
|     {% for method in service.methods %} |     {% for method in service.methods %} | ||||||
|     async def {{ method.py_name }}(self |     async def {{ method.py_name }}(self | ||||||
|         {%- if not method.client_streaming -%} |         {%- if not method.client_streaming -%} | ||||||
|             {%- if method.py_input_message and method.py_input_message.fields -%}, |             {%- if method.py_input_message -%}, {{ method.py_input_message_param }}: "{{ method.py_input_message_type }}"{%- endif -%} | ||||||
|                 {%- for field in method.py_input_message.fields -%} |  | ||||||
|                     {{ field.py_name }}: {% if field.py_name in method.mutable_default_args and not field.annotation.startswith("Optional[") -%} |  | ||||||
|                                             Optional[{{ field.annotation }}] |  | ||||||
|                                          {%- else -%} |  | ||||||
|                                             {{ field.annotation }} |  | ||||||
|                                          {%- endif -%} |  | ||||||
|                     {%- if not loop.last %}, {% endif -%} |  | ||||||
|                 {%- endfor -%} |  | ||||||
|             {%- endif -%} |  | ||||||
|         {%- else -%} |         {%- else -%} | ||||||
|             {# Client streaming: need a request iterator instead #} |             {# Client streaming: need a request iterator instead #} | ||||||
|             , request_iterator: AsyncIterator["{{ method.py_input_message_type }}"] |             , {{ method.py_input_message_param }}_iterator: AsyncIterator["{{ method.py_input_message_type }}"] | ||||||
|         {%- endif -%} |         {%- endif -%} | ||||||
|             ) -> {% if method.server_streaming %}AsyncIterator["{{ method.py_output_message_type }}"]{% else %}"{{ method.py_output_message_type }}"{% endif %}: |             ) -> {% if method.server_streaming %}AsyncIterator["{{ method.py_output_message_type }}"]{% else %}"{{ method.py_output_message_type }}"{% endif %}: | ||||||
|         {% if method.comment %} |         {% if method.comment %} | ||||||
| @@ -194,25 +157,17 @@ class {{ service.py_name }}Base(ServiceBase): | |||||||
|     async def __rpc_{{ method.py_name }}(self, stream: grpclib.server.Stream) -> None: |     async def __rpc_{{ method.py_name }}(self, stream: grpclib.server.Stream) -> None: | ||||||
|         {% if not method.client_streaming %} |         {% if not method.client_streaming %} | ||||||
|         request = await stream.recv_message() |         request = await stream.recv_message() | ||||||
|  |  | ||||||
|         request_kwargs = { |  | ||||||
|         {% for field in method.py_input_message.fields %} |  | ||||||
|             "{{ field.py_name }}": request.{{ field.py_name }}, |  | ||||||
|         {% endfor %} |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         {% else %} |         {% else %} | ||||||
|         request_kwargs = {"request_iterator": stream.__aiter__()} |         request = stream.__aiter__() | ||||||
|         {% endif %} |         {% endif %} | ||||||
|  |  | ||||||
|         {% if not method.server_streaming %} |         {% if not method.server_streaming %} | ||||||
|         response = await self.{{ method.py_name }}(**request_kwargs) |         response = await self.{{ method.py_name }}(request) | ||||||
|         await stream.send_message(response) |         await stream.send_message(response) | ||||||
|         {% else %} |         {% else %} | ||||||
|         await self._call_rpc_handler_server_stream( |         await self._call_rpc_handler_server_stream( | ||||||
|             self.{{ method.py_name }}, |             self.{{ method.py_name }}, | ||||||
|             stream, |             stream, | ||||||
|             request_kwargs, |             request, | ||||||
|         ) |         ) | ||||||
|         {% endif %} |         {% endif %} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,13 +60,15 @@ async def generate(whitelist: Set[str], verbose: bool): | |||||||
|         if result != 0: |         if result != 0: | ||||||
|             failed_test_cases.append(test_case_name) |             failed_test_cases.append(test_case_name) | ||||||
|  |  | ||||||
|     if failed_test_cases: |     if len(failed_test_cases) > 0: | ||||||
|         sys.stderr.write( |         sys.stderr.write( | ||||||
|             "\n\033[31;1;4mFailed to generate the following test cases:\033[0m\n" |             "\n\033[31;1;4mFailed to generate the following test cases:\033[0m\n" | ||||||
|         ) |         ) | ||||||
|         for failed_test_case in failed_test_cases: |         for failed_test_case in failed_test_cases: | ||||||
|             sys.stderr.write(f"- {failed_test_case}\n") |             sys.stderr.write(f"- {failed_test_case}\n") | ||||||
|  |  | ||||||
|  |         sys.exit(1) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def generate_test_case_output( | async def generate_test_case_output( | ||||||
|     test_case_input_path: Path, test_case_name: str, verbose: bool |     test_case_input_path: Path, test_case_name: str, verbose: bool | ||||||
| @@ -92,21 +94,41 @@ async def generate_test_case_output( | |||||||
|         protoc(test_case_input_path, test_case_output_path_betterproto, False), |         protoc(test_case_input_path, test_case_output_path_betterproto, False), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     message = f"Generated output for {test_case_name!r}" |     if ref_code == 0: | ||||||
|     if verbose: |         print(f"\033[31;1;4mGenerated reference output for {test_case_name!r}\033[0m") | ||||||
|         print(f"\033[31;1;4m{message}\033[0m") |  | ||||||
|         if ref_out: |  | ||||||
|             sys.stdout.buffer.write(ref_out) |  | ||||||
|         if ref_err: |  | ||||||
|             sys.stderr.buffer.write(ref_err) |  | ||||||
|         if plg_out: |  | ||||||
|             sys.stdout.buffer.write(plg_out) |  | ||||||
|         if plg_err: |  | ||||||
|             sys.stderr.buffer.write(plg_err) |  | ||||||
|         sys.stdout.buffer.flush() |  | ||||||
|         sys.stderr.buffer.flush() |  | ||||||
|     else: |     else: | ||||||
|         print(message) |         print( | ||||||
|  |             f"\033[31;1;4mFailed to generate reference output for {test_case_name!r}\033[0m" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     if verbose: | ||||||
|  |         if ref_out: | ||||||
|  |             print("Reference stdout:") | ||||||
|  |             sys.stdout.buffer.write(ref_out) | ||||||
|  |             sys.stdout.buffer.flush() | ||||||
|  |  | ||||||
|  |         if ref_err: | ||||||
|  |             print("Reference stderr:") | ||||||
|  |             sys.stderr.buffer.write(ref_err) | ||||||
|  |             sys.stderr.buffer.flush() | ||||||
|  |  | ||||||
|  |     if plg_code == 0: | ||||||
|  |         print(f"\033[31;1;4mGenerated plugin output for {test_case_name!r}\033[0m") | ||||||
|  |     else: | ||||||
|  |         print( | ||||||
|  |             f"\033[31;1;4mFailed to generate plugin output for {test_case_name!r}\033[0m" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     if verbose: | ||||||
|  |         if plg_out: | ||||||
|  |             print("Plugin stdout:") | ||||||
|  |             sys.stdout.buffer.write(plg_out) | ||||||
|  |             sys.stdout.buffer.flush() | ||||||
|  |  | ||||||
|  |         if plg_err: | ||||||
|  |             print("Plugin stderr:") | ||||||
|  |             sys.stderr.buffer.write(plg_err) | ||||||
|  |             sys.stderr.buffer.flush() | ||||||
|  |  | ||||||
|     return max(ref_code, plg_code) |     return max(ref_code, plg_code) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,23 +1,24 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
|  | import grpclib | ||||||
|  | import grpclib.metadata | ||||||
|  | import grpclib.server | ||||||
|  | import pytest | ||||||
|  | from betterproto.grpc.util.async_channel import AsyncChannel | ||||||
|  | from grpclib.testing import ChannelFor | ||||||
| from tests.output_betterproto.service.service import ( | from tests.output_betterproto.service.service import ( | ||||||
|     DoThingRequest, |     DoThingRequest, | ||||||
|     DoThingResponse, |     DoThingResponse, | ||||||
|     GetThingRequest, |     GetThingRequest, | ||||||
|     TestStub as ThingServiceClient, |  | ||||||
| ) | ) | ||||||
| import grpclib | from tests.output_betterproto.service.service import TestStub as ThingServiceClient | ||||||
| import grpclib.metadata |  | ||||||
| import grpclib.server |  | ||||||
| from grpclib.testing import ChannelFor |  | ||||||
| import pytest |  | ||||||
| from betterproto.grpc.util.async_channel import AsyncChannel |  | ||||||
| from .thing_service import ThingService | from .thing_service import ThingService | ||||||
|  |  | ||||||
|  |  | ||||||
| async def _test_client(client, name="clean room", **kwargs): | async def _test_client(client: ThingServiceClient, name="clean room", **kwargs): | ||||||
|     response = await client.do_thing(name=name) |     response = await client.do_thing(DoThingRequest(name=name)) | ||||||
|     assert response.names == [name] |     assert response.names == [name] | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -62,7 +63,7 @@ async def test_trailer_only_error_unary_unary( | |||||||
|     ) |     ) | ||||||
|     async with ChannelFor([service]) as channel: |     async with ChannelFor([service]) as channel: | ||||||
|         with pytest.raises(grpclib.exceptions.GRPCError) as e: |         with pytest.raises(grpclib.exceptions.GRPCError) as e: | ||||||
|             await ThingServiceClient(channel).do_thing(name="something") |             await ThingServiceClient(channel).do_thing(DoThingRequest(name="something")) | ||||||
|         assert e.value.status == grpclib.Status.UNAUTHENTICATED |         assert e.value.status == grpclib.Status.UNAUTHENTICATED | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -80,7 +81,7 @@ async def test_trailer_only_error_stream_unary( | |||||||
|     async with ChannelFor([service]) as channel: |     async with ChannelFor([service]) as channel: | ||||||
|         with pytest.raises(grpclib.exceptions.GRPCError) as e: |         with pytest.raises(grpclib.exceptions.GRPCError) as e: | ||||||
|             await ThingServiceClient(channel).do_many_things( |             await ThingServiceClient(channel).do_many_things( | ||||||
|                 request_iterator=[DoThingRequest(name="something")] |                 do_thing_request_iterator=[DoThingRequest(name="something")] | ||||||
|             ) |             ) | ||||||
|             await _test_client(ThingServiceClient(channel)) |             await _test_client(ThingServiceClient(channel)) | ||||||
|         assert e.value.status == grpclib.Status.UNAUTHENTICATED |         assert e.value.status == grpclib.Status.UNAUTHENTICATED | ||||||
| @@ -178,7 +179,9 @@ async def test_async_gen_for_unary_stream_request(): | |||||||
|     async with ChannelFor([ThingService()]) as channel: |     async with ChannelFor([ThingService()]) as channel: | ||||||
|         client = ThingServiceClient(channel) |         client = ThingServiceClient(channel) | ||||||
|         expected_versions = [5, 4, 3, 2, 1] |         expected_versions = [5, 4, 3, 2, 1] | ||||||
|         async for response in client.get_thing_versions(name=thing_name): |         async for response in client.get_thing_versions( | ||||||
|  |             GetThingRequest(name=thing_name) | ||||||
|  |         ): | ||||||
|             assert response.name == thing_name |             assert response.name == thing_name | ||||||
|             assert response.version == expected_versions.pop() |             assert response.version == expected_versions.pop() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,49 +1,48 @@ | |||||||
| from typing import AsyncIterator, AsyncIterable | from typing import AsyncIterable, AsyncIterator | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
| from grpclib.testing import ChannelFor | from grpclib.testing import ChannelFor | ||||||
|  |  | ||||||
| from tests.output_betterproto.example_service.example_service import ( | from tests.output_betterproto.example_service.example_service import ( | ||||||
|     TestBase, |  | ||||||
|     TestStub, |  | ||||||
|     ExampleRequest, |     ExampleRequest, | ||||||
|     ExampleResponse, |     ExampleResponse, | ||||||
|  |     TestBase, | ||||||
|  |     TestStub, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ExampleService(TestBase): | class ExampleService(TestBase): | ||||||
|     async def example_unary_unary( |     async def example_unary_unary( | ||||||
|         self, example_string: str, example_integer: int |         self, example_request: ExampleRequest | ||||||
|     ) -> "ExampleResponse": |     ) -> "ExampleResponse": | ||||||
|         return ExampleResponse( |         return ExampleResponse( | ||||||
|             example_string=example_string, |             example_string=example_request.example_string, | ||||||
|             example_integer=example_integer, |             example_integer=example_request.example_integer, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     async def example_unary_stream( |     async def example_unary_stream( | ||||||
|         self, example_string: str, example_integer: int |         self, example_request: ExampleRequest | ||||||
|     ) -> AsyncIterator["ExampleResponse"]: |     ) -> AsyncIterator["ExampleResponse"]: | ||||||
|         response = ExampleResponse( |         response = ExampleResponse( | ||||||
|             example_string=example_string, |             example_string=example_request.example_string, | ||||||
|             example_integer=example_integer, |             example_integer=example_request.example_integer, | ||||||
|         ) |         ) | ||||||
|         yield response |         yield response | ||||||
|         yield response |         yield response | ||||||
|         yield response |         yield response | ||||||
|  |  | ||||||
|     async def example_stream_unary( |     async def example_stream_unary( | ||||||
|         self, request_iterator: AsyncIterator["ExampleRequest"] |         self, example_request_iterator: AsyncIterator["ExampleRequest"] | ||||||
|     ) -> "ExampleResponse": |     ) -> "ExampleResponse": | ||||||
|         async for example_request in request_iterator: |         async for example_request in example_request_iterator: | ||||||
|             return ExampleResponse( |             return ExampleResponse( | ||||||
|                 example_string=example_request.example_string, |                 example_string=example_request.example_string, | ||||||
|                 example_integer=example_request.example_integer, |                 example_integer=example_request.example_integer, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     async def example_stream_stream( |     async def example_stream_stream( | ||||||
|         self, request_iterator: AsyncIterator["ExampleRequest"] |         self, example_request_iterator: AsyncIterator["ExampleRequest"] | ||||||
|     ) -> AsyncIterator["ExampleResponse"]: |     ) -> AsyncIterator["ExampleResponse"]: | ||||||
|         async for example_request in request_iterator: |         async for example_request in example_request_iterator: | ||||||
|             yield ExampleResponse( |             yield ExampleResponse( | ||||||
|                 example_string=example_request.example_string, |                 example_string=example_request.example_string, | ||||||
|                 example_integer=example_request.example_integer, |                 example_integer=example_request.example_integer, | ||||||
| @@ -52,44 +51,32 @@ class ExampleService(TestBase): | |||||||
|  |  | ||||||
| @pytest.mark.asyncio | @pytest.mark.asyncio | ||||||
| async def test_calls_with_different_cardinalities(): | async def test_calls_with_different_cardinalities(): | ||||||
|     test_string = "test string" |     example_request = ExampleRequest("test string", 42) | ||||||
|     test_int = 42 |  | ||||||
|  |  | ||||||
|     async with ChannelFor([ExampleService()]) as channel: |     async with ChannelFor([ExampleService()]) as channel: | ||||||
|         stub = TestStub(channel) |         stub = TestStub(channel) | ||||||
|  |  | ||||||
|         # unary unary |         # unary unary | ||||||
|         response = await stub.example_unary_unary( |         response = await stub.example_unary_unary(example_request) | ||||||
|             example_string="test string", |         assert response.example_string == example_request.example_string | ||||||
|             example_integer=42, |         assert response.example_integer == example_request.example_integer | ||||||
|         ) |  | ||||||
|         assert response.example_string == test_string |  | ||||||
|         assert response.example_integer == test_int |  | ||||||
|  |  | ||||||
|         # unary stream |         # unary stream | ||||||
|         async for response in stub.example_unary_stream( |         async for response in stub.example_unary_stream(example_request): | ||||||
|             example_string="test string", |             assert response.example_string == example_request.example_string | ||||||
|             example_integer=42, |             assert response.example_integer == example_request.example_integer | ||||||
|         ): |  | ||||||
|             assert response.example_string == test_string |  | ||||||
|             assert response.example_integer == test_int |  | ||||||
|  |  | ||||||
|         # stream unary |         # stream unary | ||||||
|         request = ExampleRequest( |  | ||||||
|             example_string=test_string, |  | ||||||
|             example_integer=42, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         async def request_iterator(): |         async def request_iterator(): | ||||||
|             yield request |             yield example_request | ||||||
|             yield request |             yield example_request | ||||||
|             yield request |             yield example_request | ||||||
|  |  | ||||||
|         response = await stub.example_stream_unary(request_iterator()) |         response = await stub.example_stream_unary(request_iterator()) | ||||||
|         assert response.example_string == test_string |         assert response.example_string == example_request.example_string | ||||||
|         assert response.example_integer == test_int |         assert response.example_integer == example_request.example_integer | ||||||
|  |  | ||||||
|         # stream stream |         # stream stream | ||||||
|         async for response in stub.example_stream_stream(request_iterator()): |         async for response in stub.example_stream_stream(request_iterator()): | ||||||
|             assert response.example_string == test_string |             assert response.example_string == example_request.example_string | ||||||
|             assert response.example_integer == test_int |             assert response.example_integer == example_request.example_integer | ||||||
|   | |||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | { | ||||||
|  |     "int": 26, | ||||||
|  |     "float": 26.0, | ||||||
|  |     "str": "value-for-str", | ||||||
|  |     "bytes": "001a", | ||||||
|  |     "bool": true | ||||||
|  | } | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | syntax = "proto3"; | ||||||
|  |  | ||||||
|  | // Tests that messages may contain fields with names that are identical to their python types (PR #294) | ||||||
|  |  | ||||||
|  | message Test { | ||||||
|  |     int32 int = 1; | ||||||
|  |     float float = 2; | ||||||
|  |     string str = 3; | ||||||
|  |     bytes bytes = 4; | ||||||
|  |     bool bool = 5; | ||||||
|  | } | ||||||
| @@ -2,9 +2,8 @@ from typing import Any, Callable, Optional | |||||||
|  |  | ||||||
| import betterproto.lib.google.protobuf as protobuf | import betterproto.lib.google.protobuf as protobuf | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from tests.mocks import MockChannel | from tests.mocks import MockChannel | ||||||
| from tests.output_betterproto.googletypes_response import TestStub | from tests.output_betterproto.googletypes_response import Input, TestStub | ||||||
|  |  | ||||||
| test_cases = [ | test_cases = [ | ||||||
|     (TestStub.get_double, protobuf.DoubleValue, 2.5), |     (TestStub.get_double, protobuf.DoubleValue, 2.5), | ||||||
| @@ -22,14 +21,15 @@ test_cases = [ | |||||||
| @pytest.mark.asyncio | @pytest.mark.asyncio | ||||||
| @pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases) | @pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases) | ||||||
| async def test_channel_receives_wrapped_type( | async def test_channel_receives_wrapped_type( | ||||||
|     service_method: Callable[[TestStub], Any], wrapper_class: Callable, value |     service_method: Callable[[TestStub, Input], Any], wrapper_class: Callable, value | ||||||
| ): | ): | ||||||
|     wrapped_value = wrapper_class() |     wrapped_value = wrapper_class() | ||||||
|     wrapped_value.value = value |     wrapped_value.value = value | ||||||
|     channel = MockChannel(responses=[wrapped_value]) |     channel = MockChannel(responses=[wrapped_value]) | ||||||
|     service = TestStub(channel) |     service = TestStub(channel) | ||||||
|  |     method_param = Input() | ||||||
|  |  | ||||||
|     await service_method(service) |     await service_method(service, method_param) | ||||||
|  |  | ||||||
|     assert channel.requests[0]["response_type"] != Optional[type(value)] |     assert channel.requests[0]["response_type"] != Optional[type(value)] | ||||||
|     assert channel.requests[0]["response_type"] == type(wrapped_value) |     assert channel.requests[0]["response_type"] == type(wrapped_value) | ||||||
| @@ -39,7 +39,7 @@ async def test_channel_receives_wrapped_type( | |||||||
| @pytest.mark.xfail | @pytest.mark.xfail | ||||||
| @pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases) | @pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases) | ||||||
| async def test_service_unwraps_response( | async def test_service_unwraps_response( | ||||||
|     service_method: Callable[[TestStub], Any], wrapper_class: Callable, value |     service_method: Callable[[TestStub, Input], Any], wrapper_class: Callable, value | ||||||
| ): | ): | ||||||
|     """ |     """ | ||||||
|     grpclib does not unwrap wrapper values returned by services |     grpclib does not unwrap wrapper values returned by services | ||||||
| @@ -47,8 +47,9 @@ async def test_service_unwraps_response( | |||||||
|     wrapped_value = wrapper_class() |     wrapped_value = wrapper_class() | ||||||
|     wrapped_value.value = value |     wrapped_value.value = value | ||||||
|     service = TestStub(MockChannel(responses=[wrapped_value])) |     service = TestStub(MockChannel(responses=[wrapped_value])) | ||||||
|  |     method_param = Input() | ||||||
|  |  | ||||||
|     response_value = await service_method(service) |     response_value = await service_method(service, method_param) | ||||||
|  |  | ||||||
|     assert response_value == value |     assert response_value == value | ||||||
|     assert type(response_value) == type(value) |     assert type(response_value) == type(value) | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from tests.mocks import MockChannel | from tests.mocks import MockChannel | ||||||
| from tests.output_betterproto.googletypes_response_embedded import ( | from tests.output_betterproto.googletypes_response_embedded import ( | ||||||
|  |     Input, | ||||||
|     Output, |     Output, | ||||||
|     TestStub, |     TestStub, | ||||||
| ) | ) | ||||||
| @@ -26,7 +26,7 @@ async def test_service_passes_through_unwrapped_values_embedded_in_response(): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     service = TestStub(MockChannel(responses=[output])) |     service = TestStub(MockChannel(responses=[output])) | ||||||
|     response = await service.get_output() |     response = await service.get_output(Input()) | ||||||
|  |  | ||||||
|     assert response.double_value == 10.0 |     assert response.double_value == 10.0 | ||||||
|     assert response.float_value == 12.0 |     assert response.float_value == 12.0 | ||||||
|   | |||||||
| @@ -1,17 +1,21 @@ | |||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from tests.mocks import MockChannel | from tests.mocks import MockChannel | ||||||
| from tests.output_betterproto.import_service_input_message import ( | from tests.output_betterproto.import_service_input_message import ( | ||||||
|  |     NestedRequestMessage, | ||||||
|  |     RequestMessage, | ||||||
|     RequestResponse, |     RequestResponse, | ||||||
|     TestStub, |     TestStub, | ||||||
| ) | ) | ||||||
|  | from tests.output_betterproto.import_service_input_message.child import ( | ||||||
|  |     ChildRequestMessage, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.asyncio | @pytest.mark.asyncio | ||||||
| async def test_service_correctly_imports_reference_message(): | async def test_service_correctly_imports_reference_message(): | ||||||
|     mock_response = RequestResponse(value=10) |     mock_response = RequestResponse(value=10) | ||||||
|     service = TestStub(MockChannel([mock_response])) |     service = TestStub(MockChannel([mock_response])) | ||||||
|     response = await service.do_thing(argument=1) |     response = await service.do_thing(RequestMessage(1)) | ||||||
|     assert mock_response == response |     assert mock_response == response | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -19,7 +23,7 @@ async def test_service_correctly_imports_reference_message(): | |||||||
| async def test_service_correctly_imports_reference_message_from_child_package(): | async def test_service_correctly_imports_reference_message_from_child_package(): | ||||||
|     mock_response = RequestResponse(value=10) |     mock_response = RequestResponse(value=10) | ||||||
|     service = TestStub(MockChannel([mock_response])) |     service = TestStub(MockChannel([mock_response])) | ||||||
|     response = await service.do_thing2(child_argument=1) |     response = await service.do_thing2(ChildRequestMessage(1)) | ||||||
|     assert mock_response == response |     assert mock_response == response | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -27,5 +31,5 @@ async def test_service_correctly_imports_reference_message_from_child_package(): | |||||||
| async def test_service_correctly_imports_nested_reference(): | async def test_service_correctly_imports_nested_reference(): | ||||||
|     mock_response = RequestResponse(value=10) |     mock_response = RequestResponse(value=10) | ||||||
|     service = TestStub(MockChannel([mock_response])) |     service = TestStub(MockChannel([mock_response])) | ||||||
|     response = await service.do_thing3(nested_argument=1) |     response = await service.do_thing3(NestedRequestMessage(1)) | ||||||
|     assert mock_response == response |     assert mock_response == response | ||||||
|   | |||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | { | ||||||
|  |   "test1": 128, | ||||||
|  |   "test2": true, | ||||||
|  |   "test3": "A value", | ||||||
|  |   "test4": "aGVsbG8=", | ||||||
|  |   "test5": { | ||||||
|  |     "test": "Hello" | ||||||
|  |   }, | ||||||
|  |   "test6": "B", | ||||||
|  |   "test7": "8589934592", | ||||||
|  |   "test8": 2.5 | ||||||
|  | } | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | syntax = "proto3"; | ||||||
|  |  | ||||||
|  | message InnerTest { | ||||||
|  |     string test = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message Test { | ||||||
|  |     optional uint32 test1 = 1; | ||||||
|  |     optional bool test2 = 2; | ||||||
|  |     optional string test3 = 3; | ||||||
|  |     optional bytes test4 = 4; | ||||||
|  |     optional InnerTest test5 = 5; | ||||||
|  |     optional TestEnum test6 = 6; | ||||||
|  |     optional uint64 test7 = 7; | ||||||
|  |     optional float test8 = 8; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | enum TestEnum { | ||||||
|  |     A = 0; | ||||||
|  |     B = 1; | ||||||
|  | } | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | {} | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | { | ||||||
|  |   "test1": 0, | ||||||
|  |   "test2": false, | ||||||
|  |   "test3": "", | ||||||
|  |   "test4": "", | ||||||
|  |   "test6": "A", | ||||||
|  |   "test7": "0", | ||||||
|  |   "test8": 0 | ||||||
|  | } | ||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | import json | ||||||
|  |  | ||||||
|  | from tests.output_betterproto.proto3_field_presence import Test, InnerTest, TestEnum | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_null_fields_json(): | ||||||
|  |     """Ensure that using "null" in JSON is equivalent to not specifying a | ||||||
|  |     field, for fields with explicit presence""" | ||||||
|  |  | ||||||
|  |     def test_json(ref_json: str, obj_json: str) -> None: | ||||||
|  |         """`ref_json` and `obj_json` are JSON strings describing a `Test` object. | ||||||
|  |         Test that deserializing both leads to the same object, and that | ||||||
|  |         `ref_json` is the normalized format.""" | ||||||
|  |         ref_obj = Test().from_json(ref_json) | ||||||
|  |         obj = Test().from_json(obj_json) | ||||||
|  |  | ||||||
|  |         assert obj == ref_obj | ||||||
|  |         assert json.loads(obj.to_json(0)) == json.loads(ref_json) | ||||||
|  |  | ||||||
|  |     test_json("{}", '{ "test1": null, "test2": null, "test3": null }') | ||||||
|  |     test_json("{}", '{ "test4": null, "test5": null, "test6": null }') | ||||||
|  |     test_json("{}", '{ "test7": null, "test8": null }') | ||||||
|  |     test_json('{ "test5": {} }', '{ "test3": null, "test5": {} }') | ||||||
|  |  | ||||||
|  |     # Make sure that if include_default_values is set, None values are | ||||||
|  |     # exported. | ||||||
|  |     obj = Test() | ||||||
|  |     assert obj.to_dict() == {} | ||||||
|  |     assert obj.to_dict(include_default_values=True) == { | ||||||
|  |         "test1": None, | ||||||
|  |         "test2": None, | ||||||
|  |         "test3": None, | ||||||
|  |         "test4": None, | ||||||
|  |         "test5": None, | ||||||
|  |         "test6": None, | ||||||
|  |         "test7": None, | ||||||
|  |         "test8": None, | ||||||
|  |     } | ||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | { | ||||||
|  |   "nested": {} | ||||||
|  | } | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | syntax = "proto3"; | ||||||
|  |  | ||||||
|  | message Test { | ||||||
|  |     oneof kind { | ||||||
|  |         Nested nested = 1; | ||||||
|  |         WithOptional with_optional = 2; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message InnerNested { | ||||||
|  |     optional bool a = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message Nested { | ||||||
|  |     InnerNested inner = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message WithOptional { | ||||||
|  |     optional bool b = 2; | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | from tests.output_betterproto.proto3_field_presence_oneof import ( | ||||||
|  |     Test, | ||||||
|  |     InnerNested, | ||||||
|  |     Nested, | ||||||
|  |     WithOptional, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_serialization(): | ||||||
|  |     """Ensure that serialization of fields unset but with explicit field | ||||||
|  |     presence do not bloat the serialized payload with length-delimited fields | ||||||
|  |     with length 0""" | ||||||
|  |  | ||||||
|  |     def test_empty_nested(message: Test) -> None: | ||||||
|  |         # '0a' => tag 1, length delimited | ||||||
|  |         # '00' => length: 0 | ||||||
|  |         assert bytes(message) == bytearray.fromhex("0a 00") | ||||||
|  |  | ||||||
|  |     test_empty_nested(Test(nested=Nested())) | ||||||
|  |     test_empty_nested(Test(nested=Nested(inner=None))) | ||||||
|  |     test_empty_nested(Test(nested=Nested(inner=InnerNested(a=None)))) | ||||||
|  |  | ||||||
|  |     def test_empty_with_optional(message: Test) -> None: | ||||||
|  |         # '12' => tag 2, length delimited | ||||||
|  |         # '00' => length: 0 | ||||||
|  |         assert bytes(message) == bytearray.fromhex("12 00") | ||||||
|  |  | ||||||
|  |     test_empty_with_optional(Test(with_optional=WithOptional())) | ||||||
|  |     test_empty_with_optional(Test(with_optional=WithOptional(b=None))) | ||||||
| @@ -2,9 +2,16 @@ syntax = "proto3"; | |||||||
|  |  | ||||||
| package service; | package service; | ||||||
|  |  | ||||||
|  | enum ThingType { | ||||||
|  |   UNKNOWN = 0; | ||||||
|  |   LIVING = 1; | ||||||
|  |   DEAD = 2; | ||||||
|  | } | ||||||
|  |  | ||||||
| message DoThingRequest { | message DoThingRequest { | ||||||
|   string name = 1; |   string name = 1; | ||||||
|   repeated string comments = 2; |   repeated string comments = 2; | ||||||
|  |   ThingType type = 3; | ||||||
| } | } | ||||||
|  |  | ||||||
| message DoThingResponse { | message DoThingResponse { | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
| import betterproto |  | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from typing import Optional, List, Dict | from datetime import datetime | ||||||
| from datetime import datetime, timedelta | from inspect import Parameter, signature | ||||||
|  | from typing import Dict, List, Optional | ||||||
|  |  | ||||||
|  | import betterproto | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_has_field(): | def test_has_field(): | ||||||
| @@ -348,10 +350,8 @@ def test_recursive_message(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_recursive_message_defaults(): | def test_recursive_message_defaults(): | ||||||
|     from tests.output_betterproto.recursivemessage import ( |     from tests.output_betterproto.recursivemessage import Intermediate | ||||||
|         Test as RecursiveMessage, |     from tests.output_betterproto.recursivemessage import Test as RecursiveMessage | ||||||
|         Intermediate, |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     msg = RecursiveMessage(name="bob", intermediate=Intermediate(42)) |     msg = RecursiveMessage(name="bob", intermediate=Intermediate(42)) | ||||||
|  |  | ||||||
| @@ -476,3 +476,12 @@ def test_iso_datetime_list(): | |||||||
|  |  | ||||||
|     msg.from_dict({"timestamps": iso_candidates}) |     msg.from_dict({"timestamps": iso_candidates}) | ||||||
|     assert all([isinstance(item, datetime) for item in msg.timestamps]) |     assert all([isinstance(item, datetime) for item in msg.timestamps]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_service_argument__expected_parameter(): | ||||||
|  |     from tests.output_betterproto.service.service import TestStub | ||||||
|  |  | ||||||
|  |     sig = signature(TestStub.do_thing) | ||||||
|  |     do_thing_request_parameter = sig.parameters["do_thing_request"] | ||||||
|  |     assert do_thing_request_parameter.default is Parameter.empty | ||||||
|  |     assert do_thing_request_parameter.annotation == "DoThingRequest" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user