Subscription Events
Detailed guide to event types and their callback data.
Insert Events (ins)
Triggered when a new node is added to the Bag.
>>> from genro_bag import Bag
>>> bag = Bag()
>>> inserts = []
>>> def on_insert(**kw):
... node = kw['node']
... ind = kw['ind']
... inserts.append(f"[{ind}] {node.label} = {node.value}")
>>> bag.subscribe('tracker', insert=on_insert)
>>> bag['a'] = 1
>>> bag['b'] = 2
>>> inserts
['[0] a = 1', '[1] b = 2']
Callback Data
Key |
Description |
|---|---|
|
The new BagNode |
|
Always |
|
Index position where inserted |
|
Path from subscription root |
Update Events (upd_value)
Triggered when an existing node’s value changes.
>>> from genro_bag import Bag
>>> bag = Bag()
>>> updates = []
>>> def on_update(**kw):
... node = kw['node']
... pathlist = kw['pathlist']
... path = '.'.join(pathlist)
... updates.append(f"{path}: {node.value}")
>>> bag.subscribe('tracker', update=on_update)
>>> bag['count'] = 0
>>> bag['count'] = 1
>>> bag['count'] = 2
>>> updates
['count: 1', 'count: 2']
Note: The first assignment triggers ins, not upd_value.
upd_value — Bag-level callback data
Key |
Description |
|---|---|
|
The modified BagNode |
|
Always |
|
Path from subscription root |
|
The previous scalar/Bag value |
|
Always |
|
Optional reason string |
upd_value — Node-level subscribers
A subscriber registered directly on a BagNode via node.subscribe()
receives a single info argument that is a dict whose keys depend
on the event:
def callback(**kw):
info = kw['info'] # {'oldvalue': <previous_value>}
evt = kw['evt'] # 'upd_value'
For upd_value, info always contains the single key 'oldvalue'.
Attribute Update Events (upd_attrs)
Triggered when one or more node attributes are added, modified, or removed
via BagNode.set_attr. The payload carries a diff dict describing
exactly what changed:
{
"<attr_name>": {"old": <previous_value>, "new": <current_value>},
...
}
Only attributes whose effective value actually changed appear in the diff:
added:
{"old": None, "new": <value>}modified:
{"old": <prev>, "new": <curr>}removed:
{"old": <value>, "new": None}(e.g. set toNonewith the default_remove_null_attributes=True)
If set_attr does not change any effective value (no-op), no event is
emitted.
from genro_bag import Bag
bag = Bag()
bag.set_item('x', 'value', color='red')
events = []
bag.subscribe('w', update=lambda **kw: events.append(kw['attrs_diff']))
bag.get_node('x').set_attr(color='blue', size=42)
# events == [{
# "color": {"old": "red", "new": "blue"},
# "size": {"old": None, "new": 42},
# }]
upd_attrs — Bag-level callback data
Key |
Description |
|---|---|
|
The BagNode whose attributes changed |
|
Always |
|
Path from subscription root |
|
Always |
|
The diff dict (see above) |
|
Optional reason string |
upd_attrs — Node-level subscribers
def callback(**kw):
info = kw['info'] # {'attrs_diff': {<key>: {'old': ..., 'new': ...}, ...}}
evt = kw['evt'] # 'upd_attrs'
For upd_attrs, info always contains the single key 'attrs_diff'.
The payload is self-contained: a consumer can react to attribute changes
without having to snapshot node.attr independently.
Combined Value+Attribute Updates (upd_value_attr)
Triggered when a single mutation changes both the node’s value and one
or more of its attributes. This happens when set_item (or
BagNode.set_value) is called with the _attributes argument, which
carries the new attributes alongside the new value.
The payload merges both pieces of information:
oldvaluecarries the previous scalar/Bag value (same semantics asupd_value);attrs_diffcarries the attribute diff dict (same shape asupd_attrs).
from genro_bag import Bag
bag = Bag()
bag.set_item('x', 'v0', color='red')
events = []
bag.subscribe('w', update=lambda **kw: events.append({
'evt': kw['evt'],
'oldvalue': kw['oldvalue'],
'attrs_diff': kw['attrs_diff'],
}))
bag.set_item('x', 'v1', color='blue')
# events == [{
# 'evt': 'upd_value_attr',
# 'oldvalue': 'v0',
# 'attrs_diff': {'color': {'old': 'red', 'new': 'blue'}},
# }]
Note: set_item uses _updattr=False by default (full replacement of
attributes). Attributes present before the call but not passed in the new
call appear as removed in the diff (new=None).
upd_value_attr — Bag-level callback data
Key |
Description |
|---|---|
|
The modified BagNode |
|
Always |
|
Path from subscription root |
|
The previous scalar/Bag value |
|
Attribute diff dict (or |
|
Optional reason string |
upd_value_attr — Node-level subscribers
def callback(**kw):
info = kw['info'] # {'oldvalue': <prev>, 'attrs_diff': {<key>: {'old': ..., 'new': ...}, ...}}
evt = kw['evt'] # 'upd_value_attr'
For upd_value_attr, info contains both 'oldvalue' and
'attrs_diff'.
Delete Events (del)
Triggered when a node is removed.
>>> from genro_bag import Bag
>>> bag = Bag({'x': 1, 'y': 2, 'z': 3})
>>> deletes = []
>>> def on_delete(**kw):
... node = kw['node']
... ind = kw['ind']
... deletes.append(f"deleted [{ind}]: {node.label}")
>>> bag.subscribe('tracker', delete=on_delete)
>>> del bag['y']
>>> deletes
['deleted [1]: y']
Callback Data
Key |
Description |
|---|---|
|
The removed BagNode |
|
Always |
|
Index position before removal |
|
Path from subscription root |
The any Handler
Subscribe to all event types with a single callback:
>>> from genro_bag import Bag
>>> bag = Bag()
>>> events = []
>>> def on_any(**kw):
... evt = kw['evt']
... node = kw['node']
... events.append(f"{evt}: {node.label}")
>>> bag.subscribe('tracker', any=on_any)
>>> bag['x'] = 1 # ins
>>> bag['x'] = 2 # upd_value
>>> del bag['x'] # del
>>> events
['ins: x', 'upd_value: x', 'del: x']
Nested Path Events
Changes to nested paths emit events for each level:
>>> from genro_bag import Bag
>>> bag = Bag()
>>> events = []
>>> def on_any(**kw):
... events.append(kw['node'].label)
>>> bag.subscribe('w', any=on_any)
>>> bag['config.database.host'] = 'localhost'
>>> events
['config', 'database', 'host']
Pathlist
The pathlist shows the path from the subscription point:
>>> from genro_bag import Bag
>>> bag = Bag()
>>> paths = []
>>> def track_path(**kw):
... path = '.'.join(kw['pathlist'])
... if path: # Skip empty root
... paths.append(path)
>>> bag.subscribe('tracker', any=track_path)
>>> bag['a.b.c'] = 1
>>> paths
['a', 'a.b']
Note: Events fire for each intermediate bag created.
Combining Handlers
You can set different callbacks for different events:
>>> from genro_bag import Bag
>>> bag = Bag()
>>> log = []
>>> bag.subscribe('audit',
... insert=lambda **kw: log.append(f"ADD: {kw['node'].label}"),
... update=lambda **kw: log.append(f"MOD: {kw['node'].label}"),
... delete=lambda **kw: log.append(f"DEL: {kw['node'].label}")
... )
>>> bag['x'] = 1
>>> bag['x'] = 2
>>> del bag['x']
>>> log
['ADD: x', 'MOD: x', 'DEL: x']
Timer Events (tmr)
Triggered periodically on a time interval. Unlike other events, timer events are not caused by data changes — they fire on a schedule.
from genro_bag import Bag
bag = Bag()
events = []
def on_tick(**kw):
events.append(f"tick on {kw['subscriber_id']}")
bag.subscribe('poller', timer=on_tick, interval=5)
# After 5 seconds: events == ['tick on poller']
# After 10 seconds: events == ['tick on poller', 'tick on poller']
bag.unsubscribe('poller', timer=True) # Stop the timer
Callback Data
Key |
Description |
|---|---|
|
The Bag where the timer subscription is registered |
|
Always |
|
The subscription ID |
Important Notes
any=callbackdoes not include timer events — usetimer=callbackexplicitlyintervalis required whentimeris set (raisesValueErrorotherwise)unsubscribe(..., any=True)cancels timers tooTimer events propagate to parent bags like other events
The reason Field
Every change event carries an optional reason string that lets subscribers
distinguish why the event was emitted. Today the field uses two values:
Value |
Meaning |
|---|---|
|
Default — the event comes from an explicit user-driven write ( |
|
The event was emitted by structural autocreation of an intermediate container while traversing a missing path in write mode. See below. |
Any other string is whatever the caller passed via the _reason kwarg on
set_item / set_value / set_attr. The framework propagates it verbatim.
Filtering structural autocreate
When bag["a.b.c"] = value is called and a or b do not exist yet,
Bag creates them as empty intermediate containers. These structural
births fire ins events that look identical to a real leaf insert. The
reason field disambiguates them:
>>> from genro_bag import Bag
>>> bag = Bag()
>>> events = []
>>> def watch(**kw):
... events.append((kw['evt'], kw['node'].label, kw.get('reason')))
>>> bag.subscribe('w', any=watch)
>>> bag['a.b.c'] = 1
>>> events
[('ins', 'a', 'autocreate'), ('ins', 'b', 'autocreate'), ('ins', 'c', None)]
A reactive subscriber that only cares about real data can filter the structural noise with a single guard:
def react(**kw):
if kw.get('reason') == 'autocreate':
return # ignore intermediate container births
# ... actual reaction logic ...
bag.subscribe('reactor', any=react)
The same marker is applied when an existing scalar node is promoted to
a container by an incoming write (e.g. bag['x'] = 'scalar' followed by
bag['x.y'] = 1): the upd_value event of the promotion carries
reason='autocreate'.
The final leaf write (the actual datum) is never marked: it stays a
genuine insert/update with reason=None.
Stop Propagation
Any callback (ins, upd, del, tmr) can return False to stop event
propagation to parent bags:
from genro_bag import Bag
root = Bag()
root['child'] = Bag()
root_events = []
def stop_here(**kw):
# Handle locally, don't propagate
return False
root.subscribe('root_sub', update=lambda **kw: root_events.append(1))
root['child'].subscribe('child_sub', update=stop_here)
root['child']['x'] = 1
root['child']['x'] = 2 # root_events is still empty
Callbacks that return None (the default) do not stop propagation —
this is fully backwards-compatible.
Fired Events (_fired)
The _fired parameter on set_item creates event-like signals: set a value
to trigger subscribers, then immediately reset it to None without firing
again. Similar to GenroPy’s fireItem.
>>> from genro_bag import Bag
>>> bag = Bag()
>>> events = []
>>> bag.subscribe('w', insert=lambda **kw: events.append(kw['node'].value))
>>> bag.set_item('click', 'button_ok', _fired=True)
>>> events
['button_ok']
>>> bag['click'] is None
True
The sequence is:
set_item('click', 'button_ok')— creates the node, firesinseventSubscribers see
node.value == 'button_ok'Value is immediately reset to
Nonewithtrigger=False(no second event)
This is useful for signaling events through the subscription system without leaving stale values in the tree.
Event Order
Events fire in order of operation:
Insert fires immediately when node is created
Update fires immediately when value changes
Delete fires immediately when node is removed
Timer fires periodically on the configured interval
Nested operations fire in depth order (parent first, then children).