Skip to content

psygnal.containers #

Containers backed by psygnal events.

These classes provide "evented" versions of mutable python containers. They each have an events attribute (SignalGroup) that has a variety of signals that will emit whenever the container is mutated. See Container SignalGroups for the corresponding container type for details on the available signals.

Classes:

CallableProxyEvents #

CallableProxyEvents(instance: Any = None)

Bases: ProxyEvents

Events emitted by EventedCallableObjectProxy.

Attributes:

  • called –

    Emitted when the object is called.

Source code in src/psygnal/_group.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def __init__(self, instance: Any = None) -> None:
    cls = type(self)
    if not hasattr(cls, "_psygnal_signals"):
        raise TypeError(
            "Cannot instantiate `SignalGroup` directly.  Use a subclass instead."
        )

    self._psygnal_instances = {
        name: (
            sig._create_signal_instance(self)
            if name in cls._psygnal_name_conflicts
            else sig.__get__(self, cls)
        )
        for name, sig in cls._psygnal_signals.items()
    }
    self._psygnal_relay = SignalRelay(self._psygnal_instances, instance)
    self._psygnal_child_events_connected = False

called class-attribute instance-attribute #

called = Signal(tuple, dict)

Emitted when the object is called.

DictEvents #

DictEvents(instance: Any = None)

Bases: SignalGroup

Events available on EventedDict.

Attributes:

  • added –

    (key, value) emitted after a value is added at key

  • adding –

    (key,) emitted before an item is added at key

  • changed –

    (key, old_value, new_value) emitted before old_value is replaced with

  • changing –

    (key, old_value, new_value) emitted before old_value is replaced with

  • removed –

    (key, value) emitted after value is removed at key

  • removing –

    (key,) emitted before an item is removed at key

Source code in src/psygnal/_group.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def __init__(self, instance: Any = None) -> None:
    cls = type(self)
    if not hasattr(cls, "_psygnal_signals"):
        raise TypeError(
            "Cannot instantiate `SignalGroup` directly.  Use a subclass instead."
        )

    self._psygnal_instances = {
        name: (
            sig._create_signal_instance(self)
            if name in cls._psygnal_name_conflicts
            else sig.__get__(self, cls)
        )
        for name, sig in cls._psygnal_signals.items()
    }
    self._psygnal_relay = SignalRelay(self._psygnal_instances, instance)
    self._psygnal_child_events_connected = False

added class-attribute instance-attribute #

added = DictSignal(object, object)

(key, value) emitted after a value is added at key

adding class-attribute instance-attribute #

adding = DictSignal(object)

(key,) emitted before an item is added at key

changed class-attribute instance-attribute #

changed = DictSignal(object, object, object)

(key, old_value, new_value) emitted before old_value is replaced with new_value at key

changing class-attribute instance-attribute #

changing = DictSignal(object)

(key, old_value, new_value) emitted before old_value is replaced with new_value at key

removed class-attribute instance-attribute #

removed = DictSignal(object, object)

(key, value) emitted after value is removed at key

removing class-attribute instance-attribute #

removing = DictSignal(object)

(key,) emitted before an item is removed at key

EventedCallableObjectProxy #

EventedCallableObjectProxy(target: Callable)

Bases: EventedObjectProxy

Create a proxy of target that includes an events psygnal.SignalGroup.

target must be callable.

Important

This class requires wrapt to be installed. You can install directly (pip install wrapt) or by using the psygnal extra: pip install psygnal[proxy]

Signals will be emitted whenever an attribute is set or deleted, or (if the object implements __getitem__) whenever an item is set or deleted. If the object supports in-place modification (i.e. any of the __i{}__ magic methods), then an in_place event is emitted (with the name of the method) whenever any of them are used. Lastly, if the item is called, a called event is emitted with the (args, kwargs) used in the call.

The events available at target.events include:

  • attribute_set: Signal(str, object)
  • attribute_deleted: Signal(str)
  • item_set: Signal(object, object)
  • item_deleted: Signal(object)
  • in_place: Signal(str, object)
  • called: Signal(tuple, dict)

Parameters:

  • target #

    (Callable) –

    An callable object to wrap

Attributes:

Source code in src/psygnal/containers/_evented_proxy.py
215
216
def __init__(self, target: Callable):
    super().__init__(target)

events property #

events: CallableProxyEvents

SignalGroup containing events for this object proxy.

EventedDict #

EventedDict(
    data: DictArg | None = None,
    *,
    basetype: TypeOrSequenceOfTypes = (),
    **kwargs: _V,
)

