Compare commits
12 Commits
285-semant
...
changelog1
Author | SHA1 | Date | |
---|---|---|---|
|
3eaff291c4 | ||
|
9b5594adbe | ||
|
d991040ff6 | ||
|
d260f071e0 | ||
|
6dd7baa26c | ||
|
573c7292a6 | ||
|
d77f44ebb7 | ||
|
671c0ff4ac | ||
|
9cecc8c3ff | ||
|
bc3cfc5562 | ||
|
b0a36d12e4 | ||
|
a4d2d39546 |
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,9 +0,0 @@
|
|||||||
## Description
|
|
||||||
|
|
||||||
<!-- Thanks for contributing to betterproto! Add a thorough explanation of what your changes do below this line: -->
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] This PR targets the `rc` branch (**not** `master`).
|
|
||||||
- [ ] [If this should release a new version to PyPI when merged] The title of the PR follows the [Angular Conventional Commit](https://www.conventionalcommits.org/) syntax (`feat:` or `fix:`, with `BREAKING CHANGE:` in the commit message body if appropriate), and clearly describes the fix or feature.
|
|
||||||
- [ ] Documentation is updated (`README.md` and docstrings).
|
|
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/
|
||||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -16,18 +16,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.8
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
- name: Install poetry
|
- name: Install poetry
|
||||||
run: python -m pip install poetry
|
run: python -m pip install poetry
|
||||||
- name: Semantic Release
|
|
||||||
uses: cycjimmy/semantic-release-action@v2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build package
|
- name: Build package
|
||||||
run: poetry build
|
run: poetry build
|
||||||
- name: Publish package to PyPI
|
- name: Publish package to PyPI
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,7 +6,6 @@
|
|||||||
.pytest_cache
|
.pytest_cache
|
||||||
.python-version
|
.python-version
|
||||||
build/
|
build/
|
||||||
node_modules/
|
|
||||||
tests/output_*
|
tests/output_*
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
dist
|
dist
|
||||||
@@ -18,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)
|
||||||
|
26
README.md
26
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():
|
||||||
@@ -498,16 +498,6 @@ protoc \
|
|||||||
- [x] Automate running tests
|
- [x] Automate running tests
|
||||||
- [ ] Cleanup!
|
- [ ] Cleanup!
|
||||||
|
|
||||||
|
|
||||||
## Release
|
|
||||||
|
|
||||||
New versions are versioned and released using [Semantic Release](https://github.com/semantic-release/semantic-release). When new commits
|
|
||||||
using the Angular Conventional Commits syntax land on `master` or `rc`, those commits are used to determine what new version to release.
|
|
||||||
|
|
||||||
All Pull Requests must target the `rc` branch; when merged into `rc` they will publish new release candidate (`rc`) versions to PyPI
|
|
||||||
automatically. When maintainers want to publish a new full release, they simply merge `rc` into `master`. This flow ensures that features
|
|
||||||
and fixes are published quickly and continuously rather than awaiting a manual release process.
|
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
Join us on [Slack](https://join.slack.com/t/betterproto/shared_invite/zt-f0n0uolx-iN8gBNrkPxtKHTLpG3o1OQ)!
|
Join us on [Slack](https://join.slack.com/t/betterproto/shared_invite/zt-f0n0uolx-iN8gBNrkPxtKHTLpG3o1OQ)!
|
||||||
|
52
package.json
52
package.json
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "python-betterproto-semantic-release",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Encapsulate dependencies needed to use semantic-release",
|
|
||||||
"dependencies": {
|
|
||||||
"@semantic-release/exec": "^5.0.0",
|
|
||||||
"@semantic-release/git": "^9.0.0",
|
|
||||||
"@semantic-release/gitlab": "^6.0.4",
|
|
||||||
"conventional-changelog-eslint": "^3.0.8",
|
|
||||||
"semantic-release": "^17.1.1"
|
|
||||||
},
|
|
||||||
"release": {
|
|
||||||
"branches": [
|
|
||||||
"master",
|
|
||||||
{
|
|
||||||
"name": "rc",
|
|
||||||
"prerelease": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"@semantic-release/commit-analyzer",
|
|
||||||
{
|
|
||||||
"preset": "angular"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@semantic-release/release-notes-generator",
|
|
||||||
{
|
|
||||||
"preset": "angular"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@semantic-release/exec",
|
|
||||||
{
|
|
||||||
"prepareCmd": "poetry version ${nextRelease.version}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@semantic-release/github",
|
|
||||||
[
|
|
||||||
"@semantic-release/git",
|
|
||||||
{
|
|
||||||
"assets": [
|
|
||||||
"pyproject.toml"
|
|
||||||
],
|
|
||||||
"message": "Release v${nextRelease.version} [skip ci]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"repositoryUrl": "ssh://git@github.com/danielgtaylor/python-betterproto.git"
|
|
||||||
}
|
|
||||||
}
|
|
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