Skip to content

Commit d5f8dc7

Browse files
authored
Merge branch 'dev' into fix/multi-set-props
2 parents 8229a0e + fbaed89 commit d5f8dc7

File tree

5 files changed

+159
-7
lines changed

5 files changed

+159
-7
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## UNRELEASED
6+
7+
## Fixed
8+
9+
- [#2854](https://github.com/plotly/dash/pull/2854) Fix dcc.Dropdown resetting empty values to null and triggering callbacks. Fixes [#2850](https://github.com/plotly/dash/issues/2850)
10+
- [#2859](https://github.com/plotly/dash/pull/2859) Fix base patch operators. fixes [#2855](https://github.com/plotly/dash/issues/2855)
11+
512
## [2.17.0] - 2024-05-03
613

714
## Added

components/dash-core-components/src/fragments/Dropdown.react.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {isNil, pluck, without, pick} from 'ramda';
1+
import {isNil, pluck, without, pick, isEmpty} from 'ramda';
22
import React, {useState, useCallback, useEffect, useMemo, useRef} from 'react';
33
import ReactDropdown from 'react-virtualized-select';
44
import createFilterOptions from 'react-select-fast-filter-options';
@@ -125,7 +125,8 @@ const Dropdown = props => {
125125
!search_value &&
126126
!isNil(sanitizedOptions) &&
127127
optionsCheck !== sanitizedOptions &&
128-
!isNil(value)
128+
!isNil(value) &&
129+
!isEmpty(value)
129130
) {
130131
const values = sanitizedOptions.map(option => option.value);
131132
if (multi && Array.isArray(value)) {

components/dash-core-components/tests/integration/dropdown/test_remove_option.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
import pytest
44

5-
from dash import Dash, html, dcc, Output, Input, State
5+
from dash import Dash, html, dcc, Output, Input, State, Patch
66
from dash.exceptions import PreventUpdate
77

8+
from selenium.webdriver.common.keys import Keys
89

910
sample_dropdown_options = [
1011
{"label": "New York City", "value": "NYC"},
@@ -147,3 +148,44 @@ def print_value(n_clicks, options, value):
147148
btn.click()
148149
dash_dcc.wait_for_text_to_equal("#value-output", '["NYC"]')
149150
dash_dcc.wait_for_text_to_equal("#options-output", '["MTL", "NYC"]')
151+
152+
153+
def test_ddro004_empty_string_not_updated(dash_dcc):
154+
# The value should stay as empty string and not null.
155+
app = Dash()
156+
app.layout = html.Div(
157+
[
158+
dcc.Dropdown(["a", "b", "c"], value="", id="drop"),
159+
html.Div(id="output"),
160+
dcc.Store(data={"count": 0}, id="count"),
161+
html.Div(id="count-output"),
162+
]
163+
)
164+
165+
@app.callback(
166+
Output("output", "children"),
167+
Output("count", "data"),
168+
Input("drop", "value"),
169+
)
170+
def on_value(value):
171+
count = Patch()
172+
count.count += 1
173+
if value is None:
174+
return "Value is none", count
175+
return f"Value={value}", count
176+
177+
app.clientside_callback(
178+
"data => data.count", Output("count-output", "children"), Input("count", "data")
179+
)
180+
181+
dash_dcc.start_server(app)
182+
dash_dcc.wait_for_text_to_equal("#output", "Value=")
183+
184+
dash_dcc.wait_for_text_to_equal("#count-output", "1")
185+
186+
select_input = dash_dcc.find_element("#drop input")
187+
select_input.send_keys("a")
188+
select_input.send_keys(Keys.ENTER)
189+
190+
dash_dcc.wait_for_text_to_equal("#output", "Value=a")
191+
dash_dcc.wait_for_text_to_equal("#count-output", "2")

dash/_patch.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,18 @@ def __getstate__(self):
3636
def __setstate__(self, state):
3737
vars(self).update(state)
3838

39-
def __getitem__(self, item):
39+
def __getitem__(self, item) -> "Patch":
4040
validate_slice(item)
4141
return Patch(location=self._location + [item], parent=self)
4242

43-
def __getattr__(self, item):
43+
def __getattr__(self, item) -> "Patch":
4444
if item == "tolist":
4545
# to_json fix
4646
raise AttributeError
4747
if item == "_location":
48-
return self._location
48+
return self._location # type: ignore
4949
if item == "_operations":
50-
return self._operations
50+
return self._operations # type: ignore
5151
return self.__getitem__(item)
5252

5353
def __setattr__(self, key, value):
@@ -81,22 +81,32 @@ def __iadd__(self, other):
8181
self.extend(other)
8282
else:
8383
self._operations.append(_operation("Add", self._location, value=other))
84+
if not self._location:
85+
return self
8486
return _noop
8587

8688
def __isub__(self, other):
8789
self._operations.append(_operation("Sub", self._location, value=other))
90+
if not self._location:
91+
return self
8892
return _noop
8993

9094
def __imul__(self, other):
9195
self._operations.append(_operation("Mul", self._location, value=other))
96+
if not self._location:
97+
return self
9298
return _noop
9399

94100
def __itruediv__(self, other):
95101
self._operations.append(_operation("Div", self._location, value=other))
102+
if not self._location:
103+
return self
96104
return _noop
97105

98106
def __ior__(self, other):
99107
self.update(E=other)
108+
if not self._location:
109+
return self
100110
return _noop
101111

102112
def __iter__(self):

tests/integration/test_patch.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,3 +452,95 @@ def test_pch005_clientside_duplicate(dash_duo):
452452

453453
dash_duo.find_element("#click2").click()
454454
dash_duo.wait_for_text_to_equal("#output", "click2")
455+
456+
457+
def test_pch006_base_operators(dash_duo):
458+
app = Dash()
459+
460+
app.layout = [
461+
dcc.Store(data=2, id="store"),
462+
html.Button("add-one", id="add-one"),
463+
html.Button("remove-one", id="remove-one"),
464+
html.Button("mul-two", id="mul-two"),
465+
html.Button("div-two", id="div-two"),
466+
html.Button("merge", id="merge"),
467+
dcc.Store(data={"initial": "initial"}, id="dict-store"),
468+
html.Div(id="store-output"),
469+
html.Div(id="dict-store-output"),
470+
]
471+
472+
@app.callback(
473+
Output("store", "data"), Input("add-one", "n_clicks"), prevent_initial_call=True
474+
)
475+
def on_add(_):
476+
patched = Patch()
477+
patched += 1
478+
return patched
479+
480+
@app.callback(
481+
Output("store", "data", allow_duplicate=True),
482+
Input("remove-one", "n_clicks"),
483+
prevent_initial_call=True,
484+
)
485+
def on_remove(_):
486+
patched = Patch()
487+
patched -= 1
488+
return patched
489+
490+
@app.callback(
491+
Output("store", "data", allow_duplicate=True),
492+
Input("mul-two", "n_clicks"),
493+
prevent_initial_call=True,
494+
)
495+
def on_mul(_):
496+
patched = Patch()
497+
patched *= 2
498+
return patched
499+
500+
@app.callback(
501+
Output("store", "data", allow_duplicate=True),
502+
Input("div-two", "n_clicks"),
503+
prevent_initial_call=True,
504+
)
505+
def on_div(_):
506+
patched = Patch()
507+
patched /= 2
508+
return patched
509+
510+
@app.callback(
511+
Output("dict-store", "data", allow_duplicate=True),
512+
Input("merge", "n_clicks"),
513+
prevent_initial_call=True,
514+
)
515+
def on_merge(_):
516+
patched = Patch()
517+
patched |= {"merged": "merged"}
518+
return patched
519+
520+
app.clientside_callback(
521+
"data => data", Output("store-output", "children"), Input("store", "data")
522+
)
523+
app.clientside_callback(
524+
"data => JSON.stringify(data)",
525+
Output("dict-store-output", "children"),
526+
Input("dict-store", "data"),
527+
)
528+
529+
dash_duo.start_server(app)
530+
531+
dash_duo.find_element("#add-one").click()
532+
dash_duo.wait_for_text_to_equal("#store-output", "3")
533+
534+
dash_duo.find_element("#remove-one").click()
535+
dash_duo.wait_for_text_to_equal("#store-output", "2")
536+
537+
dash_duo.find_element("#mul-two").click()
538+
dash_duo.wait_for_text_to_equal("#store-output", "4")
539+
540+
dash_duo.find_element("#div-two").click()
541+
dash_duo.wait_for_text_to_equal("#store-output", "2")
542+
543+
dash_duo.find_element("#merge").click()
544+
dash_duo.wait_for_text_to_equal(
545+
"#dict-store-output", '{"initial":"initial","merged":"merged"}'
546+
)

0 commit comments

Comments
 (0)