Bases: TypedMutableMapping[_K, _V]

Mutable mapping that emits events when altered.

This class is designed to behave exactly like the builtin dict, but will emit events before and after all mutations (addition, removal, and changing).

Parameters:

  • data #

    (Union[Mapping[_K, _V], Iterable[Tuple[_K, _V]], None], default: None ) –

    Data suitable of passing to dict(). Mapping of {key: value} pairs, or Iterable of two-tuples [(key, value), ...], or None to create an

  • basetype #

    (TypeOrSequenceOfTypes, default: () ) –

    Type or Sequence of Type objects. If provided, values entered into this Mapping must be an instance of one of the provided types. by default ().

Attributes:

  • events (DictEvents) –

    The SignalGroup object that emits all events available on an EventedDict.

Source code in src/psygnal/containers/_evented_dict.py
186
187
188
189
190
191
192
193
194
def __init__(
    self,
    data: DictArg | None = None,
    *,
    basetype: TypeOrSequenceOfTypes = (),
    **kwargs: _V,
):
    self.events = DictEvents()
    super().__init__(data, basetype=basetype, **kwargs)

EventedList #

EventedList(
    data: Iterable[_T] = (),
    *,
    hashable: bool = True,
    child_events: bool = True,
)

Bases: MutableSequence[_T]

Mutable Sequence that emits events when altered.

This class is designed to behave exactly like the builtin list, but will emit events before and after all mutations (insertion, removal, setting, and moving).

Parameters:

  • data #

    (iterable, default: () ) –

    Elements to initialize the list with.

  • hashable #

    (bool, default: True ) –

    Whether the list should be hashable as id(self). By default True.

  • child_events #

    (bool, default: True ) –

    Whether to re-emit events from emitted from evented items in the list (i.e. items that have SignalInstances). If True, child events can be connected at EventedList.events.child_event. By default, True.

Attributes:

  • events (ListEvents) –

    SignalGroup that with events related to list mutation. (see ListEvents)

Methods:

  • clear –

    Remove all items from the list.

  • copy –

    Return a shallow copy of the list.

  • extend –

    Extend list by appending all items from values.

  • insert –

    Insert value before index.

  • move –

    Insert object at src_index before dest_index.

  • move_multiple –

    Move a batch of sources indices, to a single destination.

  • reverse –

    Reverse list IN PLACE.

Source code in src/psygnal/containers/_evented_list.py
181
182
183
184
185
186
187
188
189
190
191
192
193
def __init__(
    self,
    data: Iterable[_T] = (),
    *,
    hashable: bool = True,
    child_events: bool = True,
):
    super().__init__()
    self._data: list[_T] = []
    self._hashable = hashable
    self._child_events = child_events
    self.events = ListEvents(instance=self)
    self.extend(data)

clear #

clear() -> None

Remove all items from the list.

Overrides MutableSequence.clear (which pops one at a time) so the whole list is removed as a single contiguous block (one batch_removing/ batch_removed pair). Items still go through __delitem__, so the per-item removing/removed events fire for each, highest index first, as before.

Source code in src/psygnal/containers/_evented_list.py
253
254
255
256
257
258
259
260
261
262
def clear(self) -> None:
    """Remove all items from the list.

    Overrides `MutableSequence.clear` (which pops one at a time) so the whole
    list is removed as a single contiguous block (one `batch_removing`/
    `batch_removed` pair). Items still go through `__delitem__`, so the per-item
    `removing`/`removed` events fire for each, highest index first, as before.
    """
    if self._data:
        del self[:]

copy #

copy() -> Self

Return a shallow copy of the list.

Source code in src/psygnal/containers/_evented_list.py
354
355
356
def copy(self) -> Self:
    """Return a shallow copy of the list."""
    return self.__newlike__(self)

extend #

extend(values: Iterable[_T]) -> None

Extend list by appending all items from values.

Overrides MutableSequence.extend (which appends one at a time) so the whole batch is bracketed by a single batch_inserting/batch_inserted pair. Items still go through insert one by one, emitting the per-item events.

Source code in src/psygnal/containers/_evented_list.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def extend(self, values: Iterable[_T]) -> None:
    """Extend list by appending all items from `values`.

    Overrides `MutableSequence.extend` (which appends one at a time) so the whole
    batch is bracketed by a single `batch_inserting`/`batch_inserted` pair. Items
    still go through `insert` one by one, emitting the per-item events.
    """
    values = list(values)
    if not values:
        return
    start, stop = len(self), len(self) + len(values)
    if self.events.batch_inserting:
        self.events.batch_inserting.emit(start, stop)
    self._batch_depth += 1
    try:
        for value in values:
            self.append(value)
    finally:
        self._batch_depth -= 1
    if self.events.batch_inserted:
        self.events.batch_inserted.emit(start, stop, values)

