Skip to content

Commit a6005e0

Browse files
authored
V4: Have side-effect renderers show figure on display (#1629)
* Have side-effect renderers show figure on display. mimetype renderers already did this. Now, px can be used easily with the 'browser' renderer for non-jupyter contexts. * Fix renderer tests * Have orca retry image export request on "522: client socket timeout" * fix renderers test case * Change name of is_share_key_included -> is_share_key_included2 to work around "An intermediate folder or the file specified by 'path' is non-unique!" error * Only use the new _create_or_overwrite on grids, use the old create_or_update for figures * Run chart studio tests sequentially * Use grid "destory" rather than "trash"+"permanent delete" endpoints
1 parent 7b6fad9 commit a6005e0

File tree

10 files changed

+165
-46
lines changed

10 files changed

+165
-46
lines changed

.circleci/config.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,9 @@ workflows:
373373
# 3.7 optional disabled due to current shapely incompatibility
374374
# - python-3.7-optional
375375
- python-2.7-plot_ly
376-
- python-3.5-plot_ly
377-
- python-3.7-plot_ly
376+
- python-3.7-plot_ly:
377+
requires:
378+
- python-2.7-plot_ly
378379
- python-2-7-orca
379380
- python-3-5-orca
380381
- python-3-7-orca

packages/python/chart-studio/chart_studio/api/v2/grids.py

+12
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ def permanent_delete(fid):
9595
return request("delete", url)
9696

9797

98+
def destroy(fid):
99+
"""
100+
Permanently delete a grid file from Plotly.
101+
102+
:param (str) fid: The `{username}:{idlocal}` identifier. E.g. `foo:88`.
103+
:returns: (requests.Response) Returns response directly from requests.
104+
105+
"""
106+
url = build_url(RESOURCE, id=fid)
107+
return request("delete", url)
108+
109+
98110
def lookup(path, parent=None, user=None, exists=None):
99111
"""
100112
Retrieve a grid file from Plotly without needing a fid.

packages/python/chart-studio/chart_studio/plotly/plotly.py

+82-13
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ def plot(figure_or_data, validate=True, **plot_options):
282282
_set_grid_column_references(figure, grid)
283283
payload["figure"] = figure
284284

285-
file_info = _create_or_overwrite(payload, "plot")
285+
file_info = _create_or_update(payload, "plot")
286286

287287
# Compute viewing URL
288288
if sharing == "secret":
@@ -1094,7 +1094,7 @@ def upload(
10941094
if parent_path != "":
10951095
payload["parent_path"] = parent_path
10961096

1097-
file_info = _create_or_overwrite(payload, "grid")
1097+
file_info = _create_or_overwrite_grid(payload)
10981098

10991099
cols = file_info["cols"]
11001100
fid = file_info["fid"]
@@ -1445,10 +1445,73 @@ def get_grid(grid_url, raw=False):
14451445
return Grid(parsed_content, fid)
14461446

14471447

1448-
def _create_or_overwrite(data, filetype):
1448+
def _create_or_update(data, filetype):
14491449
"""
1450-
Create or overwrite (if file exists) and grid, plot, spectacle,
1451-
or dashboard object
1450+
Create or update (if file exists) and plot, spectacle, or dashboard
1451+
object
1452+
Parameters
1453+
----------
1454+
data: dict
1455+
update/create API payload
1456+
filetype: str
1457+
One of 'plot', 'grid', 'spectacle_presentation', or 'dashboard'
1458+
Returns
1459+
-------
1460+
dict
1461+
File info from API response
1462+
"""
1463+
api_module = getattr(v2, filetype + "s")
1464+
1465+
# lookup if pre-existing filename already exists
1466+
if "parent_path" in data:
1467+
filename = data["parent_path"] + "/" + data["filename"]
1468+
else:
1469+
filename = data.get("filename", None)
1470+
1471+
if filename:
1472+
try:
1473+
lookup_res = v2.files.lookup(filename)
1474+
if isinstance(lookup_res.content, bytes):
1475+
content = lookup_res.content.decode("utf-8")
1476+
else:
1477+
content = lookup_res.content
1478+
1479+
matching_file = json.loads(content)
1480+
1481+
if matching_file["filetype"] == filetype:
1482+
fid = matching_file["fid"]
1483+
res = api_module.update(fid, data)
1484+
else:
1485+
raise _plotly_utils.exceptions.PlotlyError(
1486+
"""
1487+
'{filename}' is already a {other_filetype} in your account.
1488+
While you can overwrite {filetype}s with the same name, you can't overwrite
1489+
files with a different type. Try deleting '{filename}' in your account or
1490+
changing the filename.""".format(
1491+
filename=filename,
1492+
filetype=filetype,
1493+
other_filetype=matching_file["filetype"],
1494+
)
1495+
)
1496+
1497+
except exceptions.PlotlyRequestError:
1498+
res = api_module.create(data)
1499+
else:
1500+
res = api_module.create(data)
1501+
1502+
# Check response
1503+
res.raise_for_status()
1504+
1505+
# Get resulting file content
1506+
file_info = res.json()
1507+
file_info = file_info.get("file", file_info)
1508+
1509+
return file_info
1510+
1511+
1512+
def _create_or_overwrite_grid(data, max_retries=3):
1513+
"""
1514+
Create or overwrite (if file exists) a grid
14521515
14531516
Parameters
14541517
----------
@@ -1462,7 +1525,7 @@ def _create_or_overwrite(data, filetype):
14621525
dict
14631526
File info from API response
14641527
"""
1465-
api_module = getattr(v2, filetype + "s")
1528+
api_module = v2.grids
14661529

14671530
# lookup if pre-existing filename already exists
14681531
if "parent_path" in data:
@@ -1484,21 +1547,27 @@ def _create_or_overwrite(data, filetype):
14841547

14851548
# Delete fid
14861549
# This requires sending file to trash and then deleting it
1487-
res = api_module.trash(fid)
1550+
res = api_module.destroy(fid)
14881551
res.raise_for_status()
14891552

1490-
res = api_module.permanent_delete(fid)
1491-
res.raise_for_status()
14921553
except exceptions.PlotlyRequestError as e:
14931554
# Raise on trash or permanent delete
14941555
# Pass through to try creating the file anyway
14951556
pass
14961557

14971558
# Create file
1498-
res = api_module.create(data)
1499-
res.raise_for_status()
1559+
try:
1560+
res = api_module.create(data)
1561+
except exceptions.PlotlyRequestError as e:
1562+
if max_retries > 0 and "already exists" in e.message:
1563+
# Retry _create_or_overwrite
1564+
time.sleep(1)
1565+
return _create_or_overwrite_grid(data, max_retries=max_retries - 1)
1566+
else:
1567+
raise
15001568

15011569
# Get resulting file content
1570+
res.raise_for_status()
15021571
file_info = res.json()
15031572
file_info = file_info.get("file", file_info)
15041573

@@ -1586,7 +1655,7 @@ def upload(cls, dashboard, filename, sharing="public", auto_open=True):
15861655
"world_readable": world_readable,
15871656
}
15881657

1589-
file_info = _create_or_overwrite(data, "dashboard")
1658+
file_info = _create_or_update(data, "dashboard")
15901659

15911660
url = file_info["web_url"]
15921661

@@ -1683,7 +1752,7 @@ def upload(cls, presentation, filename, sharing="public", auto_open=True):
16831752
"world_readable": world_readable,
16841753
}
16851754

1686-
file_info = _create_or_overwrite(data, "spectacle_presentation")
1755+
file_info = _create_or_update(data, "spectacle_presentation")
16871756

16881757
url = file_info["web_url"]
16891758

packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_plotly/test_plot.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def test_plot_url_given_sharing_key(self):
166166
self.simple_figure, validate
167167
)
168168
kwargs = {
169-
"filename": "is_share_key_included",
169+
"filename": "is_share_key_included2",
170170
"world_readable": False,
171171
"auto_open": False,
172172
"sharing": "secret",
@@ -182,7 +182,7 @@ def test_plot_url_response_given_sharing_key(self):
182182
# be 200
183183

184184
kwargs = {
185-
"filename": "is_share_key_included",
185+
"filename": "is_share_key_included2",
186186
"auto_open": False,
187187
"world_readable": False,
188188
"sharing": "secret",
@@ -203,7 +203,7 @@ def test_private_plot_response_with_and_without_share_key(self):
203203
# share_key is added it should be 200
204204

205205
kwargs = {
206-
"filename": "is_share_key_included",
206+
"filename": "is_share_key_included2",
207207
"world_readable": False,
208208
"auto_open": False,
209209
"sharing": "private",

packages/python/chart-studio/chart_studio/tests/test_plot_ly/test_stream/test_stream.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_initialize_stream_plot(self):
3636
[Scatter(x=[], y=[], mode="markers", stream=stream)],
3737
auto_open=False,
3838
world_readable=True,
39-
filename="stream-test",
39+
filename="stream-test2",
4040
)
4141
self.assertTrue(url.startswith("https://plot.ly/~PythonAPI/"))
4242
time.sleep(0.5)
@@ -49,7 +49,7 @@ def test_stream_single_points(self):
4949
[Scatter(x=[], y=[], mode="markers", stream=stream)],
5050
auto_open=False,
5151
world_readable=True,
52-
filename="stream-test",
52+
filename="stream-test2",
5353
)
5454
time.sleep(0.5)
5555
my_stream = py.Stream(tk)
@@ -66,7 +66,7 @@ def test_stream_multiple_points(self):
6666
[Scatter(x=[], y=[], mode="markers", stream=stream)],
6767
auto_open=False,
6868
world_readable=True,
69-
filename="stream-test",
69+
filename="stream-test2",
7070
)
7171
time.sleep(0.5)
7272
my_stream = py.Stream(tk)
@@ -83,7 +83,7 @@ def test_stream_layout(self):
8383
[Scatter(x=[], y=[], mode="markers", stream=stream)],
8484
auto_open=False,
8585
world_readable=True,
86-
filename="stream-test",
86+
filename="stream-test2",
8787
)
8888
time.sleep(0.5)
8989
title_0 = "some title i picked first"

packages/python/plotly/plotly/basedatatypes.py

+5-11
Original file line numberDiff line numberDiff line change
@@ -427,22 +427,16 @@ def __repr__(self):
427427

428428
return repr_str
429429

430-
def _repr_mimebundle_(self, include, exclude, **kwargs):
430+
def _ipython_display_(self):
431431
"""
432-
repr_mimebundle should accept include, exclude and **kwargs
432+
Handle rich display of figures in ipython contexts
433433
"""
434434
import plotly.io as pio
435435

436-
if pio.renderers.render_on_display:
437-
data = pio.renderers._build_mime_bundle(self.to_dict())
438-
439-
if include:
440-
data = {k: v for (k, v) in data.items() if k in include}
441-
if exclude:
442-
data = {k: v for (k, v) in data.items() if k not in exclude}
443-
return data
436+
if pio.renderers.render_on_display and pio.renderers.default:
437+
pio.show(self)
444438
else:
445-
return None
439+
print (repr(self))
446440

447441
def update(self, dict1=None, **kwargs):
448442
"""

packages/python/plotly/plotly/basewidget.py

+9
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,15 @@ def _handler_js2py_pointsCallback(self, change):
730730

731731
self._js2py_pointsCallback = None
732732

733+
# Display
734+
# -------
735+
def _ipython_display_(self):
736+
"""
737+
Handle rich display of figures in ipython contexts
738+
"""
739+
# Override BaseFigure's display to make sure we display the widget version
740+
widgets.DOMWidget._ipython_display_(self)
741+
733742
# Callbacks
734743
# ---------
735744
def on_edits_completed(self, fn):

packages/python/plotly/plotly/io/_orca.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1387,7 +1387,7 @@ def ensure_server():
13871387
orca_state["shutdown_timer"] = t
13881388

13891389

1390-
@retrying.retry(wait_random_min=5, wait_random_max=10, stop_max_delay=30000)
1390+
@retrying.retry(wait_random_min=5, wait_random_max=10, stop_max_delay=60000)
13911391
def request_image_with_retrying(**kwargs):
13921392
"""
13931393
Helper method to perform an image request to a running orca server process
@@ -1402,6 +1402,13 @@ def request_image_with_retrying(**kwargs):
14021402
request_params = {k: v for k, v, in kwargs.items() if v is not None}
14031403
json_str = json.dumps(request_params, cls=_plotly_utils.utils.PlotlyJSONEncoder)
14041404
response = post(server_url + "/", data=json_str)
1405+
1406+
if response.status_code == 522:
1407+
# On "522: client socket timeout", return server and keep trying
1408+
shutdown_server()
1409+
ensure_server()
1410+
raise OSError("522: client socket timeout")
1411+
14051412
return response
14061413

14071414

@@ -1546,6 +1553,7 @@ def to_image(fig, format=None, width=None, height=None, scale=None, validate=Tru
15461553
# orca code base.
15471554
# statusMsg: {
15481555
# 400: 'invalid or malformed request syntax',
1556+
# 522: client socket timeout
15491557
# 525: 'plotly.js error',
15501558
# 526: 'plotly.js version 1.11.0 or up required',
15511559
# 530: 'image conversion error'

packages/python/plotly/plotly/tests/test_io/test_renderers.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,17 @@ def test_json_renderer_mimetype(fig1):
4343
expected = {"application/json": json.loads(pio.to_json(fig1, remove_uids=False))}
4444

4545
pio.renderers.render_on_display = False
46-
assert fig1._repr_mimebundle_(None, None) is None
46+
47+
with mock.patch("IPython.display.display") as mock_display:
48+
fig1._ipython_display_()
49+
50+
mock_display.assert_not_called()
4751

4852
pio.renderers.render_on_display = True
49-
bundle = fig1._repr_mimebundle_(None, None)
50-
assert bundle == expected
53+
with mock.patch("IPython.display.display") as mock_display:
54+
fig1._ipython_display_()
55+
56+
mock_display.assert_called_once_with(expected, raw=True)
5157

5258

5359
def test_json_renderer_show(fig1):
@@ -88,11 +94,17 @@ def test_plotly_mimetype_renderer_mimetype(fig1, renderer):
8894
expected[plotly_mimetype]["config"] = {"plotlyServerURL": "https://plot.ly"}
8995

9096
pio.renderers.render_on_display = False
91-
assert fig1._repr_mimebundle_(None, None) is None
97+
98+
with mock.patch("IPython.display.display") as mock_display:
99+
fig1._ipython_display_()
100+
101+
mock_display.assert_not_called()
92102

93103
pio.renderers.render_on_display = True
94-
bundle = fig1._repr_mimebundle_(None, None)
95-
assert bundle == expected
104+
with mock.patch("IPython.display.display") as mock_display:
105+
fig1._ipython_display_()
106+
107+
mock_display.assert_called_once_with(expected, raw=True)
96108

97109

98110
@pytest.mark.parametrize("renderer", plotly_mimetype_renderers)

0 commit comments

Comments
 (0)