Skip to content

Commit 242a5ce

Browse files
TomAugspurgerproost
authored andcommitted
VIS: Validate plot backend when setting. (pandas-dev#28164)
* Validate plot backend when setting. Closes pandas-dev#28163
1 parent 3ce3963 commit 242a5ce

File tree

5 files changed

+58
-61
lines changed

5 files changed

+58
-61
lines changed

doc/source/whatsnew/v1.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ Plotting
172172
- Bug in :meth:`DataFrame.plot` producing incorrect legend markers when plotting multiple series on the same axis (:issue:`18222`)
173173
- Bug in :meth:`DataFrame.plot` when ``kind='box'`` and data contains datetime or timedelta data. These types are now automatically dropped (:issue:`22799`)
174174
- Bug in :meth:`DataFrame.plot.line` and :meth:`DataFrame.plot.area` produce wrong xlim in x-axis (:issue:`27686`, :issue:`25160`, :issue:`24784`)
175+
- :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`)
175176

176177
Groupby/resample/rolling
177178
^^^^^^^^^^^^^^^^^^^^^^^^

pandas/core/config_init.py

+6-23
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,35 +579,20 @@ 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+
if key == "matplotlib":
583+
# We defer matplotlib validation, since it's the default
584+
return
585+
from pandas.plotting._core import _get_plot_backend
595586

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-
)
587+
_get_plot_backend(key)
604588

605589

606590
with cf.config_prefix("plotting"):
607591
cf.register_option(
608592
"backend",
609593
defval="matplotlib",
610594
doc=plotting_backend_doc,
611-
validator=str,
612-
cb=register_plotting_backend_cb,
595+
validator=register_plotting_backend_cb,
613596
)
614597

615598

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

+31-32
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,38 @@
88

99
import pandas
1010

11+
dummy_backend = types.ModuleType("pandas_dummy_backend")
12+
dummy_backend.plot = lambda *args, **kwargs: None
1113

12-
def test_matplotlib_backend_error():
13-
msg = (
14-
"matplotlib is required for plotting when the default backend "
15-
'"matplotlib" is selected.'
16-
)
17-
try:
18-
import matplotlib # noqa
19-
except ImportError:
20-
with pytest.raises(ImportError, match=msg):
21-
pandas.set_option("plotting.backend", "matplotlib")
14+
15+
@pytest.fixture
16+
def restore_backend():
17+
"""Restore the plotting backend to matplotlib"""
18+
pandas.set_option("plotting.backend", "matplotlib")
19+
yield
20+
pandas.set_option("plotting.backend", "matplotlib")
2221

2322

2423
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-
)
24+
msg = "Could not find plotting backend 'not_an_existing_module'."
2925
with pytest.raises(ValueError, match=msg):
3026
pandas.set_option("plotting.backend", "not_an_existing_module")
3127

28+
assert pandas.options.plotting.backend == "matplotlib"
3229

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"
3930

40-
# Restore backend for other tests (matplotlib can be not installed)
41-
try:
42-
pandas.set_option("plotting.backend", "matplotlib")
43-
except ImportError:
44-
pass
31+
def test_backend_is_correct(monkeypatch, restore_backend):
32+
monkeypatch.setitem(sys.modules, "pandas_dummy_backend", dummy_backend)
33+
34+
pandas.set_option("plotting.backend", "pandas_dummy_backend")
35+
assert pandas.get_option("plotting.backend") == "pandas_dummy_backend"
36+
assert (
37+
pandas.plotting._core._get_plot_backend("pandas_dummy_backend") is dummy_backend
38+
)
4539

4640

4741
@td.skip_if_no_mpl
48-
def test_register_entrypoint():
42+
def test_register_entrypoint(restore_backend):
4943

5044
dist = pkg_resources.get_distribution("pandas")
5145
if dist.module_path not in pandas.__file__:
@@ -74,13 +68,18 @@ def test_register_entrypoint():
7468
assert result is mod
7569

7670

77-
def test_register_import():
78-
mod = types.ModuleType("my_backend2")
79-
mod.plot = lambda *args, **kwargs: 1
80-
sys.modules["my_backend2"] = mod
71+
def test_setting_backend_without_plot_raises():
72+
# GH-28163
73+
module = types.ModuleType("pandas_plot_backend")
74+
sys.modules["pandas_plot_backend"] = module
8175

82-
result = pandas.plotting._core._get_plot_backend("my_backend2")
83-
assert result is mod
76+
assert pandas.options.plotting.backend == "matplotlib"
77+
with pytest.raises(
78+
ValueError, match="Could not find plotting backend 'pandas_plot_backend'."
79+
):
80+
pandas.set_option("plotting.backend", "pandas_plot_backend")
81+
82+
assert pandas.options.plotting.backend == "matplotlib"
8483

8584

8685
@td.skip_if_mpl

pandas/tests/plotting/test_misc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_import_error_message():
2121
# GH-19810
2222
df = DataFrame({"A": [1, 2]})
2323

24-
with pytest.raises(ImportError, match="No module named 'matplotlib'"):
24+
with pytest.raises(ImportError, match="matplotlib is required for plotting"):
2525
df.plot()
2626

2727

0 commit comments

Comments
 (0)