insert #

insert(index: int, value: _T) -> None

Insert value before index.

Source code in src/psygnal/containers/_evented_list.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def insert(self, index: int, value: _T) -> None:
    """Insert `value` before index."""
    # `_pre_insert` may reject the value; run it before emitting anything so a
    # rejected value never leaves an unterminated batch_inserting behind.
    _value = self._pre_insert(value)
    if self._batch_depth:
        # inside extend(), which brackets the whole batch itself
        self._insert_one(index, value, _value)
        return
    # normalize for the (range-aware) batch_* signals; the per-item events
    # keep emitting the raw `index` exactly as before.
    norm = max(0, len(self) + index) if index < 0 else min(index, len(self))
    if self.events.batch_inserting:
        self.events.batch_inserting.emit(norm, norm + 1)
    self._insert_one(index, value, _value)
    if self.events.batch_inserted:
        self.events.batch_inserted.emit(norm, norm + 1, [value])

move #

move(src_index: int, dest_index: int = 0) -> bool

Insert object at src_index before dest_index.

Both indices refer to the list prior to any object removal (pre-move space).

Source code in src/psygnal/containers/_evented_list.py
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
def move(self, src_index: int, dest_index: int = 0) -> bool:
    """Insert object at `src_index` before `dest_index`.

    Both indices refer to the list prior to any object removal
    (pre-move space).
    """
    if dest_index < 0:
        dest_index += len(self) + 1
    if dest_index in (src_index, src_index + 1):
        # this is a no-op
        return False

    self.events.moving.emit(src_index, dest_index)
    item = self._data.pop(src_index)
    if dest_index > src_index:
        dest_index -= 1
    self._data.insert(dest_index, item)
    self.events.moved.emit(src_index, dest_index, item)
    self.events.reordered.emit()
    return True

move_multiple #

move_multiple(
    sources: Iterable[Index], dest_index: int = 0
) -> int

Move a batch of sources indices, to a single destination.

Note, if dest_index is higher than any of the sources, then the resulting position of the moved objects after the move operation is complete will be lower than dest_index.

Parameters:

  • sources #

    (Iterable[Union[int, slice]]) –

    A sequence of indices

  • dest_index #

    (int, default: 0 ) –

    The destination index. All sources will be inserted before this index (in pre-move space), by default 0... which has the effect of "bringing to front" everything in sources, or acting as a "reorder" method if sources contains all indices.

Returns:

  • int –

    The number of successful move operations completed.

Raises:

  • TypeError –

    If the destination index is a slice, or any of the source indices are not int or slice.

Source code in src/psygnal/containers/_evented_list.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
def move_multiple(self, sources: Iterable[Index], dest_index: int = 0) -> int:
    """Move a batch of `sources` indices, to a single destination.

    Note, if `dest_index` is higher than any of the `sources`, then
    the resulting position of the moved objects after the move operation
    is complete will be lower than `dest_index`.

    Parameters
    ----------
    sources : Iterable[Union[int, slice]]
        A sequence of indices
    dest_index : int, optional
        The destination index.  All sources will be inserted before this
        index (in pre-move space), by default 0... which has the effect of
        "bringing to front" everything in `sources`, or acting as a
        "reorder" method if `sources` contains all indices.

    Returns
    -------
    int
        The number of successful move operations completed.

    Raises
    ------
    TypeError
        If the destination index is a slice, or any of the source indices
        are not `int` or `slice`.
    """
    # calling list here makes sure that there are no index errors up front
    move_plan = list(self._move_plan(sources, dest_index))

    # don't assume index adjacency ... so move objects one at a time
    # this *could* be simplified with an intermediate list ... but this way
    # allows any views (such as QtViews) to update themselves more easily.
    # If this needs to be changed in the future for performance reasons,
    # then the associated QtListView will need to changed from using
    # `beginMoveRows` & `endMoveRows` to using `layoutAboutToBeChanged` &
    # `layoutChanged` while *manually* updating model indices with
    # `changePersistentIndexList`.  That becomes much harder to do with
    # nested tree-like models.
    with self.events.reordered.blocked():
        for src, dest in move_plan:
            self.move(src, dest)

    self.events.reordered.emit()
    return len(move_plan)

reverse #

reverse(*, emit_individual_events: bool = False) -> None

