diff --git a/CHANGELOG.md b/CHANGELOG.md index ea837e3d0bd..fa6a873dfb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fix concurrent first access to lazily initialized graph object properties, which could raise `ValueError("Invalid value")` [[#5691](https://github.com/plotly/plotly.py/pull/5691)], with thanks to @hb1915 for the contribution! - Fix `mpl_to_plotly` crashing on touching bars (such as `plt.hist`) due to floating-point noise producing negative `bargap` values by clamping `bargap` to `[0, 1]` [[#5696](https://github.com/plotly/plotly.py/pull/5696)], with thanks to @robertoffmoura for the contribution! - Fix `px.sunburst`, `px.treemap` and `px.icicle` listing sectors in a different order on every run when `path` is used with a Polars DataFrame; sectors now follow their order of first appearance for all dataframe backends [[#5766](https://github.com/plotly/plotly.py/pull/5766)], with thanks to @Irahan2 for the contribution! +- Fix `write_image`, `to_image` and `write_images` emitting `UserWarning: The kopts argument is ignored if using a server.` on every call under `kaleido.start_sync_server()` when no Kaleido options are set; the warning still appears when `plotly.io.defaults` sets `plotlyjs`, `mathjax` or custom `headers` [[#5787](https://github.com/plotly/plotly.py/pull/5787)], with thanks to @jayzhou2309 for the contribution! ## [7.1.0] - 2026-09-15 diff --git a/plotly/io/_defaults.py b/plotly/io/_defaults.py index ba4e48b18e1..db3d000dc10 100644 --- a/plotly/io/_defaults.py +++ b/plotly/io/_defaults.py @@ -1,5 +1,9 @@ # Default settings for image generation +# This header is necessary to comply with Open Street Map tile policy: +# https://openstreetmap.github.io/owg-website/policies/tiles/#31-identification +DEFAULT_HEADERS = {"X-Requested-With": "plotly.py"} + class _Defaults(object): """ @@ -14,9 +18,7 @@ def __init__(self): self.mathjax = None self.topojson = None self.plotlyjs = None - # This header is necessary to comply with Open Street Map tile policy: - # https://openstreetmap.github.io/owg-website/policies/tiles/#31-identification - self.headers = {"X-Requested-With": "plotly.py"} + self.headers = dict(DEFAULT_HEADERS) defaults = _Defaults() diff --git a/plotly/io/_kaleido.py b/plotly/io/_kaleido.py index 42adcf1e58e..a0d3d376744 100644 --- a/plotly/io/_kaleido.py +++ b/plotly/io/_kaleido.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager import os import json from pathlib import Path @@ -8,7 +9,7 @@ import plotly from plotly.io._utils import validate_coerce_fig_to_dict, broadcast_args_to_dicts -from plotly.io._defaults import defaults +from plotly.io._defaults import defaults, DEFAULT_HEADERS from _plotly_utils.optional_imports import get_module kaleido = get_module("kaleido", should_load=True) @@ -35,6 +36,18 @@ _KALEIDO_AVAILABLE = None +@contextmanager +def _ignore_default_kopts_warning(kopts): + # A running kaleido sync server ignores kopts and warns about any. Only + # warn when the user set an option, not for plotly's default headers. + with warnings.catch_warnings(): + if kopts == {"headers": DEFAULT_HEADERS}: + warnings.filterwarnings( + "ignore", message="The kopts argument", category=UserWarning + ) + yield + + def kaleido_available() -> bool: """ Returns True if Kaleido version 1.0.0 or greater is installed, otherwise False. @@ -182,17 +195,18 @@ def to_image( or defaults.default_height ) - img_bytes = kaleido.calc_fig_sync( - fig_dict, - opts=dict( - format=format or defaults.default_format, - width=width, - height=height, - scale=scale or defaults.default_scale, - ), - topojson=defaults.topojson, - kopts=kopts, - ) + with _ignore_default_kopts_warning(kopts): + img_bytes = kaleido.calc_fig_sync( + fig_dict, + opts=dict( + format=format or defaults.default_format, + width=width, + height=height, + scale=scale or defaults.default_scale, + ), + topojson=defaults.topojson, + kopts=kopts, + ) except ChromeNotFoundError: raise RuntimeError(PLOTLY_GET_CHROME_ERROR_MSG) @@ -430,10 +444,11 @@ def write_images( kopts["mathjax"] = defaults.mathjax if defaults.headers: kopts["headers"] = defaults.headers - kaleido.write_fig_from_object_sync( - kaleido_specs, - kopts=kopts, - ) + with _ignore_default_kopts_warning(kopts): + kaleido.write_fig_from_object_sync( + kaleido_specs, + kopts=kopts, + ) except ChromeNotFoundError: raise RuntimeError(PLOTLY_GET_CHROME_ERROR_MSG) diff --git a/tests/test_optional/test_kaleido/test_kaleido.py b/tests/test_optional/test_kaleido/test_kaleido.py index d1573009e4a..532ef7ff00d 100644 --- a/tests/test_optional/test_kaleido/test_kaleido.py +++ b/tests/test_optional/test_kaleido/test_kaleido.py @@ -4,10 +4,12 @@ from pathlib import Path import tempfile from unittest.mock import patch +import warnings import xml.etree.ElementTree as ET from pdfrw import PdfReader from PIL import Image +import kaleido import plotly.graph_objects as go import plotly.io as pio @@ -335,3 +337,34 @@ def test_width_height_priority(): assert height == pio.defaults.default_height, ( "Default height should be used when no layout or argument" ) + + +def kopts_warnings(export): + """Run `export` under a kaleido sync server and return its kopts warnings.""" + kaleido.start_sync_server() + try: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + export() + finally: + kaleido.stop_sync_server() + return [str(w.message) for w in caught if "kopts" in str(w.message)] + + +def test_sync_server_no_kopts_warning_for_default_options(tmp_path): + def export(): + pio.to_image(fig, format="svg") + pio.write_image(fig, tmp_path / "fig.png") + pio.write_images([fig], [tmp_path / "figs.png"]) + + assert kopts_warnings(export) == [] + + +def test_sync_server_warns_when_options_are_ignored(): + pio.defaults.plotlyjs = "https://cdn.plot.ly/plotly-3.0.0.js" + try: + warned = kopts_warnings(lambda: pio.to_image(fig, format="svg")) + finally: + pio.defaults.plotlyjs = None + + assert warned == ["The kopts argument is ignored if using a server."]