Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 9c8e1f5

Browse files
authored
Handle pathlib.Path in pio (#2974)
* Handle pathlib.Path in write_image * Allow pathlib.Path in write_html * Make file URI from absolute path (bugfix) * pathlib for orca * pathlib.Path support for read_json and write_json
1 parent 3fc9c82 commit 9c8e1f5

File tree

9 files changed

+346
-89
lines changed

9 files changed

+346
-89
lines changed

‎packages/python/plotly/plotly/basedatatypes.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3578,7 +3578,7 @@ def write_html(self, *args, **kwargs):
35783578
----------
35793579
file: str or writeable
35803580
A string representing a local file path or a writeable object
3581-
(e.g. an open file descriptor)
3581+
(e.g. a pathlib.Path object or an open file descriptor)
35823582
config: dict or None (default None)
35833583
Plotly.js figure config options
35843584
auto_play: bool (default=True)
@@ -3751,7 +3751,7 @@ def write_image(self, *args, **kwargs):
37513751
----------
37523752
file: str or writeable
37533753
A string representing a local file path or a writeable object
3754-
(e.g. an open file descriptor)
3754+
(e.g. a pathlib.Path object or an open file descriptor)
37553755
37563756
format: str or None
37573757
The desired image format. One of

‎packages/python/plotly/plotly/io/_html.py‎

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import uuid
22
import json
33
import os
4+
from pathlib import Path
45
import webbrowser
56

67
import six
@@ -401,7 +402,7 @@ def write_html(
401402
Figure object or dict representing a figure
402403
file: str or writeable
403404
A string representing a local file path or a writeable object
404-
(e.g. an open file descriptor)
405+
(e.g. a pathlib.Path object or an open file descriptor)
405406
config: dict or None (default None)
406407
Plotly.js figure config options
407408
auto_play: bool (default=True)
@@ -520,24 +521,31 @@ def write_html(
520521
)
521522

522523
# Check if file is a string
523-
file_is_str = isinstance(file, six.string_types)
524+
if isinstance(file, six.string_types):
525+
# Use the standard pathlib constructor to make a pathlib object.
526+
path = Path(file)
527+
elif isinstance(file, Path): # PurePath is the most general pathlib object.
528+
# `file` is already a pathlib object.
529+
path = file
530+
else:
531+
# We could not make a pathlib object out of file. Either `file` is an open file
532+
# descriptor with a `write()` method or it's an invalid object.
533+
path = None
524534

525535
# Write HTML string
526-
if file_is_str:
527-
with open(file, "w") as f:
528-
f.write(html_str)
536+
if path is not None:
537+
path.write_text(html_str)
529538
else:
530539
file.write(html_str)
531540

532541
# Check if we should copy plotly.min.js to output directory
533-
if file_is_str and full_html and include_plotlyjs == "directory":
534-
bundle_path = os.path.join(os.path.dirname(file), "plotly.min.js")
542+
if pathisnotNone and full_html and include_plotlyjs == "directory":
543+
bundle_path = path.parent/"plotly.min.js"
535544

536-
if not os.path.exists(bundle_path):
537-
with open(bundle_path, "w") as f:
538-
f.write(get_plotlyjs())
545+
if not bundle_path.exists():
546+
bundle_path.write_text(get_plotlyjs())
539547

540548
# Handle auto_open
541-
if file_is_str and full_html and auto_open:
542-
url = "file://"+os.path.abspath(file)
549+
if pathisnotNone and full_html and auto_open:
550+
url = path.absolute().as_uri()
543551
webbrowser.open(url)

‎packages/python/plotly/plotly/io/_json.py‎

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from six import string_types
44
import json
5-
5+
frompathlibimportPath
66

77
from plotly.io._utils import validate_coerce_fig_to_dict, validate_coerce_output_type
88

@@ -68,7 +68,7 @@ def write_json(fig, file, validate=True, pretty=False, remove_uids=True):
6868
6969
file: str or writeable
7070
A string representing a local file path or a writeable object
71-
(e.g. an open file descriptor)
71+
(e.g. a pathlib.Path object or an open file descriptor)
7272
7373
pretty: bool (default False)
7474
True if JSON representation should be pretty-printed, False if
@@ -87,17 +87,40 @@ def write_json(fig, file, validate=True, pretty=False, remove_uids=True):
8787
# Pass through validate argument and let to_json handle validation logic
8888
json_str = to_json(fig, validate=validate, pretty=pretty, remove_uids=remove_uids)
8989

90-
# Check if file is a string
91-
# -------------------------
92-
file_is_str = isinstance(file, string_types)
90+
# Try to cast `file` as a pathlib object `path`.
91+
# ----------------------------------------------
92+
if isinstance(file, string_types):
93+
# Use the standard Path constructor to make a pathlib object.
94+
path = Path(file)
95+
elif isinstance(file, Path):
96+
# `file` is already a Path object.
97+
path = file
98+
else:
99+
# We could not make a Path object out of file. Either `file` is an open file
100+
# descriptor with a `write()` method or it's an invalid object.
101+
path = None
93102

94103
# Open file
95104
# ---------
96-
if file_is_str:
97-
with open(file, "w") as f:
98-
f.write(json_str)
105+
if path is None:
106+
# We previously failed to make sense of `file` as a pathlib object.
107+
# Attempt to write to `file` as an open file descriptor.
108+
try:
109+
file.write(json_str)
110+
return
111+
except AttributeError:
112+
pass
113+
raise ValueError(
114+
"""
115+
The 'file' argument '{file}' is not a string, pathlib.Path object, or file descriptor.
116+
""".format(
117+
file=file
118+
)
119+
)
99120
else:
100-
file.write(json_str)
121+
# We previously succeeded in interpreting `file` as a pathlib object.
122+
# Now we can use `write_bytes()`.
123+
path.write_text(json_str)
101124

102125

103126
def from_json(value, output_type="Figure", skip_invalid=False):
@@ -162,7 +185,7 @@ def read_json(file, output_type="Figure", skip_invalid=False):
162185
----------
163186
file: str or readable
164187
A string containing the path to a local file or a read-able Python
165-
object (e.g. an open file descriptor)
188+
object (e.g. a pathlib.Path object or an open file descriptor)
166189
167190
output_type: type or str (default 'Figure')
168191
The output figure type or type name.
@@ -177,17 +200,25 @@ def read_json(file, output_type="Figure", skip_invalid=False):
177200
Figure or FigureWidget
178201
"""
179202

180-
# Check if file is a string
203+
# Try to cast `file` as a pathlib object `path`.
181204
# -------------------------
182-
# If it's a string we assume it's a local file path. If it's not a string
183-
# then we assume it's a read-able Python object
205+
# ----------------------------------------------
184206
file_is_str = isinstance(file, string_types)
207+
if isinstance(file, string_types):
208+
# Use the standard Path constructor to make a pathlib object.
209+
path = Path(file)
210+
elif isinstance(file, Path):
211+
# `file` is already a Path object.
212+
path = file
213+
else:
214+
# We could not make a Path object out of file. Either `file` is an open file
215+
# descriptor with a `write()` method or it's an invalid object.
216+
path = None
185217

186218
# Read file contents into JSON string
187219
# -----------------------------------
188-
if file_is_str:
189-
with open(file, "r") as f:
190-
json_str = f.read()
220+
if path is not None:
221+
json_str = path.read_text()
191222
else:
192223
json_str = file.read()
193224

‎packages/python/plotly/plotly/io/_kaleido.py‎

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from six import string_types
33
import os
44
import json
5+
from pathlib import Path
56
import plotly
67
from plotly.io._utils import validate_coerce_fig_to_dict
78

@@ -169,7 +170,7 @@ def write_image(
169170
170171
file: str or writeable
171172
A string representing a local file path or a writeable object
172-
(e.g. an open file descriptor)
173+
(e.g. a pathlib.Path object or an open file descriptor)
173174
174175
format: str or None
175176
The desired image format. One of
@@ -228,14 +229,23 @@ def write_image(
228229
-------
229230
None
230231
"""
231-
# Check if file is a string
232-
# -------------------------
233-
file_is_str = isinstance(file, string_types)
232+
# Try to cast `file` as a pathlib object `path`.
233+
# ----------------------------------------------
234+
if isinstance(file, string_types):
235+
# Use the standard Path constructor to make a pathlib object.
236+
path = Path(file)
237+
elif isinstance(file, Path):
238+
# `file` is already a Path object.
239+
path = file
240+
else:
241+
# We could not make a Path object out of file. Either `file` is an open file
242+
# descriptor with a `write()` method or it's an invalid object.
243+
path = None
234244

235245
# Infer format if not specified
236246
# -----------------------------
237-
if file_is_str and format is None:
238-
_, ext = os.path.splitext(file)
247+
if pathisnotNone and format is None:
248+
ext = path.suffix
239249
if ext:
240250
format = ext.lstrip(".")
241251
else:
@@ -267,11 +277,25 @@ def write_image(
267277

268278
# Open file
269279
# ---------
270-
if file_is_str:
271-
with open(file, "wb") as f:
272-
f.write(img_data)
280+
if path is None:
281+
# We previously failed to make sense of `file` as a pathlib object.
282+
# Attempt to write to `file` as an open file descriptor.
283+
try:
284+
file.write(img_data)
285+
return
286+
except AttributeError:
287+
pass
288+
raise ValueError(
289+
"""
290+
The 'file' argument '{file}' is not a string, pathlib.Path object, or file descriptor.
291+
""".format(
292+
file=file
293+
)
294+
)
273295
else:
274-
file.write(img_data)
296+
# We previously succeeded in interpreting `file` as a pathlib object.
297+
# Now we can use `write_bytes()`.
298+
path.write_bytes(img_data)
275299

276300

277301
def full_figure_for_development(fig, warn=True, as_dict=False):

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

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import warnings
1111
from copy import copy
1212
from contextlib import contextmanager
13+
from pathlib import Path
1314

1415
import tenacity
1516
from six import string_types
@@ -1695,7 +1696,7 @@ def write_image(
16951696
16961697
file: str or writeable
16971698
A string representing a local file path or a writeable object
1698-
(e.g. an open file descriptor)
1699+
(e.g. a pathlib.Path object or an open file descriptor)
16991700
17001701
format: str or None
17011702
The desired image format. One of
@@ -1741,16 +1742,25 @@ def write_image(
17411742
None
17421743
"""
17431744

1744-
# Check if file is a string
1745-
# -------------------------
1746-
file_is_str = isinstance(file, string_types)
1745+
# Try to cast `file` as a pathlib object `path`.
1746+
# ----------------------------------------------
1747+
if isinstance(file, string_types):
1748+
# Use the standard Path constructor to make a pathlib object.
1749+
path = Path(file)
1750+
elif isinstance(file, Path):
1751+
# `file` is already a Path object.
1752+
path = file
1753+
else:
1754+
# We could not make a Path object out of file. Either `file` is an open file
1755+
# descriptor with a `write()` method or it's an invalid object.
1756+
path = None
17471757

17481758
# Infer format if not specified
17491759
# -----------------------------
1750-
if file_is_str and format is None:
1751-
_, ext = os.path.splitext(file)
1760+
if pathisnotNone and format is None:
1761+
ext = path.suffix
17521762
if ext:
1753-
format = validate_coerce_format(ext)
1763+
format = ext.lstrip(".")
17541764
else:
17551765
raise ValueError(
17561766
"""
@@ -1774,8 +1784,22 @@ def write_image(
17741784

17751785
# Open file
17761786
# ---------
1777-
if file_is_str:
1778-
with open(file, "wb") as f:
1779-
f.write(img_data)
1787+
if path is None:
1788+
# We previously failed to make sense of `file` as a pathlib object.
1789+
# Attempt to write to `file` as an open file descriptor.
1790+
try:
1791+
file.write(img_data)
1792+
return
1793+
except AttributeError:
1794+
pass
1795+
raise ValueError(
1796+
"""
1797+
The 'file' argument '{file}' is not a string, pathlib.Path object, or file descriptor.
1798+
""".format(
1799+
file=file
1800+
)
1801+
)
17801802
else:
1781-
file.write(img_data)
1803+
# We previously succeeded in interpreting `file` as a pathlib object.
1804+
# Now we can use `write_bytes()`.
1805+
path.write_bytes(img_data)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Test compatibility with pathlib.Path.
2+
3+
See also relevant tests in
4+
packages/python/plotly/plotly/tests/test_optional/test_kaleido/test_kaleido.py
5+
"""
6+
7+
from unittest import mock
8+
import plotly.io as pio
9+
from io import StringIO
10+
from pathlib import Path
11+
import re
12+
from unittest.mock import Mock
13+
14+
fig = {"layout": {"title": {"text": "figure title"}}}
15+
16+
17+
def test_write_html():
18+
"""Verify that various methods for producing HTML have equivalent results.
19+
20+
The results will not be identical because the div id is pseudorandom. Thus
21+
we compare the results after replacing the div id.
22+
23+
We test the results of
24+
- pio.to_html
25+
- pio.write_html with a StringIO buffer
26+
- pio.write_html with a mock pathlib Path
27+
- pio.write_html with a mock file descriptor
28+
"""
29+
# Test pio.to_html
30+
html = pio.to_html(fig)
31+
32+
# Test pio.write_html with a StringIO buffer
33+
sio = StringIO()
34+
pio.write_html(fig, sio)
35+
sio.seek(0) # Rewind to the beginning of the buffer, otherwise read() returns ''.
36+
sio_html = sio.read()
37+
assert replace_div_id(html) == replace_div_id(sio_html)
38+
39+
# Test pio.write_html with a mock pathlib Path
40+
mock_pathlib_path = Mock(spec=Path)
41+
pio.write_html(fig, mock_pathlib_path)
42+
mock_pathlib_path.write_text.assert_called_once()
43+
(pl_html,) = mock_pathlib_path.write_text.call_args[0]
44+
assert replace_div_id(html) == replace_div_id(pl_html)
45+
46+
# Test pio.write_html with a mock file descriptor
47+
mock_file_descriptor = Mock()
48+
del mock_file_descriptor.write_bytes
49+
pio.write_html(fig, mock_file_descriptor)
50+
mock_file_descriptor.write.assert_called_once()
51+
(fd_html,) = mock_file_descriptor.write.call_args[0]
52+
assert replace_div_id(html) == replace_div_id(fd_html)
53+
54+
55+
def replace_div_id(s):
56+
uuid = re.search(r'<div id="([^"]*)"', s).groups()[0]
57+
return s.replace(uuid, "XXXX")

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /