File: queries.md

package info (click to toggle)
textual 2.1.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 55,056 kB
  • sloc: python: 85,423; lisp: 1,669; makefile: 101
file content (194 lines) | stat: -rw-r--r-- 8,603 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
106
107
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
# DOM Queries

In the [CSS chapter](./CSS.md) we introduced the [DOM](../guide/CSS.md#the-dom) which is how Textual apps keep track of widgets. We saw how you can apply styles to the DOM with CSS [selectors](./CSS.md#selectors).

Selectors are a very useful idea and can do more than apply styles. We can also find widgets in Python code with selectors, and make updates to widgets in a simple expressive way. Let's look at how!

!!! tip

    See the [Textual Query Sandbox](https://github.com/davep/textual-query-sandbox/) project for an interactive way of experimenting with DOM queries.

## Query one

The [query_one][textual.dom.DOMNode.query_one] method is used to retrieve a single widget that matches a selector or a type.

Let's say we have a widget with an ID of `send` and we want to get a reference to it in our app.
We could do this with the following line of code:

```python
send_button = self.query_one("#send")
```

This will retrieve the first widget discovered with an ID of `send`.
If there are no matching widgets, Textual will raise a [NoMatches][textual.css.query.NoMatches] exception.

You can also add a second parameter for the expected type, which will ensure that you get the type you are expecting.

```python
send_button = self.query_one("#send", Button)
```

If the matched widget is *not* a button (i.e. if `isinstance(widget, Button)` equals `False`), Textual will raise a [WrongType][textual.css.query.WrongType] exception.

!!! tip

    The second parameter allows type-checkers like MyPy to know the exact return type. Without it, MyPy would only know the result of `query_one` is a Widget (the base class).

You can also specify a widget type in place of a selector, which will return a widget of that type.
For instance, the following would return a `Button` instance (assuming there is a single Button).

```python
my_button = self.query_one(Button)
```

`query_one` searches the DOM *below* the widget it is called on, so if you call `query_one` on a widget, it will only find widgets that are descendants of that widget.

If you wish to search the entire DOM, you should call `query_one` on the `App` or `Screen` instance.

```python
# Search the entire Screen for a widget with an ID of "send-email"
self.screen.query_one("#send-email")
```

## Making queries

Apps and widgets also have a [query][textual.dom.DOMNode.query] method which finds (or queries) widgets. This method returns a [DOMQuery][textual.css.query.DOMQuery] object which is a list-like container of widgets.

If you call `query` with no arguments, you will get back a `DOMQuery` containing all widgets. This method is *recursive*, meaning it will also return child widgets (as many levels as required).

Here's how you might iterate over all the widgets in your app:

```python
for widget in self.query():
    print(widget)
```

Called on the `app`, this will retrieve all widgets in the app. If you call the same method on a widget, it will return the children of that widget.

!!! note

    All the query and related methods work on both App and Widget sub-classes.

### Query selectors

You can call `query` with a CSS selector. Let's look a few examples:

If we want to find all the button widgets, we could do something like the following:

```python
for button in self.query("Button"):
    print(button)
```

Any selector that works in CSS will work with the `query` method. For instance, if we want to find all the disabled buttons in a Dialog widget, we could do this:

```python
for button in self.query("Dialog Button.disabled"):
    print(button)
```

!!! info

    The selector `Dialog Button.disabled` says find all the `Button` with a CSS class of `disabled` that are a child of a `Dialog` widget.

### Results

Query objects have a [results][textual.css.query.DOMQuery.results] method which is an alternative way of iterating over widgets. If you supply a type (i.e. a Widget class) then this method will generate only objects of that type.

The following example queries for widgets with the `disabled` CSS class and iterates over just the Button objects.

```python
for button in self.query(".disabled").results(Button):
    print(button)
```

!!! tip

    This method allows type-checkers like MyPy to know the exact type of the object in the loop. Without it, MyPy would only know that `button` is a `Widget` (the base class).

## Query objects

We've seen that the [query][textual.dom.DOMNode.query] method returns a [DOMQuery][textual.css.query.DOMQuery] object you can iterate over in a for loop. Query objects behave like Python lists and support all of the same operations (such as `query[0]`, `len(query)` ,`reverse(query)` etc). They also have a number of other methods to simplify retrieving and modifying widgets.

## First and last

The [first][textual.css.query.DOMQuery.first] and [last][textual.css.query.DOMQuery.last] methods return the first or last matching widget from the selector, respectively.

Here's how we might find the _last_ button in an app:

```python
last_button = self.query("Button").last()
```

If there are no buttons, Textual will raise a [NoMatches][textual.css.query.NoMatches] exception. Otherwise it will return a button widget.

Both `first()` and `last()` accept an `expect_type` argument that should be the class of the widget you are expecting. Let's say we want to get the last widget with class `.disabled`, and we want to check it really is a button. We could do this:

```python
disabled_button = self.query(".disabled").last(Button)
```

The query selects all widgets with a `disabled` CSS class. The `last` method gets the last disabled widget and checks it is a `Button` and not any other kind of widget.

If the last widget is *not* a button, Textual will raise a [WrongType][textual.css.query.WrongType] exception.

!!! tip

    Specifying the expected type allows type-checkers like MyPy to know the exact return type.

## Filter

Query objects have a [filter][textual.css.query.DOMQuery.filter] method which further refines a query. This method will return a new query object with widgets that match both the original query _and_ the new selector.

Let's say we have a query which gets all the buttons in an app, and we want a new query object with just the disabled buttons. We could write something like this:

```python
# Get all the Buttons
buttons_query = self.query("Button")
# Buttons with 'disabled' CSS class
disabled_buttons = buttons_query.filter(".disabled")
```

Iterating over `disabled_buttons` will give us all the disabled buttons.

## Exclude

Query objects have an [exclude][textual.css.query.DOMQuery.exclude] method which is the logical opposite of [filter][textual.css.query.DOMQuery.filter]. The `exclude` method removes any widgets from the query object which match a selector.

Here's how we could get all the buttons which *don't* have the `disabled` class set.

```python
# Get all the Buttons
buttons_query = self.query("Button")
# Remove all the Buttons with the 'disabled' CSS class
enabled_buttons = buttons_query.exclude(".disabled")
```

## Loop-free operations

Once you have a query object, you can loop over it to call methods on the matched widgets. Query objects also support a number of methods which make an update to every matched widget without an explicit loop.

For instance, let's say we want to disable all buttons in an app. We could do this by calling [add_class()][textual.css.query.DOMQuery.add_class] on a query object.

```python
self.query("Button").add_class("disabled")
```

This single line is equivalent to the following:

```python
for widget in self.query("Button"):
    widget.add_class("disabled")
```

Here are the other loop-free methods on query objects:

- [add_class][textual.css.query.DOMQuery.add_class] Adds a CSS class (or classes) to matched widgets.
- [blur][textual.css.query.DOMQuery.focus] Blurs (removes focus) from matching widgets.
- [focus][textual.css.query.DOMQuery.focus] Focuses the first matching widgets.
- [refresh][textual.css.query.DOMQuery.refresh] Refreshes matched widgets.
- [remove_class][textual.css.query.DOMQuery.remove_class] Removes a CSS class (or classes) from matched widgets.
- [remove][textual.css.query.DOMQuery.remove] Removes matched widgets from the DOM.
- [set_class][textual.css.query.DOMQuery.set_class] Sets a CSS class (or classes) on matched widgets.
- [set][textual.css.query.DOMQuery.set] Sets common attributes on a widget.
- [toggle_class][textual.css.query.DOMQuery.toggle_class] Sets a CSS class (or classes) if it is not set, or removes the class (or classes) if they are set on the matched widgets.