diff --git a/.github/workflows/run-pytest.yml b/.github/workflows/run-pytest.yml index 29cc7f0032d..ca1ff533fb6 100644 --- a/.github/workflows/run-pytest.yml +++ b/.github/workflows/run-pytest.yml @@ -135,7 +135,7 @@ jobs: python -m pytest -x test_init/test_lazy_imports.py test-kaleido-v0: - name: Optional tests (Kaleido only), Kaleido v0 (Python 3.12, kaleido 0.2.1) + name: Kaleido v0 RuntimeError verification (Python 3.12, kaleido 0.2.1) runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -157,7 +157,7 @@ jobs: uv pip uninstall kaleido uv pip install kaleido==0.2.1 python --version - - name: Test plotly.io image output with Kaleido v0 + - name: Test that plotly.io raises correct error with Kaleido v0 run: | source .venv/bin/activate - python -m pytest tests/test_optional/test_kaleido + python -m pytest tests/test_optional/test_kaleido/test_kaleido_v0_error.py diff --git a/.gitignore b/.gitignore index d266d02cd33..5ec09e1b417 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ 0 0.html iframe_figures/ -tests/test_orca/images/linux/failed/ *.egg-info @@ -49,8 +48,6 @@ plotly.egg-info/ # macOS utility file **/.DS_Store -tests/test_orca/images/*/failed -tests/test_orca/images/*/tmp tests/test_core/test_offline/plotly.min.js temp-plot.html .vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 688e344393d..fb8aa5a506c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Removed - Remove the deprecated Figure Factory functions `create_2d_density`, `create_annotated_heatmap`, `create_bullet`, `create_candlestick`, `create_choropleth`, `create_distplot`, `create_facet_grid`, `create_gantt`, `create_hexbin_mapbox`, `create_ohlc`, `create_scatterplotmatrix`, and `create_violin` [[#5627](https://github.com/plotly/plotly.py/pull/5627)] +- Remove support for Kaleido versions less than v1.0.0 for static image generation [[#5677](https://github.com/plotly/plotly.py/pull/5677)] +- Remove support for Orca for static image generation [[#5677](https://github.com/plotly/plotly.py/pull/5677)] +- Remove `engine` argument from functions `fig.write_image()`,`fig.to_image()`, `pio.write_image()`, `pio.write_images()`, `pio.to_image()`, `pio.full_figure_for_development()`, and from renderer constructors [[#5677](https://github.com/plotly/plotly.py/pull/5677)] ### Fixed - Raise a clear `ValueError` when an unsupported marginal plot type is passed to Plotly Express, instead of failing later with a cryptic `'NoneType' object has no attribute 'constructor'` message [[#5625](https://github.com/plotly/plotly.py/pull/5625)], with thanks to @eugen-goebel for the contribution! diff --git a/README.md b/README.md index 1f86173f319..d64e748fc9f 100644 --- a/README.md +++ b/README.md @@ -106,13 +106,10 @@ conda install jupyter anywidget ### Static Image Export plotly.py supports [static image export](https://plotly.com/python/static-image-export/), -using either the [`kaleido`](https://github.com/plotly/Kaleido) -package (recommended, supported as of `plotly` version 4.9) or the [orca](https://github.com/plotly/orca) -command line utility (legacy as of `plotly` version 4.9). +using the [`kaleido`](https://github.com/plotly/Kaleido) +package (version 1.0 or greater). -#### Kaleido - -The [`kaleido`](https://github.com/plotly/Kaleido) package has no dependencies and can be installed +Kaleido has minimal dependencies and can be installed using pip ``` @@ -125,6 +122,12 @@ or conda conda install -c conda-forge python-kaleido ``` +Kaleido requires Chrome or Chromium to generate images. By default, Kaleido will use the Chrome or Chromium version already installed on your system. If you don't have it installed or Kaleido can't find it, you may need to install it by running the command: + +`plotly_get_chrome` + +on your command line. + ## Copyright and Licenses Code and documentation copyright 2019 Plotly, Inc. diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 3db99465618..d81ba0950c1 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -3462,9 +3462,6 @@ def full_figure_for_development(self, warn=True, as_dict=False): Parameters ---------- - fig: - Figure object or dict representing a figure - warn: bool If False, suppress warnings about not using this in production. @@ -3717,86 +3714,43 @@ def to_image(self, *args, **kwargs): Parameters ---------- - format: str or None - The desired image format. One of - - 'png' - - 'jpg' or 'jpeg' - - 'webp' - - 'svg' - - 'pdf' - - 'eps' (deprecated) (Requires the poppler library to be installed) - - If not specified, will default to: - - `plotly.io.defaults.default_format` if engine is "kaleido" - - `plotly.io.orca.config.default_format` if engine is "orca" (deprecated) - - 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.defaults.default_width` if engine is "kaleido" - - `plotly.io.orca.config.default_width` if engine is "orca" (deprecated) - - 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.defaults.default_height` if engine is "kaleido" - - `plotly.io.orca.config.default_height` if engine is "orca" (deprecated) - - scale: int or float or None - The scale factor to use when exporting the figure. A scale factor - larger than 1.0 will increase the image resolution with respect - 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.defaults.default_scale` if engine is "kaliedo" - - `plotly.io.orca.config.default_scale` if engine is "orca" (deprecated) + format: str + (optional) The desired image format. One of: + - 'png' + - 'jpg' or 'jpeg' + - 'webp' + - 'svg' + - 'pdf' + Defaults to `plotly.io.defaults.default_format`. + + width: int + (optional) 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. Defaults to `plotly.io.defaults.default_width`. + + height: int + (optional) 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. Defaults to `plotly.io.defaults.default_height`. + + scale: int or float + (optional) The scale factor to use when exporting the figure. A scale + factor larger than 1.0 will increase the image resolution with respect + to the figure's layout pixel dimensions, while a scale factor of + less than 1.0 will decrease the image resolution. Defaults to + `plotly.io.defaults.default_scale`. validate: bool - True if the figure should be validated before being converted to - an image, False otherwise. - - engine (deprecated): str - Image export engine to use. This parameter is deprecated and Orca engine support will be - dropped in the next major Plotly version. Until then, the following values are supported: - - "kaleido": Use Kaleido for image export - - "orca": Use Orca for image export - - "auto" (default): Use Kaleido if installed, otherwise use Orca + (optional) True if the figure should be validated before being converted + to an image, False otherwise. Defaults to True. Returns ------- bytes The image data """ - import plotly.io as pio - from plotly.io.kaleido import ( - kaleido_available, - kaleido_major, - ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS, - KALEIDO_DEPRECATION_MSG, - ORCA_DEPRECATION_MSG, - ENGINE_PARAM_DEPRECATION_MSG, - ) - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - if ( - kwargs.get("engine", None) in {None, "auto", "kaleido"} - and kaleido_available() - and kaleido_major() < 1 - ): - warnings.warn(KALEIDO_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - if kwargs.get("engine", None) == "orca": - warnings.warn(ORCA_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - if kwargs.get("engine", None): - warnings.warn( - ENGINE_PARAM_DEPRECATION_MSG, DeprecationWarning, stacklevel=2 - ) + import plotly.io as pio return pio.to_image(self, *args, **kwargs) @@ -3811,87 +3765,43 @@ def write_image(self, *args, **kwargs): A string representing a local file path or a writeable object (e.g. a pathlib.Path object or an open file descriptor) - format: str or None - The desired image format. One of - - 'png' - - 'jpg' or 'jpeg' - - 'webp' - - 'svg' - - 'pdf' - - 'eps' (deprecated) (Requires the poppler library to be installed) - - If not specified and `file` is a string then this will default to the - file extension. If not specified and `file` is not a string then this - will default to: - - `plotly.io.defaults.default_format` if engine is "kaleido" - - `plotly.io.orca.config.default_format` if engine is "orca" (deprecated) - - 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.defaults.default_width` if engine is "kaleido" - - `plotly.io.orca.config.default_width` if engine is "orca" (deprecated) - - 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.defaults.default_height` if engine is "kaleido" - - `plotly.io.orca.config.default_height` if engine is "orca" (deprecated) - - scale: int or float or None - The scale factor to use when exporting the figure. A scale factor - larger than 1.0 will increase the image resolution with respect - 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.defaults.default_scale` if engine is "kaleido" - - `plotly.io.orca.config.default_scale` if engine is "orca" (deprecated) + format: str + (optional) The desired image format. One of: + - 'png' + - 'jpg' or 'jpeg' + - 'webp' + - 'svg' + - 'pdf' + Defaults to the file extension of `file`, if given; otherwise + defaults to `plotly.io.defaults.default_format`. + + width: int + (optional) 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. Defaults to `plotly.io.defaults.default_width`. + + height: int + (optional) 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. Defaults to `plotly.io.defaults.default_height`. + + scale: int or float + (optional) The scale factor to use when exporting the figure. A scale + factor larger than 1.0 will increase the image resolution with respect + to the figure's layout pixel dimensions, while a scale factor of + less than 1.0 will decrease the image resolution. Defaults to + `plotly.io.defaults.default_scale`. validate: bool - True if the figure should be validated before being converted to - an image, False otherwise. - - engine (deprecated): str - Image export engine to use. This parameter is deprecated and Orca engine support will be - dropped in the next major Plotly version. Until then, the following values are supported: - - "kaleido": Use Kaleido for image export - - "orca": Use Orca for image export - - "auto" (default): Use Kaleido if installed, otherwise use Orca + (optional) True if the figure should be validated before being converted + to an image, False otherwise. Defaults to True. Returns ------- None """ import plotly.io as pio - from plotly.io.kaleido import ( - kaleido_available, - kaleido_major, - ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS, - KALEIDO_DEPRECATION_MSG, - ORCA_DEPRECATION_MSG, - ENGINE_PARAM_DEPRECATION_MSG, - ) - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - if ( - kwargs.get("engine", None) in {None, "auto", "kaleido"} - and kaleido_available() - and kaleido_major() < 1 - ): - warnings.warn(KALEIDO_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - if kwargs.get("engine", None) == "orca": - warnings.warn(ORCA_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - if kwargs.get("engine", None): - warnings.warn( - ENGINE_PARAM_DEPRECATION_MSG, DeprecationWarning, stacklevel=2 - ) return pio.write_image(self, *args, **kwargs) # Static helpers diff --git a/plotly/io/__init__.py b/plotly/io/__init__.py index 121cd60522e..752aec4733e 100644 --- a/plotly/io/__init__.py +++ b/plotly/io/__init__.py @@ -10,7 +10,6 @@ write_images, full_figure_for_development, ) - from . import orca, kaleido from . import json from ._json import to_json, from_json, read_json, write_json from ._templates import templates, to_templated @@ -23,7 +22,6 @@ "to_image", "write_image", "write_images", - "orca", "json", "to_json", "from_json", @@ -43,7 +41,7 @@ else: __all__, __getattr__, __dir__ = relative_import( __name__, - [".orca", ".kaleido", ".json", ".base_renderers"], + [".kaleido", ".json", ".base_renderers"], [ "._kaleido.to_image", "._kaleido.write_image", diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index 25cadac4f1a..1b56a0128b1 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -109,7 +109,6 @@ def __init__( width=None, height=None, scale=None, - engine="auto", ): self.mime_type = mime_type self.b64_encode = b64_encode @@ -117,7 +116,6 @@ def __init__( self.width = width self.height = height self.scale = scale - self.engine = engine def to_mimebundle(self, fig_dict): image_bytes = to_image( @@ -127,7 +125,6 @@ def to_mimebundle(self, fig_dict): height=self.height, scale=self.scale, validate=False, - engine=self.engine, ) if self.b64_encode: @@ -141,14 +138,14 @@ def to_mimebundle(self, fig_dict): class PngRenderer(ImageRenderer): """ Renderer to display figures as static PNG images. This renderer requires - either the kaleido package or the orca command-line utility and is broadly - compatible across IPython environments (classic Jupyter Notebook, JupyterLab, - QtConsole, VSCode, PyCharm, etc) and nbconvert targets (HTML, PDF, etc.). + the kaleido package and is broadly compatible across IPython environments + (classic Jupyter Notebook, JupyterLab, QtConsole, VSCode, PyCharm, etc) + and nbconvert targets (HTML, PDF, etc.). mime type: 'image/png' """ - def __init__(self, width=None, height=None, scale=None, engine=None): + def __init__(self, width=None, height=None, scale=None): super(PngRenderer, self).__init__( mime_type="image/png", b64_encode=True, @@ -156,21 +153,20 @@ def __init__(self, width=None, height=None, scale=None, engine=None): width=width, height=height, scale=scale, - engine=engine, ) class SvgRenderer(ImageRenderer): """ Renderer to display figures as static SVG images. This renderer requires - either the kaleido package or the orca command-line utility and is broadly - compatible across IPython environments (classic Jupyter Notebook, JupyterLab, - QtConsole, VSCode, PyCharm, etc) and nbconvert targets (HTML, PDF, etc.). + the kaleido package and is broadly compatible across IPython environments + (classic Jupyter Notebook, JupyterLab, QtConsole, VSCode, PyCharm, etc) + and nbconvert targets (HTML, PDF, etc.). mime type: 'image/svg+xml' """ - def __init__(self, width=None, height=None, scale=None, engine=None): + def __init__(self, width=None, height=None, scale=None): super(SvgRenderer, self).__init__( mime_type="image/svg+xml", b64_encode=False, @@ -178,21 +174,20 @@ def __init__(self, width=None, height=None, scale=None, engine=None): width=width, height=height, scale=scale, - engine=engine, ) class JpegRenderer(ImageRenderer): """ Renderer to display figures as static JPEG images. This renderer requires - either the kaleido package or the orca command-line utility and is broadly - compatible across IPython environments (classic Jupyter Notebook, JupyterLab, - QtConsole, VSCode, PyCharm, etc) and nbconvert targets (HTML, PDF, etc.). + the kaleido package and is broadly compatible across IPython environments + (classic Jupyter Notebook, JupyterLab, QtConsole, VSCode, PyCharm, etc) + and nbconvert targets (HTML, PDF, etc.). mime type: 'image/jpeg' """ - def __init__(self, width=None, height=None, scale=None, engine=None): + def __init__(self, width=None, height=None, scale=None): super(JpegRenderer, self).__init__( mime_type="image/jpeg", b64_encode=True, @@ -200,20 +195,19 @@ def __init__(self, width=None, height=None, scale=None, engine=None): width=width, height=height, scale=scale, - engine=engine, ) class PdfRenderer(ImageRenderer): """ Renderer to display figures as static PDF images. This renderer requires - either the kaleido package or the orca command-line utility and is compatible - with JupyterLab and the LaTeX-based nbconvert export to PDF. + the kaleido package and is compatible with JupyterLab and the LaTeX-based + nbconvert export to PDF. mime type: 'application/pdf' """ - def __init__(self, width=None, height=None, scale=None, engine=None): + def __init__(self, width=None, height=None, scale=None): super(PdfRenderer, self).__init__( mime_type="application/pdf", b64_encode=True, @@ -221,7 +215,6 @@ def __init__(self, width=None, height=None, scale=None, engine=None): width=width, height=height, scale=scale, - engine=engine, ) @@ -509,8 +502,6 @@ def __init__( self.html_directory = html_directory def to_mimebundle(self, fig_dict): - from plotly.io import write_html - # Make iframe size slightly larger than figure size to avoid # having iframe have its own scroll bar. iframe_buffer = 20 @@ -821,7 +812,10 @@ def to_mimebundle(self, fig_dict): return {"text/html": html} -class SphinxGalleryOrcaRenderer(ExternalRenderer): +class SphinxGalleryPngRenderer(ExternalRenderer): + # Note: This renderer was originally designed for use with the orca image generation utility + # before the introduction of kaleido. It has not been tested with kaleido, but I'm not aware + # of any reason why it shouldn't work. def render(self, fig_dict): stack = inspect.stack() # Name of script from which plot function was called is retrieved @@ -836,11 +830,11 @@ def render(self, fig_dict): _ = write_html(fig_dict, file=filename_html, include_plotlyjs="cdn") try: write_image(figure, filename_png) - except (ValueError, ImportError): - raise ImportError( - "orca and psutil are required to use the `sphinx-gallery-orca` renderer. " + except (ValueError, ImportError, RuntimeError) as e: + raise RuntimeError( + "kaleido and psutil are required to use the `sphinx_gallery_png` renderer. " "See https://plotly.com/python/static-image-export/ for instructions on " - "how to install orca. Alternatively, you can use the `sphinx-gallery` " + "how to install kaleido. Alternatively, you can use the `sphinx_gallery` " "renderer (note that png thumbnails can only be generated with " - "the `sphinx-gallery-orca` renderer)." - ) + "the `sphinx_gallery_png` renderer)." + ) from e diff --git a/plotly/io/_json.py b/plotly/io/_json.py index 0e741711f4f..7668de48cdf 100644 --- a/plotly/io/_json.py +++ b/plotly/io/_json.py @@ -9,8 +9,6 @@ from _plotly_utils.basevalidators import ImageUriValidator -# Orca configuration class -# ------------------------ class JsonConfig(object): _valid_engines = ("json", "orjson", "auto") diff --git a/plotly/io/_kaleido.py b/plotly/io/_kaleido.py index 09a6edafdf6..42adcf1e58e 100644 --- a/plotly/io/_kaleido.py +++ b/plotly/io/_kaleido.py @@ -9,9 +9,9 @@ import plotly from plotly.io._utils import validate_coerce_fig_to_dict, broadcast_args_to_dicts from plotly.io._defaults import defaults +from _plotly_utils.optional_imports import get_module -ENGINE_SUPPORT_TIMELINE = "September 2025" -ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS = True +kaleido = get_module("kaleido", should_load=True) PLOTLY_GET_CHROME_ERROR_MSG = """ @@ -24,166 +24,34 @@ """ -KALEIDO_DEPRECATION_MSG = f""" -Support for Kaleido versions less than 1.0.0 is deprecated and will be removed after {ENGINE_SUPPORT_TIMELINE}. -Please upgrade Kaleido to version 1.0.0 or greater (`pip install 'kaleido>=1.0.0'` or `pip install 'plotly[kaleido]'`). -""" -ORCA_DEPRECATION_MSG = f""" -Support for the Orca engine is deprecated and will be removed after {ENGINE_SUPPORT_TIMELINE}. -Please install Kaleido (`pip install 'kaleido>=1.0.0'` or `pip install 'plotly[kaleido]'`) to use the Kaleido engine. -""" -ENGINE_PARAM_DEPRECATION_MSG = f""" -Support for the 'engine' argument is deprecated and will be removed after {ENGINE_SUPPORT_TIMELINE}. -Kaleido will be the only supported engine at that time. -""" - -_KALEIDO_AVAILABLE = None -_KALEIDO_MAJOR = None - +KALEIDO_REQUIRED_MSG = """ +Image export requires the Kaleido package, v1.0.0 or greater, +which can be installed using pip: -def kaleido_scope_default_warning_func(x): - return f""" -Use of plotly.io.kaleido.scope.{x} is deprecated and support will be removed after {ENGINE_SUPPORT_TIMELINE}. -Please use plotly.io.defaults.{x} instead. + $ pip install --upgrade "kaleido>=1" """ -def bad_attribute_error_msg_func(x): - return f""" -Attribute plotly.io.defaults.{x} is not valid. -Also, use of plotly.io.kaleido.scope.* is deprecated and support will be removed after {ENGINE_SUPPORT_TIMELINE}. -Please use plotly.io.defaults.* instead. -""" +_KALEIDO_AVAILABLE = None def kaleido_available() -> bool: """ - Returns True if any version of Kaleido is installed, otherwise False. + Returns True if Kaleido version 1.0.0 or greater is installed, otherwise False. """ global _KALEIDO_AVAILABLE - global _KALEIDO_MAJOR if _KALEIDO_AVAILABLE is not None: return _KALEIDO_AVAILABLE - try: - import kaleido # noqa: F401 + if kaleido is not None and Version( + importlib_metadata.version("kaleido") + ) >= Version("1.0.0"): _KALEIDO_AVAILABLE = True - except ImportError: + else: _KALEIDO_AVAILABLE = False return _KALEIDO_AVAILABLE -def kaleido_major() -> int: - """ - Returns the major version number of Kaleido if it is installed, - otherwise raises a ValueError. - """ - global _KALEIDO_MAJOR - if _KALEIDO_MAJOR is not None: - return _KALEIDO_MAJOR - if not kaleido_available(): - raise ValueError("Kaleido is not installed.") - else: - _KALEIDO_MAJOR = Version(importlib_metadata.version("kaleido")).major - return _KALEIDO_MAJOR - - -try: - if kaleido_available() and kaleido_major() < 1: - # Kaleido v0 - import kaleido - from kaleido.scopes.plotly import PlotlyScope - - # Show a deprecation warning if the old method of setting defaults is used - class PlotlyScopeWrapper(PlotlyScope): - def __setattr__(self, name, value): - if name in defaults.__dict__: - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - warnings.warn( - kaleido_scope_default_warning_func(name), - DeprecationWarning, - stacklevel=2, - ) - super().__setattr__(name, value) - - def __getattr__(self, name): - if hasattr(defaults, name): - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - warnings.warn( - kaleido_scope_default_warning_func(name), - DeprecationWarning, - stacklevel=2, - ) - return super().__getattr__(name) - - # Ensure the new method of setting defaults is backwards compatible with Kaleido v0 - # DefaultsBackwardsCompatible sets the attributes on `scope` object at the same time - # as they are set on the `defaults` object - class DefaultsBackwardsCompatible(defaults.__class__): - def __init__(self, scope): - self._scope = scope - super().__init__() - - def __setattr__(self, name, value): - if not name == "_scope": - if ( - hasattr(self._scope, name) - and getattr(self._scope, name) != value - ): - setattr(self._scope, name, value) - super().__setattr__(name, value) - - scope = PlotlyScopeWrapper() - defaults = DefaultsBackwardsCompatible(scope) - # 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: - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", message=r".*scope\.mathjax.*", category=DeprecationWarning - ) - scope.mathjax = ( - "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js" - ) - else: - # Kaleido v1 - import kaleido - - # Show a deprecation warning if the old method of setting defaults is used - class DefaultsWrapper: - def __getattr__(self, name): - if hasattr(defaults, name): - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - warnings.warn( - kaleido_scope_default_warning_func(name), - DeprecationWarning, - stacklevel=2, - ) - return getattr(defaults, name) - else: - raise AttributeError(bad_attribute_error_msg_func(name)) - - def __setattr__(self, name, value): - if hasattr(defaults, name): - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - warnings.warn( - kaleido_scope_default_warning_func(name), - DeprecationWarning, - stacklevel=2, - ) - setattr(defaults, name, value) - else: - raise AttributeError(bad_attribute_error_msg_func(name)) - - scope = DefaultsWrapper() - -except ImportError: - PlotlyScope = None - scope = None - - def as_path_object(file: Union[str, Path]) -> Union[Path, None]: """ Cast the `file` argument, which may be either a string or a Path object, @@ -229,8 +97,6 @@ def to_image( height: Union[int, None] = None, scale: Union[int, float, None] = None, validate: bool = True, - # Deprecated - engine: Union[str, None] = None, ) -> bytes: """ Convert a figure to a static image bytes string @@ -240,184 +106,95 @@ def to_image( fig: Figure object or dict representing a figure - format: str or None - The desired image format. One of + format: str + (optional) The desired image format. One of: - 'png' - 'jpg' or 'jpeg' - 'webp' - 'svg' - 'pdf' - - 'eps' (deprecated) (Requires the poppler library to be installed and on the PATH) - - If not specified, will default to: - - `plotly.io.defaults.default_format` if engine is "kaleido" - - `plotly.io.orca.config.default_format` if engine is "orca" (deprecated) - - 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. + Defaults to `plotly.io.defaults.default_format`. - If not specified, will default to: - - `plotly.io.defaults.default_width` if engine is "kaleido" - - `plotly.io.orca.config.default_width` if engine is "orca" (deprecated) + width: int + (optional) 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. Defaults to `plotly.io.defaults.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.defaults.default_height` if engine is "kaleido" - - `plotly.io.orca.config.default_height` if engine is "orca" (deprecated) - - scale: int or float or None - The scale factor to use when exporting the figure. A scale factor - larger than 1.0 will increase the image resolution with respect - to the figure's layout pixel dimensions. Whereas as scale factor of - less than 1.0 will decrease the image resolution. + height: int + (optional) 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. Defaults to `plotly.io.defaults.default_height`. - If not specified, will default to: - - `plotly.io.defaults.default_scale` if engine is "kaleido" - - `plotly.io.orca.config.default_scale` if engine is "orca" (deprecated) + scale: int or float + (optional) The scale factor to use when exporting the figure. A scale + factor larger than 1.0 will increase the image resolution with respect + to the figure's layout pixel dimensions, while a scale factor of + less than 1.0 will decrease the image resolution. Defaults to + `plotly.io.defaults.default_scale`. validate: bool - True if the figure should be validated before being converted to - an image, False otherwise. - - engine (deprecated): str - Image export engine to use. This parameter is deprecated and Orca engine support will be - dropped in the next major Plotly version. Until then, the following values are supported: - - "kaleido": Use Kaleido for image export - - "orca": Use Orca for image export - - "auto" (default): Use Kaleido if installed, otherwise use Orca + (optional) True if the figure should be validated before being converted + to an image, False otherwise. Defaults to True. Returns ------- bytes The image data """ - - # Handle engine - if engine is not None: - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - warnings.warn( - ENGINE_PARAM_DEPRECATION_MSG, DeprecationWarning, stacklevel=2 - ) - else: - 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 Exception: - # If orca not configured properly, make sure we display the error - # message advising the installation of kaleido - engine = "kaleido" - - if engine == "orca": - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - warnings.warn(ORCA_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - # 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(f"Invalid image export engine specified: {repr(engine)}") - - # Raise informative error message if Kaleido is not installed if not kaleido_available(): - raise ValueError( - """ -Image export using the "kaleido" engine requires the Kaleido package, -which can be installed using pip: - - $ pip install --upgrade kaleido -""" - ) + raise RuntimeError(KALEIDO_REQUIRED_MSG) # Convert figure to dict (and validate if requested) fig_dict = validate_coerce_fig_to_dict(fig, validate) - # 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( - f""" -EPS export is not supported by Kaleido v1. Please use SVG or PDF instead. -You can also downgrade to Kaleido v0, but support for Kaleido v0 will be removed after {ENGINE_SUPPORT_TIMELINE}. -To downgrade to Kaleido v0, run: - $ pip install 'kaleido<1.0.0' -""" - ) - from kaleido.errors import ChromeNotFoundError + # Check if trying to export to EPS format, which is not supported in Kaleido v1 + if format == "eps": + raise ValueError( + "EPS export is no longer supported by Kaleido. Please use SVG or PDF instead." + ) + 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 - - width = ( - width - or fig_dict.get("layout", {}).get("width") - or fig_dict.get("layout", {}) - .get("template", {}) - .get("layout", {}) - .get("width") - or defaults.default_width - ) - height = ( - height - or fig_dict.get("layout", {}).get("height") - or fig_dict.get("layout", {}) - .get("template", {}) - .get("layout", {}) - .get("height") - or defaults.default_height - ) + try: + kopts = {} + if defaults.plotlyjs: + kopts["plotlyjs"] = defaults.plotlyjs + if defaults.mathjax: + kopts["mathjax"] = defaults.mathjax + if defaults.headers: + kopts["headers"] = defaults.headers - 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) + width = ( + width + or fig_dict.get("layout", {}).get("width") + or fig_dict.get("layout", {}) + .get("template", {}) + .get("layout", {}) + .get("width") + or defaults.default_width + ) + height = ( + height + or fig_dict.get("layout", {}).get("height") + or fig_dict.get("layout", {}) + .get("template", {}) + .get("layout", {}) + .get("height") + or defaults.default_height + ) - else: - # Kaleido v0 - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - warnings.warn(KALEIDO_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - img_bytes = scope.transform( - fig_dict, format=format, width=width, height=height, scale=scale + 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) return img_bytes @@ -430,8 +207,6 @@ def write_image( width: Union[int, None] = None, height: Union[int, None] = None, validate: bool = True, - # Deprecated - engine: Union[str, None] = None, ): """ Convert a figure to a static image and write it to a file or writeable @@ -446,78 +221,44 @@ def write_image( A string representing a local file path or a writeable object (e.g. a pathlib.Path object or an open file descriptor) - format: str or None - The desired image format. One of + format: str + (optional) The desired image format. One of: - 'png' - 'jpg' or 'jpeg' - 'webp' - 'svg' - 'pdf' - - 'eps' (deprecated) (Requires the poppler library to be installed and on the PATH) - - If not specified and `file` is a string then this will default to the - file extension. If not specified and `file` is not a string then this - will default to: - - `plotly.io.defaults.default_format` if engine is "kaleido" - - `plotly.io.orca.config.default_format` if engine is "orca" (deprecated) - - 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.defaults.default_width` if engine is "kaleido" - - `plotly.io.orca.config.default_width` if engine is "orca" (deprecated) - - 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.defaults.default_height` if engine is "kaleido" - - `plotly.io.orca.config.default_height` if engine is "orca" (deprecated) - - scale: int or float or None - The scale factor to use when exporting the figure. A scale factor - larger than 1.0 will increase the image resolution with respect - 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.defaults.default_scale` if engine is "kaleido" - - `plotly.io.orca.config.default_scale` if engine is "orca" (deprecated) + Defaults to the file extension of `file`, if given; otherwise + defaults to `plotly.io.defaults.default_format`. + + width: int + (optional) 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. Defaults to `plotly.io.defaults.default_width`. + + height: int + (optional) 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. Defaults to `plotly.io.defaults.default_height`. + + scale: int or float + (optional) The scale factor to use when exporting the figure. A scale + factor larger than 1.0 will increase the image resolution with respect + to the figure's layout pixel dimensions, while a scale factor of + less than 1.0 will decrease the image resolution. Defaults to + `plotly.io.defaults.default_scale`. validate: bool - True if the figure should be validated before being converted to - an image, False otherwise. - - engine (deprecated): str - Image export engine to use. This parameter is deprecated and Orca engine support will be - dropped in the next major Plotly version. Until then, the following values are supported: - - "kaleido": Use Kaleido for image export - - "orca": Use Orca for image export - - "auto" (default): Use Kaleido if installed, otherwise use Orca + (optional) True if the figure should be validated before being converted + to an image, False otherwise. Defaults to True. Returns ------- None """ - # Show Kaleido deprecation warning if needed - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - if ( - engine in {None, "auto", "kaleido"} - and kaleido_available() - and kaleido_major() < 1 - ): - warnings.warn(KALEIDO_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - if engine == "orca": - warnings.warn(ORCA_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - if engine not in {None, "auto"}: - warnings.warn( - ENGINE_PARAM_DEPRECATION_MSG, DeprecationWarning, stacklevel=2 - ) + + if not kaleido_available(): + raise RuntimeError(KALEIDO_REQUIRED_MSG) # Try to cast `file` as a pathlib object `path`. path = as_path_object(file) @@ -534,7 +275,6 @@ def write_image( width=width, height=height, validate=validate, - engine=engine, ) # Open file @@ -589,85 +329,59 @@ def write_images( Can also be a single str or pathlib.Path object if fig argument is a single figure or dict representing a figure. - format: str, None, or list of (str or None) - The image format to use for exported images. - Supported formats are: + format: str or list of (str or None) + (optional) The image format to use for exported images. One of: - 'png' - 'jpg' or 'jpeg' - 'webp' - 'svg' - 'pdf' + Defaults to the file extension of `file`, if given; otherwise + defaults to `plotly.io.defaults.default_format`. - Use a list to specify formats for each figure or dict in the list - provided to the `fig` argument. - Specify format as a `str` to apply the same format to all exported images. - If not specified, and the corresponding `file` argument has a file extension, then `format` will default to the - file extension. Otherwise, will default to `plotly.io.defaults.default_format`. + Pass a list to specify formats for each figure in the list passed to `fig`. + Pass a str to apply the same format to all exported images. - width: int, None, or list of (int or None) - The width of the exported image in layout pixels. If the `scale` + width: int or list of (int or None) + (optional) 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. + in physical pixels. Defaults to `plotly.io.defaults.default_width`. - Use a list to specify widths for each figure or dict in the list - provided to the `fig` argument. - Specify width as an `int` to apply the same width to all exported images. - If not specified, will default to `plotly.io.defaults.default_width`. + Pass a list to specify widths for each figure in the list passed to `fig`. + Pass an int to apply the same width to all exported images. - height: int, None, or list of (int or None) - The height of the exported image in layout pixels. If the `scale` + height: int or list of (int or None) + (optional) 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. + in physical pixels. Defaults to `plotly.io.defaults.default_height`. - Use a list to specify heights for each figure or dict in the list - provided to the `fig` argument. - Specify height as an `int` to apply the same height to all exported images. - If not specified, will default to `plotly.io.defaults.default_height`. + Pass a list to specify heights for each figure in the list passed to `fig`. + Pass an int to apply the same height to all exported images. - scale: int, float, None, or list of (int, float, or None) - The scale factor to use when exporting the figure. A scale factor + scale: int, float, or list of (int, float, or None) + (optional) The scale factor to use when exporting the figure. A scale factor larger than 1.0 will increase the image resolution with respect - to the figure's layout pixel dimensions. Whereas as scale factor of - less than 1.0 will decrease the image resolution. + to the figure's layout pixel dimensions, while a scale factor of + less than 1.0 will decrease the image resolution. Defaults to + `plotly.io.defaults.default_scale`. - Use a list to specify scale for each figure or dict in the list - provided to the `fig` argument. - Specify scale as an `int` or `float` to apply the same scale to all exported images. - If not specified, will default to `plotly.io.defaults.default_scale`. + Pass a list to specify scale for each figure in the list passed to `fig`. + Pass an int or float to apply the same scale to all exported images. validate: bool or list of bool - True if the figure should be validated before being converted to - an image, False otherwise. + (optional) True if the figure should be validated before being converted + to an image, False otherwise. Defaults to True. - Use a list to specify validation setting for each figure in the list - provided to the `fig` argument. - Specify validate as a boolean to apply the same validation setting to all figures. + Pass a list to specify validation for each figure in the list passed to `fig`. + Pass a boolean to apply the same validation setting to all figures. Returns ------- None """ - # Raise informative error message if Kaleido v1 is not installed if not kaleido_available(): - raise ValueError( - """ -The `write_images()` function requires the Kaleido package, -which can be installed using pip: - - $ pip install --upgrade kaleido -""" - ) - elif kaleido_major() < 1: - raise ValueError( - f""" -You have Kaleido version {Version(importlib_metadata.version("kaleido"))} installed. -The `write_images()` function requires the Kaleido package version 1.0.0 or greater, -which can be installed using pip: - - $ pip install 'kaleido>=1.0.0' -""" - ) + raise RuntimeError(KALEIDO_REQUIRED_MSG) # Broadcast arguments into correct format for passing to Kaleido arg_dicts = broadcast_args_to_dicts( @@ -754,16 +468,8 @@ def full_figure_for_development( The full figure """ - # Raise informative error message if Kaleido is not installed if not kaleido_available(): - raise ValueError( - """ -Full figure generation requires the Kaleido package, -which can be installed using pip: - - $ pip install --upgrade kaleido -""" - ) + raise RuntimeError(KALEIDO_REQUIRED_MSG) if warn: warnings.warn( @@ -772,22 +478,11 @@ def full_figure_for_development( "To suppress this warning, set warn=False" ) - if kaleido_available() and kaleido_major() > 0: - # Kaleido v1 - bytes = kaleido.calc_fig_sync( - fig, - opts=dict(format="json"), - ) - fig = json.loads(bytes.decode("utf-8")) - else: - # Kaleido v0 - if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: - warnings.warn( - f"Support for Kaleido versions less than 1.0.0 is deprecated and will be removed after {ENGINE_SUPPORT_TIMELINE}. " - + "Please upgrade Kaleido to version 1.0.0 or greater (`pip install 'kaleido>=1.0.0'`).", - DeprecationWarning, - ) - fig = json.loads(scope.transform(fig, format="json").decode("utf-8")) + bytes = kaleido.calc_fig_sync( + fig, + opts=dict(format="json"), + ) + fig = json.loads(bytes.decode("utf-8")) if as_dict: return fig @@ -817,14 +512,6 @@ def plotly_get_chrome() -> None: --help Show this message and exit. """ - if not kaleido_available() or kaleido_major() < 1: - raise ValueError( - """ -This command requires Kaleido v1.0.0 or greater. -Install it using `pip install 'kaleido>=1.0.0'` or `pip install 'plotly[kaleido]'`." -""" - ) - # Handle command line arguments import sys @@ -877,13 +564,8 @@ def get_chrome(path: Union[str, Path, None] = None) -> Path: The path to the directory where Chrome should be installed. If None, the default download path will be used. """ - if not kaleido_available() or kaleido_major() < 1: - raise ValueError( - """ -This command requires Kaleido v1.0.0 or greater. -Install it using `pip install 'kaleido>=1.0.0'` or `pip install 'plotly[kaleido]'`." -""" - ) + if not kaleido_available(): + raise RuntimeError(KALEIDO_REQUIRED_MSG) # Use default download path if no path was specified if path: @@ -919,4 +601,4 @@ def get_chrome(path: Union[str, Path, None] = None) -> Path: return kaleido.get_chrome_sync(path=chrome_install_path) -__all__ = ["to_image", "write_image", "scope", "full_figure_for_development"] +__all__ = ["to_image", "write_image", "full_figure_for_development"] diff --git a/plotly/io/_orca.py b/plotly/io/_orca.py deleted file mode 100644 index 6b1d8f67c00..00000000000 --- a/plotly/io/_orca.py +++ /dev/null @@ -1,1614 +0,0 @@ -import atexit -import functools -import json -import os -import random -import socket -import subprocess -import sys -import threading -import time -import warnings -from contextlib import contextmanager -from copy import copy -from pathlib import Path -from shutil import which - -import plotly -from plotly.files import PLOTLY_DIR, ensure_writable_plotly_dir -from plotly.io._utils import validate_coerce_fig_to_dict -from plotly.optional_imports import get_module - -psutil = get_module("psutil") - -# Valid image format constants -# ---------------------------- -valid_formats = ("png", "jpeg", "webp", "svg", "pdf", "eps") -format_conversions = {fmt: fmt for fmt in valid_formats} -format_conversions.update({"jpg": "jpeg"}) - - -# Utility functions -# ----------------- -def raise_format_value_error(val): - raise ValueError( - """ -Invalid value of type {typ} receive as an image format specification. - Received value: {v} - -An image format must be specified as one of the following string values: - {valid_formats}""".format( - typ=type(val), v=val, valid_formats=sorted(format_conversions.keys()) - ) - ) - - -def validate_coerce_format(fmt): - """ - Validate / coerce a user specified image format, and raise an informative - exception if format is invalid. - - Parameters - ---------- - fmt - A value that may or may not be a valid image format string. - - Returns - ------- - str or None - A valid image format string as supported by orca. This may not - be identical to the input image designation. For example, - the resulting string will always be lower case and 'jpg' is - converted to 'jpeg'. - - If the input format value is None, then no exception is raised and - None is returned. - - Raises - ------ - ValueError - if the input `fmt` cannot be interpreted as a valid image format. - """ - - # Let None pass through - if fmt is None: - return None - - # Check format type - if not isinstance(fmt, str) or not fmt: - raise_format_value_error(fmt) - - # Make lower case - fmt = fmt.lower() - - # Remove leading period, if any. - # For example '.png' is accepted and converted to 'png' - if fmt[0] == ".": - fmt = fmt[1:] - - # Check string value - if fmt not in format_conversions: - raise_format_value_error(fmt) - - # Return converted string specification - return format_conversions[fmt] - - -def find_open_port(): - """ - Use the socket module to find an open port. - - Returns - ------- - int - An open port - """ - s = socket.socket() - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind(("", 0)) - _, port = s.getsockname() - s.close() - - return port - - -def retry(min_wait=5, max_wait=10, max_delay=60000): - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - start_time = time.time() - - while True: - try: - return func(*args, **kwargs) - except Exception as e: - elapsed_time = time.time() - start_time - if elapsed_time * 1000 >= max_delay: - raise TimeoutError( - f"Retry limit of {max_delay} milliseconds reached." - ) from e - - wait_time = random.uniform(min_wait, max_wait) - print(f"Retrying in {wait_time:.2f} seconds due to {e}...") - time.sleep(wait_time) - - return wrapper - - return decorator - - -# Orca configuration class -# ------------------------ -class OrcaConfig(object): - """ - Singleton object containing the current user defined configuration - properties for orca. - - These parameters may optionally be saved to the user's ~/.plotly - directory using the `save` method, in which case they are automatically - restored in future sessions. - """ - - def __init__(self): - # Initialize properties dict - self._props = {} - - # Compute absolute path to the 'plotly/package_data/' directory - root_dir = os.path.dirname(os.path.abspath(plotly.__file__)) - self.package_dir = os.path.join(root_dir, "package_data") - - # Load pre-existing configuration - self.reload(warn=False) - - # Compute constants - plotlyjs = os.path.join(self.package_dir, "plotly.min.js") - self._constants = { - "plotlyjs": plotlyjs, - "config_file": os.path.join(PLOTLY_DIR, ".orca"), - } - - def restore_defaults(self, reset_server=True): - """ - Reset all orca configuration properties to their default values - """ - self._props = {} - - if reset_server: - # Server must restart before setting is active - reset_status() - - def update(self, d={}, **kwargs): - """ - Update one or more properties from a dict or from input keyword - arguments. - - Parameters - ---------- - d: dict - Dictionary from property names to new property values. - - kwargs - Named argument value pairs where the name is a configuration - property name and the value is the new property value. - - Returns - ------- - None - - Examples - -------- - Update configuration properties using a dictionary - - >>> import plotly.io as pio - >>> pio.orca.config.update({'timeout': 30, 'default_format': 'svg'}) - - Update configuration properties using keyword arguments - - >>> pio.orca.config.update(timeout=30, default_format='svg'}) - """ - # Combine d and kwargs - if not isinstance(d, dict): - raise ValueError( - """ -The first argument to update must be a dict, \ -but received value of type {typ}l - Received value: {val}""".format(typ=type(d), val=d) - ) - - updates = copy(d) - updates.update(kwargs) - - # Validate keys - for k in updates: - if k not in self._props: - raise ValueError("Invalid property name: {k}".format(k=k)) - - # Apply keys - for k, v in updates.items(): - setattr(self, k, v) - - def reload(self, warn=True): - """ - Reload orca settings from ~/.plotly/.orca, if any. - - Note: Settings are loaded automatically when plotly is imported. - This method is only needed if the setting are changed by some outside - process (e.g. a text editor) during an interactive session. - - Parameters - ---------- - warn: bool - If True, raise informative warnings if settings cannot be restored. - If False, do not raise warnings if setting cannot be restored. - - Returns - ------- - None - """ - if os.path.exists(self.config_file): - # ### Load file into a string ### - try: - with open(self.config_file, "r") as f: - orca_str = f.read() - except Exception: - if warn: - warnings.warn( - """\ -Unable to read orca configuration file at {path}""".format(path=self.config_file) - ) - return - - # ### Parse as JSON ### - try: - orca_props = json.loads(orca_str) - except ValueError: - if warn: - warnings.warn( - """\ -Orca configuration file at {path} is not valid JSON""".format(path=self.config_file) - ) - return - - # ### Update _props ### - for k, v in orca_props.items(): - self._props[k] = v - - elif warn: - warnings.warn( - """\ -Orca configuration file at {path} not found""".format(path=self.config_file) - ) - - def save(self): - """ - Attempt to save current settings to disk, so that they are - automatically restored for future sessions. - - This operation requires write access to the path returned by - in the `config_file` property. - - Returns - ------- - None - """ - if ensure_writable_plotly_dir(): - with open(self.config_file, "w") as f: - json.dump(self._props, f, indent=4) - else: - warnings.warn( - """\ -Failed to write orca configuration file at '{path}'""".format(path=self.config_file) - ) - - @property - def server_url(self): - """ - The server URL to use for an external orca server, or None if orca - should be managed locally - - Overrides executable, port, timeout, mathjax, and topojson - - Returns - ------- - str or None - """ - return self._props.get("server_url", None) - - @server_url.setter - def server_url(self, val): - if val is None: - self._props.pop("server_url", None) - return - if not isinstance(val, str): - raise ValueError( - """ -The server_url property must be a string, but received value of type {typ}. - Received value: {val}""".format(typ=type(val), val=val) - ) - - if not val.startswith("http://") and not val.startswith("https://"): - val = "http://" + val - - shutdown_server() - self.executable = None - self.port = None - self.timeout = None - self.mathjax = None - self.topojson = None - self._props["server_url"] = val - - @property - def port(self): - """ - The specific port to use to communicate with the orca server, or - None if the port is to be chosen automatically. - - If an orca server is active, the port in use is stored in the - plotly.io.orca.status.port property. - - Returns - ------- - int or None - """ - return self._props.get("port", None) - - @port.setter - def port(self, val): - if val is None: - self._props.pop("port", None) - return - if not isinstance(val, int): - raise ValueError( - """ -The port property must be an integer, but received value of type {typ}. - Received value: {val}""".format(typ=type(val), val=val) - ) - - self._props["port"] = val - - @property - def executable(self): - """ - The name or full path of the orca executable. - - - If a name (e.g. 'orca'), then it should be the name of an orca - executable on the PATH. The directories on the PATH can be - displayed by running the following command: - - >>> import os - >>> print(os.environ.get('PATH').replace(os.pathsep, os.linesep)) - - - If a full path (e.g. '/path/to/orca'), then - it should be the full path to an orca executable. In this case - the executable does not need to reside on the PATH. - - If an orca server has been validated, then the full path to the - validated orca executable is stored in the - plotly.io.orca.status.executable property. - - Returns - ------- - str - """ - executable_list = self._props.get("executable_list", ["orca"]) - if executable_list is None: - return None - else: - return " ".join(executable_list) - - @executable.setter - def executable(self, val): - if val is None: - self._props.pop("executable", None) - else: - if not isinstance(val, str): - raise ValueError( - """ -The executable property must be a string, but received value of type {typ}. - Received value: {val}""".format(typ=type(val), val=val) - ) - if isinstance(val, str): - val = [val] - self._props["executable_list"] = val - - # Server and validation must restart before setting is active - reset_status() - - @property - def timeout(self): - """ - The number of seconds of inactivity required before the orca server - is shut down. - - For example, if timeout is set to 20, then the orca - server will shutdown once is has not been used for at least - 20 seconds. If timeout is set to None, then the server will not be - automatically shut down due to inactivity. - - Regardless of the value of timeout, a running orca server may be - manually shut down like this: - - >>> import plotly.io as pio - >>> pio.orca.shutdown_server() - - Returns - ------- - int or float or None - """ - return self._props.get("timeout", None) - - @timeout.setter - def timeout(self, val): - if val is None: - self._props.pop("timeout", None) - else: - if not isinstance(val, (int, float)): - raise ValueError( - """ -The timeout property must be a number, but received value of type {typ}. - Received value: {val}""".format(typ=type(val), val=val) - ) - self._props["timeout"] = val - - # Server must restart before setting is active - shutdown_server() - - @property - def default_width(self): - """ - The default width to use on image export. This value is only - applied if no width value is supplied to the plotly.io - to_image or write_image functions. - - Returns - ------- - int or None - """ - return self._props.get("default_width", None) - - @default_width.setter - def default_width(self, val): - if val is None: - self._props.pop("default_width", None) - return - if not isinstance(val, int): - raise ValueError( - """ -The default_width property must be an int, but received value of type {typ}. - Received value: {val}""".format(typ=type(val), val=val) - ) - self._props["default_width"] = val - - @property - def default_height(self): - """ - The default height to use on image export. This value is only - applied if no height value is supplied to the plotly.io - to_image or write_image functions. - - Returns - ------- - int or None - """ - return self._props.get("default_height", None) - - @default_height.setter - def default_height(self, val): - if val is None: - self._props.pop("default_height", None) - return - if not isinstance(val, int): - raise ValueError( - """ -The default_height property must be an int, but received value of type {typ}. - Received value: {val}""".format(typ=type(val), val=val) - ) - self._props["default_height"] = val - - @property - def default_format(self): - """ - The default image format to use on image export. - - Valid image formats strings are: - - 'png' - - 'jpg' or 'jpeg' - - 'webp' - - 'svg' - - 'pdf' - - 'eps' (Requires the poppler library to be installed) - - This value is only applied if no format value is supplied to the - plotly.io to_image or write_image functions. - - Returns - ------- - str or None - """ - return self._props.get("default_format", "png") - - @default_format.setter - def default_format(self, val): - if val is None: - self._props.pop("default_format", None) - return - - val = validate_coerce_format(val) - self._props["default_format"] = val - - @property - def default_scale(self): - """ - The default image scaling factor to use on image export. - This value is only applied if no scale value is supplied to the - plotly.io to_image or write_image functions. - - Returns - ------- - int or None - """ - return self._props.get("default_scale", 1) - - @default_scale.setter - def default_scale(self, val): - if val is None: - self._props.pop("default_scale", None) - return - if not isinstance(val, (int, float)): - raise ValueError( - """ -The default_scale property must be a number, but received value of type {typ}. - Received value: {val}""".format(typ=type(val), val=val) - ) - self._props["default_scale"] = val - - @property - def topojson(self): - """ - Path to the topojson files needed to render choropleth traces. - - If None, topojson files from the plot.ly CDN are used. - - Returns - ------- - str - """ - return self._props.get("topojson", None) - - @topojson.setter - def topojson(self, val): - if val is None: - self._props.pop("topojson", None) - else: - if not isinstance(val, str): - raise ValueError( - """ -The topojson property must be a string, but received value of type {typ}. - Received value: {val}""".format(typ=type(val), val=val) - ) - self._props["topojson"] = val - - # Server must restart before setting is active - shutdown_server() - - @property - def mathjax(self): - """ - Path to the MathJax bundle needed to render LaTeX characters - - Returns - ------- - str - """ - return self._props.get( - "mathjax", - "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js", - ) - - @mathjax.setter - def mathjax(self, val): - if val is None: - self._props.pop("mathjax", None) - else: - if not isinstance(val, str): - raise ValueError( - """ -The mathjax property must be a string, but received value of type {typ}. - Received value: {val}""".format(typ=type(val), val=val) - ) - self._props["mathjax"] = val - - # Server must restart before setting is active - shutdown_server() - - @property - def use_xvfb(self): - dflt = "auto" - return self._props.get("use_xvfb", dflt) - - @use_xvfb.setter - def use_xvfb(self, val): - valid_vals = [True, False, "auto"] - if val is None: - self._props.pop("use_xvfb", None) - else: - if val not in valid_vals: - raise ValueError( - """ -The use_xvfb property must be one of {valid_vals} - Received value of type {typ}: {val}""".format( - valid_vals=valid_vals, typ=type(val), val=repr(val) - ) - ) - - self._props["use_xvfb"] = val - - # Server and validation must restart before setting is active - reset_status() - - @property - def plotlyjs(self): - """ - The plotly.js bundle being used for image rendering. - - Returns - ------- - str - """ - return self._constants.get("plotlyjs", None) - - @property - def config_file(self): - """ - Path to orca configuration file - - Using the `plotly.io.config.save()` method will save the current - configuration settings to this file. Settings in this file are - restored at the beginning of each sessions. - - Returns - ------- - str - """ - return os.path.join(PLOTLY_DIR, ".orca") - - def __repr__(self): - """ - Display a nice representation of the current orca configuration. - """ - return """\ -orca configuration ------------------- - server_url: {server_url} - executable: {executable} - port: {port} - timeout: {timeout} - default_width: {default_width} - default_height: {default_height} - default_scale: {default_scale} - default_format: {default_format} - mathjax: {mathjax} - topojson: {topojson} - use_xvfb: {use_xvfb} - -constants ---------- - plotlyjs: {plotlyjs} - config_file: {config_file} - -""".format( - server_url=self.server_url, - port=self.port, - executable=self.executable, - timeout=self.timeout, - default_width=self.default_width, - default_height=self.default_height, - default_scale=self.default_scale, - default_format=self.default_format, - mathjax=self.mathjax, - topojson=self.topojson, - plotlyjs=self.plotlyjs, - config_file=self.config_file, - use_xvfb=self.use_xvfb, - ) - - -# Make config a singleton object -# ------------------------------ -config = OrcaConfig() -del OrcaConfig - - -# Orca status class -# ------------------------ -class OrcaStatus(object): - """ - Class to store information about the current status of the orca server. - """ - - _props = { - "state": "unvalidated", # or 'validated' or 'running' - "executable_list": None, - "version": None, - "pid": None, - "port": None, - "command": None, - } - - @property - def state(self): - """ - A string representing the state of the orca server process - - One of: - - unvalidated: The orca executable has not yet been searched for or - tested to make sure its valid. - - validated: The orca executable has been located and tested for - validity, but it is not running. - - running: The orca server process is currently running. - """ - return self._props["state"] - - @property - def executable(self): - """ - If the `state` property is 'validated' or 'running', this property - contains the full path to the orca executable. - - This path can be specified explicitly by setting the `executable` - property of the `plotly.io.orca.config` object. - - This property will be None if the `state` is 'unvalidated'. - """ - executable_list = self._props["executable_list"] - if executable_list is None: - return None - else: - return " ".join(executable_list) - - @property - def version(self): - """ - If the `state` property is 'validated' or 'running', this property - contains the version of the validated orca executable. - - This property will be None if the `state` is 'unvalidated'. - """ - return self._props["version"] - - @property - def pid(self): - """ - The process id of the orca server process, if any. This property - will be None if the `state` is not 'running'. - """ - return self._props["pid"] - - @property - def port(self): - """ - The port number that the orca server process is listening to, if any. - This property will be None if the `state` is not 'running'. - - This port can be specified explicitly by setting the `port` - property of the `plotly.io.orca.config` object. - """ - return self._props["port"] - - @property - def command(self): - """ - The command arguments used to launch the running orca server, if any. - This property will be None if the `state` is not 'running'. - """ - return self._props["command"] - - def __repr__(self): - """ - Display a nice representation of the current orca server status. - """ - return """\ -orca status ------------ - state: {state} - executable: {executable} - version: {version} - port: {port} - pid: {pid} - command: {command} - -""".format( - executable=self.executable, - version=self.version, - port=self.port, - pid=self.pid, - state=self.state, - command=self.command, - ) - - -# Make status a singleton object -# ------------------------------ -status = OrcaStatus() -del OrcaStatus - - -@contextmanager -def orca_env(): - """ - Context manager to clear and restore environment variables that are - problematic for orca to function properly - - NODE_OPTIONS: When this variable is set, orca >> plotly.io.orca.config.executable = '/path/to/orca' - -After updating this executable property, try the export operation again. -If it is successful then you may want to save this configuration so that it -will be applied automatically in future sessions. You can do this as follows: - - >>> plotly.io.orca.config.save() - -If you're still having trouble, feel free to ask for help on the forums at -https://community.plot.ly/c/api/python -""" - - # Try to find an executable - # ------------------------- - # Search for executable name or path in config.executable - executable = which(config.executable) - path = os.environ.get("PATH", os.defpath) - formatted_path = path.replace(os.pathsep, "\n ") - - if executable is None: - raise ValueError( - """ -The orca executable is required to export figures as static images, -but it could not be found on the system path. - -Searched for executable '{executable}' on the following path: - {formatted_path} - -{instructions}""".format( - executable=config.executable, - formatted_path=formatted_path, - instructions=install_location_instructions, - ) - ) - - # Check if we should run with Xvfb - # -------------------------------- - xvfb_args = [ - "--auto-servernum", - "--server-args", - "-screen 0 640x480x24 +extension RANDR +extension GLX", - executable, - ] - - if config.use_xvfb: - # Use xvfb - xvfb_run_executable = which("xvfb-run") - if not xvfb_run_executable: - raise ValueError( - """ -The plotly.io.orca.config.use_xvfb property is set to True, but the -xvfb-run executable could not be found on the system path. - -Searched for the executable 'xvfb-run' on the following path: - {formatted_path}""".format(formatted_path=formatted_path) - ) - - executable_list = [xvfb_run_executable] + xvfb_args - elif ( - config.use_xvfb == "auto" - and sys.platform.startswith("linux") - and not os.environ.get("DISPLAY") - and which("xvfb-run") - ): - # use_xvfb is 'auto', we're on linux without a display server, - # and xvfb-run is available. Use it. - xvfb_run_executable = which("xvfb-run") - executable_list = [xvfb_run_executable] + xvfb_args - else: - # Do not use xvfb - executable_list = [executable] - - # Run executable with --help and see if it's our orca - # --------------------------------------------------- - invalid_executable_msg = """ -The orca executable is required in order to export figures as static images, -but the executable that was found at '{executable}' -does not seem to be a valid plotly orca executable. Please refer to the end of -this message for details on what went wrong. - -{instructions}""".format( - executable=executable, instructions=install_location_instructions - ) - - # ### Run with Popen so we get access to stdout and stderr - with orca_env(): - p = subprocess.Popen( - executable_list + ["--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - - help_result, help_error = p.communicate() - - if p.returncode != 0: - err_msg = ( - invalid_executable_msg - + """ -Here is the error that was returned by the command - $ {executable} --help - -[Return code: {returncode}] -{err_msg} -""".format( - executable=" ".join(executable_list), - err_msg=help_error.decode("utf-8"), - returncode=p.returncode, - ) - ) - - # Check for Linux without X installed. - if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"): - err_msg += """\ -Note: When used on Linux, orca requires an X11 display server, but none was -detected. Please install Xvfb and configure plotly.py to run orca using Xvfb -as follows: - - >>> import plotly.io as pio - >>> pio.orca.config.use_xvfb = True - -You can save this configuration for use in future sessions as follows: - - >>> pio.orca.config.save() - -See https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml -for more info on Xvfb -""" - raise ValueError(err_msg) - - if not help_result: - raise ValueError( - invalid_executable_msg - + """ -The error encountered is that no output was returned by the command - $ {executable} --help -""".format(executable=" ".join(executable_list)) - ) - - if "Plotly's image-exporting utilities" not in help_result.decode("utf-8"): - raise ValueError( - invalid_executable_msg - + """ -The error encountered is that unexpected output was returned by the command - $ {executable} --help - -{help_result} -""".format(executable=" ".join(executable_list), help_result=help_result) - ) - - # Get orca version - # ---------------- - # ### Run with Popen so we get access to stdout and stderr - with orca_env(): - p = subprocess.Popen( - executable_list + ["--version"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - version_result, version_error = p.communicate() - - if p.returncode != 0: - raise ValueError( - invalid_executable_msg - + """ -An error occurred while trying to get the version of the orca executable. -Here is the command that plotly.py ran to request the version - $ {executable} --version - -This command returned the following error: - -[Return code: {returncode}] -{err_msg} - """.format( - executable=" ".join(executable_list), - err_msg=version_error.decode("utf-8"), - returncode=p.returncode, - ) - ) - - if not version_result: - raise ValueError( - invalid_executable_msg - + """ -The error encountered is that no version was reported by the orca executable. -Here is the command that plotly.py ran to request the version: - - $ {executable} --version -""".format(executable=" ".join(executable_list)) - ) - else: - version_result = version_result.decode() - - status._props["executable_list"] = executable_list - status._props["version"] = version_result.strip() - status._props["state"] = "validated" - - -def reset_status(): - """ - Shutdown the running orca server, if any, and reset the orca status - to unvalidated. - - This command is only needed if the desired orca executable is changed - during an interactive session. - - Returns - ------- - None - """ - shutdown_server() - status._props["executable_list"] = None - status._props["version"] = None - status._props["state"] = "unvalidated" - - -# Initialze process control variables -# ----------------------------------- -orca_lock = threading.Lock() -orca_state = {"proc": None, "shutdown_timer": None} - - -# Shutdown -# -------- -# The @atexit.register annotation ensures that the shutdown function is -# is run when the Python process is terminated -@atexit.register -def cleanup(): - shutdown_server() - - -def shutdown_server(): - """ - Shutdown the running orca server process, if any - - Returns - ------- - None - """ - # Use double-check locking to make sure the properties of orca_state - # are updated consistently across threads. - if orca_state["proc"] is not None: - with orca_lock: - if orca_state["proc"] is not None: - # We use psutil to kill all child processes of the main orca - # process. This prevents any zombie processes from being - # left over, and it saves us from needing to write - # OS-specific process management code here. - - parent = psutil.Process(orca_state["proc"].pid) - for child in parent.children(recursive=True): - try: - child.terminate() - except Exception: - # We tried, move on - pass - - try: - # Kill parent process - orca_state["proc"].terminate() - - # Wait for the process to shutdown - orca_state["proc"].wait() - except Exception: - # We tried, move on - pass - - # Update our internal process management state - orca_state["proc"] = None - - if orca_state["shutdown_timer"] is not None: - orca_state["shutdown_timer"].cancel() - orca_state["shutdown_timer"] = None - - orca_state["port"] = None - - # Update orca.status so the user has an accurate view - # of the state of the orca server - status._props["state"] = "validated" - status._props["pid"] = None - status._props["port"] = None - status._props["command"] = None - - -# Launch or get server -def ensure_server(): - """ - Start an orca server if none is running. If a server is already running, - then reset the timeout countdown - - Returns - ------- - None - """ - - # Validate psutil - if psutil is None: - raise ValueError( - """\ -Image generation requires the psutil package. - -Install using pip: - $ pip install psutil - -Install using conda: - $ conda install psutil -""" - ) - - # Validate requests - if not get_module("requests"): - raise ValueError( - """\ -Image generation requires the requests package. - -Install using pip: - $ pip install requests - -Install using conda: - $ conda install requests -""" - ) - - if not config.server_url: - # Validate orca executable only if server_url is not provided - if status.state == "unvalidated": - validate_executable() - # Acquire lock to make sure that we keep the properties of orca_state - # consistent across threads - with orca_lock: - # Cancel the current shutdown timer, if any - if orca_state["shutdown_timer"] is not None: - orca_state["shutdown_timer"].cancel() - - # Start a new server process if none is active - if orca_state["proc"] is None: - # Determine server port - if config.port is None: - orca_state["port"] = find_open_port() - else: - orca_state["port"] = config.port - - # Build orca command list - cmd_list = status._props["executable_list"] + [ - "serve", - "-p", - str(orca_state["port"]), - "--plotly", - config.plotlyjs, - "--graph-only", - ] - - if config.topojson: - cmd_list.extend(["--topojson", config.topojson]) - - if config.mathjax: - cmd_list.extend(["--mathjax", config.mathjax]) - - # Create subprocess that launches the orca server on the - # specified port. - DEVNULL = open(os.devnull, "wb") - with orca_env(): - stderr = DEVNULL if "CI" in os.environ else None # fix for CI - orca_state["proc"] = subprocess.Popen( - cmd_list, stdout=DEVNULL, stderr=stderr - ) - - # Update orca.status so the user has an accurate view - # of the state of the orca server - status._props["state"] = "running" - status._props["pid"] = orca_state["proc"].pid - status._props["port"] = orca_state["port"] - status._props["command"] = cmd_list - - # Create new shutdown timer if a timeout was specified - if config.timeout is not None: - t = threading.Timer(config.timeout, shutdown_server) - # Make it a daemon thread so that exit won't wait for timer to - # complete - t.daemon = True - t.start() - orca_state["shutdown_timer"] = t - - -@retry(min_wait=5, max_wait=10, max_delay=60000) -def request_image_with_retrying(**kwargs): - """ - Helper method to perform an image request to a running orca server process - with retrying logic. - """ - from requests import post - from plotly.io.json import to_json_plotly - - if config.server_url: - server_url = config.server_url - else: - server_url = "http://{hostname}:{port}".format( - hostname="localhost", port=orca_state["port"] - ) - - request_params = {k: v for k, v in kwargs.items() if v is not None} - json_str = to_json_plotly(request_params) - response = post(server_url + "/", data=json_str) - - if response.status_code == 522: - # On "522: client socket timeout", return server and keep trying - shutdown_server() - ensure_server() - raise OSError("522: client socket timeout") - - return response - - -def to_image(fig, format=None, width=None, height=None, scale=None, validate=True): - """ - Convert a figure to a static image bytes string - - Parameters - ---------- - fig: - Figure object or dict representing a figure - - 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) - - If not specified, will default to `plotly.io.config.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.config.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.config.default_height` - - scale: int or float or None - The scale factor to use when exporting the figure. A scale factor - larger than 1.0 will increase the image resolution with respect - 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.config.default_scale` - - validate: bool - True if the figure should be validated before being converted to - an image, False otherwise. - - Returns - ------- - bytes - The image data - """ - # Make sure orca sever is running - # ------------------------------- - ensure_server() - - # Handle defaults - # --------------- - # Apply configuration defaults to unspecified arguments - if format is None: - format = config.default_format - - format = validate_coerce_format(format) - - if scale is None: - scale = config.default_scale - - if width is None: - width = config.default_width - - if height is None: - height = config.default_height - - # Validate figure - # --------------- - fig_dict = validate_coerce_fig_to_dict(fig, validate) - - # Request image from server - # ------------------------- - try: - response = request_image_with_retrying( - figure=fig_dict, format=format, scale=scale, width=width, height=height - ) - except OSError: - # Get current status string - status_str = repr(status) - - if config.server_url: - raise ValueError( - """ -Plotly.py was unable to communicate with the orca server at {server_url} - -Please check that the server is running and accessible. -""".format(server_url=config.server_url) - ) - - else: - # Check if the orca server process exists - pid_exists = psutil.pid_exists(status.pid) - - # Raise error message based on whether the server process existed - if pid_exists: - raise ValueError( - """ -For some reason plotly.py was unable to communicate with the -local orca server process, even though the server process seems to be running. - -Please review the process and connection information below: - -{info} -""".format(info=status_str) - ) - else: - # Reset the status so that if the user tries again, we'll try to - # start the server again - reset_status() - raise ValueError( - """ -For some reason the orca server process is no longer running. - -Please review the process and connection information below: - -{info} -plotly.py will attempt to start the local server process again the next time -an image export operation is performed. -""".format(info=status_str) - ) - - # Check response - # -------------- - if response.status_code == 200: - # All good - return response.content - else: - # ### Something went wrong ### - err_message = """ -The image request was rejected by the orca conversion utility -with the following error: - {status}: {msg} -""".format(status=response.status_code, msg=response.content.decode("utf-8")) - - # ### Try to be helpful ### - # Status codes from /src/component/plotly-graph/constants.js in the - # orca code base. - # statusMsg: { - # 400: 'invalid or malformed request syntax', - # 522: client socket timeout - # 525: 'plotly.js error', - # 526: 'plotly.js version 1.11.0 or up required', - # 530: 'image conversion error' - # } - if response.status_code == 400 and isinstance(fig, dict) and not validate: - err_message += """ -Try setting the `validate` argument to True to check for errors in the -figure specification""" - elif response.status_code == 530 and format == "eps": - err_message += """ -Exporting to EPS format requires the poppler library. You can install -poppler on MacOS or Linux with: - - $ conda install poppler - -Or, you can install it on MacOS using homebrew with: - - $ brew install poppler - -Or, you can install it on Linux using your distribution's package manager to -install the 'poppler-utils' package. - -Unfortunately, we don't yet know of an easy way to install poppler on Windows. -""" - raise ValueError(err_message) - - -def write_image( - fig, file, format=None, scale=None, width=None, height=None, validate=True -): - """ - Convert a figure to a static image and write it to a file or writeable - object - - Parameters - ---------- - fig: - Figure object or dict representing a figure - - file: str or writeable - A string representing a local file path or a writeable object - (e.g. a pathlib.Path object or an open file descriptor) - - 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) - - If not specified and `file` is a string then this will default to the - file extension. If not specified and `file` is not a string then this - will default to `plotly.io.config.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.config.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.config.default_height` - - scale: int or float or None - The scale factor to use when exporting the figure. A scale factor - larger than 1.0 will increase the image resolution with respect - 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.config.default_scale` - - validate: bool - True if the figure should be validated before being converted to - an image, False otherwise. - - Returns - ------- - None - """ - - # Try to cast `file` as a pathlib object `path`. - # ---------------------------------------------- - if isinstance(file, str): - # Use the standard Path constructor to make a pathlib object. - path = Path(file) - elif isinstance(file, Path): - # `file` is already a Path object. - path = file - else: - # We could not make a Path object out of file. Either `file` is an open file - # descriptor with a `write()` method or it's an invalid object. - path = None - - # Infer format if not specified - # ----------------------------- - if path is not None and format is None: - ext = path.suffix - if ext: - format = ext.lstrip(".") - else: - raise ValueError( - """ -Cannot infer image type from output path '{file}'. -Please add a file extension or specify the type using the format parameter. -For example: - - >>> import plotly.io as pio - >>> pio.write_image(fig, file_path, format='png') -""".format(file=file) - ) - - # Request image - # ------------- - # Do this first so we don't create a file if image conversion fails - img_data = to_image( - fig, format=format, scale=scale, width=width, height=height, validate=validate - ) - - # Open file - # --------- - if path is None: - # We previously failed to make sense of `file` as a pathlib object. - # Attempt to write to `file` as an open file descriptor. - try: - file.write(img_data) - return - except AttributeError: - pass - raise ValueError( - """ -The 'file' argument '{file}' is not a string, pathlib.Path object, or file descriptor. -""".format(file=file) - ) - else: - # We previously succeeded in interpreting `file` as a pathlib object. - # Now we can use `write_bytes()`. - path.write_bytes(img_data) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 8eea1fe8bf8..bd51b741772 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -22,7 +22,7 @@ BrowserRenderer, IFrameRenderer, SphinxGalleryHtmlRenderer, - SphinxGalleryOrcaRenderer, + SphinxGalleryPngRenderer, CoCalcRenderer, DatabricksRenderer, ) @@ -465,7 +465,7 @@ def show(fig, renderer=None, validate=True, **kwargs): renderers["iframe"] = IFrameRenderer(config=config, include_plotlyjs=True) renderers["iframe_connected"] = IFrameRenderer(config=config, include_plotlyjs="cdn") renderers["sphinx_gallery"] = SphinxGalleryHtmlRenderer() -renderers["sphinx_gallery_png"] = SphinxGalleryOrcaRenderer() +renderers["sphinx_gallery_png"] = SphinxGalleryPngRenderer() # Set default renderer # -------------------- @@ -516,17 +516,6 @@ def show(fig, renderer=None, validate=True, **kwargs): if not default_renderer and "DATABRICKS_RUNTIME_VERSION" in os.environ: default_renderer = "databricks" - # Check if we're running in spyder and orca is installed - if not default_renderer and "SPYDER_ARGS" in os.environ: - try: - from plotly.io.orca import validate_executable - - validate_executable() - default_renderer = "svg" - except ValueError: - # orca not found - pass - # Check if we're running in ipython terminal ipython_info = ipython.get_ipython() shell = ipython_info.__class__.__name__ diff --git a/plotly/io/kaleido.py b/plotly/io/kaleido.py index c086ea39ffe..2f02aa9b2de 100644 --- a/plotly/io/kaleido.py +++ b/plotly/io/kaleido.py @@ -2,11 +2,4 @@ from ._kaleido import ( to_image, write_image, - scope, - kaleido_available, - kaleido_major, - ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS, - KALEIDO_DEPRECATION_MSG, - ORCA_DEPRECATION_MSG, - ENGINE_PARAM_DEPRECATION_MSG, ) diff --git a/plotly/io/orca.py b/plotly/io/orca.py deleted file mode 100644 index 4fd5c19250f..00000000000 --- a/plotly/io/orca.py +++ /dev/null @@ -1,9 +0,0 @@ -# ruff: noqa: F401 -from ._orca import ( - ensure_server, - shutdown_server, - validate_executable, - reset_status, - config, - status, -) diff --git a/tests/test_io/test_renderers.py b/tests/test_io/test_renderers.py index 87d9430ebf7..9907bbcebd3 100644 --- a/tests/test_io/test_renderers.py +++ b/tests/test_io/test_renderers.py @@ -121,11 +121,6 @@ def test_plotly_mimetype_renderer_show(fig1, renderer): mock_display.assert_called_once_with(expected, raw=True) -# Static Image -# ------------ -# See tests/test_orca/test_image_renderers.py - - # HTML # ---- def assert_full_html(html): @@ -327,7 +322,7 @@ def test_repr_html(renderer): assert str_html.replace(id_html, "") == template.replace(id_pattern, "") -all_renderers_without_orca = [ +all_renderers = [ "plotly_mimetype", "jupyterlab", "nteract", @@ -350,7 +345,7 @@ def test_repr_html(renderer): ] -@pytest.mark.parametrize("renderer_str", all_renderers_without_orca) +@pytest.mark.parametrize("renderer_str", all_renderers) def test_repr_mimebundle(renderer_str): pio.renderers.default = renderer_str fig = go.Figure() diff --git a/tests/test_optional/test_kaleido/test_kaleido.py b/tests/test_optional/test_kaleido/test_kaleido.py index 91950782915..d1573009e4a 100644 --- a/tests/test_optional/test_kaleido/test_kaleido.py +++ b/tests/test_optional/test_kaleido/test_kaleido.py @@ -10,8 +10,6 @@ from PIL import Image import plotly.graph_objects as go import plotly.io as pio -from plotly.io.kaleido import kaleido_available, kaleido_major -import pytest fig = {"data": [], "layout": {"title": {"text": "figure title"}}} @@ -56,8 +54,8 @@ def check_image(path_or_buffer, size=(700, 500), format="PNG"): assert img.format == format -def test_kaleido_engine_to_image_returns_bytes(): - result = pio.to_image(fig, format="svg", engine="kaleido", validate=False) +def test_kaleido_to_image_returns_bytes(): + result = pio.to_image(fig, format="svg", validate=False) assert result.startswith(b" 0: - # Check that all the defaults values are passed through to the function call to calc_fig_sync - with patch( - "plotly.io._kaleido.kaleido.calc_fig_sync", - return_value=test_image_bytes, - ) as mock_calc_fig: - result = pio.to_image(test_fig, validate=False) - - # Verify calc_fig_sync was called with correct args - # taken from pio.defaults - mock_calc_fig.assert_called_once() - args, kwargs = mock_calc_fig.call_args - assert args[0] == test_fig.to_dict() - assert kwargs["opts"]["format"] == "svg" - assert kwargs["opts"]["width"] == 701 - assert kwargs["opts"]["height"] == 501 - assert kwargs["opts"]["scale"] == 2 - assert kwargs["topojson"] == "path/to/topojson/files/" - # mathjax and plotlyjs are passed through in kopts - assert ( - kwargs["kopts"]["mathjax"] - == "https://cdn.jsdelivr.net/npm/mathjax@3.1.2/es5/tex-svg.js" - ) - assert ( - kwargs["kopts"]["plotlyjs"] == "https://cdn.plot.ly/plotly-3.0.0.js" - ) - - else: - # Check that all the default values have been set in pio._kaleido.scope - assert pio._kaleido.scope.default_format == "svg" - assert pio._kaleido.scope.default_width == 701 - assert pio._kaleido.scope.default_height == 501 - assert pio._kaleido.scope.default_scale == 2 + # Check that all the defaults values are passed through to the function call to calc_fig_sync + with patch( + "plotly.io._kaleido.kaleido.calc_fig_sync", + return_value=test_image_bytes, + ) as mock_calc_fig: + result = pio.to_image(test_fig, validate=False) + + # Verify calc_fig_sync was called with correct args + # taken from pio.defaults + mock_calc_fig.assert_called_once() + args, kwargs = mock_calc_fig.call_args + assert args[0] == test_fig.to_dict() + assert kwargs["opts"]["format"] == "svg" + assert kwargs["opts"]["width"] == 701 + assert kwargs["opts"]["height"] == 501 + assert kwargs["opts"]["scale"] == 2 + assert kwargs["topojson"] == "path/to/topojson/files/" + # mathjax and plotlyjs are passed through in kopts assert ( - pio._kaleido.scope.mathjax + kwargs["kopts"]["mathjax"] == "https://cdn.jsdelivr.net/npm/mathjax@3.1.2/es5/tex-svg.js" ) - assert pio._kaleido.scope.topojson == "path/to/topojson/files/" - assert pio._kaleido.scope.plotlyjs == "https://cdn.plot.ly/plotly-3.0.0.js" + assert kwargs["kopts"]["plotlyjs"] == "https://cdn.plot.ly/plotly-3.0.0.js" # Set topojson default back to None # (otherwise image generation will fail) @@ -282,12 +257,9 @@ def test_fig_write_image(): test_fig = go.Figure(fig) test_image_bytes = b"mock image data" - if kaleido_major() > 0: - patch_funcname = "plotly.io._kaleido.kaleido.calc_fig_sync" - else: - patch_funcname = "plotly.io._kaleido.scope.transform" - - with patch(patch_funcname, return_value=test_image_bytes) as mock_calc_fig: + with patch( + "plotly.io._kaleido.kaleido.calc_fig_sync", return_value=test_image_bytes + ) as mock_calc_fig: test_fig.write_image("test_path.png") # Verify patched function was called once with fig dict as first argument @@ -302,12 +274,9 @@ def test_fig_to_image(): test_fig = go.Figure(fig) test_image_bytes = b"mock image data" - if kaleido_major() > 0: - patch_funcname = "plotly.io._kaleido.kaleido.calc_fig_sync" - else: - patch_funcname = "plotly.io._kaleido.scope.transform" - - with patch(patch_funcname, return_value=test_image_bytes) as mock_calc_fig: + with patch( + "plotly.io._kaleido.kaleido.calc_fig_sync", return_value=test_image_bytes + ) as mock_calc_fig: test_fig.to_image() # Verify patched function was called once with fig dict as first argument @@ -319,22 +288,14 @@ def test_fig_to_image(): def test_get_chrome(): """Test that plotly.io.get_chrome() can be called.""" - if not kaleido_available() or kaleido_major() < 1: - # Test that ValueError is raised when Kaleido requirements aren't met - with pytest.raises( - ValueError, match="This command requires Kaleido v1.0.0 or greater" - ): - pio.get_chrome() - else: - # Test normal operation when Kaleido v1+ is available - with patch( - "plotly.io._kaleido.kaleido.get_chrome_sync", - return_value="/mock/path/to/chrome", - ) as mock_get_chrome: - pio.get_chrome() + with patch( + "plotly.io._kaleido.kaleido.get_chrome_sync", + return_value="/mock/path/to/chrome", + ) as mock_get_chrome: + pio.get_chrome() - # Verify that kaleido.get_chrome_sync was called - mock_get_chrome.assert_called_once() + # Verify that kaleido.get_chrome_sync was called + mock_get_chrome.assert_called_once() def test_width_height_priority(): diff --git a/tests/test_optional/test_kaleido/test_kaleido_v0_error.py b/tests/test_optional/test_kaleido/test_kaleido_v0_error.py new file mode 100644 index 00000000000..6935366c86b --- /dev/null +++ b/tests/test_optional/test_kaleido/test_kaleido_v0_error.py @@ -0,0 +1,86 @@ +""" +Tests that every function requiring Kaleido raises a RuntimeError with the +correct message when Kaleido v0 is installed. +""" + +import pytest + +import plotly.graph_objects as go +import plotly.io as pio +from plotly.io._kaleido import kaleido_available + + +# Skip every test in this module if Kaleido v1 is installed +pytestmark = pytest.mark.skipif( + kaleido_available(), + reason="These tests only apply when Kaleido v1 is not installed.", +) + + +fig = {"data": [], "layout": {"title": {"text": "figure title"}}} + + +def assert_error_message_contents(excinfo): + """Check that the raised RuntimeError has the expected wording.""" + assert "Image export requires the Kaleido package, v1.0.0 or greater" in str( + excinfo.value + ) + + +# plotly/io/_kaleido.py + + +def test_to_image_raises(): + with pytest.raises(RuntimeError) as excinfo: + pio.to_image(fig, format="png", validate=False) + assert_error_message_contents(excinfo) + + +def test_write_image_raises(tmp_path): + with pytest.raises(RuntimeError) as excinfo: + pio.write_image(fig, tmp_path / "test.png", validate=False) + assert_error_message_contents(excinfo) + + +def test_write_images_raises(tmp_path): + figs = [dict(fig), dict(fig)] + paths = [tmp_path / f"test_{i}.png" for i in range(len(figs))] + with pytest.raises(RuntimeError) as excinfo: + pio.write_images(fig, paths, validate=False) + assert_error_message_contents(excinfo) + + +def test_full_figure_for_development_raises(): + with pytest.raises(RuntimeError) as excinfo: + pio.full_figure_for_development(fig, warn=False) + assert_error_message_contents(excinfo) + + +def test_get_chrome_raises(): + with pytest.raises(RuntimeError) as excinfo: + pio.get_chrome() + assert_error_message_contents(excinfo) + + +# plotly/basedatatypes.py + + +def test_figure_to_image_raises(): + test_fig = go.Figure(fig) + with pytest.raises(RuntimeError) as excinfo: + test_fig.to_image(format="png", validate=False) + assert_error_message_contents(excinfo) + + +def test_figure_write_image_raises(tmp_path): + test_fig = go.Figure(fig) + with pytest.raises(RuntimeError) as excinfo: + test_fig.write_image(tmp_path / "test.png", validate=False) + assert_error_message_contents(excinfo) + + +def test_figure_full_figure_for_development_raises(): + test_fig = go.Figure(fig) + with pytest.raises(RuntimeError) as excinfo: + test_fig.full_figure_for_development(warn=False) + assert_error_message_contents(excinfo)