Reverse list IN PLACE.

Parameters:

  • emit_individual_events #

    (bool, default: False ) –

    If True, the list is reversed one item at a time, emitting per-item events; otherwise the underlying data is reversed directly, by default False

Source code in src/psygnal/containers/_evented_list.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
def reverse(self, *, emit_individual_events: bool = False) -> None:
    """Reverse list *IN PLACE*.

    Parameters
    ----------
    emit_individual_events : bool
        If True, the list is reversed one item at a time, emitting per-item
        events; otherwise the underlying data is reversed directly, by
        default False
    """
    if emit_individual_events:
        super().reverse()
    else:
        self._data.reverse()
    self.events.reordered.emit()

EventedObjectProxy #

EventedObjectProxy(target: Any)

Bases: ObjectProxy, Generic[T]

Create a proxy of target that includes an events psygnal.SignalGroup.

Provides an "evented" subclasses of wrapt.ObjectProxy

Important

This class requires wrapt to be installed. You can install directly (pip install wrapt) or by using the psygnal extra: pip install psygnal[proxy]

Signals will be emitted whenever an attribute is set or deleted, or (if the object implements __getitem__) whenever an item is set or deleted. If the object supports in-place modification (i.e. any of the __i{}__ magic methods), then an in_place event is emitted (with the name of the method) whenever any of them are used.

The events available at target.events include:

  • attribute_set: Signal(str, object)
  • attribute_deleted: Signal(str)
  • item_set: Signal(object, object)
  • item_deleted: Signal(object)
  • in_place: Signal(str, object)

Experimental

This object is experimental! They may affect the behavior of the wrapped object in unanticipated ways. Please consult the wrapt documentation for details on how the Object Proxy works.

Parameters:

  • target #

    (Any) –

    An object to wrap

Attributes:

Source code in src/psygnal/containers/_evented_proxy.py
89
90
def __init__(self, target: Any):
    super().__init__(target)

events property #

events: ProxyEvents

SignalGroup containing events for this object proxy.

EventedOrderedSet #

EventedOrderedSet(iterable: Iterable[_T] = ())

Bases: EventedSet, OrderedSet[_T]

A ordered variant of EventedSet that maintains insertion order.

Parameters:

  • iterable #

    (Iterable[_T], default: () ) –

    Data to populate the set. If omitted, an empty set is created.

Attributes:

  • events (SetEvents) –

    SignalGroup that with events related to set mutation. (see SetEvents)

Source code in src/psygnal/containers/_evented_set.py
361
362
def __init__(self, iterable: Iterable[_T] = ()):
    super().__init__(iterable)

EventedSet #

EventedSet(iterable: Iterable[_T] = ())

Bases: _BaseMutableSet[_T]

A set with an items_changed signal that emits when items are added/removed.

Parameters:

  • iterable #

    (Iterable[_T], default: () ) –

    Data to populate the set. If omitted, an empty set is created.

Attributes:

  • events (SetEvents) –

    SignalGroup that with events related to set mutation. (see SetEvents)

Examples:

>>> from psygnal.containers import EventedSet
>>>
>>> my_set = EventedSet([1, 2, 3])
>>> my_set.events.items_changed.connect(
>>>     lambda a, r: print(f"added={a}, removed={r}")
>>> )
>>> my_set.update({3, 4, 5})
added=(4, 5), removed=()

Multi-item events will be reduced into a single emission:

>>> my_set.symmetric_difference_update({4, 5, 6, 7})
added=(6, 7), removed=(4, 5)
>>> my_set
EventedSet({1, 2, 3, 6, 7})

Methods:

  • clear –

    Remove all elements from this set.

  • difference_update –

    Remove all elements of another set from this set.

  • intersection_update –

    Update this set with the intersection of itself and another.

  • symmetric_difference_update –

    Update this set with the symmetric difference of itself and another.

  • update –

    Update this set with the union of this set and others.

Source code in src/psygnal/containers/_evented_set.py
286
287
288
def __init__(self, iterable: Iterable[_T] = ()):
    self.events = self._get_events_class()
    super().__init__(iterable)

clear #

clear() -> None

Remove all elements from this set.

Source code in src/psygnal/containers/_evented_set.py
295
296
297
298
def clear(self) -> None:
    """Remove all elements from this set."""
    with self.events.items_changed.paused(_reduce_events):
        super().clear()

difference_update #

difference_update(*s: Iterable[_T]) -> None

Remove all elements of another set from this set.

