Skip to content

Commit 7645f49

Browse files
authored
Merge branch 'dev' into antrg-fix-datatable
2 parents a25cca9 + 8c193bc commit 7645f49

File tree

15 files changed

+486
-430
lines changed

15 files changed

+486
-430
lines changed

.circleci/config.yml

Lines changed: 304 additions & 365 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ This project adheres to [Semantic Versioning](https://semver.org/).
44

55
## [Unreleased]
66

7+
- [#1745](https://github.com/plotly/dash/pull/1745):
8+
Improve our `extras_require`: there are now five options here, each with a well-defined role:
9+
- `dash[dev]`: for developing and building dash components.
10+
- `dash[testing]`: for using the `pytest` plugins in the `dash.testing` module
11+
- `dash[diskcache]`: required if you use `DiskcacheLongCallbackManager`
12+
- `dash[celery]`: required if you use `CeleryLongCallbackManager`
13+
- `dash[ci]`: mainly for internal use, these are additional requirements for the Dash CI tests, exposed for other component libraries to use a matching configuration.
14+
15+
- [#1779](https://github.com/plotly/dash/pull/1779):
16+
- Clean up our handling of serialization problems, including fixing `orjson` for Python 3.6
17+
- Added the ability for `dash.testing` `percy_snapshot` methods to choose widths to generate.
18+
719
- [#1763](https://github.com/plotly/dash/pull/1763):
820
## Dash and Dash Renderer
921

@@ -29,7 +41,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
2941
@dash.callback(Output(my_output, 'children'), Input(my_input, 'value'))
3042
def update(value):
3143
return f'You have entered {value}'
32-
44+
3345
```
3446
## Dash Core Components
3547

@@ -138,7 +150,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
138150
```python
139151
dcc.Checklist(inline=True)
140152
```
141-
153+
142154
## [2.0.0] - 2021-08-03
143155

144156
## Dash and Dash Renderer

components/dash-table/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"lint": "run-s private::lint.*",
3535
"test.server": "pytest --nopercyfinalize tests/selenium",
3636
"test.unit": "run-s private::test.python private::test.unit",
37-
"test.visual": "build-storybook && percy-storybook",
37+
"test.visual": "build-storybook && percy-storybook --widths=1280",
3838
"test.visual-local": "build-storybook"
3939
},
4040
"author": "Chris Parmer <[email protected]>",

dash/_validate.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import collections
1+
from collections.abc import MutableSequence
22
import re
33
from textwrap import dedent
44

55
from ._grouping import grouping_len, map_grouping
66
from .development.base_component import Component
77
from . import exceptions
8-
from ._utils import patch_collections_abc, stringify_id
8+
from ._utils import patch_collections_abc, stringify_id, to_json
99

1010

1111
def validate_callback(outputs, inputs, state, extra_args, types):
@@ -198,7 +198,8 @@ def validate_multi_return(outputs_list, output_value, callback_id):
198198

199199

200200
def fail_callback_output(output_value, output):
201-
valid = (str, dict, int, float, type(None), Component)
201+
valid_children = (str, int, float, type(None), Component)
202+
valid_props = (str, int, float, type(None), tuple, MutableSequence)
202203

203204
def _raise_invalid(bad_val, outer_val, path, index=None, toplevel=False):
204205
bad_type = type(bad_val).__name__
@@ -247,43 +248,74 @@ def _raise_invalid(bad_val, outer_val, path, index=None, toplevel=False):
247248
)
248249
)
249250

250-
def _value_is_valid(val):
251-
return isinstance(val, valid)
251+
def _valid_child(val):
252+
return isinstance(val, valid_children)
253+
254+
def _valid_prop(val):
255+
return isinstance(val, valid_props)
256+
257+
def _can_serialize(val):
258+
if not (_valid_child(val) or _valid_prop(val)):
259+
return False
260+
try:
261+
to_json(val)
262+
except TypeError:
263+
return False
264+
return True
252265

253266
def _validate_value(val, index=None):
254267
# val is a Component
255268
if isinstance(val, Component):
269+
unserializable_items = []
256270
# pylint: disable=protected-access
257271
for p, j in val._traverse_with_paths():
258272
# check each component value in the tree
259-
if not _value_is_valid(j):
273+
if not _valid_child(j):
260274
_raise_invalid(bad_val=j, outer_val=val, path=p, index=index)
261275

276+
if not _can_serialize(j):
277+
# collect unserializable items separately, so we can report
278+
# only the deepest level, not all the parent components that
279+
# are just unserializable because of their children.
280+
unserializable_items = [
281+
i for i in unserializable_items if not p.startswith(i[0])
282+
]
283+
if unserializable_items:
284+
# we already have something unserializable in a different
285+
# branch - time to stop and fail
286+
break
287+
if all(not i[0].startswith(p) for i in unserializable_items):
288+
unserializable_items.append((p, j))
289+
262290
# Children that are not of type Component or
263291
# list/tuple not returned by traverse
264292
child = getattr(j, "children", None)
265-
if not isinstance(child, (tuple, collections.MutableSequence)):
266-
if child and not _value_is_valid(child):
293+
if not isinstance(child, (tuple, MutableSequence)):
294+
if child and not _can_serialize(child):
267295
_raise_invalid(
268296
bad_val=child,
269297
outer_val=val,
270298
path=p + "\n" + "[*] " + type(child).__name__,
271299
index=index,
272300
)
301+
if unserializable_items:
302+
p, j = unserializable_items[0]
303+
# just report the first one, even if there are multiple,
304+
# as that's how all the other errors work
305+
_raise_invalid(bad_val=j, outer_val=val, path=p, index=index)
273306

274307
# Also check the child of val, as it will not be returned
275308
child = getattr(val, "children", None)
276-
if not isinstance(child, (tuple, collections.MutableSequence)):
277-
if child and not _value_is_valid(child):
309+
if not isinstance(child, (tuple, MutableSequence)):
310+
if child and not _can_serialize(val):
278311
_raise_invalid(
279312
bad_val=child,
280313
outer_val=val,
281314
path=type(child).__name__,
282315
index=index,
283316
)
284317

285-
# val is not a Component, but is at the top level of tree
286-
elif not _value_is_valid(val):
318+
if not _can_serialize(val):
287319
_raise_invalid(
288320
bad_val=val,
289321
outer_val=type(val).__name__,
@@ -301,13 +333,13 @@ def _validate_value(val, index=None):
301333
# if we got this far, raise a generic JSON error
302334
raise exceptions.InvalidCallbackReturnValue(
303335
"""
304-
The callback for property `{property:s}` of component `{id:s}`
336+
The callback for output `{output}`
305337
returned a value which is not JSON serializable.
306338
307339
In general, Dash properties can only be dash components, strings,
308340
dictionaries, numbers, None, or lists of those.
309341
""".format(
310-
property=output.component_property, id=output.component_id
342+
output=repr(output)
311343
)
312344
)
313345

dash/long_callback/managers/celery_manager.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,18 @@ def __init__(self, celery_app, cache_by=None, expire=None):
2525
for ``expire`` seconds. If not provided, the lifetime of cache entries
2626
is determined by the default behavior of the celery result backend.
2727
"""
28-
import celery # pylint: disable=import-outside-toplevel,import-error
29-
from celery.backends.base import ( # pylint: disable=import-outside-toplevel,import-error
30-
DisabledBackend,
31-
)
28+
try:
29+
import celery # pylint: disable=import-outside-toplevel,import-error
30+
from celery.backends.base import ( # pylint: disable=import-outside-toplevel,import-error
31+
DisabledBackend,
32+
)
33+
except ImportError as missing_imports:
34+
raise ImportError(
35+
"""\
36+
CeleryLongCallbackManager requires extra dependencies which can be installed doing
37+
38+
$ pip install "dash[celery]"\n"""
39+
) from missing_imports
3240

3341
if not isinstance(celery_app, celery.Celery):
3442
raise ValueError("First argument must be a celery.Celery object")

dash/long_callback/managers/diskcache_manager.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55

66
class DiskcacheLongCallbackManager(BaseLongCallbackManager):
7-
def __init__(self, cache, cache_by=None, expire=None):
7+
def __init__(self, cache=None, cache_by=None, expire=None):
88
"""
99
Long callback manager that runs callback logic in a subprocess and stores
1010
results on disk using diskcache
1111
1212
:param cache:
1313
A diskcache.Cache or diskcache.FanoutCache instance. See the diskcache
14-
documentation for information on configuration options.
14+
documentation for information on configuration options. If not provided,
15+
a diskcache.Cache instance will be created with default values.
1516
:param cache_by:
1617
A list of zero-argument functions. When provided, caching is enabled and
1718
the return values of these functions are combined with the callback
@@ -28,20 +29,22 @@ def __init__(self, cache, cache_by=None, expire=None):
2829
except ImportError as missing_imports:
2930
raise ImportError(
3031
"""\
31-
DiskcacheLongCallbackManager requires the multiprocess, diskcache, and psutil packages
32-
which can be installed using pip...
32+
DiskcacheLongCallbackManager requires extra dependencies which can be installed doing
3333
34-
$ pip install multiprocess diskcache psutil
35-
36-
or conda.
37-
38-
$ conda install -c conda-forge multiprocess diskcache psutil\n"""
34+
$ pip install "dash[diskcache]"\n"""
3935
) from missing_imports
4036

41-
if not isinstance(cache, (diskcache.Cache, diskcache.FanoutCache)):
42-
raise ValueError("First argument must be a diskcache.Cache object")
37+
if cache is None:
38+
self.handle = diskcache.Cache()
39+
else:
40+
if not isinstance(cache, (diskcache.Cache, diskcache.FanoutCache)):
41+
raise ValueError(
42+
"First argument must be a diskcache.Cache "
43+
"or diskcache.FanoutCache object"
44+
)
45+
self.handle = cache
46+
4347
super().__init__(cache_by)
44-
self.handle = cache
4548
self.expire = expire
4649

4750
def terminate_job(self, job):

dash/testing/browser.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def visit_and_snapshot(
106106
convert_canvases=False,
107107
assert_check=True,
108108
stay_on_page=False,
109+
widths=None,
109110
):
110111
try:
111112
path = resource_path.lstrip("/")
@@ -119,6 +120,7 @@ def visit_and_snapshot(
119120
path,
120121
wait_for_callbacks=wait_for_callbacks,
121122
convert_canvases=convert_canvases,
123+
widths=widths,
122124
)
123125
if assert_check:
124126
assert not self.driver.find_elements_by_css_selector(
@@ -130,10 +132,26 @@ def visit_and_snapshot(
130132
logger.exception("snapshot at resource %s error", path)
131133
raise e
132134

133-
def percy_snapshot(self, name="", wait_for_callbacks=False, convert_canvases=False):
135+
def percy_snapshot(
136+
self, name="", wait_for_callbacks=False, convert_canvases=False, widths=None
137+
):
134138
"""percy_snapshot - visual test api shortcut to `percy_runner.snapshot`.
135-
It also combines the snapshot `name` with the Python version.
139+
It also combines the snapshot `name` with the Python version,
140+
args:
141+
- name: combined with the python version to give the final snapshot name
142+
- wait_for_callbacks: default False, whether to wait for Dash callbacks,
143+
after an extra second to ensure that any relevant callbacks have
144+
been initiated
145+
- convert_canvases: default False, whether to convert all canvas elements
146+
in the DOM into static images for percy to see. They will be restored
147+
after the snapshot is complete.
148+
- widths: a list of pixel widths for percy to render the page with. Note
149+
that this does not change the browser in which the DOM is constructed,
150+
so the width will only affect CSS, not JS-driven layout.
151+
Defaults to [375, 1280]
136152
"""
153+
if widths is None:
154+
widths = [375, 1280]
137155
snapshot_name = "{} - py{}.{}".format(
138156
name, sys.version_info.major, sys.version_info.minor
139157
)
@@ -172,7 +190,7 @@ def percy_snapshot(self, name="", wait_for_callbacks=False, convert_canvases=Fal
172190
"""
173191
)
174192

175-
self.percy_runner.snapshot(name=snapshot_name)
193+
self.percy_runner.snapshot(name=snapshot_name, widths=widths)
176194

177195
self.driver.execute_script(
178196
"""
@@ -189,7 +207,7 @@ def percy_snapshot(self, name="", wait_for_callbacks=False, convert_canvases=Fal
189207
)
190208

191209
else:
192-
self.percy_runner.snapshot(name=snapshot_name)
210+
self.percy_runner.snapshot(name=snapshot_name, widths=widths)
193211

194212
def take_snapshot(self, name):
195213
"""Hook method to take snapshot when a selenium test fails. The

dash/testing/plugin.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,5 @@ def diskcache_manager():
192192
from dash.long_callback import ( # pylint: disable=import-outside-toplevel
193193
DiskcacheLongCallbackManager,
194194
)
195-
import diskcache # pylint: disable=import-outside-toplevel
196195

197-
cache = diskcache.Cache()
198-
return DiskcacheLongCallbackManager(cache)
196+
return DiskcacheLongCallbackManager()

requires-celery.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Dependencies used by the CeleryLongCallbackManager
2+
redis>=3.5.3
3+
celery[redis]>=5.1.2

requires-ci.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Dependencies used by CI on github.com/plotly/dash
2+
black==21.6b0
3+
dash-flow-example==0.0.5
4+
dash-dangerously-set-inner-html
5+
flake8==3.9.2
6+
flaky==3.7.0
7+
flask-talisman==0.8.1
8+
isort==4.3.21;python_version<"3.7"
9+
mock==4.0.3
10+
orjson==3.5.4;python_version<"3.7"
11+
orjson==3.6.3;python_version>="3.7"
12+
pylint==2.10.2
13+
pytest-mock==3.2.0
14+
pytest-sugar==0.9.4

0 commit comments

Comments
 (0)