From ba90db0775284afa9cda735f776a0298cad4f44e Mon Sep 17 00:00:00 2001 From: ioseba Date: Sun, 11 Oct 2026 01:29:22 +0200 Subject: [PATCH] Fix spurious UserWarning from Kaleido when start_sync_server is active (#5786) --- CHANGELOG.md | 1 + plotly/io/_kaleido.py | 64 +++++++++++++------ .../test_kaleido/test_kaleido.py | 55 ++++++++++++++++ 3 files changed, 102 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c92c3ea6d64..a25030d0cae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### Fixed +- Avoid emitting spurious `UserWarning` from Kaleido when exporting images with `kaleido.start_sync_server()` active [[#5786](https://github.com/plotly/plotly.py/issues/5786)], with thanks to @ioseba for the contribution! - Fix `mpl_to_plotly` tick marker mirroring, axis positioning, and trace visibility for twinned and overlaid subplots (such as `twinx` and `twiny`) [[#5310](https://github.com/plotly/plotly.py/pull/5310)], with thanks to @robertoffmoura for the contribution! - Fix `mpl_to_plotly` losing matplotlib axis line colors by exporting the color of each axis's displayed spine to the plotly x and y axis `linecolor` [[#5311](https://github.com/plotly/plotly.py/pull/5311)], with thanks to @robertoffmoura for the contribution! - 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! diff --git a/plotly/io/_kaleido.py b/plotly/io/_kaleido.py index 42adcf1e58e..7241a194250 100644 --- a/plotly/io/_kaleido.py +++ b/plotly/io/_kaleido.py @@ -90,6 +90,38 @@ def infer_format(path: Union[Path, None], format: Union[str, None]) -> Union[str return format +def _is_kaleido_server_running() -> bool: + """Check if the Kaleido sync server singleton is currently running.""" + server = getattr(kaleido, "_global_server", None) + return bool(server and getattr(server, "is_running", lambda: False)()) + + +def _get_kaleido_opts() -> dict: + """ + Build kwargs dictionary for Kaleido's kopts argument. + + When the Kaleido sync server is running, the server is already active with the + options provided at startup. Passing options to calc_fig_sync or + write_fig_from_object_sync emits an unnecessary UserWarning: + "The kopts argument is ignored if using a server." + To avoid this warning, we only include options if the server is not running, + or if non-default options were explicitly set. + """ + kopts = {} + if defaults.plotlyjs: + kopts["plotlyjs"] = defaults.plotlyjs + if defaults.mathjax: + kopts["mathjax"] = defaults.mathjax + if defaults.headers: + # Defaults include {'X-Requested-With': 'plotly.py'} for OSM tile compliance. + # If the server is running, skip this default header to avoid the warning. + if not _is_kaleido_server_running() or defaults.headers != { + "X-Requested-With": "plotly.py" + }: + kopts["headers"] = defaults.headers + return kopts + + def to_image( fig: Union[dict, plotly.graph_objects.Figure], format: Union[str, None] = None, @@ -155,13 +187,7 @@ def to_image( from kaleido.errors import ChromeNotFoundError try: - kopts = {} - if defaults.plotlyjs: - kopts["plotlyjs"] = defaults.plotlyjs - if defaults.mathjax: - kopts["mathjax"] = defaults.mathjax - if defaults.headers: - kopts["headers"] = defaults.headers + kopts = _get_kaleido_opts() width = ( width @@ -182,8 +208,7 @@ def to_image( or defaults.default_height ) - img_bytes = kaleido.calc_fig_sync( - fig_dict, + calc_fig_kwargs = dict( opts=dict( format=format or defaults.default_format, width=width, @@ -191,7 +216,13 @@ def to_image( scale=scale or defaults.default_scale, ), topojson=defaults.topojson, - kopts=kopts, + ) + if kopts: + calc_fig_kwargs["kopts"] = kopts + + img_bytes = kaleido.calc_fig_sync( + fig_dict, + **calc_fig_kwargs, ) except ChromeNotFoundError: raise RuntimeError(PLOTLY_GET_CHROME_ERROR_MSG) @@ -423,16 +454,13 @@ def write_images( from kaleido.errors import ChromeNotFoundError try: - kopts = {} - if defaults.plotlyjs: - kopts["plotlyjs"] = defaults.plotlyjs - if defaults.mathjax: - kopts["mathjax"] = defaults.mathjax - if defaults.headers: - kopts["headers"] = defaults.headers + kopts = _get_kaleido_opts() + write_fig_kwargs = {} + if kopts: + write_fig_kwargs["kopts"] = kopts kaleido.write_fig_from_object_sync( kaleido_specs, - kopts=kopts, + **write_fig_kwargs, ) 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..111dd5db1cf 100644 --- a/tests/test_optional/test_kaleido/test_kaleido.py +++ b/tests/test_optional/test_kaleido/test_kaleido.py @@ -335,3 +335,58 @@ def test_width_height_priority(): assert height == pio.defaults.default_height, ( "Default height should be used when no layout or argument" ) + + +def test_kaleido_sync_server_no_kopts_warning(): + """Test that fig.write_image() and fig.to_image() do not emit UserWarning about kopts when using kaleido.start_sync_server() (Fixes #5786).""" + import warnings + import kaleido + + test_fig = go.Figure(fig) + kaleido.start_sync_server() + try: + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + test_fig.to_image(format="png") + for w in recorded_warnings: + assert not ( + issubclass(w.category, UserWarning) + and "The kopts argument is ignored" in str(w.message) + ), f"Unexpected warning: {w.message}" + finally: + kaleido.stop_sync_server() + + +def test_kaleido_get_opts_helper(): + """Test that _get_kaleido_opts only excludes default headers when sync server is running.""" + import kaleido + from plotly.io._kaleido import _get_kaleido_opts, _is_kaleido_server_running + + # Server stopped: default headers should be included + assert not _is_kaleido_server_running() + opts = _get_kaleido_opts() + assert opts == {"headers": {"X-Requested-With": "plotly.py"}} + + kaleido.start_sync_server() + try: + assert _is_kaleido_server_running() + # Server running with default headers: kopts should be empty to prevent spurious warning + assert _get_kaleido_opts() == {} + + # Server running with custom mathjax: custom option should be included + pio.defaults.mathjax = "custom_mathjax_path" + assert _get_kaleido_opts() == {"mathjax": "custom_mathjax_path"} + + # Server running with custom headers: custom header should be included + pio.defaults.headers = {"Custom-Header": "Value"} + assert _get_kaleido_opts() == { + "mathjax": "custom_mathjax_path", + "headers": {"Custom-Header": "Value"}, + } + finally: + pio.defaults.mathjax = None + pio.defaults.headers = {"X-Requested-With": "plotly.py"} + kaleido.stop_sync_server() + + assert not _is_kaleido_server_running() + assert _get_kaleido_opts() == {"headers": {"X-Requested-With": "plotly.py"}}