Source code in src/psygnal/containers/_evented_set.py
300
301
302
303
def difference_update(self, *s: Iterable[_T]) -> None:
    """Remove all elements of another set from this set."""
    with self.events.items_changed.paused(_reduce_events):
        super().difference_update(*s)

intersection_update #

intersection_update(*s: Iterable[_T]) -> None

Update this set with the intersection of itself and another.

Source code in src/psygnal/containers/_evented_set.py
305
306
307
308
def intersection_update(self, *s: Iterable[_T]) -> None:
    """Update this set with the intersection of itself and another."""
    with self.events.items_changed.paused(_reduce_events):
        super().intersection_update(*s)

symmetric_difference_update #

symmetric_difference_update(__s: Iterable[_T]) -> None

Update this set with the symmetric difference of itself and another.

This will remove any items in this set that are also in other, and add any items in others that are not present in this set.

Source code in src/psygnal/containers/_evented_set.py
310
311
312
313
314
315
316
317
def symmetric_difference_update(self, __s: Iterable[_T]) -> None:
    """Update this set with the symmetric difference of itself and another.

    This will remove any items in this set that are also in `other`, and
    add any items in others that are not present in this set.
    """
    with self.events.items_changed.paused(_reduce_events, ((), ())):
        super().symmetric_difference_update(__s)

update #

update(*others: Iterable[_T]) -> None

Update this set with the union of this set and others.

Source code in src/psygnal/containers/_evented_set.py
290
291
292
293
def update(self, *others: Iterable[_T]) -> None:
    """Update this set with the union of this set and others."""
    with self.events.items_changed.paused(_reduce_events):
        super().update(*others)

ListEvents #

ListEvents(instance: Any = None)

Bases: SignalGroup

Events available on EventedList.

Attributes:

  • batch_inserted –

    (start, stop, values) emitted once after a contiguous block of items has been

  • batch_inserting –

    (start, stop) emitted once before a contiguous block of items is inserted

  • batch_removed –

    (start, stop, values) emitted once after a contiguous block of items has been

  • batch_removing –

    (start, stop) emitted once before a contiguous block of items is removed from

  • changed –

    (index_or_slice, old_value, value) emitted when index is set from

  • child_event –

    (EmissionInfo) emitted when an object in the list emits an

  • inserted –

    (index, value) emitted after value is inserted at index

  • inserting –

    (index) emitted before an item is inserted at index

  • moved –

    (index, new_index, value) emitted after value is moved from

  • moving –

    (index, new_index) emitted before an item is moved from index to

  • removed –

    (index, value) emitted after value is removed at index

  • removing –

    (index) emitted before an item is removed at index

  • reordered –

    Emitted when the list is reordered (eg. moved/reversed).

Source code in src/psygnal/_group.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def __init__(self, instance: Any = None) -> None:
    cls = type(self)
    if not hasattr(cls, "_psygnal_signals"):
        raise TypeError(
            "Cannot instantiate `SignalGroup` directly.  Use a subclass instead."
        )

    self._psygnal_instances = {
        name: (
            sig._create_signal_instance(self)
            if name in cls._psygnal_name_conflicts
            else sig.__get__(self, cls)
        )
        for name, sig in cls._psygnal_signals.items()
    }
    self._psygnal_relay = SignalRelay(self._psygnal_instances, instance)
    self._psygnal_child_events_connected = False

batch_inserted class-attribute instance-attribute #

batch_inserted = ListSignal(int, int, object)

(start, stop, values) emitted once after a contiguous block of items has been inserted into the half-open range [start, stop) (values is the inserted list).

insert always pairs this with its batch_inserting. extend does too, unless _pre_insert rejects a value part way through the batch, in which case the exception propagates and the batch is left unterminated (the list is partially extended, as it is without the batch signals).

batch_inserting class-attribute instance-attribute #

batch_inserting = ListSignal(int, int)

(start, stop) emitted once before a contiguous block of items is inserted into the half-open range [start, stop).

This brackets the per-item inserting events (which still fire for each item), so that a batch insert (e.g. extend/+=) can be handled with a single update. A single insert is just a length-1 range.

Note that the batch_* signals track structural changes only; slice assignment (el[a:b] = ...) may change the length of the list but emits only changed.

batch_removed class-attribute instance-attribute #

batch_removed = ListSignal(int, int, object)

(start, stop, values) emitted once after a contiguous block of items has been removed from the half-open range [start, stop) (values is the removed list, in list order, even though items are popped highest-index first).

batch_removing class-attribute instance-attribute #

batch_removing = ListSignal(int, int)

