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.