Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
64 changes: 46 additions & 18 deletions plotly/io/_kaleido.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -182,16 +208,21 @@ 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,
height=height,
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)
Expand Down Expand Up @@ -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)
Expand Down
55 changes: 55 additions & 0 deletions tests/test_optional/test_kaleido/test_kaleido.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}