From 7382f58688935a5987030b117131b709b11ba152 Mon Sep 17 00:00:00 2001 From: Pavel Perestoronin Date: Fri, 16 Aug 2024 16:34:21 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Do=20not=20reset=20a=20one-of=20?= =?UTF-8?q?group=20with=20a=20`None`=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #171 #172 --- Makefile | 4 ++++ docs/annotating_fields.md | 11 ++++++++++- mkdocs.yml | 12 +++++++++--- pure_protobuf/message.py | 3 +-- pure_protobuf/one_of.py | 6 ++++++ tests/test_message.py | 21 +++++++++++++++++++++ 6 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 tests/test_message.py diff --git a/Makefile b/Makefile index 27b8642..b67a61d 100644 --- a/Makefile +++ b/Makefile @@ -49,3 +49,7 @@ build: .PHONY: docs docs: poetry run mkdocs build --site-dir _site + +.PHONY: docs/serve +docs/serve: + poetry run mkdocs serve diff --git a/docs/annotating_fields.md b/docs/annotating_fields.md index 054a10c..3a5ce7f 100644 --- a/docs/annotating_fields.md +++ b/docs/annotating_fields.md @@ -1,6 +1,11 @@ # Annotating fields -Field types are specified via [`#!python Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated) [type hints](https://www.python.org/dev/peps/pep-0484/). Each field may include a `#!python pure_protobuf.annotations.Field` annotation, otherwise it gets ignored by `#!python BaseMessage`. For older Python versions one can use `#!python typing_extensions.Annotated`. +Field types are specified via [`#!python Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated) [type hints](https://www.python.org/dev/peps/pep-0484/). Each field may include a [`#!python Field`][pure_protobuf.annotations.Field] annotation, otherwise it gets ignored by `#!python BaseMessage`. For older Python versions one can use `#!python typing_extensions.Annotated`. + +::: pure_protobuf.annotations.Field + options: + show_root_heading: true + heading_level: 2 ## Supported types @@ -255,3 +260,7 @@ assert message.which_one() == "bar" - When assigning a one-of member, `#!python BaseMessage` resets the other fields to `#!python None`, **regardless** of any defaults defined by, for example, `#!python dataclasses.field`. - The `#!python OneOf` descriptor simply iterates over its members in order to return an assigned `Oneof` value, so it takes [linear time](https://en.wikipedia.org/wiki/Time_complexity#Linear_time). - It's impossible to set a value via a `OneOf` descriptor, one needs to assign the value to a specific attribute. + +::: pure_protobuf.one_of.OneOf + options: + heading_level: 3 diff --git a/mkdocs.yml b/mkdocs.yml index 164af52..7d504e4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,22 +21,28 @@ theme: - navigation.footer - navigation.indexes - navigation.instant + - navigation.instant.progress - navigation.sections # - navigation.tabs - navigation.top - navigation.tracking - search.suggest + - search.highlight palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: System theme - media: "(prefers-color-scheme: light)" scheme: default toggle: icon: material/brightness-7 - name: Switch to dark mode + name: Dark mode - media: "(prefers-color-scheme: dark)" scheme: slate toggle: icon: material/brightness-4 - name: Switch to light mode + name: Light mode plugins: - git-revision-date-localized: @@ -87,7 +93,7 @@ extra: provider: google property: G-CPNSYW2HX7 -copyright: Copyright © 2011-2023 Pavel Perestoronin +copyright: Copyright © 2011-2024 Pavel Perestoronin site_url: "https://eigenein.github.io/protobuf" diff --git a/pure_protobuf/message.py b/pure_protobuf/message.py index a333035..fc2f8b8 100644 --- a/pure_protobuf/message.py +++ b/pure_protobuf/message.py @@ -126,8 +126,7 @@ def dumps(self) -> bytes: def __setattr__(self, name: str, value: Any) -> None: # noqa: D105 super().__setattr__(name, value) descriptor = self.__PROTOBUF_FIELDS_BY_NAME__[name] - one_of = descriptor.one_of - if one_of is not None: + if (one_of := descriptor.one_of) is not None and value is not None: one_of._keep_attribute(self, descriptor.number) @classmethod diff --git a/pure_protobuf/one_of.py b/pure_protobuf/one_of.py index 9876d80..cff29a5 100644 --- a/pure_protobuf/one_of.py +++ b/pure_protobuf/one_of.py @@ -16,6 +16,12 @@ class OneOf(Generic[OneOfT]): __slots__ = ("_fields",) def __init__(self) -> None: + """ + Define a one-of group of fields. + + A [`Field`][pure_protobuf.annotations.Field] then should be assigned to the group + via the [`one_of`][pure_protobuf.annotations.Field.one_of] parameter. + """ self._fields: List[Tuple[int, str]] = [] def _add_field(self, number: int, name: str) -> None: diff --git a/tests/test_message.py b/tests/test_message.py new file mode 100644 index 0000000..9e0ab44 --- /dev/null +++ b/tests/test_message.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass +from typing import ClassVar, Optional + +from typing_extensions import Annotated + +from pure_protobuf.annotations import Field +from pure_protobuf.message import BaseMessage +from pure_protobuf.one_of import OneOf + + +def test_initialize_dataclass_with_one_of() -> None: + """Verify the fix for https://github.com/eigenein/protobuf/issues/171.""" + + @dataclass + class Message(BaseMessage): + payload: ClassVar[OneOf] = OneOf() + + foo: Annotated[Optional[int], Field(1, one_of=payload)] = None + bar: Annotated[Optional[bool], Field(2, one_of=payload)] = None + + assert Message(foo=3).foo == 3