Merge branch 'master_gh'
This commit is contained in:
commit
63458e2da0
23
CHANGELOG.md
23
CHANGELOG.md
@ -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)
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Better Protobuf / gRPC Support for Python
|
||||
|
||||

|
||||

|
||||
|
||||
> :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:
|
||||
|
@ -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"
|
||||
|
@ -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,27 +1174,26 @@ 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:
|
||||
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
|
||||
elif issubclass(t, Enum):
|
||||
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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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()}
|
||||
|
@ -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 %}
|
||||
|
@ -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; };
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user