Skip to content

Partial updates #680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 8, 2019
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- Support for "Clientside Callbacks" - an escape hatch to execute your callbacks in JavaScript instead of Python [#672](https://github.com/plotly/dash/pull/672)
- Added `dev_tools_ui` config flag in `app.run_server` (serialized in `<script id="_dash-config" type="application/json">`)
to display or hide the forthcoming Dev Tools UI in Dash's front-end (dash-renderer). [#676](https://github.com/plotly/dash/pull/676)
- Partial updates: leave some multi-output updates unchanged while updating others [#680](https://github.com/plotly/dash/pull/680)

## [0.40.0] - 2019-03-25
### Changed
Expand Down
2 changes: 1 addition & 1 deletion dash/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .dash import Dash # noqa: F401
from .dash import Dash, no_update # noqa: F401
from . import dependencies # noqa: F401
from . import development # noqa: F401
from . import exceptions # noqa: F401
Expand Down
23 changes: 21 additions & 2 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@
_re_renderer_scripts_id = re.compile(r'id="_dash-renderer')


class _NoUpdate(object):
# pylint: disable=too-few-public-methods
pass


# Singleton signal to not update an output, alternative to PreventUpdate
no_update = _NoUpdate()


# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments, too-many-locals
class Dash(object):
Expand Down Expand Up @@ -1053,15 +1062,25 @@ def add_context(*args, **kwargs):
)

component_ids = collections.defaultdict(dict)
has_update = False
for i, o in enumerate(output):
component_ids[o.component_id][o.component_property] =\
output_value[i]
val = output_value[i]
if val is not no_update:
has_update = True
o_id, o_prop = o.component_id, o.component_property
component_ids[o_id][o_prop] = val
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad that we're returning a dict instead of a list here 😸 I was sure that we would've had to change the renderer to do this.


if not has_update:
raise exceptions.PreventUpdate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use flask.abort instead of going the slow exception way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what flask.abort does, but I wanted to trigger the PreventDefault handler, in case the renderer does any optimizations with that specific response to maximize performance.

dash/dash/dash.py

Lines 180 to 183 in bb5ed70

@self.server.errorhandler(exceptions.PreventUpdate)
def _handle_error(_):
"""Handle a halted callback and return an empty 204 response"""
return '', 204

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an empty 204 which would be the same as flask.abort(204). Raising the exception takes longer because it has to be caught and then the handler is called whereas abort is immediate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually seems like flask.abort is doing more than that... if I try to do flask.abort(204) it fails with LookupError: no exception for 204. The docs say:

If a status code is given it’s looked up in the list of exceptions and will raise that exception

Which anyway sounds at least as complicated as what we're doing with raise PreventUpdate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, 204 is not an error. But then you are at the dispatch level, think you can just do return '', 204 and avoid any error handling.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're in the weeds now - the return from this function feeds into response.set_data() in the actual dispatch, which I don't believe accepts a status code.

dash/dash/dash.py

Line 1094 in b2a97b4

response.set_data(self.callback_map[output]['callback'](*args))

There may be a way to improve on this but I'd like to defer it for this PR, if that's alright.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's new. Anyway, this works well for partial updates.


response = {
'response': component_ids,
'multi': True
}
else:
if output_value is no_update:
raise exceptions.PreventUpdate

response = {
'response': {
'props': {
Expand Down
22 changes: 15 additions & 7 deletions tests/IntegrationTests.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,23 @@ def percy_snapshot(cls, name=''):
name=snapshot_name
)

def wait_for_element_by_css_selector(self, selector):
return WebDriverWait(self.driver, TIMEOUT).until(
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
def wait_for_element_by_css_selector(self, selector, timeout=TIMEOUT):
return WebDriverWait(self.driver, timeout).until(
EC.presence_of_element_located((By.CSS_SELECTOR, selector)),
'Could not find element with selector "{}"'.format(selector)
)

def wait_for_text_to_equal(self, selector, assertion_text):
return WebDriverWait(self.driver, TIMEOUT).until(
EC.text_to_be_present_in_element((By.CSS_SELECTOR, selector),
assertion_text)
def wait_for_text_to_equal(self, selector, assertion_text, timeout=TIMEOUT):
el = self.wait_for_element_by_css_selector(selector)
WebDriverWait(self.driver, timeout).until(
lambda *args: (
(str(el.text) == assertion_text) or
(str(el.get_attribute('value')) == assertion_text)
),
"Element '{}' text was supposed to equal '{}' but it didn't".format(
selector,
assertion_text
)
)

@classmethod
Expand Down
Loading