Skip to content

How to follow a whole reference graph

Boxes can point at boxes that point at further boxes: a stage at a motor, the motor at an encoder. When a box's reference fields lead on like this, they form a graph. This guide shows how one callback can hear about a change anywhere in it.

Before you start

What you need

Classes with reference fields. This guide uses a stage that refers to a motor, which refers to an encoder:

import queue
from typing import Any

from sharedbox import SharedBox


class Encoder(SharedBox, name="example-encoder"):
    count: int = 0


class Motor(SharedBox, name="example-motor"):
    position: int = 0
    encoder: Encoder | None = None


class Stage(SharedBox, name="example-stage"):
    motor: Motor | None = None

Referring to another box follows a single reference field; here you follow all of them at once.

1. Follow every reference

The script creates an encoder, a motor that points at it and a stage that points at the motor. Call follow on the stage's events without naming a field, then connect to the nested signal:

changes: queue.Queue[tuple[tuple[str, ...], Any]] = queue.Queue()
stage.events.follow()
stage.events.nested.connect(lambda path, new, old: changes.put((path, new)))
encoder.count = 3
print(changes.get(timeout=5))  # (('motor', 'encoder', 'count'), 3)
motor.position = 7
print(changes.get(timeout=5))  # (('motor', 'position'), 7)

Your callback gets (path, new, old), where path lists the field names on the way from the stage to the field that changed. When any process points a reference field at a different box, the following moves to the new box by itself.

The callbacks run on background threads, one for each box you follow. Hand the values over to your own thread, as the queue does here, or connect with thread="main", as the docstring of events describes.

2. Follow one path instead

If you only care about one box deep in the graph, call follow with a field at each level instead. Each call gives you the signals of the class that field points at:

stage.events.unfollow()
counts: queue.Queue[int] = queue.Queue()
encoder_events = stage.events.follow("motor").follow("encoder")
encoder_events.count.connect(lambda new, old: counts.put(new))
encoder.count = 4
print(counts.get(timeout=5))  # 4

3. Stop following

unfollow with a field stops what follow with that field started, and without a field it stops everything, as the script does before step 2. Closing the box stops all following too.

Following isn't free: each box you follow takes one waiter slot and one thread. Limits says what that adds up to for a large graph.

The whole script
"""The script of the guide "How to follow a whole reference graph"."""

import queue
from typing import Any

from sharedbox import SharedBox


class Encoder(SharedBox, name="example-encoder"):
    count: int = 0


class Motor(SharedBox, name="example-motor"):
    position: int = 0
    encoder: Encoder | None = None


class Stage(SharedBox, name="example-stage"):
    motor: Motor | None = None



with Encoder() as encoder, Motor(0, encoder) as motor, Stage(motor) as stage:
    changes: queue.Queue[tuple[tuple[str, ...], Any]] = queue.Queue()
    stage.events.follow()
    stage.events.nested.connect(lambda path, new, old: changes.put((path, new)))
    encoder.count = 3
    print(changes.get(timeout=5))  # (('motor', 'encoder', 'count'), 3)
    motor.position = 7
    print(changes.get(timeout=5))  # (('motor', 'position'), 7)

    stage.events.unfollow()
    counts: queue.Queue[int] = queue.Queue()
    encoder_events = stage.events.follow("motor").follow("encoder")
    encoder_events.count.connect(lambda new, old: counts.put(new))
    encoder.count = 4
    print(counts.get(timeout=5))  # 4
Stage.unlink()
Motor.unlink()
Encoder.unlink()