(start, stop) emitted once before a contiguous block of items is removed from the half-open range [start, stop).

Brackets the per-item removing events. Non-contiguous removals (e.g. a strided slice) emit this once per contiguous block, highest block first.

As with batch_inserting, slice assignment emits only changed.

changed class-attribute instance-attribute #

changed = ListSignal(object, object, object)

(index_or_slice, old_value, value) emitted when index is set from old_value to value

child_event class-attribute instance-attribute #

child_event = Signal(EmissionInfo)

(EmissionInfo) emitted when an object in the list emits an event. Note that the EventedList must be created with child_events=True in order for this to be emitted.

inserted class-attribute instance-attribute #

inserted = ListSignal(int, object)

(index, value) emitted after value is inserted at index

inserting class-attribute instance-attribute #

inserting = ListSignal(int)

(index) emitted before an item is inserted at index

moved class-attribute instance-attribute #

moved = ListSignal(int, int, object)

(index, new_index, value) emitted after value is moved from index to new_index

moving class-attribute instance-attribute #

moving = ListSignal(int, int)

(index, new_index) emitted before an item is moved from index to new_index

removed class-attribute instance-attribute #

removed = ListSignal(int, object)

(index, value) emitted after value is removed at index

removing class-attribute instance-attribute #

removing = ListSignal(int)

(index) emitted before an item is removed at index

reordered class-attribute instance-attribute #

reordered = Signal()

Emitted when the list is reordered (eg. moved/reversed).

OrderedSet #

OrderedSet(iterable: Iterable[_T] = ())

Bases: _BaseMutableSet[_T]

A set that preserves insertion order, uses dict behind the scenes.

Source code in src/psygnal/containers/_evented_set.py
216
217
218
def __init__(self, iterable: Iterable[_T] = ()):
    self._data = {}
    self.update(iterable)

ProxyEvents #

ProxyEvents(instance: Any = None)

Bases: SignalGroup

Events emitted by EventedObjectProxy and EventedCallableObjectProxy.

Attributes:

Source code in src/psygnal/_group.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def __init__(self, instance: Any = None) -> None:
    cls = type(self)
    if not hasattr(cls, "_psygnal_signals"):
        raise TypeError(
            "Cannot instantiate `SignalGroup` directly.  Use a subclass instead."
        )

    self._psygnal_instances = {
        name: (
            sig._create_signal_instance(self)
            if name in cls._psygnal_name_conflicts
            else sig.__get__(self, cls)
        )
        for name, sig in cls._psygnal_signals.items()
    }
    self._psygnal_relay = SignalRelay(self._psygnal_instances, instance)
    self._psygnal_child_events_connected = False

attribute_deleted class-attribute instance-attribute #

attribute_deleted = Signal(str)

Emitted when an attribute is deleted.

attribute_set class-attribute instance-attribute #

attribute_set = Signal(str, object)

Emitted when an attribute is set.

in_place class-attribute instance-attribute #

in_place = Signal(str, object)

Emitted when an in-place operation is performed.

item_deleted class-attribute instance-attribute #

item_deleted = Signal(object)

Emitted when an item is deleted.

item_set class-attribute instance-attribute #

item_set = Signal(object, object)

Emitted when an item is set.

SelectableEventedList #

SelectableEventedList(
    data: Iterable[_T] = (),
    *,
    hashable: bool = True,
    child_events: bool = False,
)

Bases: Selectable[_T], EventedList[_T]

EventedList subclass with a built in selection model.

In addition to all EventedList properties, this class also has a selection attribute that manages a set of selected items in the list.

Parameters:

  • data #

    (iterable, default: () ) –

    Elements to initialize the list with.

  • hashable #

    (bool, default: True ) –

    Whether the list should be hashable as id(self). By default True.

  • child_events #

    (bool, default: False ) –

    Whether to re-emit events from emitted from evented items in the list (i.e. items that have SignalInstances). If True, child events can be connected at EventedList.events.child_event. By default, False.

Attributes:

  • events (ListEvents) –

    SignalGroup that with events related to list mutation. (see ListEvents)

  • selection (Selection) –

    An evented set containing the currently selected items, along with an active and current item. (See Selection)

Methods:

Source code in src/psygnal/containers/_selectable_evented_list.py
40
41
42
43
44
45
46
47
48
49
def __init__(
    self,
    data: Iterable[_T] = (),
    *,
    hashable: bool = True,
    child_events: bool = False,
):
    self._activate_on_insert: bool = True
    super().__init__(data=data, hashable=hashable, child_events=child_events)
    self.events.removed.connect(self._on_item_removed)

