Merge branch 'master_gh'

This commit is contained in:
Georg K 2024-09-10 19:41:24 +03:00
commit 63458e2da0
11 changed files with 123 additions and 39 deletions

View File

@ -7,6 +7,29 @@ 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`.
## [2.0.0b7] - 2024-08-11
- **Breaking**: Support `Pydantic` v2 and dropping support for v1 [#588](https://github.com/danielgtaylor/python-betterproto/pull/588)
- **Breaking**: The attempting to access an unset `oneof` now raises an `AttributeError`
field. To see how to access `oneof` fields now, refer to [#558](https://github.com/danielgtaylor/python-betterproto/pull/558)
and [README.md](https://github.com/danielgtaylor/python-betterproto#one-of-support).
- **Breaking**: A custom `Enum` has been implemented to match the behaviour of being an open set. Any checks for `isinstance(enum_member, enum.Enum)` and `issubclass(EnumSubclass, enum.Enum)` will now return `False`. This change also has the side effect of
preventing any passthrough of `Enum` members (i.e. `Foo.RED.GREEN` doesn't work any more). See [#293](https://github.com/danielgtaylor/python-betterproto/pull/293) for more info, this fixed many bugs related to `Enum` handling.
- Add support for `pickle` methods [#535](https://github.com/danielgtaylor/python-betterproto/pull/535)
- Add support for `Struct` and `Value` types [#551](https://github.com/danielgtaylor/python-betterproto/pull/551)
- Add support for [`Rich` package](https://rich.readthedocs.io/en/latest/index.html) for pretty printing [#508](https://github.com/danielgtaylor/python-betterproto/pull/508)
- Improve support for streaming messages [#518](https://github.com/danielgtaylor/python-betterproto/pull/518) [#529](https://github.com/danielgtaylor/python-betterproto/pull/529)
- Improve performance of serializing / de-serializing messages [#545](https://github.com/danielgtaylor/python-betterproto/pull/545)
- Improve the handling of message name collisions with typing by allowing the method / type of imports to be configured.
Refer to [#582](https://github.com/danielgtaylor/python-betterproto/pull/582)
and [README.md](https://github.com/danielgtaylor/python-betterproto#configuration-typing-imports).
- Fix roundtrip parsing of `datetime`s [#534](https://github.com/danielgtaylor/python-betterproto/pull/534)
- Fix accessing unset optional fields [#523](https://github.com/danielgtaylor/python-betterproto/pull/523)
- Fix `Message` equality comparison [#513](https://github.com/danielgtaylor/python-betterproto/pull/513)
- Fix behaviour with long comment messages [#532](https://github.com/danielgtaylor/python-betterproto/pull/532)
- Add a warning when calling a deprecated message [#596](https://github.com/danielgtaylor/python-betterproto/pull/596)
## [2.0.0b6] - 2023-06-25
- **Breaking**: the minimum Python version has been bumped to `3.7` [#444](https://github.com/danielgtaylor/python-betterproto/pull/444)

View File

@ -1,6 +1,7 @@
# Better Protobuf / gRPC Support for Python
![](https://github.com/danielgtaylor/python-betterproto/workflows/CI/badge.svg)
![](https://github.com/danielgtaylor/python-betterproto/actions/workflows/ci.yml/badge.svg)
> :octocat: If you're reading this on github, please be aware that it might mention unreleased features! See the latest released README on [pypi](https://pypi.org/project/betterproto/).
This project aims to provide an improved experience when using Protobuf / gRPC in a modern Python environment by making use of modern language features and generating readable, understandable, idiomatic Python code. It will not support legacy features or environments (e.g. Protobuf 2). The following are supported:

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "betterproto"
version = "2.0.0b6"
version = "2.0.0b7"
description = "A better Protobuf / gRPC generator & library"
authors = ["Daniel G. Taylor <danielgtaylor@gmail.com>"]
readme = "README.md"

View File

@ -62,6 +62,13 @@ if TYPE_CHECKING:
SupportsWrite,
)
if sys.version_info >= (3, 10):
from types import UnionType as _types_UnionType
else:
class _types_UnionType:
...
# Proto 3 data types
TYPE_ENUM = "enum"
@ -148,6 +155,7 @@ def datetime_default_gen() -> datetime:
DATETIME_ZERO = datetime_default_gen()
# Special protobuf json doubles
INFINITY = "Infinity"
NEG_INFINITY = "-Infinity"
@ -1166,30 +1174,29 @@ class Message(ABC):
def _get_field_default_gen(cls, field: dataclasses.Field) -> Any:
t = cls._type_hint(field.name)
if hasattr(t, "__origin__"):
if t.__origin__ is dict:
# This is some kind of map (dict in Python).
return dict
elif t.__origin__ is list:
# This is some kind of list (repeated) field.
return list
elif t.__origin__ is Union and t.__args__[1] is type(None):
is_310_union = isinstance(t, _types_UnionType)
if hasattr(t, "__origin__") or is_310_union:
if is_310_union or t.__origin__ is Union:
# This is an optional field (either wrapped, or using proto3
# field presence). For setting the default we really don't care
# what kind of field it is.
return type(None)
else:
return t
elif issubclass(t, Enum):
if t.__origin__ is list:
# This is some kind of list (repeated) field.
return list
if t.__origin__ is dict:
# This is some kind of map (dict in Python).
return dict
return t
if issubclass(t, Enum):
# Enums always default to zero.
return t.try_value
elif t is datetime:
if t is datetime:
# Offsets are relative to 1970-01-01T00:00:00Z
return datetime_default_gen
else:
# This is either a primitive scalar or another message type. Calling
# it should result in its zero value.
return t
# This is either a primitive scalar or another message type. Calling
# it should result in its zero value.
return t
def _postprocess_single(
self, wire_type: int, meta: FieldMetadata, field_name: str, value: Any

View File

@ -1,6 +1,9 @@
from __future__ import annotations
import os
import re
from typing import (
TYPE_CHECKING,
Dict,
List,
Set,
@ -13,6 +16,9 @@ from ..lib.google import protobuf as google_protobuf
from .naming import pythonize_class_name
if TYPE_CHECKING:
from ..plugin.typing_compiler import TypingCompiler
WRAPPER_TYPES: Dict[str, Type] = {
".google.protobuf.DoubleValue": google_protobuf.DoubleValue,
".google.protobuf.FloatValue": google_protobuf.FloatValue,
@ -47,7 +53,7 @@ def get_type_reference(
package: str,
imports: set,
source_type: str,
typing_compiler: "TypingCompiler",
typing_compiler: TypingCompiler,
unwrap: bool = True,
pydantic: bool = False,
) -> str:

View File

@ -275,8 +275,21 @@ class OutputTemplate:
@property
def python_module_imports(self) -> Set[str]:
imports = set()
has_deprecated = False
if any(m.deprecated for m in self.messages):
has_deprecated = True
if any(x for x in self.messages if any(x.deprecated_fields)):
has_deprecated = True
if any(
any(m.proto_obj.options.deprecated for m in s.methods)
for s in self.services
):
has_deprecated = True
if has_deprecated:
imports.add("warnings")
if self.builtins_import:
imports.add("builtins")
return imports

View File

@ -139,29 +139,35 @@ class TypingImportTypingCompiler(TypingCompiler):
class NoTyping310TypingCompiler(TypingCompiler):
_imports: Dict[str, Set[str]] = field(default_factory=lambda: defaultdict(set))
@staticmethod
def _fmt(type: str) -> str: # for now this is necessary till 3.14
if type.startswith('"'):
return type[1:-1]
return type
def optional(self, type: str) -> str:
return f"{type} | None"
return f'"{self._fmt(type)} | None"'
def list(self, type: str) -> str:
return f"list[{type}]"
return f'"list[{self._fmt(type)}]"'
def dict(self, key: str, value: str) -> str:
return f"dict[{key}, {value}]"
return f'"dict[{key}, {self._fmt(value)}]"'
def union(self, *types: str) -> str:
return " | ".join(types)
return f'"{" | ".join(map(self._fmt, types))}"'
def iterable(self, type: str) -> str:
self._imports["typing"].add("Iterable")
return f"Iterable[{type}]"
self._imports["collections.abc"].add("Iterable")
return f'"Iterable[{type}]"'
def async_iterable(self, type: str) -> str:
self._imports["typing"].add("AsyncIterable")
return f"AsyncIterable[{type}]"
self._imports["collections.abc"].add("AsyncIterable")
return f'"AsyncIterable[{type}]"'
def async_iterator(self, type: str) -> str:
self._imports["typing"].add("AsyncIterator")
return f"AsyncIterator[{type}]"
self._imports["collections.abc"].add("AsyncIterator")
return f'"AsyncIterator[{type}]"'
def imports(self) -> Dict[str, Optional[Set[str]]]:
return {k: v if v else None for k, v in self._imports.items()}

View File

@ -84,6 +84,10 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub):
{% if method.comment %}
{{ method.comment }}
{% endif %}
{% if method.proto_obj.options.deprecated %}
warnings.warn("{{ service.py_name }}.{{ method.py_name }} is deprecated", DeprecationWarning)
{% endif %}
{% if method.server_streaming %}
{% if method.client_streaming %}

View File

@ -12,3 +12,10 @@ message Message {
option deprecated = true;
string value = 1;
}
message Empty {}
service TestService {
rpc func(Empty) returns (Empty);
rpc deprecated_func(Empty) returns (Empty) { option deprecated = true; };
}

View File

@ -2,9 +2,12 @@ import warnings
import pytest
from tests.mocks import MockChannel
from tests.output_betterproto.deprecated import (
Empty,
Message,
Test,
TestServiceStub,
)
@ -43,3 +46,19 @@ def test_message_with_deprecated_field_not_set_default(message):
_ = Test(value=10).message
assert not record
@pytest.mark.asyncio
async def test_service_with_deprecated_method():
stub = TestServiceStub(MockChannel([Empty(), Empty()]))
with pytest.warns(DeprecationWarning) as record:
await stub.deprecated_func(Empty())
assert len(record) == 1
assert str(record[0].message) == f"TestService.deprecated_func is deprecated"
with pytest.warns(None) as record:
await stub.func(Empty())
assert not record

View File

@ -62,19 +62,17 @@ def test_typing_import_typing_compiler():
def test_no_typing_311_typing_compiler():
compiler = NoTyping310TypingCompiler()
assert compiler.imports() == {}
assert compiler.optional("str") == "str | None"
assert compiler.optional("str") == '"str | None"'
assert compiler.imports() == {}
assert compiler.list("str") == "list[str]"
assert compiler.list("str") == '"list[str]"'
assert compiler.imports() == {}
assert compiler.dict("str", "int") == "dict[str, int]"
assert compiler.dict("str", "int") == '"dict[str, int]"'
assert compiler.imports() == {}
assert compiler.union("str", "int") == "str | int"
assert compiler.union("str", "int") == '"str | int"'
assert compiler.imports() == {}
assert compiler.iterable("str") == "Iterable[str]"
assert compiler.imports() == {"typing": {"Iterable"}}
assert compiler.async_iterable("str") == "AsyncIterable[str]"
assert compiler.imports() == {"typing": {"Iterable", "AsyncIterable"}}
assert compiler.async_iterator("str") == "AsyncIterator[str]"
assert compiler.iterable("str") == '"Iterable[str]"'
assert compiler.async_iterable("str") == '"AsyncIterable[str]"'
assert compiler.async_iterator("str") == '"AsyncIterator[str]"'
assert compiler.imports() == {
"typing": {"Iterable", "AsyncIterable", "AsyncIterator"}
"collections.abc": {"Iterable", "AsyncIterable", "AsyncIterator"}
}