Skip to content

ListSource

Usage

Data sources are abstractions that allow you to define the data being managed by your application independent of the GUI representation of that data. For details on the use of data sources, see the topic guide.

ListSource is an implementation of an ordered list of data. When a ListSource is created, it can be provided a list of accessors - these are the attributes that all items managed by the ListSource will have. The API provided by ListSource is [list][]-like; the operations you'd expect on a normal Python list, such as insert, remove, index, and indexing with [], are also possible on a ListSource:

from toga.sources import ListSource

source = ListSource(
    accessors=["name", "weight"],
    data=[
        {"name": "Platypus", "weight": 2.4},
        {"name": "Numbat", "weight": 0.597},
        {"name": "Thylacine", "weight": 30.0},
    ]
)

# Get the first item in the source
item = source[0]
print(f"Animal's name is {item.name}")

# Find an item with a name of "Thylacine"
item = source.find({"name": "Thylacine"})

# Remove that item from the data
source.remove(item)

# Insert a new item at the start of the data
source.insert(0, {"name": "Bettong", "weight": 1.2})

The ListSource manages a list of Row objects. Each Row has all the attributes described by the source's accessors. A Row object will be constructed for each item that is added to the ListSource, and each item can be:

  • A dictionary; or
  • Any other iterable object (except for a string), with the accessors being mapped onto the items in the iterable in order of definition; or
  • Any other object, which will be mapped onto the first accessor.

If the ListSource was constructed without specifying accessors, item data must be in dictionary form.

Although Toga provides ListSource, you are not required to create one directly. A ListSource will be transparently constructed if you provide an iterable object to a GUI widget that displays list-like data (i.e., toga.Table, toga.Selection, or toga.DetailedList).

Custom list sources

For more complex applications, you can replace ListSource with a custom data source class. Such a class must:

  • Inherit from Source
  • Provide the same methods as ListSource
  • Return items whose attributes match the accessors expected by the widget
  • Generate a change notification when any of those attributes change
  • Generate insert, remove and clear notifications when items are added or removed

Reference

Bases: Generic[T]

Source code in core/src/toga/sources/list_source.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 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
class Row(Generic[T]):
    def __init__(self, **data: T):
        """Create a new Row object.

        The keyword arguments specified in the constructor will be converted into
        attributes on the new Row object.

        When any public attributes of the Row are modified (i.e., any attribute whose
        name doesn't start with `_`), the source to which the row belongs will be
        notified.
        """
        self._source: Source | None = None
        for name, value in data.items():
            setattr(self, name, value)

    def __repr__(self) -> str:
        descriptor = " ".join(
            f"{attr}={getattr(self, attr)!r}"
            for attr in sorted(self.__dict__)
            if not attr.startswith("_")
        )
        return f"<Row {id(self):x} {descriptor if descriptor else '(no attributes)'}>"

    ######################################################################
    # Utility wrappers
    ######################################################################

    def __setattr__(self, attr: str, value: T) -> None:
        """Set an attribute on the Row object, notifying the source of the change.

        :param attr: The attribute to change.
        :param value: The new attribute value.
        """
        super().__setattr__(attr, value)
        if not attr.startswith("_"):
            if self._source is not None:
                self._source.notify("change", item=self)

    def __delattr__(self, attr: str) -> None:
        """Remove an attribute from the Row object, notifying the source of the change.

        :param attr: The attribute to change.
        """
        super().__delattr__(attr)
        if not attr.startswith("_"):
            if self._source is not None:
                self._source.notify("change", item=self)

__delattr__(attr)

Remove an attribute from the Row object, notifying the source of the change.

:param attr: The attribute to change.

Source code in core/src/toga/sources/list_source.py
 97
 98
 99
100
101
102
103
104
105
def __delattr__(self, attr: str) -> None:
    """Remove an attribute from the Row object, notifying the source of the change.

    :param attr: The attribute to change.
    """
    super().__delattr__(attr)
    if not attr.startswith("_"):
        if self._source is not None:
            self._source.notify("change", item=self)

__init__(**data)

Create a new Row object.

The keyword arguments specified in the constructor will be converted into attributes on the new Row object.

