Skip to content

Commit

Permalink
Enable new_value_mode="add" for dict options (#2467)
Browse files Browse the repository at this point in the history
* Enable new_value_mode="add" for dict options

Enable new_value_mode="add" for dict options

* Forgot to update the check in __init__

* Fix typos

* Enable _new_id function to receive value argument to generate an id for.

Signed-off-by: Alexander Zarubkin <[email protected]>

* code review

* Fix bug with duplicates not adding to dict with mode == 'add'

Forgot to handle the case when mode == 'add'.

* add pytest

* code review

---------

Signed-off-by: Alexander Zarubkin <[email protected]>
Co-authored-by: Alexander Zarubkin <[email protected]>
Co-authored-by: Falko Schindler <[email protected]>
  • Loading branch information
3 people authored Mar 5, 2024
1 parent c7fd5d8 commit ed8c94c
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 10 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"--disable=R0913", // Too many arguments
"--disable=R0914", // Too many local variables
"--disable=R0915", // Too many statements
"--disable=R1705", // Unnecessary "else" after "return"
"--disable=W0102", // Dangerous default value as argument
"--disable=W0718", // Catching too general exception
"--disable=W1203", // Use % formatting in logging functions
Expand Down
43 changes: 33 additions & 10 deletions nicegui/elements/select.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections.abc import Generator, Iterable
from copy import deepcopy
from typing import Any, Callable, Dict, List, Literal, Optional, Union
from typing import Any, Callable, Dict, Iterator, List, Literal, Optional, Union

from ..events import GenericEventArguments
from .choice_element import ChoiceElement
Expand All @@ -19,6 +20,7 @@ def __init__(self,
multiple: bool = False,
clearable: bool = False,
validation: Optional[Union[Callable[..., Optional[str]], Dict[str, Callable[..., bool]]]] = None,
key_generator: Optional[Union[Callable[[Any], Any], Iterator[Any]]] = None,
) -> None:
"""Dropdown Selection
Expand Down Expand Up @@ -48,6 +50,7 @@ def __init__(self,
:param multiple: whether to allow multiple selections
:param clearable: whether to add a button to clear the selection
:param validation: dictionary of validation rules or a callable that returns an optional error message
:param key_generator: a callback or iterator to generate a dictionary key for new values
"""
self.multiple = multiple
if multiple:
Expand All @@ -58,9 +61,12 @@ def __init__(self,
super().__init__(options=options, value=value, on_change=on_change, validation=validation)
if label is not None:
self._props['label'] = label
if isinstance(key_generator, Generator):
next(key_generator) # prime the key generator, prepare it to receive the first value
self.key_generator = key_generator
if new_value_mode is not None:
if isinstance(options, dict) and new_value_mode == 'add':
raise ValueError('new_value_mode "add" is not supported for dict options')
if isinstance(options, dict) and new_value_mode == 'add' and key_generator is None:
raise ValueError('new_value_mode "add" is not supported for dict options without key_generator')
self._props['new-value-mode'] = new_value_mode
with_input = True
if with_input:
Expand Down Expand Up @@ -88,8 +94,8 @@ def _event_args_to_value(self, e: GenericEventArguments) -> Any:
return None
else:
if isinstance(e.args, str):
self._handle_new_value(e.args)
return e.args if e.args in self._values else None
new_value = self._handle_new_value(e.args)
return new_value if new_value in self._values else None
else:
return self._values[e.args['value']]

Expand All @@ -111,7 +117,16 @@ def _value_to_model_value(self, value: Any) -> Any:
except ValueError:
return None

def _handle_new_value(self, value: str) -> None:
def _generate_key(self, value: str) -> Any:
if isinstance(self.key_generator, Generator):
return self.key_generator.send(value)
if isinstance(self.key_generator, Iterable):
return next(self.key_generator)
if callable(self.key_generator):
return self.key_generator(value)
return value

def _handle_new_value(self, value: str) -> Any:
mode = self._props['new-value-mode']
if isinstance(self.options, list):
if mode == 'add':
Expand All @@ -125,13 +140,21 @@ def _handle_new_value(self, value: str) -> None:
else:
self.options.append(value)
# NOTE: self._labels and self._values are updated via self.options since they share the same references
return value
else:
if mode in 'add-unique':
if value not in self.options:
self.options[value] = value
key = value
if mode == 'add':
key = self._generate_key(value)
self.options[key] = value
elif mode == 'add-unique':
if value not in self.options.values():
key = self._generate_key(value)
self.options[key] = value
elif mode == 'toggle':
if value in self.options:
self.options.pop(value)
else:
self.options.update({value: value})
key = self._generate_key(value)
self.options.update({key: value})
self._update_values_and_labels()
return key
12 changes: 12 additions & 0 deletions tests/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,18 @@ def test_add_new_values(screen: Screen, option_dict: bool, multiple: bool, new_
"options = ['a', 'b', 'c']")


def test_id_generator(screen: Screen):
options = {'a': 'A', 'b': 'B', 'c': 'C'}
select = ui.select(options, value='b', new_value_mode='add', key_generator=lambda _: len(options))
ui.label().bind_text_from(select, 'options', lambda v: f'options = {v}')

screen.open('/')
screen.find_by_tag('input').send_keys(Keys.BACKSPACE + 'd')
screen.wait(0.5)
screen.find_by_tag('input').send_keys(Keys.ENTER)
screen.should_contain("options = {'a': 'A', 'b': 'B', 'c': 'C', 3: 'd'}")


@pytest.mark.parametrize('multiple', [False, True])
def test_keep_filtered_options(multiple: bool, screen: Screen):
ui.select(options=['A1', 'A2', 'B1', 'B2'], with_input=True, multiple=multiple)
Expand Down

0 comments on commit ed8c94c

Please sign in to comment.