Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- It is no longer a NOOP and warning to dismiss a non-active screen. The dismiss will still work, but the screen may not update if the current mode is not active. https://github.com/Textualize/textual/pull/6362
- Added 50ms delay when switching screens to allow state to udpate and prevent janky flash of old content https://github.com/Textualize/textual/pull/6362
- Breaking change: Changed `Select.BLANK` to `Select.NULL` to avoid clash with newer `Widget.BLANK` Classvar https://github.com/Textualize/textual/pull/6374

## [7.5.0] - 2026-01-30

Expand Down
10 changes: 5 additions & 5 deletions docs/widgets/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ The following example presents a `Select` created using the `from_values` class
## Blank state

The `Select` widget has an option `allow_blank` for its constructor.
If set to `True`, the widget may be in a state where there is no selection, in which case its value will be the special constant [`Select.BLANK`][textual.widgets.Select.BLANK].
If set to `True`, the widget may be in a state where there is no selection, in which case its value will be the special constant [`Select.NULL`][textual.widgets.Select.NULL].
The auxiliary methods [`Select.is_blank`][textual.widgets.Select.is_blank] and [`Select.clear`][textual.widgets.Select.clear] provide a convenient way to check if the widget is in this state and to set this state, respectively.

## Type to search
Expand All @@ -98,10 +98,10 @@ The `Select` widget has a `type_to_search` attribute which allows you to type to

## Reactive Attributes

| Name | Type | Default | Description |
|------------|--------------------------------|------------------------------------------------|-------------------------------------|
| `expanded` | `bool` | `False` | True to expand the options overlay. |
| `value` | `SelectType` \| `_NoSelection` | [`Select.BLANK`][textual.widgets.Select.BLANK] | Current value of the Select. |
| Name | Type | Default | Description |
| ---------- | ------------------------------ | -------------------------------------------- | ----------------------------------- |
| `expanded` | `bool` | `False` | True to expand the options overlay. |
| `value` | `SelectType` \| `_NoSelection` | [`Select.NULL`][textual.widgets.Select.NULL] | Current value of the Select. |

## Messages

Expand Down
50 changes: 24 additions & 26 deletions src/textual/widgets/_select.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Generic, Hashable, Iterable, TypeVar, Union
from typing import TYPE_CHECKING, Generic, Hashable, Iterable, TypeVar

import rich.repr
from rich.console import RenderableType
Expand All @@ -28,13 +28,13 @@ class NonSelectableStatic(Static):


class NoSelection:
"""Used by the `Select` widget to flag the unselected state. See [`Select.BLANK`][textual.widgets.Select.BLANK]."""
"""Used by the `Select` widget to flag the unselected state. See [`Select.NULL`][textual.widgets.Select.NULL]."""

def __repr__(self) -> str:
return "Select.BLANK"
return "Select.NULL"


BLANK = NoSelection()
NULL = NoSelection()


class InvalidSelectValueError(Exception):
Expand Down Expand Up @@ -240,7 +240,7 @@ def __init__(self, placeholder: str) -> None:
"""
super().__init__()
self.placeholder = placeholder
self.label: RenderableType | NoSelection = Select.BLANK
self.label: RenderableType | NoSelection = Select.NULL

def update(self, label: RenderableType | NoSelection) -> None:
"""Update the content in the widget.
Expand All @@ -249,7 +249,7 @@ def update(self, label: RenderableType | NoSelection) -> None:
label: A renderable to display, or `None` for the placeholder.
"""
self.label = label
self.has_value = label is not Select.BLANK
self.has_value = label is not Select.NULL
self.query_one("#label", Static).update(
self.placeholder if isinstance(label, NoSelection) else label
)
Expand Down Expand Up @@ -283,7 +283,7 @@ class Select(Generic[SelectType], Vertical, can_focus=True):
When activated with ++enter++ the widget displays an overlay with a list of all possible options.
"""

BLANK = BLANK
NULL = NULL
"""Constant to flag that the widget has no selection."""

