Skip to content

Commit

Permalink
Allow callables to be used as options for NestedSelect (#5969)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuang11 authored Dec 2, 2023
1 parent cdb9e70 commit 6690bd3
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 23 deletions.
73 changes: 71 additions & 2 deletions examples/reference/widgets/NestedSelect.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
"\n",
"##### Core\n",
"\n",
"* **``options``** (dict): The options to select from. The options may be nested dictionaries or lists.\n",
"* **``options``** (dict | callable): The options to select from. The options may be nested dictionaries, lists, or callables that return those types. If callables are used, the callables must accept `level` and `value` keyword arguments, where `level` is the level that updated and `value` is a dictionary of the current values, containing keys up to the level that was updated.\n",
"* **``value``** (dict): The value from all the Select widgets; the keys are the levels names. If no levels names are specified, the keys are the levels indices.\n",
"* **``levels``** (list): Either a list of strings or a list of dictionaries. If a list of strings, the strings are used as the names of the levels. If a list of dictionaries, each dictionary may have a \"name\" key, which is used as the name of the level, a \"type\" key, which is used as the type of widget, and any corresponding widget keyword arguments.\n",
"* **``levels``** (list): Either a list of strings or a list of dictionaries. If a list of strings, the strings are used as the names of the levels. If a list of dictionaries, each dictionary may have a \"name\" key, which is used as the name of the level, a \"type\" key, which is used as the type of widget, and any corresponding widget keyword arguments. Must be specified if options is callable.\n",
"\n",
"##### Display\n",
"\n",
Expand Down Expand Up @@ -207,6 +207,75 @@
"select.value = {\"Model\": \"NAME\"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Alternatively, `options` can be a callable. Its nested levels too: e.g. `options={\"Daily\": list_options, \"Monthly\": list_options}`.\n",
"\n",
"If callables are used, the callables must accept `level` and `value` keyword arguments, where `level` is the level that updated and `value` is a dictionary of the current values, containing keys up to the level that was updated.\n",
"\n",
"Note, the callable can vary across `options`, and `levels` must be provided if any of the `options` is callable."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def list_options(level, value):\n",
" if level == \"time_step\":\n",
" options = {\"Daily\": list_options, \"Monthly\": list_options}\n",
" elif level == \"level_type\":\n",
" options = {f\"{value['time_step']}_upper\": list_options, f\"{value['time_step']}_lower\": list_options}\n",
" else:\n",
" options = [f\"{value['level_type']}.json\", f\"{value['level_type']}.csv\"]\n",
"\n",
" return options\n",
"\n",
"pn.widgets.NestedSelect(\n",
" options=list_options,\n",
" levels=[\"time_step\", \"level_type\", \"file_type\"],\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This is useful if you are trying to use options from a hosted source.\n",
"\n",
"Using `pn.cache` here can help improve user experience and reduce the risk of rate limits.\n",
"\n",
"```python\n",
"import panel as pn\n",
"pn.extension()\n",
"\n",
"@pn.cache()\n",
"def list_options(level, value):\n",
" value_path = \"/\".join(list(value.values()))\n",
" url = f\"https://downloads.psl.noaa.gov/Datasets/ncep.reanalysis/{value_path}\"\n",
"\n",
" options = [var.rstrip(\"/\") for var in pd.read_html(url)[0][\"Name\"].dropna()[1:]]\n",
" if level == \"time_step\":\n",
" options = {option: list_options for option in options if option[0].isupper()}\n",
" elif level == \"level_type\":\n",
" options = {option: list_options for option in options if option[0].islower()}\n",
" else:\n",
" options = [option for option in options if option.endswith(\".nc\")]\n",
"\n",
" return options\n",
"\n",
"\n",
"select = pn.widgets.NestedSelect(\n",
" options=list_options,\n",
" levels=[\"time_step\", \"level_type\", \"file\"],\n",
")\n",
"select\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
66 changes: 66 additions & 0 deletions panel/tests/widgets/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,72 @@ def test_nested_select_custom_widgets(document, comm):
assert widget_2.options == [1000, 925, 700, 500, 300]


def test_nested_select_callable_top_level(document, comm):
def list_options(level, value):
if level == "time_step":
options = {"Daily": list_options, "Monthly": list_options}
elif level == "level_type":
options = {f"{value['time_step']}_upper": list_options, f"{value['time_step']}_lower": list_options}
else:
options = [f"{value['level_type']}.json", f"{value['level_type']}.csv"]

return options

select = NestedSelect(
options=list_options,
levels=["time_step", "level_type", "file"],
)
assert select._widgets[0].options == ["Daily", "Monthly"]
assert select._widgets[1].options == ["Daily_upper", "Daily_lower"]
assert select._widgets[2].options == ["Daily_upper.json", "Daily_upper.csv"]
assert select.value == {"time_step": "Daily", "level_type": "Daily_upper", "file": "Daily_upper.json"}
assert select._max_depth == 3

select.value = {"time_step": "Monthly"}
assert select._widgets[0].options == ["Daily", "Monthly"]
assert select._widgets[1].options == ["Monthly_upper", "Monthly_lower"]
assert select._widgets[2].options == ["Monthly_upper.json", "Monthly_upper.csv"]
assert select.value == {"time_step": "Monthly", "level_type": "Monthly_upper", "file": "Monthly_upper.json"}
assert select._max_depth == 3


def test_nested_select_callable_mid_level(document, comm):
def list_options(level, value):
if level == "level_type":
options = {f"{value['time_step']}_upper": list_options, f"{value['time_step']}_lower": list_options}
else:
options = [f"{value['level_type']}.json", f"{value['level_type']}.csv"]

return options

select = NestedSelect(
options={"Daily": list_options, "Monthly": list_options},
levels=["time_step", "level_type", "file"],
)
assert select._widgets[0].options == ["Daily", "Monthly"]
assert select._widgets[1].options == ["Daily_upper", "Daily_lower"]
assert select._widgets[2].options == ["Daily_upper.json", "Daily_upper.csv"]
assert select.value == {"time_step": "Daily", "level_type": "Daily_upper", "file": "Daily_upper.json"}
assert select._max_depth == 3

select.value = {"time_step": "Monthly"}
assert select._widgets[0].options == ["Daily", "Monthly"]
assert select._widgets[1].options == ["Monthly_upper", "Monthly_lower"]
assert select._widgets[2].options == ["Monthly_upper.json", "Monthly_upper.csv"]
assert select.value == {"time_step": "Monthly", "level_type": "Monthly_upper", "file": "Monthly_upper.json"}
assert select._max_depth == 3


def test_nested_select_callable_must_have_levels(document, comm):
def list_options(level, value):
pass

with pytest.raises(ValueError, match="levels must be specified"):
NestedSelect(
options={"Daily": list_options, "Monthly": list_options},
)


@pytest.mark.parametrize('options', [[10, 20], dict(A=10, B=20)], ids=['list', 'dict'])
@pytest.mark.parametrize('size', [1, 2], ids=['size=1', 'size>1'])
def test_select_disabled_options_init(options, size, document, comm):
Expand Down
Loading

0 comments on commit 6690bd3

Please sign in to comment.