deselect_all #

deselect_all() -> None

Deselect all items in the list.

Source code in src/psygnal/containers/_selectable_evented_list.py
72
73
74
def deselect_all(self) -> None:
    """Deselect all items in the list."""
    self.selection.clear()

insert #

insert(index: int, value: _T) -> None

Insert item(s) into the list and update the selection.

Parameters:

  • index #

    (int) –

    The index at which to insert the item.

  • value #

    (_T) –

    The item to insert.

Source code in src/psygnal/containers/_selectable_evented_list.py
54
55
56
57
58
59
60
61
62
63
64
65
66
def insert(self, index: int, value: _T) -> None:
    """Insert item(s) into the list and update the selection.

    Parameters
    ----------
    index : int
        The index at which to insert the item.
    value : _T
        The item to insert.
    """
    super().insert(index, value)
    if self._activate_on_insert:
        self.selection.active = value

remove_selected #

remove_selected() -> tuple[_T, ...]

Remove selected items from the list and the selection.

Returns:

  • Tuple[_T, ...] –

    The items that were removed.

Source code in src/psygnal/containers/_selectable_evented_list.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def remove_selected(self) -> tuple[_T, ...]:
    """Remove selected items from the list and the selection.

    Returns
    -------
    Tuple[_T, ...]
        The items that were removed.
    """
    selected_items = tuple(self.selection)
    idx = 0
    for item in list(self.selection):
        idx = self.index(item)
        self.remove(item)
    new_idx = max(0, idx - 1)
    if len(self) > new_idx:
        self.selection.add(self[new_idx])
    return selected_items

select_all #

select_all() -> None

Select all items in the list.

Source code in src/psygnal/containers/_selectable_evented_list.py
68
69
70
def select_all(self) -> None:
    """Select all items in the list."""
    self.selection.update(self)

select_next #

select_next(
    step: int = 1,
    expand_selection: bool = False,
    wraparound: bool = False,
) -> None

Select the next item in the list.

Parameters:

  • step #

    (int, default: 1 ) –

    The step size to take when picking the next item, by default 1

  • expand_selection #

    (bool, default: False ) –

    If True, will expand the selection to contain the both the current item and the next item, by default False

  • wraparound #

    (bool, default: False ) –

    Whether to return to the beginning of the list of the end has been reached, by default False

Source code in src/psygnal/containers/_selectable_evented_list.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def select_next(
    self, step: int = 1, expand_selection: bool = False, wraparound: bool = False
) -> None:
    """Select the next item in the list.

    Parameters
    ----------
    step : int
        The step size to take when picking the next item, by default 1
    expand_selection : bool
        If True, will expand the selection to contain the both the current item and
        the next item, by default False
    wraparound : bool
        Whether to return to the beginning of the list of the end has been reached,
        by default False
    """
    if len(self) == 0:
        return
    elif not self.selection:
        idx = -1 if step > 0 else 0
    else:
        idx = self.index(self.selection._current) + step
    idx_in_sequence = len(self) > idx >= 0
    if wraparound:
        idx = idx % len(self)
    elif not idx_in_sequence:
        idx = -1 if step > 0 else 0
    next_item = self[idx]
    if expand_selection:
        self.selection.add(next_item)
        self.selection._current = next_item
    else:
        self.selection.active = next_item

select_previous #

select_previous(
    expand_selection: bool = False, wraparound: bool = False
) -> None

Select the previous item in the list.

Parameters:

  • expand_selection #

    (bool, default: False ) –

    If True, will expand the selection to contain both the current item and the previous item, by default False

  • wraparound #

    (bool, default: False ) –

    Whether to return to the end of the list if the beginning has been reached, by default False

Source code in src/psygnal/containers/_selectable_evented_list.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def select_previous(
    self, expand_selection: bool = False, wraparound: bool = False
) -> None:
    """Select the previous item in the list.

    Parameters
    ----------
    expand_selection : bool
        If True, will expand the selection to contain both the current item and
        the previous item, by default False
    wraparound : bool
        Whether to return to the end of the list if the beginning has been
        reached, by default False
    """
    self.select_next(
        step=-1, expand_selection=expand_selection, wraparound=wraparound
    )

Selection #

Selection(
    data: Iterable[_T] = (), parent: Container | None = None
)

Bases: EventedOrderedSet[_T]

An model of selected items, with a active and current item.

