-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Support Kaleido v1 in Plotly.py #5062
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
Changes from all commits
f0a78a6
a681c84
a7a6f24
2e9c8af
60bb748
b87f752
e203623
8054331
1a195a7
577d3ca
89209ad
96bf9a0
350dd48
08c1d4e
8b47a0a
249cc9f
ad9dbd9
e75a5df
a2b4f3c
2549299
ab3b700
de473e1
71696fe
100b955
5541a79
95a05db
0a73d0e
4d3dd56
d871e74
b3e8d36
ef5f520
611e2e4
c92a1ee
b56d5ec
c01cb8a
bcd40f3
54985b8
b4af0d5
0f61cc3
fe12aec
692842f
aac9c20
ad57031
b9e5f7a
720ada5
53d486a
c990736
96bb17a
f736f4c
aca1620
564aa51
2b45f47
691fce6
0799bf5
651eafd
0413341
0c1b1c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Default settings for image generation | ||
|
||
|
||
class _Defaults(object): | ||
""" | ||
Class to store default settings for image generation. | ||
""" | ||
|
||
def __init__(self): | ||
self.default_format = "png" | ||
self.default_width = 700 | ||
self.default_height = 500 | ||
self.default_scale = 1 | ||
self.mathjax = None | ||
self.topojson = None | ||
|
||
|
||
defaults = _Defaults() |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,10 @@ | ||
from ._kaleido import to_image, write_image, scope | ||
from ._kaleido import ( | ||
to_image, | ||
write_image, | ||
scope, | ||
kaleido_available, | ||
kaleido_major, | ||
KALEIDO_DEPRECATION_MSG, | ||
ORCA_DEPRECATION_MSG, | ||
ENGINE_PARAM_DEPRECATION_MSG, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,66 +1,35 @@ | ||
import plotly.io as pio | ||
import plotly.io.kaleido | ||
from contextlib import contextmanager | ||
from io import BytesIO | ||
from io import BytesIO, StringIO | ||
from pathlib import Path | ||
from unittest.mock import Mock | ||
|
||
fig = {"layout": {"title": {"text": "figure title"}}} | ||
|
||
|
||
def make_writeable_mocks(): | ||
"""Produce some mocks which we will use for testing the `write_image()` function. | ||
These mocks should be passed as the `file=` argument to `write_image()`. | ||
The tests should verify that the method specified in the `active_write_function` | ||
attribute is called once, and that scope.transform is called with the `format=` | ||
argument specified by the `.expected_format` attribute. | ||
In total we provide two mocks: one for a writable file descriptor, and other for a | ||
pathlib.Path object. | ||
""" | ||
|
||
# Part 1: A mock for a file descriptor | ||
# ------------------------------------ | ||
mock_file_descriptor = Mock() | ||
|
||
# A file descriptor has no write_bytes method, unlike a pathlib Path. | ||
del mock_file_descriptor.write_bytes | ||
|
||
# The expected write method for a file descriptor is .write | ||
mock_file_descriptor.active_write_function = mock_file_descriptor.write | ||
import tempfile | ||
from contextlib import redirect_stdout | ||
import base64 | ||
|
||
# Since there is no filename, there should be no format detected. | ||
mock_file_descriptor.expected_format = None | ||
|
||
# Part 2: A mock for a pathlib path | ||
# --------------------------------- | ||
mock_pathlib_path = Mock(spec=Path) | ||
|
||
# A pathlib Path object has no write method, unlike a file descriptor. | ||
del mock_pathlib_path.write | ||
|
||
# The expected write method for a pathlib Path is .write_bytes | ||
mock_pathlib_path.active_write_function = mock_pathlib_path.write_bytes | ||
|
||
# Mock a path with PNG suffix | ||
mock_pathlib_path.suffix = ".png" | ||
mock_pathlib_path.expected_format = "png" | ||
|
||
return mock_file_descriptor, mock_pathlib_path | ||
|
||
|
||
@contextmanager | ||
def mocked_scope(): | ||
# Code to acquire resource, e.g.: | ||
scope_mock = Mock() | ||
original_scope = pio._kaleido.scope | ||
pio._kaleido.scope = scope_mock | ||
try: | ||
yield scope_mock | ||
finally: | ||
pio._kaleido.scope = original_scope | ||
from pdfrw import PdfReader | ||
from PIL import Image | ||
import plotly.io as pio | ||
from plotly.io.kaleido import kaleido_available, kaleido_major | ||
import pytest | ||
|
||
fig = {"data": [], "layout": {"title": {"text": "figure title"}}} | ||
|
||
|
||
def check_image(path_or_buffer, size=(700, 500), format="PNG"): | ||
if format == "PDF": | ||
img = PdfReader(path_or_buffer) | ||
# TODO: There is a conversion factor needed here | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Part of the TODO is for me to educate myself on how Plotly currently determines PDF size when writing to PDF. :) But the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will also take a looksy on that issue, lets say for this and the above tomorrow. I am flying today. |
||
# In Kaleido v0 the conversion factor is 0.75 | ||
factor = 0.75 | ||
expected_size = tuple(int(s * factor) for s in size) | ||
actual_size = tuple(int(s) for s in img.pages[0].MediaBox[2:]) | ||
assert actual_size == expected_size | ||
else: | ||
if isinstance(path_or_buffer, (str, Path)): | ||
with open(path_or_buffer, "rb") as f: | ||
img = Image.open(f) | ||
else: | ||
img = Image.open(path_or_buffer) | ||
assert img.size == size | ||
assert img.format == format | ||
|
||
|
||
def test_kaleido_engine_to_image_returns_bytes(): | ||
|
@@ -75,80 +44,95 @@ def test_kaleido_fulljson(): | |
|
||
|
||
def test_kaleido_engine_to_image(): | ||
with mocked_scope() as scope: | ||
pio.to_image(fig, engine="kaleido", validate=False) | ||
bytes = pio.to_image(fig, engine="kaleido", validate=False) | ||
|
||
scope.transform.assert_called_with( | ||
fig, format=None, width=None, height=None, scale=None | ||
) | ||
# Check that image dimensions match default dimensions (700x500) | ||
# and format is default format (png) | ||
check_image(BytesIO(bytes)) | ||
|
||
|
||
def test_kaleido_engine_write_image(): | ||
for writeable_mock in make_writeable_mocks(): | ||
with mocked_scope() as scope: | ||
pio.write_image(fig, writeable_mock, engine="kaleido", validate=False) | ||
def test_kaleido_engine_write_image(tmp_path): | ||
path_str = tempfile.mkstemp(suffix=".png", dir=tmp_path)[1] | ||
path_path = Path(tempfile.mkstemp(suffix=".png", dir=tmp_path)[1]) | ||
|
||
scope.transform.assert_called_with( | ||
fig, | ||
format=writeable_mock.expected_format, | ||
width=None, | ||
height=None, | ||
scale=None, | ||
) | ||
|
||
assert writeable_mock.active_write_function.call_count == 1 | ||
for out_path in [path_str, path_path]: | ||
pio.write_image(fig, out_path, engine="kaleido", validate=False) | ||
check_image(out_path) | ||
|
||
|
||
def test_kaleido_engine_to_image_kwargs(): | ||
with mocked_scope() as scope: | ||
pio.to_image( | ||
bytes = pio.to_image( | ||
fig, | ||
format="pdf", | ||
width=700, | ||
height=600, | ||
scale=2, | ||
engine="kaleido", | ||
validate=False, | ||
) | ||
check_image(BytesIO(bytes), size=(700 * 2, 600 * 2), format="PDF") | ||
|
||
|
||
def test_kaleido_engine_write_image_kwargs(tmp_path): | ||
path_str = tempfile.mkstemp(suffix=".png", dir=tmp_path)[1] | ||
path_path = Path(tempfile.mkstemp(suffix=".png", dir=tmp_path)[1]) | ||
|
||
for out_path in [path_str, path_path]: | ||
pio.write_image( | ||
fig, | ||
format="pdf", | ||
out_path, | ||
format="jpg", | ||
width=700, | ||
height=600, | ||
scale=2, | ||
engine="kaleido", | ||
validate=False, | ||
) | ||
|
||
scope.transform.assert_called_with( | ||
fig, format="pdf", width=700, height=600, scale=2 | ||
check_image(out_path, size=(700 * 2, 600 * 2), format="JPEG") | ||
|
||
|
||
@pytest.mark.skipif( | ||
not kaleido_available() or kaleido_major() < 1, | ||
reason="requires Kaleido v1.0.0 or higher", | ||
) | ||
def test_kaleido_engine_write_images(tmp_path): | ||
fig1 = {"data": [], "layout": {"title": {"text": "figure 1"}}} | ||
fig2 = {"data": [], "layout": {"title": {"text": "figure 2"}}} | ||
|
||
path_str = tempfile.mkstemp(suffix=".png", dir=tmp_path)[1] | ||
path_path = Path(tempfile.mkstemp(suffix=".png", dir=tmp_path)[1]) | ||
|
||
pio.write_images( | ||
[fig1, fig2], | ||
[path_str, path_path], | ||
format=["jpg", "png"], | ||
width=[700, 900], | ||
height=600, | ||
scale=2, | ||
validate=False, | ||
) | ||
|
||
|
||
def test_kaleido_engine_write_image_kwargs(): | ||
for writeable_mock in make_writeable_mocks(): | ||
with mocked_scope() as scope: | ||
pio.write_image( | ||
fig, | ||
writeable_mock, | ||
format="jpg", | ||
width=700, | ||
height=600, | ||
scale=2, | ||
engine="kaleido", | ||
validate=False, | ||
) | ||
|
||
scope.transform.assert_called_with( | ||
fig, format="jpg", width=700, height=600, scale=2 | ||
) | ||
|
||
assert writeable_mock.active_write_function.call_count == 1 | ||
check_image(path_str, size=(700 * 2, 600 * 2), format="JPEG") | ||
check_image(str(path_path), size=(900 * 2, 600 * 2), format="PNG") | ||
|
||
|
||
def test_image_renderer(): | ||
with mocked_scope() as scope: | ||
pio.show(fig, renderer="svg", engine="kaleido", validate=False) | ||
|
||
renderer = pio.renderers["svg"] | ||
scope.transform.assert_called_with( | ||
fig, | ||
format="svg", | ||
width=None, | ||
height=None, | ||
scale=renderer.scale, | ||
"""Verify that the image renderer returns the expected mimebundle.""" | ||
with redirect_stdout(StringIO()) as f: | ||
pio.show(fig, renderer="png", engine="kaleido", validate=False) | ||
mimebundle = f.getvalue().strip() | ||
mimebundle_expected = str( | ||
{ | ||
"image/png": base64.b64encode( | ||
pio.to_image( | ||
fig, | ||
format="png", | ||
engine="kaleido", | ||
validate=False, | ||
) | ||
).decode("utf8") | ||
} | ||
) | ||
assert mimebundle == mimebundle_expected | ||
|
||
|
||
def test_bytesio(): | ||
|
@@ -163,3 +147,16 @@ def test_bytesio(): | |
bio_bytes = bio.read() | ||
to_image_bytes = pio.to_image(fig, format="jpg", engine="kaleido", validate=False) | ||
assert bio_bytes == to_image_bytes | ||
|
||
|
||
def test_defaults(): | ||
"""Test that image output defaults can be set using pio.defaults.*""" | ||
try: | ||
assert pio.defaults.default_format == "png" | ||
pio.defaults.default_format = "svg" | ||
assert pio.defaults.default_format == "svg" | ||
result = pio.to_image(fig, format="svg", validate=False) | ||
assert result.startswith(b"<svg") | ||
finally: | ||
pio.defaults.default_format = "png" | ||
assert pio.defaults.default_format == "png" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there percy tests that use kaleido to make sure that the actual images generated are correct?