BINDINGS = [
Expand Down Expand Up @@ -356,12 +356,10 @@ class Select(Generic[SelectType], Vertical, can_focus=True):
"""True to show the overlay, otherwise False."""
prompt: var[str] = var[str]("Select")
"""The prompt to show when no value is selected."""
value: var[SelectType | NoSelection] = var[Union[SelectType, NoSelection]](
BLANK, init=False
)
value: var[SelectType | NoSelection] = var(NULL, init=False)
"""The value of the selection.

If the widget has no selection, its value will be [`Select.BLANK`][textual.widgets.Select.BLANK].
If the widget has no selection, its value will be [`Select.NULL`][textual.widgets.Select.NULL].
Setting this to an illegal value will raise a [`InvalidSelectValueError`][textual.widgets.select.InvalidSelectValueError]
exception.
"""
Expand Down Expand Up @@ -403,7 +401,7 @@ def __init__(
*,
prompt: str = "Select",
allow_blank: bool = True,
value: SelectType | NoSelection = BLANK,
value: SelectType | NoSelection = NULL,
type_to_search: bool = True,
name: str | None = None,
id: str | None = None,
Expand All @@ -420,7 +418,7 @@ def __init__(
prompt: Text to show in the control when no option is selected.
allow_blank: Enables or disables the ability to have the widget in a state
with no selection made, in which case its value is set to the constant
[`Select.BLANK`][textual.widgets.Select.BLANK].
[`Select.NULL`][textual.widgets.Select.NULL].
value: Initial value selected. Should be one of the values in `options`.
If no initial value is set and `allow_blank` is `False`, the widget
will auto-select the first available option.
Expand Down Expand Up @@ -452,7 +450,7 @@ def from_values(
*,
prompt: str = "Select",
allow_blank: bool = True,
value: SelectType | NoSelection = BLANK,
value: SelectType | NoSelection = NULL,
type_to_search: bool = True,
name: str | None = None,
id: str | None = None,
Expand All @@ -470,7 +468,7 @@ def from_values(
prompt: Text to show in the control when no option is selected.
allow_blank: Enables or disables the ability to have the widget in a state
with no selection made, in which case its value is set to the constant
[`Select.BLANK`][textual.widgets.Select.BLANK].
[`Select.NULL`][textual.widgets.Select.NULL].
value: Initial value selected. Should be one of the values in `values`.
If no initial value is set and `allow_blank` is `False`, the widget
will auto-select the first available value.
Expand Down Expand Up @@ -522,7 +520,7 @@ def _setup_variables_for_options(
"""
self._options: list[tuple[RenderableType, SelectType | NoSelection]] = []
if self._allow_blank:
self._options.append(("", self.BLANK))
self._options.append(("", self.NULL))
self._options.extend(options)

if not self._options:
Expand All @@ -539,7 +537,7 @@ def _setup_options_renderables(self) -> None:
options: list[Option] = [
(
Option(Text(self.prompt, style="dim"))
if value == self.BLANK
if value == self.NULL
else Option(prompt)
)
for prompt, value in self._options
Expand All @@ -549,9 +547,9 @@ def _setup_options_renderables(self) -> None:
option_list.clear_options()
option_list.add_options(options)

def _init_selected_option(self, hint: SelectType | NoSelection = BLANK) -> None:
def _init_selected_option(self, hint: SelectType | NoSelection = NULL) -> None:
"""Initialises the selected option for the `Select`."""
if hint == self.BLANK and not self._allow_blank:
if hint == self.NULL and not self._allow_blank:
hint = self._options[0][1]
self.value = hint

Expand Down Expand Up @@ -604,8 +602,8 @@ def _watch_value(self, value: SelectType | NoSelection) -> None:
except NoMatches:
pass
else:
if value == self.BLANK:
select_current.update(self.BLANK)
if value == self.NULL:
select_current.update(self.NULL)
else:
for index, (prompt, _value) in enumerate(self._options):
if _value == value:
Expand Down Expand Up @@ -637,7 +635,7 @@ def _watch_expanded(self, expanded: bool) -> None:
self.set_class(expanded, "-expanded")
if expanded:
overlay.focus(scroll_visible=False)
if self.value is self.BLANK:
if self.value is self.NULL:
overlay.select(None)
self.query_one(SelectCurrent).has_value = False
else:
Expand Down Expand Up @@ -690,7 +688,7 @@ def is_blank(self) -> bool:
Returns:
True if the selection is blank, False otherwise.
"""
return self.value == self.BLANK
return self.value == self.NULL

def clear(self) -> None:
"""Clear the selection if `allow_blank` is `True`.
Expand All @@ -699,7 +697,7 @@ def clear(self) -> None:
InvalidSelectValueError: If `allow_blank` is set to `False`.
"""
try:
self.value = self.BLANK
self.value = self.NULL
except InvalidSelectValueError:
raise InvalidSelectValueError(
"Can't clear selection if allow_blank is set to False."
Expand All @@ -712,7 +710,7 @@ def _watch_prompt(self, prompt: str) -> None:
select_current.placeholder = prompt
if not self._allow_blank:
return
if self.value == self.BLANK:
select_current.update(self.BLANK)
if self.value == self.NULL:
select_current.update(self.NULL)
option_list = self.query_one(SelectOverlay)
option_list.replace_option_prompt_at_index(0, Text(prompt, style="dim"))
4 changes: 2 additions & 2 deletions src/textual/widgets/select.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from textual.widgets._select import (
BLANK,
NULL,
EmptySelectError,
InvalidSelectValueError,
NoSelection,
)

__all__ = ["EmptySelectError", "InvalidSelectValueError", "NoSelection", "BLANK"]
__all__ = ["EmptySelectError", "InvalidSelectValueError", "NoSelection", "NULL"]
11 changes: 6 additions & 5 deletions tests/select/test_blank_and_clear.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def compose(self):
app = SelectApp()
async with app.run_test():
select = app.query_one(Select)
assert select.value == Select.BLANK
assert select.value == Select.NULL
assert select.is_blank()


Expand All @@ -27,15 +27,15 @@ def compose(self):
app = SelectApp()
async with app.run_test():
select = app.query_one(Select)
assert select.value == Select.BLANK
assert select.value == Select.NULL
assert select.is_blank()

select.value = 0
assert select.value != Select.BLANK
assert select.value != Select.NULL
assert not select.is_blank()

select.value = Select.BLANK
assert select.value == Select.BLANK
select.value = Select.NULL
assert select.value == Select.NULL
assert select.is_blank()


Expand Down Expand Up @@ -64,6 +64,7 @@ def compose(self):
with pytest.raises(InvalidSelectValueError):
select.clear()


async def test_selection_is_none_with_blank():
class SelectApp(App[None]):
def compose(self):
Expand Down
12 changes: 6 additions & 6 deletions tests/select/test_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


class SelectApp(App[None]):
def __init__(self, initial_value=Select.BLANK):
def __init__(self, initial_value=Select.NULL):
self.initial_value = initial_value
super().__init__()

Expand Down Expand Up @@ -51,12 +51,12 @@ def compose(self):


async def test_value_assign_to_blank():
"""Setting the value to BLANK should work with default `allow_blank` value."""
"""Setting the value to NULL should work with default `allow_blank` value."""
app = SelectApp(1)
async with app.run_test():
select = app.query_one(Select)
assert select.value == 1
select.value = Select.BLANK
select.value = Select.NULL
assert select.is_blank()


Expand Down Expand Up @@ -85,7 +85,7 @@ def compose(self):


async def test_set_value_to_blank_with_allow_blank_false():
"""Setting the value to BLANK with allow_blank=False should raise an error."""
"""Setting the value to NULL with allow_blank=False should raise an error."""

class SelectApp(App[None]):
def compose(self):
Expand All @@ -94,11 +94,11 @@ def compose(self):
app = SelectApp()
async with app.run_test():
with pytest.raises(InvalidSelectValueError):
app.query_one(Select).value = Select.BLANK
app.query_one(Select).value = Select.NULL


async def test_set_options_resets_value_to_blank():
"""Resetting the options should reset the value to BLANK."""
"""Resetting the options should reset the value to NULL."""

class SelectApp(App[None]):
def compose(self):
Expand Down
Loading