When any public attributes of the Row are modified (i.e., any attribute whose name doesn't start with _), the source to which the row belongs will be notified.

Source code in core/src/toga/sources/list_source.py
60
61
62
63
64
65
66
67
68
69
70
71
72
def __init__(self, **data: T):
    """Create a new Row object.

    The keyword arguments specified in the constructor will be converted into
    attributes on the new Row object.

    When any public attributes of the Row are modified (i.e., any attribute whose
    name doesn't start with `_`), the source to which the row belongs will be
    notified.
    """
    self._source: Source | None = None
    for name, value in data.items():
        setattr(self, name, value)

__setattr__(attr, value)

Set an attribute on the Row object, notifying the source of the change.

:param attr: The attribute to change. :param value: The new attribute value.

Source code in core/src/toga/sources/list_source.py
86
87
88
89
90
91
92
93
94
95
def __setattr__(self, attr: str, value: T) -> None:
    """Set an attribute on the Row object, notifying the source of the change.

    :param attr: The attribute to change.
    :param value: The new attribute value.
    """
    super().__setattr__(attr, value)
    if not attr.startswith("_"):
        if self._source is not None:
            self._source.notify("change", item=self)

A type describing any object adhering to the same interface as ListSource.

Bases: Source

Source code in core/src/toga/sources/list_source.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
class ListSource(Source):
    _data: list[Row]
    _accessors: list[str] | None

    def __init__(
        self,
        accessors: Iterable[str] | None = None,
        data: Iterable | None = None,
    ):
        """A data source to store an ordered list of multiple data values.

        :param accessors: A list of attribute names for accessing the value in each
            column of the row. If omitted, only row data must be specified as a mapping.
        :param data: The initial list of items in the source. Items are converted as
            shown [above][listsource-item].
        """
        super().__init__()
        if accessors is None:
            self._accessors = None
        elif isinstance(accessors, str) or not hasattr(accessors, "__iter__"):
            raise ValueError("accessors should be a list of attribute names")
        else:
            # Copy the list of accessors
            self._accessors = list(accessors)

        # Convert the data into row objects
        if data is not None:
            self._data = [self._create_row(value) for value in data]
        else:
            self._data = []

    @property
    def accessors(self) -> list[str] | None:
        """The attribute names for accessing the value in each column of a row."""
        if self._accessors is None:
            return None
        return self._accessors.copy()

    ######################################################################
    # Methods required by the ListSource interface
    ######################################################################

    def __len__(self) -> int:
        """Returns the number of items in the list."""
        return len(self._data)

    def __getitem__(self, index: int | slice) -> Row:
        """Returns the item at position `index` of the list."""
        return self._data[index]

    def __delitem__(self, index: int) -> None:
        """Deletes the item at position `index` of the list."""
        row = self._data[index]
        del self._data[index]
        self.notify("remove", index=index, item=row)

    ######################################################################
    # Factory methods for new rows
    ######################################################################

    # This behavior is documented in list_source.rst.
    def _create_row(self, data: object) -> Row:
        if isinstance(data, Mapping):
            row = Row(**data)
        elif self._accessors is not None:
            if hasattr(data, "__iter__") and not isinstance(data, str):
                row = Row(**dict(zip(self._accessors, data, strict=False)))
            else:
                row = Row(**{self._accessors[0]: data})
        else:
            raise ValueError("ListSource requires accessors for non-mapping row data")
        row._source = self
        return row

    ######################################################################
    # Utility methods to make ListSources more list-like
    ######################################################################

    def __setitem__(self, index: int, value: object) -> None:
        """Set the value of a specific item in the data source.

        :param index: The item to change
        :param value: The data for the updated item. This data will be converted
            into a Row object.
        """
        row = self._create_row(value)
        self._data[index] = row
        self.notify("change", item=row)

    def clear(self) -> None:
        """Clear all data from the data source."""
        self._data = []
        self.notify("clear")

    def insert(self, index: int, data: object) -> Row:
        """Insert a row into the data source at a specific index.

        :param index: The index at which to insert the item.
        :param data: The data to insert into the ListSource. This data will be converted
            into a Row object.
        :returns: The newly constructed Row object.
        """
        row = self._create_row(data)
        self._data.insert(index, row)
        self.notify("insert", index=index, item=row)
        return row

    def append(self, data: object) -> Row:
        """Insert a row at the end of the data source.

        :param data: The data to append to the ListSource. This data will be converted
            into a Row object.
        :returns: The newly constructed Row object.
        """
        return self.insert(len(self), data)

    def remove(self, row: Row) -> None:
        """Remove a row from the data source.

        :param row: The row to remove from the data source.
        """
        del self[self._data.index(row)]

    def index(self, row: Row) -> int:
        """The index of a specific row in the data source.

        This search uses Row instances, and searches for an *instance* match.
        If two Row instances have the same values, only the Row that is the
        same Python instance will match. To search for values based on equality,
        use [`ListSource.find()`][toga.sources.ListSource.find].

        :param row: The row to find in the data source.
        :returns: The index of the row in the data source.
        :raises ValueError: If the row cannot be found in the data source.
        """
        return self._data.index(row)

    def find(
        self, data: object, start: Row | None = None, default: Any = UNDEFINED
    ) -> Row:
        """Find the first item in the data that matches all the provided
        attributes.

        This is a value based search, rather than an instance search. If two Row
        instances have the same values, the first instance that matches will be
        returned. To search for a second instance, provide the first found instance
        as the `start` argument. To search for a specific Row instance, use the
        [`ListSource.index()`][toga.sources.ListSource.index].

        :param data: The data to search for. Only the values specified in data will be
            used as matching criteria; if the row contains additional data attributes,
            they won't be considered as part of the match.
        :param start: The instance from which to start the search. Defaults to `None`,
            indicating that the first match should be returned.
        :param default: If provided, this value will be returned if no match is found.
        :return: The matching Row object if found, or the value of `default` if
            provided.
        :raises ValueError: If no match is found and `default` is not provided.
        """
        try:
            return _find_item(
                candidates=self._data,
                data=data,
                accessors=self._accessors,
                start=start,
                error=f"No row matching {data!r} in data",
                value_type="row",
            )
        except ValueError:
            if default is UNDEFINED:
                raise
            else:
                return default

accessors property

The attribute names for accessing the value in each column of a row.

__delitem__(index)

Deletes the item at position index of the list.

Source code in core/src/toga/sources/list_source.py
158
159
160
161
162
def __delitem__(self, index: int) -> None:
    """Deletes the item at position `index` of the list."""
    row = self._data[index]
    del self._data[index]
    self.notify("remove", index=index, item=row)

__getitem__(index)

Returns the item at position index of the list.

Source code in core/src/toga/sources/list_source.py
154
155
156
def __getitem__(self, index: int | slice) -> Row:
    """Returns the item at position `index` of the list."""
    return self._data[index]

__init__(accessors=None, data=None)

A data source to store an ordered list of multiple data values.

:param accessors: A list of attribute names for accessing the value in each column of the row. If omitted, only row data must be specified as a mapping. :param data: The initial list of items in the source. Items are converted as shown above.

Source code in core/src/toga/sources/list_source.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def __init__(
    self,
    accessors: Iterable[str] | None = None,
    data: Iterable | None = None,
):
    """A data source to store an ordered list of multiple data values.

    :param accessors: A list of attribute names for accessing the value in each
        column of the row. If omitted, only row data must be specified as a mapping.
    :param data: The initial list of items in the source. Items are converted as
        shown [above][listsource-item].
    """
    super().__init__()
    if accessors is None:
        self._accessors = None
    elif isinstance(accessors, str) or not hasattr(accessors, "__iter__"):
        raise ValueError("accessors should be a list of attribute names")
    else:
        # Copy the list of accessors
        self._accessors = list(accessors)

    # Convert the data into row objects
    if data is not None:
        self._data = [self._create_row(value) for value in data]
    else:
        self._data = []

__len__()

Returns the number of items in the list.

Source code in core/src/toga/sources/list_source.py
150
151
152
def __len__(self) -> int:
    """Returns the number of items in the list."""
    return len(self._data)

__setitem__(index, value)

Set the value of a specific item in the data source.

:param index: The item to change :param value: The data for the updated item. This data will be converted into a Row object.

Source code in core/src/toga/sources/list_source.py
186
187
188
189
190
191
192
193
194
195
def __setitem__(self, index: int, value: object) -> None:
    """Set the value of a specific item in the data source.

    :param index: The item to change
    :param value: The data for the updated item. This data will be converted
        into a Row object.
    """
    row = self._create_row(value)
    self._data[index] = row
    self.notify("change", item=row)

append(data)

Insert a row at the end of the data source.

:param data: The data to append to the ListSource. This data will be converted into a Row object. :returns: The newly constructed Row object.

Source code in core/src/toga/sources/list_source.py
215
216
217
218
219
220
221
222
def append(self, data: object) -> Row:
    """Insert a row at the end of the data source.

    :param data: The data to append to the ListSource. This data will be converted
        into a Row object.
    :returns: The newly constructed Row object.
    """
    return self.insert(len(self), data)

clear()

Clear all data from the data source.

Source code in core/src/toga/sources/list_source.py
197
198
199
200
def clear(self) -> None:
    """Clear all data from the data source."""
    self._data = []
    self.notify("clear")

find(data, start=None, default=UNDEFINED)

Find the first item in the data that matches all the provided attributes.

This is a value based search, rather than an instance search. If two Row instances have the same values, the first instance that matches will be returned. To search for a second instance, provide the first found instance as the start argument. To search for a specific Row instance, use the ListSource.index().

:param data: The data to search for. Only the values specified in data will be used as matching criteria; if the row contains additional data attributes, they won't be considered as part of the match. :param start: The instance from which to start the search. Defaults to None, indicating that the first match should be returned. :param default: If provided, this value will be returned if no match is found. :return: The matching Row object if found, or the value of default if provided. :raises ValueError: If no match is found and default is not provided.

Source code in core/src/toga/sources/list_source.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
def find(
    self, data: object, start: Row | None = None, default: Any = UNDEFINED
) -> Row:
    """Find the first item in the data that matches all the provided
    attributes.

    This is a value based search, rather than an instance search. If two Row
    instances have the same values, the first instance that matches will be
    returned. To search for a second instance, provide the first found instance
    as the `start` argument. To search for a specific Row instance, use the
    [`ListSource.index()`][toga.sources.ListSource.index].

    :param data: The data to search for. Only the values specified in data will be
        used as matching criteria; if the row contains additional data attributes,
        they won't be considered as part of the match.
    :param start: The instance from which to start the search. Defaults to `None`,
        indicating that the first match should be returned.
    :param default: If provided, this value will be returned if no match is found.
    :return: The matching Row object if found, or the value of `default` if
        provided.
    :raises ValueError: If no match is found and `default` is not provided.
    """
    try:
        return _find_item(
            candidates=self._data,
            data=data,
            accessors=self._accessors,
            start=start,
            error=f"No row matching {data!r} in data",
            value_type="row",
        )
    except ValueError:
        if default is UNDEFINED:
            raise
        else:
            return default

index(row)

The index of a specific row in the data source.

This search uses Row instances, and searches for an instance match. If two Row instances have the same values, only the Row that is the same Python instance will match. To search for values based on equality, use ListSource.find().

:param row: The row to find in the data source. :returns: The index of the row in the data source. :raises ValueError: If the row cannot be found in the data source.

Source code in core/src/toga/sources/list_source.py
231
232
233
234
235
236
237
238
239
240
241
242
243
def index(self, row: Row) -> int:
    """The index of a specific row in the data source.

    This search uses Row instances, and searches for an *instance* match.
    If two Row instances have the same values, only the Row that is the
    same Python instance will match. To search for values based on equality,
    use [`ListSource.find()`][toga.sources.ListSource.find].

    :param row: The row to find in the data source.
    :returns: The index of the row in the data source.
    :raises ValueError: If the row cannot be found in the data source.
    """
    return self._data.index(row)

insert(index, data)

Insert a row into the data source at a specific index.

:param index: The index at which to insert the item. :param data: The data to insert into the ListSource. This data will be converted into a Row object. :returns: The newly constructed Row object.

Source code in core/src/toga/sources/list_source.py
202
203
204
205
206
207
208
209
210
211
212
213
def insert(self, index: int, data: object) -> Row:
    """Insert a row into the data source at a specific index.

    :param index: The index at which to insert the item.
    :param data: The data to insert into the ListSource. This data will be converted
        into a Row object.
    :returns: The newly constructed Row object.
    """
    row = self._create_row(data)
    self._data.insert(index, row)
    self.notify("insert", index=index, item=row)
    return row

remove(row)

Remove a row from the data source.

:param row: The row to remove from the data source.

Source code in core/src/toga/sources/list_source.py
224
225
226
227
228
229
def remove(self, row: Row) -> None:
    """Remove a row from the data source.

    :param row: The row to remove from the data source.
    """
    del self[self._data.index(row)]

Bases: ValueListener[ItemT], Protocol, Generic[ItemT]

The protocol that must be implemented by objects that will act as a listener on a list data source.

Source code in core/src/toga/sources/base.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@runtime_checkable
class ListListener(ValueListener[ItemT], Protocol, Generic[ItemT]):
    """The protocol that must be implemented by objects that will act as a listener on
    a list data source.
    """

    def source_insert(self, *, index: int, item: ItemT) -> None:
        """An item has been added to the data source.

        :param index: The 0-index position in the data.
        :param item: The data object that was added.
        """

    def source_remove(self, *, index: int, item: ItemT) -> None:
        """An item has been removed from the data source.

        :param index: The 0-index position in the data.
        :param item: The data object that was added.
        """

    def source_clear(self) -> None:
        """All items have been removed from the data source."""

source_clear()

All items have been removed from the data source.

Source code in core/src/toga/sources/base.py
43
44
def source_clear(self) -> None:
    """All items have been removed from the data source."""

source_insert(*, index, item)

An item has been added to the data source.

:param index: The 0-index position in the data. :param item: The data object that was added.

Source code in core/src/toga/sources/base.py
29
30
31
32
33
34
def source_insert(self, *, index: int, item: ItemT) -> None:
    """An item has been added to the data source.

    :param index: The 0-index position in the data.
    :param item: The data object that was added.
    """

source_remove(*, index, item)

An item has been removed from the data source.

:param index: The 0-index position in the data. :param item: The data object that was added.

Source code in core/src/toga/sources/base.py
36
37
38
39
40
41
def source_remove(self, *, index: int, item: ItemT) -> None:
    """An item has been removed from the data source.

    :param index: The 0-index position in the data.
    :param item: The data object that was added.
    """