Skip to content

Commit

Permalink
🐛 Do not reset a one-of group with a None value
Browse files Browse the repository at this point in the history
Resolves #171 #172
  • Loading branch information
eigenein committed Aug 16, 2024
1 parent 2bb35d3 commit 7382f58
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 6 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ build:
.PHONY: docs
docs:
poetry run mkdocs build --site-dir _site

.PHONY: docs/serve
docs/serve:
poetry run mkdocs serve
11 changes: 10 additions & 1 deletion docs/annotating_fields.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
12 changes: 9 additions & 3 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"

Expand Down
3 changes: 1 addition & 2 deletions pure_protobuf/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pure_protobuf/one_of.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 21 additions & 0 deletions tests/test_message.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 7382f58

Please sign in to comment.