Skip to content

Latest commit

 

History

History
268 lines (180 loc) · 15.5 KB

File metadata and controls

268 lines (180 loc) · 15.5 KB

Annotating fields

Field types are specified via #!python Annotated type hints. 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

Built-in types

Type .proto type Notes
#!python bool #!protobuf bool Encoded normally as #!python int
#!python bytes, #!python bytearray, #!python memoryview, #!python ByteString #!protobuf bytes Always deserialized as #!python bytes
#!python float #!protobuf float 32-bit floating-point number. Use the additional #!python double type for 64-bit number
#!python int #!protobuf int32 #!protobuf int64 #!protobuf uint32 #!protobuf uint64 Variable-length integer. For negative values, two's compliments are used. See also the additional #!python uint and #!python ZigZagInt
#!python enum.IntEnum #!protobuf enum #!protobuf int32 #!protobuf int64 Supports subclasses of #!python IntEnum (see enumerations)
#!python str #!protobuf string
#!python urllib.parse.ParseResult #!protobuf string Parsed URL, represented as a string

Additional types

#!python pure_protobuf.annotations module provides additional #!python NewTypes to support different representations of the singular types:

#!python pure_protobuf.annotations type .proto type Python value type Notes
#!python double #!protobuf double #!python float 64-bit floating-point number
#!python fixed32 #!protobuf fixed32 #!python int 32-bit unsigned integer
#!python fixed64 #!protobuf fixed64 #!python int 64-bit unsigned integer
#!python sfixed32 #!protobuf sfixed32 #!python int 32-bit signed integer
#!python sfixed64 #!protobuf sfixed64 #!python int 64-bit signed integer
#!python uint #!protobuf uint32 #!protobuf uint64 #!python int Unsigned variable-length integer
#!python ZigZagInt #!protobuf sint32 #!protobuf sint64 #!python int ZigZag-encoded integer

Repeated fields

typing.List, typing.Iterable, and collections.abc.Iterable annotations are automatically converted to repeated fields. Repeated fields of scalar numeric types use packed encoding by default:

from dataclasses import dataclass, field
from typing import List
from typing_extensions import Annotated

from pure_protobuf.annotations import Field
from pure_protobuf.message import BaseMessage


@dataclass
class Message(BaseMessage):
    foo: Annotated[List[int], Field(1)] = field(default_factory=list)


assert bytes(Message(foo=[1, 2])) == b"\x0A\x02\x01\x02"

In case, unpacked encoding is explicitly wanted, you can specify #!python packed=False:

from dataclasses import dataclass, field
from typing import List
from typing_extensions import Annotated

from pure_protobuf.annotations import Field
from pure_protobuf.message import BaseMessage


@dataclass
class Message(BaseMessage):
    foo: Annotated[List[int], Field(1, packed=False)] = field(
        default_factory=list,
    )


assert bytes(Message(foo=[1, 2])) == b"\x08\x01\x08\x02"

Required fields

Required fields are deprecated in proto2 and not supported in proto3, thus in pure-protobuf fields are always optional. #!python Optional annotation is accepted for type hinting, but has no functional meaning for #!python BaseMessage.

Both traditional #!python Optional[T] and modern Python 3.10+ union syntax #!python T | None are supported and work identically.

Default values

In pure-protobuf it's developer's responsibility to take care of default values. If encoded message does not contain a particular element, the corresponding field stays unprovided:

from dataclasses import dataclass
from io import BytesIO
from typing import Optional
from typing_extensions import Annotated

from pure_protobuf.annotations import Field
from pure_protobuf.message import BaseMessage


@dataclass
class Foo(BaseMessage):
    bar: Annotated[int, Field(1)] = 42
    qux: Annotated[Optional[int], Field(2)] = None


assert bytes(Foo()) == b"\x08\x2A"
assert Foo.read_from(BytesIO()) == Foo(bar=42)

!!! warning "Make sure to set defaults for non-required fields"

`pure-protobuf` makes no assumptions on how a message class' `#!python __init__()` handles missing keyword arguments.
So, if you expect a field to be optional, you **must** specify a default value explicitly –
just as you normally do with **pydantic** or **dataclasses**.

Otherwise, a missing record would cause a missing argument error:

```python title="test_missing_default.py" hl_lines="17-18"
from dataclasses import dataclass
from io import BytesIO
from typing import Optional

from pytest import raises
from typing_extensions import Annotated

from pure_protobuf.annotations import Field
from pure_protobuf.message import BaseMessage


@dataclass
class Foo(BaseMessage):
    foo: Annotated[Optional[int], Field(1)]


with raises(TypeError):
    Foo.read_from(BytesIO())
```

Enumerations

Subclasses of the standard #!python IntEnum class are supported, their values are encoded as normal #!python int-s:

from dataclasses import dataclass
from enum import IntEnum
from io import BytesIO
from typing_extensions import Annotated

from pure_protobuf.annotations import Field
from pure_protobuf.message import BaseMessage


class TestEnum(IntEnum):
    BAR = 1


@dataclass
class Test(BaseMessage):
    foo: Annotated[TestEnum, Field(1)]


assert bytes(Test(foo=TestEnum.BAR)) == b"\x08\x01"
assert Test.read_from(BytesIO(b"\x08\x01")) == Test(foo=TestEnum.BAR)

Embedded messages

from dataclasses import dataclass, field
from typing_extensions import Annotated

from pure_protobuf.annotations import Field
from pure_protobuf.message import BaseMessage


@dataclass
class Test1(BaseMessage):
    a: Annotated[int, Field(1)] = 0


@dataclass
class Test3(BaseMessage):
    c: Annotated[Test1, Field(3)] = field(default_factory=Test1)


assert bytes(Test3(c=Test1(a=150))) == b"\x1A\x03\x08\x96\x01"

!!! tip "Self-referencing messages"

Use `#!python typing.Self` (or `#!python typing_extensions.Self` in older Python) to reference
the message class itself:

```python title="test_self.py" hl_lines="13"
from dataclasses import dataclass    
from typing import Optional

from typing_extensions import Annotated, Self

from pure_protobuf.annotations import Field
from pure_protobuf.message import BaseMessage


@dataclass
class RecursiveMessage(BaseMessage):
    payload: Annotated[int, Field(1)]
    inner: Annotated[Optional[Self], Field(2)] = None
```

!!! warning "Messages with circular dependencies are not supported"

The following example does not work at the moment:

```python
class A(BaseMessage):
    b: Annotated[B, ...]

class B(BaseMessage):
    a: Annotated[A, ...]
```

Tracking issue: [#108](https://github.com/eigenein/protobuf/issues/108).
from typing import ClassVar, Optional

from pydantic import BaseModel
from pure_protobuf.annotations import Field
from pure_protobuf.message import BaseMessage
from pure_protobuf.one_of import OneOf
from typing_extensions import Annotated


class Message(BaseMessage, BaseModel):
    foo_or_bar: ClassVar[OneOf] = OneOf()  # (1)
    which_one = foo_or_bar.which_one_of_getter()  # (2)

    foo: Annotated[Optional[int], Field(1, one_of=foo_or_bar)] = None
    bar: Annotated[Optional[int], Field(2, one_of=foo_or_bar)] = None


message = Message()
message.foo = 42
message.bar = 43

assert message.foo_or_bar == 43
assert message.foo is None
assert message.bar == 43
assert message.which_one() == "bar"
  1. #!python ClassVar is needed here because this is a descriptor and not a real attribute.
  2. Since the #!python foo_or_bar returns the value itself, we need an extra attribute for the #!python which_one() getter.

!!! warning "Limitations"

- 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