There can only be one active and one current item, but there can be multiple selected items. An "active" item is defined as a single selected item (if multiple items are selected, there is no active item). The "current" item is mostly useful for (e.g.) keyboard actions: even with multiple items selected, you may only have one current item, and keyboard events (like up and down) can modify that current item. It's possible to have a current item without an active item, but an active item will always be the current item.

An item can be the current item and selected at the same time. Qt views will ensure that there is always a current item as keyboard navigation, for example, requires a current item. This pattern mimics current/selected items from Qt: https://doc.qt.io/qt-5/model-view-programming.html#current-item-and-selected-items

Parameters:

  • data #

    (iterable, default: () ) –

    Elements to initialize the set with.

  • parent #

    (Container, default: None ) –

    The parent container, if any. This is used to provide validation upon mutation in common use cases.

Attributes:

  • events (SelectionEvents) –

    SignalGroup that with events related to selection changes. (see SelectionEvents)

  • active ((Any, optional)) –

    The active item, if any. An "active" item is defined as a single selected item (if multiple items are selected, there is no active item)

  • _current ((Any, optional)) –

    The current item, if any. This is used primarily by GUI views when handling mouse/key events.

Methods:

  • clear –

    Clear the selection.

  • replace_selection –

    Replace the current selection with new_selection.

  • select_only –

    Unselect everything but obj. Add to selection if not currently selected.

  • toggle –

    Toggle selection state of obj.

Source code in src/psygnal/containers/_selection.py
78
79
80
81
82
83
def __init__(self, data: Iterable[_T] = (), parent: Container | None = None):
    self._active: _T | None = None
    self._current_: _T | None = None
    self._parent: Container | None = parent
    super().__init__(iterable=data)
    self._update_active()

active property writable #

active: _T | None

Return the currently active item or None.

clear #

clear(keep_current: bool = False) -> None

Clear the selection.

Parameters:

  • keep_current #

    (bool, default: False ) –

    If False (the default), the "current" item will also be set to None.

Source code in src/psygnal/containers/_selection.py
116
117
118
119
120
121
122
123
124
125
126
def clear(self, keep_current: bool = False) -> None:
    """Clear the selection.

    Parameters
    ----------
    keep_current : bool
        If `False` (the default), the "current" item will also be set to None.
    """
    if not keep_current:
        self._current = None
    super().clear()

replace_selection #

replace_selection(new_selection: Iterable[_T]) -> None

Replace the current selection with new_selection.

This is equivalent to calling intersection_update followed by update, but is more efficient because it only emits a single items_changed event.

Source code in src/psygnal/containers/_selection.py
138
139
140
141
142
143
144
145
146
def replace_selection(self, new_selection: Iterable[_T]) -> None:
    """Replace the current selection with `new_selection`.

    This is equivalent to calling `intersection_update` followed by `update`,
    but is more efficient because it only emits a single `items_changed` event.
    """
    with self.events.items_changed.paused(_reduce_events):
        self.intersection_update(new_selection)
        self.update(new_selection)

select_only #

select_only(obj: _T) -> None

Unselect everything but obj. Add to selection if not currently selected.

Source code in src/psygnal/containers/_selection.py
132
133
134
135
136
def select_only(self, obj: _T) -> None:
    """Unselect everything but `obj`. Add to selection if not currently selected."""
    with self.events.items_changed.paused(_reduce_events):
        self.intersection_update({obj})
        self.add(obj)

toggle #

toggle(obj: _T) -> None

Toggle selection state of obj.

Source code in src/psygnal/containers/_selection.py
128
129
130
def toggle(self, obj: _T) -> None:
    """Toggle selection state of obj."""
    self.symmetric_difference_update({obj})

SetEvents #

SetEvents(instance: Any = None)

Bases: SignalGroup

Events available on EventedSet.

Attributes:

  • items_changed (added (Tuple[Any, ...], removed: Tuple[Any, ...])) –

    A signal that will emitted whenever an item or items are added or removed. Connected callbacks will be called with callback(added, removed), where added and removed are tuples containing the objects that have been added or removed from the set.

Source code in src/psygnal/_group.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def __init__(self, instance: Any = None) -> None:
    cls = type(self)
    if not hasattr(cls, "_psygnal_signals"):
        raise TypeError(
            "Cannot instantiate `SignalGroup` directly.  Use a subclass instead."
        )

    self._psygnal_instances = {
        name: (
            sig._create_signal_instance(self)
            if name in cls._psygnal_name_conflicts
            else sig.__get__(self, cls)
        )
        for name, sig in cls._psygnal_signals.items()
    }
    self._psygnal_relay = SignalRelay(self._psygnal_instances, instance)
    self._psygnal_child_events_connected = False