11import plotly .io as pio
22import plotly .io .kaleido
3- import sys
43from contextlib import contextmanager
5- 6- if sys .version_info >= (3 , 3 ):
7- from unittest .mock import Mock
8- else :
9- from mock import Mock
4+ from io import BytesIO
5+ from pathlib import Path
6+ from unittest .mock import Mock
107
118fig = {"layout" : {"title" : {"text" : "figure title" }}}
129
1310
11+ def make_writeable_mocks ():
12+ """Produce some mocks which we will use for testing the `write_image()` function.
13+
14+ These mocks should be passed as the `file=` argument to `write_image()`.
15+
16+ The tests should verify that the method specified in the `active_write_function`
17+ attribute is called once, and that scope.transform is called with the `format=`
18+ argument specified by the `.expected_format` attribute.
19+
20+ In total we provide two mocks: one for a writable file descriptor, and other for a
21+ pathlib.Path object.
22+ """
23+ 24+ # Part 1: A mock for a file descriptor
25+ # ------------------------------------
26+ mock_file_descriptor = Mock ()
27+ 28+ # A file descriptor has no write_bytes method, unlike a pathlib Path.
29+ del mock_file_descriptor .write_bytes
30+ 31+ # The expected write method for a file descriptor is .write
32+ mock_file_descriptor .active_write_function = mock_file_descriptor .write
33+ 34+ # Since there is no filename, there should be no format detected.
35+ mock_file_descriptor .expected_format = None
36+ 37+ # Part 2: A mock for a pathlib path
38+ # ---------------------------------
39+ mock_pathlib_path = Mock (spec = Path )
40+ 41+ # A pathlib Path object has no write method, unlike a file descriptor.
42+ del mock_pathlib_path .write
43+ 44+ # The expected write method for a pathlib Path is .write_bytes
45+ mock_pathlib_path .active_write_function = mock_pathlib_path .write_bytes
46+ 47+ # Mock a path with PNG suffix
48+ mock_pathlib_path .suffix = ".png"
49+ mock_pathlib_path .expected_format = "png"
50+ 51+ return mock_file_descriptor , mock_pathlib_path
52+ 53+ 1454@contextmanager
1555def mocked_scope ():
1656 # Code to acquire resource, e.g.:
@@ -44,15 +84,19 @@ def test_kaleido_engine_to_image():
4484
4585
4686def test_kaleido_engine_write_image ():
47- writeable_mock = Mock ()
48- with mocked_scope () as scope :
49- pio .write_image (fig , writeable_mock , engine = "kaleido" , validate = False )
87+ for writeable_mock in make_writeable_mocks ():
88+ with mocked_scope () as scope :
89+ pio .write_image (fig , writeable_mock , engine = "kaleido" , validate = False )
5090
51- scope .transform .assert_called_with (
52- fig , format = None , width = None , height = None , scale = None
53- )
91+ scope .transform .assert_called_with (
92+ fig ,
93+ format = writeable_mock .expected_format ,
94+ width = None ,
95+ height = None ,
96+ scale = None ,
97+ )
5498
55- assert writeable_mock .write .call_count == 1
99+ assert writeable_mock .active_write_function .call_count == 1
56100
57101
58102def test_kaleido_engine_to_image_kwargs ():
@@ -73,24 +117,24 @@ def test_kaleido_engine_to_image_kwargs():
73117
74118
75119def test_kaleido_engine_write_image_kwargs ():
76- writeable_mock = Mock ()
77- with mocked_scope () as scope :
78- pio .write_image (
79- fig ,
80- writeable_mock ,
81- format = "jpg" ,
82- width = 700 ,
83- height = 600 ,
84- scale = 2 ,
85- engine = "kaleido" ,
86- validate = False ,
120+ for writeable_mock in make_writeable_mocks ():
121+ with mocked_scope () as scope :
122+ pio .write_image (
123+ fig ,
124+ writeable_mock ,
125+ format = "jpg" ,
126+ width = 700 ,
127+ height = 600 ,
128+ scale = 2 ,
129+ engine = "kaleido" ,
130+ validate = False ,
131+ )
132+ 133+ scope .transform .assert_called_with (
134+ fig , format = "jpg" , width = 700 , height = 600 , scale = 2
87135 )
88136
89- scope .transform .assert_called_with (
90- fig , format = "jpg" , width = 700 , height = 600 , scale = 2
91- )
92- 93- assert writeable_mock .write .call_count == 1
137+ assert writeable_mock .active_write_function .call_count == 1
94138
95139
96140def test_image_renderer ():
@@ -105,3 +149,17 @@ def test_image_renderer():
105149 height = renderer .height ,
106150 scale = renderer .scale ,
107151 )
152+ 153+ 154+ def test_bytesio ():
155+ """Verify that writing to a BytesIO object contains the same data as to_image().
156+
157+ The goal of this test is to ensure that Plotly correctly handles a writable buffer
158+ which doesn't correspond to a filesystem path.
159+ """
160+ bio = BytesIO ()
161+ pio .write_image (fig , bio , format = "jpg" , engine = "kaleido" , validate = False )
162+ bio .seek (0 ) # Rewind to the beginning of the buffer, otherwise read() returns b''.
163+ bio_bytes = bio .read ()
164+ to_image_bytes = pio .to_image (fig , format = "jpg" , engine = "kaleido" , validate = False )
165+ assert bio_bytes == to_image_bytes
0 commit comments