Skip to content

How to set defaults and check values

If you know dataclasses, you already know how to set defaults and check values on a box. A SharedBox subclass takes its field values the way a dataclass does, and it understands the same tools: field, dataclasses.KW_ONLY, dataclasses.InitVar and __post_init__.

Give a field a default

A plain value after = is the default. If the default has to be worked out each time a box is created, such as the current time, use field(default_factory=...):

import time
from dataclasses import KW_ONLY, InitVar

from sharedbox import SharedBox, field, fields


class Pump(SharedBox, name="example-pump"):
    flow: float = 0.0
    started: float = field(default_factory=time.time)


with Pump() as pump:
    print(pump.flow, pump.started > 0)  # 0.0 True
Pump.unlink()

The factory's result is stored like any other value, so it has to be a type the field can hold.

Make fields keyword-only

Fields after a KW_ONLY annotation have to be passed by name, which keeps long constructor calls readable:

class Valve(SharedBox, name="example-valve"):
    opening: float
    _: KW_ONLY
    limit: float = 1.0
    closing_time: float = 0.5


with Valve(0.2, closing_time=2.0) as valve:
    print(valve)  # Valve(opening=0.2, limit=1.0, closing_time=2.0)
Valve.unlink()

To make every field of a class keyword-only, pass kw_only=True in the class statement instead, and for a single field use field(kw_only=True). Because keyword-only fields are passed by name, one with a default may even come before a positional field without one.

Check values when the box is created

Define __post_init__ and raise an exception when a value is wrong:

class Heater(SharedBox, name="example-heater"):
    target: float
    limit: float = 80.0

    def __post_init__(self) -> None:
        if self.target > self.limit:
            raise ValueError(f"target {self.target} is above the limit {self.limit}")


try:
    Heater(95.0)
except ValueError as error:
    print(error)  # target 95.0 is above the limit 80.0
with Heater(60.0) as heater:
    print(heater.target)  # 60.0
Heater.unlink()

__post_init__ runs after the values are written but before any other process can open the box. If it raises, the box is removed before any other process could open it, so nobody reads the wrong values, and the name is free for your next try straight away. The docstring of SharedBox has the details.

Later writes are not checked

__post_init__ runs once, when the box is created. It doesn't run when you assign a field later, or in a process that attaches the box. Check later values where you write them.

Take an argument that is not stored

Sometimes the constructor needs a value only to work out the fields, not to keep. Declare it as an InitVar: __post_init__ receives it, and the box doesn't store it:

class Scale(SharedBox, name="example-scale"):
    grams: float = 0.0
    kilograms: InitVar[float] = 0.0

    def __post_init__(self, kilograms: float) -> None:
        self.grams += kilograms * 1000


with Scale(kilograms=1.5) as scale:
    print(scale.snapshot())  # {'grams': 1500.0}
Scale.unlink()

Describe a field

You can attach notes to a field with metadata and doc. They stay in Python and are never stored in the box, and you read them back with fields:

class Stage(SharedBox, name="example-stage"):
    position: float = field(
        default=0.0, metadata={"unit": "mm"}, doc="Distance from home."
    )


print(fields(Stage)[0].metadata["unit"])  # mm
print(fields(Stage)[0].doc)  # Distance from home.
The whole script
"""The script of the guide "How to set defaults and check values"."""

import time
from dataclasses import KW_ONLY, InitVar

from sharedbox import SharedBox, field, fields


class Pump(SharedBox, name="example-pump"):
    flow: float = 0.0
    started: float = field(default_factory=time.time)


with Pump() as pump:
    print(pump.flow, pump.started > 0)  # 0.0 True
Pump.unlink()


class Valve(SharedBox, name="example-valve"):
    opening: float
    _: KW_ONLY
    limit: float = 1.0
    closing_time: float = 0.5


with Valve(0.2, closing_time=2.0) as valve:
    print(valve)  # Valve(opening=0.2, limit=1.0, closing_time=2.0)
Valve.unlink()


class Heater(SharedBox, name="example-heater"):
    target: float
    limit: float = 80.0

    def __post_init__(self) -> None:
        if self.target > self.limit:
            raise ValueError(f"target {self.target} is above the limit {self.limit}")


try:
    Heater(95.0)
except ValueError as error:
    print(error)  # target 95.0 is above the limit 80.0
with Heater(60.0) as heater:
    print(heater.target)  # 60.0
Heater.unlink()


class Scale(SharedBox, name="example-scale"):
    grams: float = 0.0
    kilograms: InitVar[float] = 0.0

    def __post_init__(self, kilograms: float) -> None:
        self.grams += kilograms * 1000


with Scale(kilograms=1.5) as scale:
    print(scale.snapshot())  # {'grams': 1500.0}
Scale.unlink()


class Stage(SharedBox, name="example-stage"):
    position: float = field(
        default=0.0, metadata={"unit": "mm"}, doc="Distance from home."
    )


print(fields(Stage)[0].metadata["unit"])  # mm
print(fields(Stage)[0].doc)  # Distance from home.