Skip to content

Commit f42ad67

Browse files
committed
Validate plot backend when setting.
```python In [1]: import pandas as pd In [2]: import sys In [3]: import types In [4]: module = types.ModuleType("foo") In [5]: sys.modules['foo'] = module In [6]: pd.set_option('plotting.backend', 'foo') --------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-6-123b6513deb2> in <module> ----> 1 pd.set_option('plotting.backend', 'foo') ... ~/sandbox/pandas/pandas/plotting/_core.py in _find_backend(backend) 1588 "top-level `.plot` method." 1589 ) -> 1590 raise ValueError(msg.format(name=backend)) 1591 1592 ValueError: Could not find plotting backend 'foo'. Ensure that you've installed the package providing the 'foo' entrypoint, or that the package has atop-level `.plot` method. ``` Closes pandas-dev#28163
1 parent 49d2019 commit f42ad67

File tree

4 files changed

+48
-36
lines changed

4 files changed

+48
-36
lines changed

doc/source/whatsnew/v1.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ Plotting
171171
- Bug in :meth:`DataFrame.plot` producing incorrect legend markers when plotting multiple series on the same axis (:issue:`18222`)
172172
- Bug in :meth:`DataFrame.plot` when ``kind='box'`` and data contains datetime or timedelta data. These types are now automatically dropped (:issue:`22799`)
173173
- Bug in :meth:`DataFrame.plot.line` and :meth:`DataFrame.plot.area` produce wrong xlim in x-axis (:issue:`27686`, :issue:`25160`, :issue:`24784`)
174+
- :func:`set_option` now validates that the plot backend provided to ``'plotting.backend'`` implements the backend when the option is set, rather than when a plot is created (:issue:`28163`)
174175

175176
Groupby/resample/rolling
176177
^^^^^^^^^^^^^^^^^^^^^^^^

pandas/core/config_init.py

+3-21
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
module is imported, register them here rather then in the module.
1010
1111
"""
12-
import importlib
13-
1412
import pandas._config.config as cf
1513
from pandas._config.config import (
1614
is_bool,
@@ -581,26 +579,10 @@ def use_inf_as_na_cb(key):
581579

582580

583581
def register_plotting_backend_cb(key):
584-
backend_str = cf.get_option(key)
585-
if backend_str == "matplotlib":
586-
try:
587-
import pandas.plotting._matplotlib # noqa
588-
except ImportError:
589-
raise ImportError(
590-
"matplotlib is required for plotting when the "
591-
'default backend "matplotlib" is selected.'
592-
)
593-
else:
594-
return
582+
from pandas.plotting._core import _get_plot_backend
595583

596-
try:
597-
importlib.import_module(backend_str)
598-
except ImportError:
599-
raise ValueError(
600-
'"{}" does not seem to be an installed module. '
601-
"A pandas plotting backend must be a module that "
602-
"can be imported".format(backend_str)
603-
)
584+
backend_str = cf.get_option(key)
585+
_get_plot_backend(backend_str)
604586

605587

606588
with cf.config_prefix("plotting"):

pandas/plotting/_core.py

+19-5
Original file line numberDiff line numberDiff line change
@@ -1576,10 +1576,18 @@ def _find_backend(backend: str):
15761576
# We re-raise later on.
15771577
pass
15781578
else:
1579-
_backends[backend] = module
1580-
return module
1581-
1582-
raise ValueError("No backend {}".format(backend))
1579+
if hasattr(module, "plot"):
1580+
# Validate that the interface is implemented when the option
1581+
# is set, rather than at plot time.
1582+
_backends[backend] = module
1583+
return module
1584+
1585+
msg = (
1586+
"Could not find plotting backend '{name}'. Ensure that you've installed the "
1587+
"package providing the '{name}' entrypoint, or that the package has a"
1588+
"top-level `.plot` method."
1589+
)
1590+
raise ValueError(msg.format(name=backend))
15831591

15841592

15851593
def _get_plot_backend(backend=None):
@@ -1600,7 +1608,13 @@ def _get_plot_backend(backend=None):
16001608
if backend == "matplotlib":
16011609
# Because matplotlib is an optional dependency and first-party backend,
16021610
# we need to attempt an import here to raise an ImportError if needed.
1603-
import pandas.plotting._matplotlib as module
1611+
try:
1612+
import pandas.plotting._matplotlib as module
1613+
except ImportError:
1614+
raise ImportError(
1615+
"matplotlib is required for plotting when the "
1616+
"default backend 'matplotlib' is selected."
1617+
) from None
16041618

16051619
_backends["matplotlib"] = module
16061620

pandas/tests/plotting/test_backend.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99
import pandas
1010

1111

12+
@pytest.fixture
13+
def dummy_backend():
14+
backend = types.ModuleType("pandas_dummy_backend")
15+
backend.plot = lambda *args, **kwargs: None
16+
sys.modules["pandas_dummy_backend"] = backend
17+
18+
yield
19+
20+
del sys.modules["pandas_dummy_backend"]
21+
22+
1223
def test_matplotlib_backend_error():
1324
msg = (
1425
"matplotlib is required for plotting when the default backend "
@@ -22,20 +33,14 @@ def test_matplotlib_backend_error():
2233

2334

2435
def test_backend_is_not_module():
25-
msg = (
26-
'"not_an_existing_module" does not seem to be an installed module. '
27-
"A pandas plotting backend must be a module that can be imported"
28-
)
36+
msg = "Could not find plotting backend 'not_an_existing_module'."
2937
with pytest.raises(ValueError, match=msg):
3038
pandas.set_option("plotting.backend", "not_an_existing_module")
3139

3240

33-
def test_backend_is_correct(monkeypatch):
34-
monkeypatch.setattr(
35-
"pandas.core.config_init.importlib.import_module", lambda name: None
36-
)
37-
pandas.set_option("plotting.backend", "correct_backend")
38-
assert pandas.get_option("plotting.backend") == "correct_backend"
41+
def test_backend_is_correct(dummy_backend):
42+
pandas.set_option("plotting.backend", "pandas_dummy_backend")
43+
assert pandas.get_option("plotting.backend") == "pandas_dummy_backend"
3944

4045
# Restore backend for other tests (matplotlib can be not installed)
4146
try:
@@ -74,6 +79,16 @@ def test_register_entrypoint():
7479
assert result is mod
7580

7681

82+
def test_setting_backend_raies():
83+
module = types.ModuleType("pandas_plot_backend")
84+
sys.modules["pandas_plot_backend"] = module
85+
86+
with pytest.raises(
87+
ValueError, match="Could not find plotting backend 'pandas_plot_backend'."
88+
):
89+
pandas.set_option("plotting.backend", "pandas_plot_backend")
90+
91+
7792
def test_register_import():
7893
mod = types.ModuleType("my_backend2")
7994
mod.plot = lambda *args, **kwargs: 1

0 commit comments

Comments
 (0)