Boxes¶
| Name | What it does |
|---|---|
SharedBox |
a record whose annotated fields live in a named shared-memory segment |
field |
sets the options of one field of a SharedBox class, as dataclasses.field does |
fields |
returns one Field per field of a SharedBox subclass or box, in declaration order |
Field |
options of one field of a SharedBox class, as field takes them |
Capacity |
maximum size of a field: bytes for text and bytes, elements for a collection. See capacity |
SharedBox
¶
A record whose annotated fields live in a named shared-memory segment.
Subclass it and annotate the fields. Calling the subclass with the
field values, as with a dataclass, creates the segment under the
class's name and writes the values;
create does the same under another
name, so one class can describe several boxes, and
attach opens an existing box from any
thread or process. Every process that opens the segment reads and
writes the same values.
A field is a public annotation with one of these types:
| Annotation | Stored as |
|---|---|
bool, int, float, complex |
fixed-size numbers; a float field takes an int |
Annotated[str, Capacity(n)], bytes, bytearray, Decimal |
at most n bytes |
date, time, datetime, timedelta, UUID |
fixed-size values; times naive or with a fixed offset |
an Enum, Flag or Literal |
the member's position, the bits, or the value's position |
an optional X (Optional[X]), a union |
the member the value's type picks |
a dataclass, NamedTuple, tuple, TypedDict, attrs class or msgspec.Struct |
its members |
Annotated[list[T], Capacity(n)], and set, frozenset, dict, tuple[T, ...] |
at most n elements |
Annotated[<array type>, Shape(...), DType(...)] |
the array's elements |
A SharedBox subclass, optional or not, makes a reference field,
described below. Capacity sets n. A read
returns a new value: changing a list, record or array read from a box
changes only that copy. Names starting with _ and ClassVar
annotations are not fields. Fields of a base class come first.
Fields are positional by default, in declaration order, as in a
dataclass. Fields declared after a dataclasses.KW_ONLY annotation in
the same class are keyword-only. A default is a plain class attribute
or given with field, and is checked against the
field's type and capacity when the class is defined.
inspect.signature of the class gives its constructor's parameters; a
default_factory default shows as <factory>.
Assigning a value of the wrong type raises TypeError, and a value
that does not fit its field raises ValueError, or OverflowError
for an int value out of range for its int, float or complex
field; either way the stored value does not change. A bytes field
takes bytes, bytearray and memoryview values. Assigning to a
name that is not a field raises AttributeError, and so does deleting
a field. A box has no __dict__: every subclass gets empty
__slots__ unless it declares its own. Two boxes are equal only if
they are the same object.
A dataclasses.InitVar[T] annotation declares a constructor argument
that is not stored. It takes a position like a field and may have a
default, as a class attribute or with field(default=...), but no
default_factory and no init=False. It is not in the layout, the
schema hash, fields,
snapshot or
events. An InitVar named like an
inherited field removes that field from the subclass, as in
dataclasses; reading or assigning that name on a box raises
AttributeError.
__post_init__(self, *initvars) runs when the class is called and in
create, after the values are written,
with each InitVar value in declaration order. It may read and assign
fields, reference fields included, and its writes go into the segment.
It does not run for attach or
unpickling. The box is published when __post_init__ returns, so no
other process sees it before then. Until then its name is taken:
creating another box under it raises
SegmentExistsError, and
attach, from any process or from
__post_init__ itself, waits at most 1 s (the lock timeout, if
shorter) and then raises
SegmentNotFoundError.
__sharedbox_box__ raises
BufferError until then too. If __post_init__ raises, the box is
closed and its name removed without ever being published, and the
exception propagates.
The class statement takes these keywords:
name: the segment name of boxes made by calling the class. By default it is 16 hex digits of SHA-256 over the class's identity, so every process that imports the class uses the same name. It must match[A-Za-z0-9_.-]{1,128}.kw_only: make every field this class declares keyword-only. Inherited fields keep the setting of the class that declares them.lock_timeout: seconds a read or write waits for a write in progress before it raisesLockTimeoutError; 5.0 by default, finite and in(0, 86400].identity: a non-empty string, by default the class'smodule.qualname, with__mp_main__read as__main__. It enters the schema hash, and names the box whennameis not given. A subclass does not inherit it. Two classes with the same identity and fields share a box, even when they live in different modules; a process whose class has another identity cannot attach it.max_waiters: how many waiter slots the box has, 1 to 4096, 64 by default. Each box handle whosewatchoreventsis in use holds one slot, counted across every process. A watcher that finds every slot taken logs a warning to thesharedboxlogger and checks for changes once a second until a slot is free.
A field annotated with a SharedBox subclass X, or with X | None
(or Optional[X]), refers to another box, which keeps its own
segment, lock and lifetime. The field stores the box's name, schema
hash and create id. X is the class being defined or a class defined
before it. A field X is never empty: assigning None raises
TypeError, and reading it returns a box. A field X | None may hold
None. The two give different schema hashes, so a class that declares
one cannot attach a box created by a class that declares the other.
Defaults work as for any field: = None, field(default=box) and
field(default_factory=...). A default box is checked like an
assigned one when the class is defined, a factory's result at each
creation. The class keeps the handle given as field(default=box) for
as long as the class exists; once that handle is closed, each creation
that uses the default raises
BoxClosedError.
Assigning a box to a reference field stores which box it is, under the
outer box's lock. A box of X, of a subclass of X, or of any class
with X's schema hash (the same identity and fields) is accepted; any
other value raises TypeError, and a closed box raises
BoxClosedError. The constructor and
update check reference values the same
way. Type checkers accept a box of X or of a subclass; a box of
another class with X's schema hash needs a cast.
Reading a reference field attaches the box with its own class, which
may be a subclass of X, and keeps that handle: later reads return
the same object while the field refers to the same box. The class is
found by the stored schema hash among the SharedBox classes this
process has defined; of several with one hash, the first one defined
that is still alive is used. After another thread or process assigns
another box, the next read attaches that one and closes the handle it
kept. close on the outer box closes the
handles its reads attached, and closing a returned box makes the next
read attach it again. A read raises
UnknownBoxClassError when this
process has not imported the module that defines the box's class, and
BrokenReferenceError when the box
was removed, or removed and created again under the same name (also by
another class), since it was assigned. A handle that already read the
field keeps its own mapping of the box and goes on returning it. The
outer box only points at the other box: closing or unlinking the outer
box leaves it alone, and no write covers both boxes at once.
A box can be pickled, and copy.copy and copy.deepcopy do the same
as a pickle round trip. The pickle holds the class, the segment name,
the schema hash and the box's create id, a random number drawn when
the box was created. Unpickling attaches a new handle: an independent
box on the same data, which the receiving process closes. It reads
reference fields from the segment like any other handle. A box pickled
inside __post_init__ can be unpickled only once it is published. A
child process started with fork uses the parent's box object, which
keeps working after the fork.
Raises:
| Type | Description |
|---|---|
TypeError
|
When the class is defined, for an annotation of another type, no
fields or more than 256, a field named like a |
ValueError
|
When the class is defined, for a |
OverflowError
|
When the class is defined or called, or a field is assigned, with
an |
SegmentExistsError
|
When the class is called and the name is taken. |
SchemaMismatchError
|
When a pickled box is loaded by a process whose class has different fields, or the box under that name was unlinked and created again since the pickle was made. |
SegmentNotFoundError
|
When a pickled box is loaded after its segment is gone. |
Notes
On Windows a lock wait ends on a timer tick, so lock_timeout can
expire up to one tick late, 15.6 ms at the default timer resolution.
The handle a class keeps for a field(default=box) default keeps that
box's segment in existence on Windows, and so does the mapping a
handle keeps of a box it read through a reference field: other handles
can still attach it.
events
property
¶
One psygnal signal per field, emitted as (new, old) when any thread or process changes it.
A BoxEvents group:
box.events.<field>.connect(cb) listens to one field and
box.events.connect(cb) to all of them.
Unlike the callbacks of a local evented dataclass, these run on the
box's watcher thread; connect with thread="main" and call
psygnal.emit_queued() to run them on the main thread instead.
Closing the box delivers writes the watcher thread had not seen
yet, so callbacks may run once on the thread that calls
close. A box that is garbage
collected drops them.
If several writes happen between two checks by the watcher thread,
only one emission happens, with the latest value; old is the
value from the last emission. A write that leaves the value
unchanged emits nothing. For the first emission of a field, old
is the value the field held when events was first accessed.
A callback that raises is logged to the sharedbox logger.
Callbacks connected before it on the same signal already ran;
callbacks connected after it do not run for that write. Other
fields still emit. The watcher thread also serves this
box's watch iterators, so a slow
callback delays them. A callback that refers to the box, such as a
lambda that reads a field, keeps the box alive until
close.
A field named like an attribute of psygnal's SignalGroup
(connect, disconnect, all, signals, block and others)
makes psygnal warn when the class is defined, and
box.events.<name> then returns that attribute;
box.events["<name>"] returns the field's signal.
psygnal's blocked, paused, throttled, debounced and
psygnal.qt.start_emitting_from_queue work on these signals. A
signal that is blocked drops its emissions in this process, and
changes made while it is blocked are not emitted later.
A reference field's signal is emitted when the field is assigned
another box or emptied, with a BoxRef or None
as new and old. Changes inside the box it refers to are emitted
by that box's own events, and by this group once
follow forwards them.
A child process created with fork while a callback is running
inherits that signal's lock as held, and hangs the first time it
connects to, disconnects from or emits that signal.
unlink
class-attribute
instance-attribute
¶
Remove the segment's name, so no later process can attach to it.
Motor.unlink(name=None) on the class removes name, by default the
class's name; box.unlink() on a box removes the name of its segment.
Boxes already open keep working. On Linux, later
attach calls fail. On Windows this does
nothing; the segment goes away with its last handle.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
SegmentNotFoundError
|
On Linux, if no segment has that name. |
create
classmethod
¶
Create a box under name instead of the class's name.
Takes the field values and runs __post_init__ as calling the
class does.
Raises:
| Type | Description |
|---|---|
SegmentExistsError
|
If the name is taken. |
ValueError
|
If |
attach
classmethod
¶
Open the box called name, by default the one named after this class.
__post_init__ does not run.
Raises:
| Type | Description |
|---|---|
SegmentNotFoundError
|
If no segment has that name, or the shared memory under it does not become a box within 1 s (the lock timeout, if shorter). |
SchemaMismatchError
|
If the segment was created by a different class or a different version of this class, uses another major version of the segment layout, or has a field of a kind this version cannot read. |
ValueError
|
If |
update
¶
Write several fields under one lock; a reader sees all of the new values or none.
Every value is checked before any is written, so an update that raises changes nothing. A reference field takes a box or None, checked as when it is assigned.
Raises:
| Type | Description |
|---|---|
TypeError
|
If a name is not a field or a value has the wrong type. |
ValueError
|
If a |
snapshot
¶
Return every field's value, with this box read at one point in time.
A reference field gives a BoxRef, or None when
it is empty.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
follow
|
bool
|
Replace each reference with the snapshot of the box it refers
to, itself taken with |
False
|
Raises:
| Type | Description |
|---|---|
BrokenReferenceError
|
With |
UnknownBoxClassError
|
With |
read_into
¶
Copy an array field into out and return out.
The copy holds one complete write, as a read does, but goes into memory the caller already has, so nothing is allocated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
out
|
A
|
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the field is not an array field, or |
ValueError
|
If the box has no such field, or |
LockTimeoutError
|
If another writer holds the lock longer than the lock timeout. |
BoxClosedError
|
If the box is closed. |
writing
¶
Hold the box's write lock and yield the array field to fill in place.
The block gets an array of the field's declared type that views the
field in shared memory and holds its current value. Leaving the
block releases the lock, counts the write and wakes watchers, also
when the block raises, since the bytes have already changed.
While the block runs, reads and writes of the box in every process
wait for the lock, so keep it short. Writing a field of this box
inside the block waits on that same lock and raises
LockTimeoutError. The array keeps
its own mapping, so using it after the box is closed cannot crash,
but writing to it after the block takes no lock and tells no one.
Raises:
| Type | Description |
|---|---|
TypeError
|
If the field is not an array field. |
ValueError
|
If the box has no such field. |
LockTimeoutError
|
If another writer holds the lock longer than the lock timeout. |
BoxClosedError
|
If the box is closed. |
watch
¶
Return a FieldWatch over the values written to field from now on.
A reference field yields a BoxRef or None.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the class has no field |
force_unlock
¶
Release a write lock left behind by a process that died while writing.
Reads and writes that wait on such a lock raise
LockTimeoutError, whose message
names the process that holds it. This does not check whether the
process that held the lock is still running: releasing the lock of
a writer that is still running lets other reads and writes run
while its write is half done.
__sharedbox_box__
¶
Return a "sharedbox_box" capsule holding a handle with its own mapping of the segment.
Python code does not call it; an extension that accepts a box does.
The capsule holds a pointer to an sbx_handle, declared in
sharedbox/sharedbox_c.h. The handle's mapping is made from the
box's OS handle, so closing or unlinking the box does not affect
it. A consumer takes the handle with
sharedbox::handle::from_capsule (C++) or sbx_import (C),
renames the capsule "used_sharedbox_box", compares the box's
schema hash with the one it expects, and destroys (C++) or releases
(C) the handle itself. A capsule that is never taken releases the
handle when it is garbage collected. Releasing touches no Python
objects and does not need the GIL, so it may run on any thread and
after interpreter shutdown.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_version
|
tuple[int, int] | None
|
The |
None
|
Raises:
| Type | Description |
|---|---|
BufferError
|
If the major version of |
NotImplementedError
|
If any other keyword is given. |
Notes
On Windows the segment stays in existence while a handle is held.
close
¶
Detach from the segment; other boxes on it keep working and the data stays.
Use unlink to remove it. Also
stops this box's watcher thread, closes every box this handle
attached to read a reference field, and stops all forwarding
started through its events. Later
reads and writes raise
BoxClosedError. Calling it again does
nothing. Leaving a with block calls it.
Called on a box's watcher thread, from an event callback, it does not wait for the watcher threads of the boxes it closes, and drops the writes those threads had not seen yet.
A box that is garbage collected is closed. Forwarding it started
keeps running until its events group and every group
follow returned are garbage
collected too, which happens only when the cycle collector runs.
field
¶
field(*, default: Any = MISSING, default_factory: Callable[[], Any] | Any = MISSING, init: bool = True, repr: bool = True, kw_only: bool | Any = MISSING, metadata: Mapping[Any, Any] | None = None, doc: str | None = None) -> Any
Set the options of one field of a SharedBox class, as dataclasses.field does.
A plain class attribute is still the field's default. There is no
compare or hash option: two boxes are equal only if they are the
same object. A field with neither default nor default_factory is
required. A default is checked against the field's type and capacity
when the class is defined, and a factory's result when a box is
created; both raise what assigning the value raises. Values are copied
into the segment, and only the stored types exist: no lists, dicts or
other objects. Capacity goes in the
annotation, not here.
A positional field without a default cannot follow one with a default;
a field with default or default_factory counts as having one, and
init=False fields take no position and are not counted.
Whether a field is keyword-only is fixed by the class that declares
it, through its kw_only class keyword, a dataclasses.KW_ONLY among
its own annotations, or kw_only here. A subclass keeps each
inherited field's setting: its own kw_only=True affects only its own
fields, and the fields of a kw_only base stay keyword-only in a
subclass without the keyword.
A subclass that declares an inherited field again, with an annotation,
gives it default options, as in dataclasses. An annotation without a
value keeps only a plain inherited default: the other options go
back to their defaults, and a field whose base gave it a
default_factory becomes required. The same holds for an InitVar
declared over an inherited field.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
default_factory
|
Callable[[], Any] | Any
|
Called once per creation that is given no value for the field. The result is stored like any other value, so the box never keeps the object the factory returned. |
MISSING
|
init
|
bool
|
False leaves the field out of the constructor; the field then
needs |
True
|
repr
|
bool
|
False leaves the field out of |
True
|
kw_only
|
bool | Any
|
Make this field keyword-only or not, whatever the |
MISSING
|
metadata
|
Mapping[Any, Any] | None
|
Kept in Python as a read-only mapping, never stored in the segment. |
None
|
doc
|
str | None
|
Kept in Python, never stored in the segment. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If both |
TypeError
|
When the class is defined, for |
fields
¶
Field
dataclass
¶
Field(name: str, type: Any, default: Any, default_factory: Any, init: bool, repr: bool, kw_only: Any, metadata: Mapping[Any, Any], doc: str | None)
Options of one field of a SharedBox class, as field takes them.
fields returns them. A Field is read-only, and
two are equal only if they are the same object, as with
dataclasses.Field.
default_factory
instance-attribute
¶
A function of no arguments, or dataclasses.MISSING.
kw_only
instance-attribute
¶
True if the constructor takes the value by keyword only; dataclasses.MISSING until the class is created.
metadata
instance-attribute
¶
Read-only; kept in Python, never stored in the segment.
Capacity
dataclass
¶
Maximum size of a field: bytes for str, bytes, bytearray and Decimal, elements for a collection.
It goes in the annotation, as in Annotated[str, Capacity(32)] or
Annotated[list[int], Capacity(16)], and allows 1 to 1048576 (1 Mi).
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
size
instance-attribute
¶
Bytes for text and bytes, not characters: a UTF-8 character can take up to 4 bytes; elements for a collection.