diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e44796f6..20bf81a8a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/widgets/select.md b/docs/widgets/select.md index 08e0ee8b72..59cf2c165a 100644 --- a/docs/widgets/select.md +++ b/docs/widgets/select.md @@ -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 @@ -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 diff --git a/src/textual/widgets/_select.py b/src/textual/widgets/_select.py index 221321b97d..6b16ecd1ec 100644 --- a/src/textual/widgets/_select.py +++ b/src/textual/widgets/_select.py @@ -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 @@ -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): @@ -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. @@ -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 ) @@ -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 = [ @@ -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. """ @@ -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, @@ -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. @@ -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, @@ -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. @@ -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: @@ -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 @@ -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 @@ -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: @@ -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: @@ -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`. @@ -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." @@ -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")) diff --git a/src/textual/widgets/select.py b/src/textual/widgets/select.py index 91b5b3657c..b69f9e121d 100644 --- a/src/textual/widgets/select.py +++ b/src/textual/widgets/select.py @@ -1,8 +1,8 @@ from textual.widgets._select import ( - BLANK, + NULL, EmptySelectError, InvalidSelectValueError, NoSelection, ) -__all__ = ["EmptySelectError", "InvalidSelectValueError", "NoSelection", "BLANK"] +__all__ = ["EmptySelectError", "InvalidSelectValueError", "NoSelection", "NULL"] diff --git a/tests/select/test_blank_and_clear.py b/tests/select/test_blank_and_clear.py index 4d1921451f..0839df1423 100644 --- a/tests/select/test_blank_and_clear.py +++ b/tests/select/test_blank_and_clear.py @@ -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() @@ -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() @@ -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): diff --git a/tests/select/test_value.py b/tests/select/test_value.py index 4cab339e56..b579105476 100644 --- a/tests/select/test_value.py +++ b/tests/select/test_value.py @@ -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__() @@ -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() @@ -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): @@ -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):