How to store records¶
When several values belong together, such as an x and a y, you can keep them
in one field as a record: a dataclass, a NamedTuple, a
fixed-length tuple, a TypedDict, an attrs class or a msgspec.Struct.
Each member can have any type a field could have on its own, so a text
member needs its capacity too.
1. Declare the record and the field¶
Define the record class as usual, then use it as a field's type:
from dataclasses import dataclass
from typing import Annotated, NamedTuple, NotRequired, TypedDict
from sharedbox import Capacity, SharedBox, field
@dataclass(frozen=True)
class Position:
x: float
y: float
class Limits(NamedTuple):
low: int
high: int
class Settings(TypedDict):
speed: float
note: NotRequired[Annotated[str, Capacity(16)]]
class Stage(SharedBox, name="example-stage"):
position: Position = Position(0.0, 0.0)
limits: Limits = Limits(0, 100)
settings: Settings = field(default_factory=lambda: {"speed": 1.0})
2. Write and read the whole record¶
When you assign a record, every member is written at once, so a reader sees all of the new values or none of them. Reading builds a new instance of the class:
stage = Stage()
stage.position = Position(1.5, -2.0)
print(stage.position) # Position(x=1.5, y=-2.0)
Your constructor runs on every read
Each read calls the class's constructor, so __post_init__, attrs
converters and validators run every time. Make sure running them a
second time changes nothing: a converter that rounds a float, for
example, must leave an already rounded float as it is, or a read won't
match what you wrote.
3. Change one member¶
A record you read from the box is a copy, so changing it changes nothing in the box. To change one member, read the record, make a new one with the change, and assign it back:
If you use a frozen dataclass or a NamedTuple, the copy is read-only, so
forgetting to assign it back gives you an error instead of a silent no-op.
4. Leave out a TypedDict key¶
A key marked NotRequired can be left out, and it is left out again when you
read the record back:
print(stage.settings) # {'speed': 1.0}
stage.settings = {"speed": 2.0, "note": "slow down"}
print(stage.settings["note"]) # slow down
Assigning a mapping with a key the TypedDict doesn't declare, or without a
required key, raises TypeError.
What a record cannot hold¶
A read has to rebuild the record from its stored members, so a record that
can't be rebuilt that way is refused with TypeError as soon as you define
the box class. That covers a dataclass or attrs field with init=False,
an InitVar without a default, and a generic record class. A record also
can't hold a reference field.
The whole script
"""The script of the guide "How to store records"."""
from dataclasses import dataclass
from typing import Annotated, NamedTuple, NotRequired, TypedDict
from sharedbox import Capacity, SharedBox, field
@dataclass(frozen=True)
class Position:
x: float
y: float
class Limits(NamedTuple):
low: int
high: int
class Settings(TypedDict):
speed: float
note: NotRequired[Annotated[str, Capacity(16)]]
class Stage(SharedBox, name="example-stage"):
position: Position = Position(0.0, 0.0)
limits: Limits = Limits(0, 100)
settings: Settings = field(default_factory=lambda: {"speed": 1.0})
stage = Stage()
stage.position = Position(1.5, -2.0)
print(stage.position) # Position(x=1.5, y=-2.0)
stage.limits = stage.limits._replace(high=50)
print(stage.limits) # Limits(low=0, high=50)
print(stage.settings) # {'speed': 1.0}
stage.settings = {"speed": 2.0, "note": "slow down"}
print(stage.settings["note"]) # slow down
stage.close()
Stage.unlink()