From f0a78a64fe0451120a7536231b45d6ac3bce5bcf Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:02:11 -0500 Subject: [PATCH 01/12] small io refactor to support kaleido v1 --- plotly/io/_kaleido.py | 184 ++++++++++++++++++++---------------------- plotly/io/kaleido.py | 2 +- 2 files changed, 89 insertions(+), 97 deletions(-) diff --git a/plotly/io/_kaleido.py b/plotly/io/_kaleido.py index 029b79f1029..6709bb7f0c9 100644 --- a/plotly/io/_kaleido.py +++ b/plotly/io/_kaleido.py @@ -1,29 +1,41 @@ import os import json from pathlib import Path +import importlib.metadata as importlib_metadata +from packaging.version import Version +import tempfile + import plotly from plotly.io._utils import validate_coerce_fig_to_dict try: - from kaleido.scopes.plotly import PlotlyScope - - scope = PlotlyScope() - - # Compute absolute path to the 'plotly/package_data/' directory - root_dir = os.path.dirname(os.path.abspath(plotly.__file__)) - package_dir = os.path.join(root_dir, "package_data") - scope.plotlyjs = os.path.join(package_dir, "plotly.min.js") - if scope.mathjax is None: - scope.mathjax = ( - "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js" - ) -except ImportError: + import kaleido + + kaleido_available = True + kaleido_major = Version(importlib_metadata.version("kaleido")).major + + if kaleido_major < 1: + # Kaleido v0 + from kaleido.scopes.plotly import PlotlyScope + + scope = PlotlyScope() + # Compute absolute path to the 'plotly/package_data/' directory + root_dir = os.path.dirname(os.path.abspath(plotly.__file__)) + package_dir = os.path.join(root_dir, "package_data") + scope.plotlyjs = os.path.join(package_dir, "plotly.min.js") + if scope.mathjax is None: + scope.mathjax = ( + "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js" + ) +except ImportError as e: + kaleido_available = False + kaleido_major = -1 PlotlyScope = None scope = None def to_image( - fig, format=None, width=None, height=None, scale=None, validate=True, engine="auto" + fig, format=None, width=None, height=None, scale=None, validate=True, engine=None ): """ Convert a figure to a static image bytes string @@ -35,34 +47,28 @@ def to_image( format: str or None The desired image format. One of - - 'png' - - 'jpg' or 'jpeg' - - 'webp' - - 'svg' - - 'pdf' - - 'eps' (Requires the poppler library to be installed and on the PATH) + - 'png' + - 'jpg' or 'jpeg' + - 'webp' + - 'svg' + - 'pdf' + - 'eps' (Requires the poppler library to be installed and on the PATH) - If not specified, will default to: - - `plotly.io.kaleido.scope.default_format` if engine is "kaleido" - - `plotly.io.orca.config.default_format` if engine is "orca" + If not specified, will default to `plotly.io.kaleido.scope.default_format` width: int or None The width of the exported image in layout pixels. If the `scale` property is 1.0, this will also be the width of the exported image in physical pixels. - If not specified, will default to: - - `plotly.io.kaleido.scope.default_width` if engine is "kaleido" - - `plotly.io.orca.config.default_width` if engine is "orca" + If not specified, will default to `plotly.io.kaleido.scope.default_width` height: int or None The height of the exported image in layout pixels. If the `scale` property is 1.0, this will also be the height of the exported image in physical pixels. - If not specified, will default to: - - `plotly.io.kaleido.scope.default_height` if engine is "kaleido" - - `plotly.io.orca.config.default_height` if engine is "orca" + If not specified, will default to `plotly.io.kaleido.scope.default_height` scale: int or float or None The scale factor to use when exporting the figure. A scale factor @@ -70,65 +76,23 @@ def to_image( to the figure's layout pixel dimensions. Whereas as scale factor of less than 1.0 will decrease the image resolution. - If not specified, will default to: - - `plotly.io.kaleido.scope.default_scale` if engine is "kaleido" - - `plotly.io.orca.config.default_scale` if engine is "orca" - + If not specified, will default to `plotly.io.kaleido.scope.default_scale` validate: bool True if the figure should be validated before being converted to an image, False otherwise. - engine: str - Image export engine to use: - - "kaleido": Use Kaleido for image export - - "orca": Use Orca for image export - - "auto" (default): Use Kaleido if installed, otherwise use orca + engine (deprecated): str + No longer used. Kaleido is the only supported engine. Returns ------- bytes The image data """ - # Handle engine - # ------------- - if engine == "auto": - if scope is not None: - # Default to kaleido if available - engine = "kaleido" - else: - # See if orca is available - from ._orca import validate_executable - - try: - validate_executable() - engine = "orca" - except: - # If orca not configured properly, make sure we display the error - # message advising the installation of kaleido - engine = "kaleido" - - if engine == "orca": - # Fall back to legacy orca image export path - from ._orca import to_image as to_image_orca - - return to_image_orca( - fig, - format=format, - width=width, - height=height, - scale=scale, - validate=validate, - ) - elif engine != "kaleido": - raise ValueError( - "Invalid image export engine specified: {engine}".format( - engine=repr(engine) - ) - ) # Raise informative error message if Kaleido is not installed - if scope is None: + if not kaleido_available: raise ValueError( """ Image export using the "kaleido" engine requires the kaleido package, @@ -137,12 +101,32 @@ def to_image( """ ) - # Validate figure - # --------------- + # Convert figure to dict (and validate if requested) fig_dict = validate_coerce_fig_to_dict(fig, validate) - img_bytes = scope.transform( - fig_dict, format=format, width=width, height=height, scale=scale - ) + + # Request image bytes + if kaleido_major > 0: + # Kaleido v1 + opts = { + k: v + for k, v in dict( + format=format, + width=width, + height=height, + scale=scale, + ).items() + if v is not None + } + img_bytes = kaleido.calc_fig_sync( + fig_dict, + path=None, + opts=opts, + ) + else: + # Kaleido v0 + img_bytes = scope.transform( + fig_dict, format=format, width=width, height=height, scale=scale + ) return img_bytes @@ -190,18 +174,14 @@ def write_image( property is 1.0, this will also be the width of the exported image in physical pixels. - If not specified, will default to: - - `plotly.io.kaleido.scope.default_width` if engine is "kaleido" - - `plotly.io.orca.config.default_width` if engine is "orca" + If not specified, will default to`plotly.io.kaleido.scope.default_width` height: int or None The height of the exported image in layout pixels. If the `scale` property is 1.0, this will also be the height of the exported image in physical pixels. - If not specified, will default to: - - `plotly.io.kaleido.scope.default_height` if engine is "kaleido" - - `plotly.io.orca.config.default_height` if engine is "orca" + If not specified, will default to `plotly.io.kaleido.scope.default_height` scale: int or float or None The scale factor to use when exporting the figure. A scale factor @@ -209,19 +189,14 @@ def write_image( to the figure's layout pixel dimensions. Whereas as scale factor of less than 1.0 will decrease the image resolution. - If not specified, will default to: - - `plotly.io.kaleido.scope.default_scale` if engine is "kaleido" - - `plotly.io.orca.config.default_scale` if engine is "orca" + If not specified, will default to `plotly.io.kaleido.scope.default_scale` validate: bool True if the figure should be validated before being converted to an image, False otherwise. - engine: str - Image export engine to use: - - "kaleido": Use Kaleido for image export - - "orca": Use Orca for image export - - "auto" (default): Use Kaleido if installed, otherwise use orca + engine (deprecated): str + No longer used. Kaleido is the only supported engine. Returns ------- @@ -323,7 +298,7 @@ def full_figure_for_development(fig, warn=True, as_dict=False): """ # Raise informative error message if Kaleido is not installed - if scope is None: + if not kaleido_available: raise ValueError( """ Full figure generation requires the kaleido package, @@ -341,7 +316,24 @@ def full_figure_for_development(fig, warn=True, as_dict=False): "To suppress this warning, set warn=False" ) - fig = json.loads(scope.transform(fig, format="json").decode("utf-8")) + if kaleido_major > 0: + # Kaleido v1 + try: + json_file = Path(tempfile.mkstemp(suffix=".json")[1]) + kaleido.write_fig_sync( + fig, + json_file, + dict(format="json"), + ) + with open(json_file, "r") as f: + fig = json.load(f) + finally: + # Cleanup: remove temp file + json_file.unlink() + else: + # Kaleido v0 + fig = json.loads(scope.transform(fig, format="json").decode("utf-8")) + if as_dict: return fig else: diff --git a/plotly/io/kaleido.py b/plotly/io/kaleido.py index c14b315047b..b9ff6c7582e 100644 --- a/plotly/io/kaleido.py +++ b/plotly/io/kaleido.py @@ -1 +1 @@ -from ._kaleido import to_image, write_image, scope +from ._kaleido import write_image, to_image From a681c84aca0a487ee1e34e92386ac1addc069cd8 Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:03:37 -0500 Subject: [PATCH 02/12] update kaleido tests to support kaleido v1 --- .../test_kaleido/test_kaleido.py | 175 +++++++----------- 1 file changed, 63 insertions(+), 112 deletions(-) diff --git a/tests/test_optional/test_kaleido/test_kaleido.py b/tests/test_optional/test_kaleido/test_kaleido.py index 263fd85483a..436f2813b4b 100644 --- a/tests/test_optional/test_kaleido/test_kaleido.py +++ b/tests/test_optional/test_kaleido/test_kaleido.py @@ -1,66 +1,31 @@ -import plotly.io as pio -import plotly.io.kaleido -from contextlib import contextmanager from io import BytesIO 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() +import tempfile - # 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 - - # 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" +from pdfrw import PdfReader +from PIL import Image +import plotly.io as pio - return mock_file_descriptor, mock_pathlib_path +fig = {"data": [], "layout": {"title": {"text": "figure title"}}} -@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 +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 + # 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 +40,66 @@ 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 - ) - - -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(out_path, size=(700 * 2, 600 * 2), format="JPEG") def test_image_renderer(): - with mocked_scope() as scope: - pio.show(fig, renderer="svg", engine="kaleido", validate=False) + # TODO: How to replicate this test using kaleido v1? + # 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, - ) + # scope.transform.assert_called_with( + # fig, + # format="svg", + # width=None, + # height=None, + # scale=renderer.scale, + # ) def test_bytesio(): From 2e9c8affa3c4133c3ed91013af6de005c1c4ab4f Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:27:28 -0500 Subject: [PATCH 03/12] update test requirements --- test_requirements/requirements_310_optional.txt | 1 + test_requirements/requirements_311_optional.txt | 1 + test_requirements/requirements_312_no_numpy_optional.txt | 1 + test_requirements/requirements_312_np2_optional.txt | 1 + test_requirements/requirements_312_optional.txt | 1 + test_requirements/requirements_38_optional.txt | 1 + test_requirements/requirements_39_optional.txt | 1 + test_requirements/requirements_39_pandas_2_optional.txt | 1 + 8 files changed, 8 insertions(+) diff --git a/test_requirements/requirements_310_optional.txt b/test_requirements/requirements_310_optional.txt index f61fef5a5dc..41eeb460980 100644 --- a/test_requirements/requirements_310_optional.txt +++ b/test_requirements/requirements_310_optional.txt @@ -22,3 +22,4 @@ polars[timezone] pyarrow narwhals>=1.15.1 anywidget==0.9.13 +pdfrw diff --git a/test_requirements/requirements_311_optional.txt b/test_requirements/requirements_311_optional.txt index 505636afaa2..9ff2d68af55 100644 --- a/test_requirements/requirements_311_optional.txt +++ b/test_requirements/requirements_311_optional.txt @@ -23,3 +23,4 @@ polars[timezone] pyarrow kaleido plotly-geo +pdfrw diff --git a/test_requirements/requirements_312_no_numpy_optional.txt b/test_requirements/requirements_312_no_numpy_optional.txt index 9786aea5f6a..482db76abeb 100644 --- a/test_requirements/requirements_312_no_numpy_optional.txt +++ b/test_requirements/requirements_312_no_numpy_optional.txt @@ -22,3 +22,4 @@ pyarrow narwhals>=1.15.1 anywidget==0.9.13 jupyter-console==6.4.4 +pdfrw diff --git a/test_requirements/requirements_312_np2_optional.txt b/test_requirements/requirements_312_np2_optional.txt index 1e02e3a8360..6d45375217c 100644 --- a/test_requirements/requirements_312_np2_optional.txt +++ b/test_requirements/requirements_312_np2_optional.txt @@ -23,3 +23,4 @@ polars[timezone] pyarrow narwhals>=1.15.1 anywidget==0.9.13 +pdfrw diff --git a/test_requirements/requirements_312_optional.txt b/test_requirements/requirements_312_optional.txt index 0e85492bb86..20f9fa1ee15 100644 --- a/test_requirements/requirements_312_optional.txt +++ b/test_requirements/requirements_312_optional.txt @@ -24,3 +24,4 @@ narwhals>=1.15.1 anywidget==0.9.13 jupyter-console==6.4.4 plotly-geo +pdfrw diff --git a/test_requirements/requirements_38_optional.txt b/test_requirements/requirements_38_optional.txt index f62c6ad6560..92736431506 100644 --- a/test_requirements/requirements_38_optional.txt +++ b/test_requirements/requirements_38_optional.txt @@ -22,3 +22,4 @@ polars[timezone] pyarrow narwhals>=1.15.1 anywidget==0.9.13 +pdfrw diff --git a/test_requirements/requirements_39_optional.txt b/test_requirements/requirements_39_optional.txt index 1a767fe4926..4ee17a1995b 100644 --- a/test_requirements/requirements_39_optional.txt +++ b/test_requirements/requirements_39_optional.txt @@ -23,3 +23,4 @@ polars[timezone] pyarrow narwhals>=1.15.1 anywidget==0.9.13 +pdfrw diff --git a/test_requirements/requirements_39_pandas_2_optional.txt b/test_requirements/requirements_39_pandas_2_optional.txt index 214bb545c0b..242e51f3200 100644 --- a/test_requirements/requirements_39_pandas_2_optional.txt +++ b/test_requirements/requirements_39_pandas_2_optional.txt @@ -24,3 +24,4 @@ pyarrow narwhals>=1.15.1 polars anywidget==0.9.13 +pdfrw From 60bb7484047512f9259a3f58180072ea94c0b48c Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:29:02 -0500 Subject: [PATCH 04/12] simplify to_image --- plotly/io/_kaleido.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/plotly/io/_kaleido.py b/plotly/io/_kaleido.py index 6709bb7f0c9..172560c6d8e 100644 --- a/plotly/io/_kaleido.py +++ b/plotly/io/_kaleido.py @@ -107,20 +107,15 @@ def to_image( # Request image bytes if kaleido_major > 0: # Kaleido v1 - opts = { - k: v - for k, v in dict( + img_bytes = kaleido.calc_fig_sync( + fig_dict, + path=None, + opts=dict( format=format, width=width, height=height, scale=scale, - ).items() - if v is not None - } - img_bytes = kaleido.calc_fig_sync( - fig_dict, - path=None, - opts=opts, + ), ) else: # Kaleido v0 From b87f752af9c52d56c77cd81fe1a043d811ccf94e Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:03:58 -0500 Subject: [PATCH 05/12] use k.calc_fig() instead of k.write_fig() in pio.full_figure_for_development() --- plotly/io/_kaleido.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/plotly/io/_kaleido.py b/plotly/io/_kaleido.py index 172560c6d8e..759589c980c 100644 --- a/plotly/io/_kaleido.py +++ b/plotly/io/_kaleido.py @@ -313,18 +313,11 @@ def full_figure_for_development(fig, warn=True, as_dict=False): if kaleido_major > 0: # Kaleido v1 - try: - json_file = Path(tempfile.mkstemp(suffix=".json")[1]) - kaleido.write_fig_sync( - fig, - json_file, - dict(format="json"), - ) - with open(json_file, "r") as f: - fig = json.load(f) - finally: - # Cleanup: remove temp file - json_file.unlink() + bytes = kaleido.calc_fig_sync( + fig, + opts=dict(format="json"), + ) + fig = json.loads(bytes.decode("utf-8")) else: # Kaleido v0 fig = json.loads(scope.transform(fig, format="json").decode("utf-8")) From 1a195a725d3ba4a6c307e31e6eed449c84c5f3af Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:38:01 -0400 Subject: [PATCH 06/12] add ci job to test with kaleido v1 --- .circleci/config.yml | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3b36cc48022..fe5371f176e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -117,6 +117,35 @@ commands: source .venv/bin/activate python -m pytest -x test_init/test_lazy_imports.py + test_io_kaleido_v1: + steps: + - checkout + - browser-tools/install-chrome + - browser-tools/install-chromedriver + - run: + name: Install dependencies + command: | + curl -LsSf https://astral.sh/uv/install.sh | sh + uv venv + source .venv/bin/activate + uv pip install . + uv pip install -r ./test_requirements/requirements_optional.txt + # Install Kaleido v1 instead of the default version + uv pip uninstall -y kaleido + uv pip install 'git+https://github.com/plotly/Kaleido.git@v1.0.0rc10#subdirectory=src/py' + - run: + name: List installed packages and python version + command: | + source .venv/bin/activate + uv pip list + python --version + - run: + name: Test plotly.io with Kaleido v1 + command: | + source .venv/bin/activate + python -m pytest tests/test_io + no_output_timeout: 20m + jobs: check-code-formatting: docker: @@ -166,6 +195,17 @@ jobs: pandas_version: <> numpy_version: <> + test_kaleido_v1: + parameters: + python_version: + default: "3.12" + type: string + executor: + name: docker-container + python_version: <> + steps: + - test_io_kaleido_v1 + # Percy python_311_percy: docker: @@ -448,5 +488,10 @@ workflows: python_version: "3.9" pandas_version: "1.2.4" numpy_version: "1.26.4" + - test_kaleido_v1: + matrix: + parameters: + python_version: + - "3.12" - python_311_percy - build-doc From 577d3ca1afe418e80bbfa6d4d487485a361db4c3 Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:13:11 -0400 Subject: [PATCH 07/12] remove -y option --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fe5371f176e..ae2a0b07157 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -131,7 +131,7 @@ commands: uv pip install . uv pip install -r ./test_requirements/requirements_optional.txt # Install Kaleido v1 instead of the default version - uv pip uninstall -y kaleido + uv pip uninstall kaleido uv pip install 'git+https://github.com/plotly/Kaleido.git@v1.0.0rc10#subdirectory=src/py' - run: name: List installed packages and python version From 89209ad8b7a9383134809f25783c3661ba0d3713 Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:18:19 -0400 Subject: [PATCH 08/12] run test_kaleido instead of test_io --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ae2a0b07157..ee1e3332782 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -140,10 +140,10 @@ commands: uv pip list python --version - run: - name: Test plotly.io with Kaleido v1 + name: Test plotly.io image output with Kaleido v1 command: | source .venv/bin/activate - python -m pytest tests/test_io + python -m pytest tests/test_optional/test_kaleido no_output_timeout: 20m jobs: From 96bf9a04dc1112636641ca0acce86162161a461b Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Thu, 13 Mar 2025 10:40:35 -0400 Subject: [PATCH 09/12] re-add orca, add deprecation warnings for orca and kaleido-v0 (exact text TBD) --- plotly/io/_kaleido.py | 61 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/plotly/io/_kaleido.py b/plotly/io/_kaleido.py index 759589c980c..7c8a172366b 100644 --- a/plotly/io/_kaleido.py +++ b/plotly/io/_kaleido.py @@ -3,7 +3,7 @@ from pathlib import Path import importlib.metadata as importlib_metadata from packaging.version import Version -import tempfile +import warnings import plotly from plotly.io._utils import validate_coerce_fig_to_dict @@ -91,6 +91,55 @@ def to_image( The image data """ + # Handle engine + # ------------- + if engine is not None: + warnings.warn( + "The 'engine' parameter is deprecated and will be removed in a future version.", + DeprecationWarning, + ) + engine = "auto" + + if engine == "auto": + if kaleido_available: + # Default to kaleido if available + engine = "kaleido" + else: + # See if orca is available + from ._orca import validate_executable + + try: + validate_executable() + engine = "orca" + except: + # If orca not configured properly, make sure we display the error + # message advising the installation of kaleido + engine = "kaleido" + + if engine == "orca": + warnings.warn( + "Support for the 'orca' engine is deprecated and will be removed in a future version. " + "Please use the 'kaleido' engine instead.", + DeprecationWarning, + ) + # Fall back to legacy orca image export path + from ._orca import to_image as to_image_orca + + return to_image_orca( + fig, + format=format, + width=width, + height=height, + scale=scale, + validate=validate, + ) + elif engine != "kaleido": + raise ValueError( + "Invalid image export engine specified: {engine}".format( + engine=repr(engine) + ) + ) + # Raise informative error message if Kaleido is not installed if not kaleido_available: raise ValueError( @@ -119,6 +168,11 @@ def to_image( ) else: # Kaleido v0 + warnings.warn( + "Support for kaleido v0 is deprecated and will be removed in a future version. " + "Please upgrade to kaleido v1 by running `pip install kaleido>=1.0.0`.", + DeprecationWarning, + ) img_bytes = scope.transform( fig_dict, format=format, width=width, height=height, scale=scale ) @@ -320,6 +374,11 @@ def full_figure_for_development(fig, warn=True, as_dict=False): fig = json.loads(bytes.decode("utf-8")) else: # Kaleido v0 + warnings.warn( + "Support for kaleido v0 is deprecated and will be removed in a future version. " + "Please upgrade to kaleido v1 by running `pip install kaleido>=1.0.0`.", + DeprecationWarning, + ) fig = json.loads(scope.transform(fig, format="json").decode("utf-8")) if as_dict: From 350dd4816f41dfb99b60ebb55049c3e763bdbfe9 Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:59:26 -0400 Subject: [PATCH 10/12] error message for 'eps' with kaleido v1 --- plotly/io/_kaleido.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plotly/io/_kaleido.py b/plotly/io/_kaleido.py index 7c8a172366b..656fe051ab4 100644 --- a/plotly/io/_kaleido.py +++ b/plotly/io/_kaleido.py @@ -156,6 +156,15 @@ def to_image( # Request image bytes if kaleido_major > 0: # Kaleido v1 + # Check if trying to export to EPS format, which is not supported in Kaleido v1 + if format == 'eps': + raise ValueError( + """ +EPS export is not supported with Kaleido v1. +Please downgrade to Kaleido v0 to use EPS export: + $ pip install kaleido==0.2.1 +""" + ) img_bytes = kaleido.calc_fig_sync( fig_dict, path=None, From 08c1d4ee5a3d05b0913c37c396354b776ee3f534 Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:30:38 -0400 Subject: [PATCH 11/12] pin kaleido v1 (prerelease) in test requirements --- .circleci/config.yml | 14 +++++++------- test_requirements/requirements_optional.txt | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ee1e3332782..ac8cce9b8fe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -117,7 +117,7 @@ commands: source .venv/bin/activate python -m pytest -x test_init/test_lazy_imports.py - test_io_kaleido_v1: + test_io_kaleido_v0: steps: - checkout - browser-tools/install-chrome @@ -130,9 +130,9 @@ commands: source .venv/bin/activate uv pip install . uv pip install -r ./test_requirements/requirements_optional.txt - # Install Kaleido v1 instead of the default version + # Install Kaleido v0 instead of the v1 specified in requirements_optional.txt uv pip uninstall kaleido - uv pip install 'git+https://github.com/plotly/Kaleido.git@v1.0.0rc10#subdirectory=src/py' + uv pip install kaleido==0.2.1 - run: name: List installed packages and python version command: | @@ -140,7 +140,7 @@ commands: uv pip list python --version - run: - name: Test plotly.io image output with Kaleido v1 + name: Test plotly.io image output with Kaleido v0 command: | source .venv/bin/activate python -m pytest tests/test_optional/test_kaleido @@ -195,7 +195,7 @@ jobs: pandas_version: <> numpy_version: <> - test_kaleido_v1: + test_kaleido_v0: parameters: python_version: default: "3.12" @@ -204,7 +204,7 @@ jobs: name: docker-container python_version: <> steps: - - test_io_kaleido_v1 + - test_io_kaleido_v0 # Percy python_311_percy: @@ -488,7 +488,7 @@ workflows: python_version: "3.9" pandas_version: "1.2.4" numpy_version: "1.26.4" - - test_kaleido_v1: + - test_kaleido_v0: matrix: parameters: python_version: diff --git a/test_requirements/requirements_optional.txt b/test_requirements/requirements_optional.txt index 811d93ce80c..ac18a3a86d5 100644 --- a/test_requirements/requirements_optional.txt +++ b/test_requirements/requirements_optional.txt @@ -17,7 +17,8 @@ pyshp matplotlib scikit-image psutil -kaleido +# kaleido>=1.0.0 # Uncomment and delete line below once Kaleido v1 is released +git+https://github.com/plotly/Kaleido.git@v1.0.0rc10#subdirectory=src/py orjson polars[timezone] pyarrow From 8b47a0a9c099cc2e12001d5c4a1d197a8e7fa6b8 Mon Sep 17 00:00:00 2001 From: Emily Kellison-Linn <4672118+emilykl@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:03:52 -0400 Subject: [PATCH 12/12] format --- plotly/io/_kaleido.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/plotly/io/_kaleido.py b/plotly/io/_kaleido.py index 656fe051ab4..3cfd0cef1ad 100644 --- a/plotly/io/_kaleido.py +++ b/plotly/io/_kaleido.py @@ -157,7 +157,7 @@ def to_image( if kaleido_major > 0: # Kaleido v1 # Check if trying to export to EPS format, which is not supported in Kaleido v1 - if format == 'eps': + if format == "eps": raise ValueError( """ EPS export is not supported with Kaleido v1. @@ -288,9 +288,7 @@ def write_image( >>> import plotly.io as pio >>> pio.write_image(fig, file_path, format='png') -""".format( - file=file - ) +""".format(file=file) ) # Request image @@ -319,9 +317,7 @@ def write_image( raise ValueError( """ The 'file' argument '{file}' is not a string, pathlib.Path object, or file descriptor. -""".format( - file=file - ) +""".format(file=file) ) else: # We previously succeeded in interpreting `file` as a pathlib object.