Annotating fields¶
Field types are specified via Annotated type hints. Each field may include a Field annotation, otherwise it gets ignored by BaseMessage. For older Python versions one can use typing_extensions.Annotated.
pure_protobuf.annotations.Field
dataclass
¶
Annotates a Protocol Buffers field.
number
instance-attribute
¶
Specifies the field's number.
See also
- https://developers.google.com/protocol-buffers/docs/proto3#assigning_field_numbers
packed
instance-attribute
class-attribute
¶
Specifies whether the field should be packed in its serialized representation.
one_of
instance-attribute
class-attribute
¶
Specifies a one-of group for this field.
Supported types¶
Built-in types¶
| Type | .proto type |
Notes |
|---|---|---|
bool |
bool |
Encoded normally as int |
bytes, bytearray, memoryview, ByteString |
bytes |
Always deserialized as bytes |
float |
float |
32-bit floating-point number. Use the additional double type for 64-bit number |
int |
int32 int64 uint32 uint64 |
Variable-length integer. For negative values, two's compliments are used. See also the additional uint and ZigZagInt |
enum.IntEnum |
enum int32 int64 |
Supports subclasses of IntEnum (see enumerations) |
str |
string |
|
urllib.parse.ParseResult |
string |
Parsed URL, represented as a string |
Additional types¶
pure_protobuf.annotations module provides additional NewTypes to support different representations of the singular types:
pure_protobuf.annotations type |
.proto type |
Python value type | Notes |
|---|---|---|---|
double |
double |
float |
64-bit floating-point number |
fixed32 |
fixed32 |
int |
32-bit unsigned integer |
fixed64 |
fixed64 |
int |
64-bit unsigned integer |
sfixed32 |
sfixed32 |
int |
32-bit signed integer |
sfixed64 |
sfixed64 |
int |
64-bit signed integer |
uint |
uint32 uint64 |
int |
Unsigned variable-length integer |
ZigZagInt |
sint32 sint64 |
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 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. Optional annotation is accepted for type hinting, but has no functional meaning for BaseMessage.
Both traditional Optional[T] and modern Python 3.10+ union syntax 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)
Make sure to set defaults for non-required fields
pure-protobuf makes no assumptions on how a message class' __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:
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 IntEnum class are supported, their values are encoded as normal 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"
Self-referencing messages
Use typing.Self (or typing_extensions.Self in older Python) to reference
the message class itself:
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
Messages with circular dependencies are not supported
The following example does not work at the moment:
Tracking issue: #108.
Oneof¶
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"
ClassVaris needed here because this is a descriptor and not a real attribute.- Since the
foo_or_barreturns the value itself, we need an extra attribute for thewhich_one()getter.
Limitations
- When assigning a one-of member,
BaseMessageresets the other fields toNone, regardless of any defaults defined by, for example,dataclasses.field. - The
OneOfdescriptor simply iterates over its members in order to return an assignedOneofvalue, so it takes linear time. - It's impossible to set a value via a
OneOfdescriptor, one needs to assign the value to a specific attribute.
See Also
- https://developers.google.com/protocol-buffers/docs/proto3#oneof.