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`.
|
- 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
|
## [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)
|
- **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
|
# 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/).
|
> :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:
|
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]
|
[tool.poetry]
|
||||||
name = "betterproto"
|
name = "betterproto"
|
||||||
version = "2.0.0b6"
|
version = "2.0.0b7"
|
||||||
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"
|
||||||
|
@ -62,6 +62,13 @@ if TYPE_CHECKING:
|
|||||||
SupportsWrite,
|
SupportsWrite,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
from types import UnionType as _types_UnionType
|
||||||
|
else:
|
||||||
|
|
||||||
|
class _types_UnionType:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
# Proto 3 data types
|
# Proto 3 data types
|
||||||
TYPE_ENUM = "enum"
|
TYPE_ENUM = "enum"
|
||||||
@ -148,6 +155,7 @@ def datetime_default_gen() -> datetime:
|
|||||||
|
|
||||||
DATETIME_ZERO = datetime_default_gen()
|
DATETIME_ZERO = datetime_default_gen()
|
||||||
|
|
||||||
|
|
||||||
# Special protobuf json doubles
|
# Special protobuf json doubles
|
||||||
INFINITY = "Infinity"
|
INFINITY = "Infinity"
|
||||||
NEG_INFINITY = "-Infinity"
|
NEG_INFINITY = "-Infinity"
|
||||||
@ -1166,30 +1174,29 @@ class Message(ABC):
|
|||||||
def _get_field_default_gen(cls, field: dataclasses.Field) -> Any:
|
def _get_field_default_gen(cls, field: dataclasses.Field) -> Any:
|
||||||
t = cls._type_hint(field.name)
|
t = cls._type_hint(field.name)
|
||||||
|
|
||||||
if hasattr(t, "__origin__"):
|
is_310_union = isinstance(t, _types_UnionType)
|
||||||
if t.__origin__ is dict:
|
if hasattr(t, "__origin__") or is_310_union:
|
||||||
# This is some kind of map (dict in Python).
|
if is_310_union or t.__origin__ is Union:
|
||||||
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):
|
|
||||||
# This is an optional field (either wrapped, or using proto3
|
# This is an optional field (either wrapped, or using proto3
|
||||||
# field presence). For setting the default we really don't care
|
# field presence). For setting the default we really don't care
|
||||||
# what kind of field it is.
|
# what kind of field it is.
|
||||||
return type(None)
|
return type(None)
|
||||||
else:
|
if t.__origin__ is list:
|
||||||
return t
|
# This is some kind of list (repeated) field.
|
||||||
elif issubclass(t, Enum):
|
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.
|
# Enums always default to zero.
|
||||||
return t.try_value
|
return t.try_value
|
||||||
elif t is datetime:
|
if t is datetime:
|
||||||
# Offsets are relative to 1970-01-01T00:00:00Z
|
# Offsets are relative to 1970-01-01T00:00:00Z
|
||||||
return datetime_default_gen
|
return datetime_default_gen
|
||||||
else:
|
# This is either a primitive scalar or another message type. Calling
|
||||||
# This is either a primitive scalar or another message type. Calling
|
# it should result in its zero value.
|
||||||
# it should result in its zero value.
|
return t
|
||||||
return t
|
|
||||||
|
|
||||||
def _postprocess_single(
|
def _postprocess_single(
|
||||||
self, wire_type: int, meta: FieldMetadata, field_name: str, value: Any
|
self, wire_type: int, meta: FieldMetadata, field_name: str, value: Any
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import (
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
Dict,
|
Dict,
|
||||||
List,
|
List,
|
||||||
Set,
|
Set,
|
||||||
@ -13,6 +16,9 @@ from ..lib.google import protobuf as google_protobuf
|
|||||||
from .naming import pythonize_class_name
|
from .naming import pythonize_class_name
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..plugin.typing_compiler import TypingCompiler
|
||||||
|
|
||||||
WRAPPER_TYPES: Dict[str, Type] = {
|
WRAPPER_TYPES: Dict[str, Type] = {
|
||||||
".google.protobuf.DoubleValue": google_protobuf.DoubleValue,
|
".google.protobuf.DoubleValue": google_protobuf.DoubleValue,
|
||||||
".google.protobuf.FloatValue": google_protobuf.FloatValue,
|
".google.protobuf.FloatValue": google_protobuf.FloatValue,
|
||||||
@ -47,7 +53,7 @@ def get_type_reference(
|
|||||||
package: str,
|
package: str,
|
||||||
imports: set,
|
imports: set,
|
||||||
source_type: str,
|
source_type: str,
|
||||||
typing_compiler: "TypingCompiler",
|
typing_compiler: TypingCompiler,
|
||||||
unwrap: bool = True,
|
unwrap: bool = True,
|
||||||
pydantic: bool = False,
|
pydantic: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
@ -275,8 +275,21 @@ class OutputTemplate:
|
|||||||
@property
|
@property
|
||||||
def python_module_imports(self) -> Set[str]:
|
def python_module_imports(self) -> Set[str]:
|
||||||
imports = set()
|
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)):
|
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")
|
imports.add("warnings")
|
||||||
|
|
||||||
if self.builtins_import:
|
if self.builtins_import:
|
||||||
imports.add("builtins")
|
imports.add("builtins")
|
||||||
return imports
|
return imports
|
||||||
|
@ -139,29 +139,35 @@ class TypingImportTypingCompiler(TypingCompiler):
|
|||||||
class NoTyping310TypingCompiler(TypingCompiler):
|
class NoTyping310TypingCompiler(TypingCompiler):
|
||||||
_imports: Dict[str, Set[str]] = field(default_factory=lambda: defaultdict(set))
|
_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:
|
def optional(self, type: str) -> str:
|
||||||
return f"{type} | None"
|
return f'"{self._fmt(type)} | None"'
|
||||||
|
|
||||||
def list(self, type: str) -> str:
|
def list(self, type: str) -> str:
|
||||||
return f"list[{type}]"
|
return f'"list[{self._fmt(type)}]"'
|
||||||
|
|
||||||
def dict(self, key: str, value: str) -> str:
|
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:
|
def union(self, *types: str) -> str:
|
||||||
return " | ".join(types)
|
return f'"{" | ".join(map(self._fmt, types))}"'
|
||||||
|
|
||||||
def iterable(self, type: str) -> str:
|
def iterable(self, type: str) -> str:
|
||||||
self._imports["typing"].add("Iterable")
|
self._imports["collections.abc"].add("Iterable")
|
||||||
return f"Iterable[{type}]"
|
return f'"Iterable[{type}]"'
|
||||||
|
|
||||||
def async_iterable(self, type: str) -> str:
|
def async_iterable(self, type: str) -> str:
|
||||||
self._imports["typing"].add("AsyncIterable")
|
self._imports["collections.abc"].add("AsyncIterable")
|
||||||
return f"AsyncIterable[{type}]"
|
return f'"AsyncIterable[{type}]"'
|
||||||
|
|
||||||
def async_iterator(self, type: str) -> str:
|
def async_iterator(self, type: str) -> str:
|
||||||
self._imports["typing"].add("AsyncIterator")
|
self._imports["collections.abc"].add("AsyncIterator")
|
||||||
return f"AsyncIterator[{type}]"
|
return f'"AsyncIterator[{type}]"'
|
||||||
|
|
||||||
def imports(self) -> Dict[str, Optional[Set[str]]]:
|
def imports(self) -> Dict[str, Optional[Set[str]]]:
|
||||||
return {k: v if v else None for k, v in self._imports.items()}
|
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 %}
|
{% if method.comment %}
|
||||||
{{ method.comment }}
|
{{ method.comment }}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% if method.proto_obj.options.deprecated %}
|
||||||
|
warnings.warn("{{ service.py_name }}.{{ method.py_name }} is deprecated", DeprecationWarning)
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if method.server_streaming %}
|
{% if method.server_streaming %}
|
||||||
{% if method.client_streaming %}
|
{% if method.client_streaming %}
|
||||||
|
@ -12,3 +12,10 @@ message Message {
|
|||||||
option deprecated = true;
|
option deprecated = true;
|
||||||
string value = 1;
|
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
|
import pytest
|
||||||
|
|
||||||
|
from tests.mocks import MockChannel
|
||||||
from tests.output_betterproto.deprecated import (
|
from tests.output_betterproto.deprecated import (
|
||||||
|
Empty,
|
||||||
Message,
|
Message,
|
||||||
Test,
|
Test,
|
||||||
|
TestServiceStub,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -43,3 +46,19 @@ def test_message_with_deprecated_field_not_set_default(message):
|
|||||||
_ = Test(value=10).message
|
_ = Test(value=10).message
|
||||||
|
|
||||||
assert not record
|
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():
|
def test_no_typing_311_typing_compiler():
|
||||||
compiler = NoTyping310TypingCompiler()
|
compiler = NoTyping310TypingCompiler()
|
||||||
assert compiler.imports() == {}
|
assert compiler.imports() == {}
|
||||||
assert compiler.optional("str") == "str | None"
|
assert compiler.optional("str") == '"str | None"'
|
||||||
assert compiler.imports() == {}
|
assert compiler.imports() == {}
|
||||||
assert compiler.list("str") == "list[str]"
|
assert compiler.list("str") == '"list[str]"'
|
||||||
assert compiler.imports() == {}
|
assert compiler.imports() == {}
|
||||||
assert compiler.dict("str", "int") == "dict[str, int]"
|
assert compiler.dict("str", "int") == '"dict[str, int]"'
|
||||||
assert compiler.imports() == {}
|
assert compiler.imports() == {}
|
||||||
assert compiler.union("str", "int") == "str | int"
|
assert compiler.union("str", "int") == '"str | int"'
|
||||||
assert compiler.imports() == {}
|
assert compiler.imports() == {}
|
||||||
assert compiler.iterable("str") == "Iterable[str]"
|
assert compiler.iterable("str") == '"Iterable[str]"'
|
||||||
assert compiler.imports() == {"typing": {"Iterable"}}
|
assert compiler.async_iterable("str") == '"AsyncIterable[str]"'
|
||||||
assert compiler.async_iterable("str") == "AsyncIterable[str]"
|
assert compiler.async_iterator("str") == '"AsyncIterator[str]"'
|
||||||
assert compiler.imports() == {"typing": {"Iterable", "AsyncIterable"}}
|
|
||||||
assert compiler.async_iterator("str") == "AsyncIterator[str]"
|
|
||||||
assert compiler.imports() == {
|
assert compiler.imports() == {
|
||||||
"typing": {"Iterable", "AsyncIterable", "AsyncIterator"}
|
"collections.abc": {"Iterable", "AsyncIterable", "AsyncIterator"}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user