Skip to content
Merged
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ Plotting
- Bug in :meth:`DataFrame.plot` producing incorrect legend markers when plotting multiple series on the same axis (:issue:`18222`)
- Bug in :meth:`DataFrame.plot` when ``kind='box'`` and data contains datetime or timedelta data. These types are now automatically dropped (:issue:`22799`)
- Bug in :meth:`DataFrame.plot.line` and :meth:`DataFrame.plot.area` produce wrong xlim in x-axis (:issue:`27686`, :issue:`25160`, :issue:`24784`)
- :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`)

Groupby/resample/rolling
^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
29 changes: 6 additions & 23 deletions pandas/core/config_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
module is imported, register them here rather then in the module.

"""
import importlib

import pandas._config.config as cf
from pandas._config.config import (
is_bool,
Expand Down Expand Up @@ -581,35 +579,20 @@ def use_inf_as_na_cb(key):


def register_plotting_backend_cb(key):
backend_str = cf.get_option(key)
if backend_str == "matplotlib":
try:
import pandas.plotting._matplotlib # noqa
except ImportError:
raise ImportError(
"matplotlib is required for plotting when the "
'default backend "matplotlib" is selected.'
)
else:
return
if key == "matplotlib":
# We defer matplotlib validation, since it's the default
return
from pandas.plotting._core import _get_plot_backend

try:
importlib.import_module(backend_str)
except ImportError:
raise ValueError(
'"{}" does not seem to be an installed module. '
"A pandas plotting backend must be a module that "
"can be imported".format(backend_str)
)
_get_plot_backend(key)


with cf.config_prefix("plotting"):
cf.register_option(
"backend",
defval="matplotlib",
doc=plotting_backend_doc,
validator=str,
cb=register_plotting_backend_cb,
validator=register_plotting_backend_cb,
)


Expand Down
24 changes: 19 additions & 5 deletions pandas/plotting/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1576,10 +1576,18 @@ def _find_backend(backend: str):
# We re-raise later on.
pass
else:
_backends[backend] = module
return module

raise ValueError("No backend {}".format(backend))
if hasattr(module, "plot"):
# Validate that the interface is implemented when the option
# is set, rather than at plot time.
_backends[backend] = module
return module

msg = (
"Could not find plotting backend '{name}'. Ensure that you've installed the "
"package providing the '{name}' entrypoint, or that the package has a"
"top-level `.plot` method."
)
raise ValueError(msg.format(name=backend))


def _get_plot_backend(backend=None):
Expand All @@ -1600,7 +1608,13 @@ def _get_plot_backend(backend=None):
if backend == "matplotlib":
# Because matplotlib is an optional dependency and first-party backend,
# we need to attempt an import here to raise an ImportError if needed.
import pandas.plotting._matplotlib as module
try:
import pandas.plotting._matplotlib as module
except ImportError:
raise ImportError(
"matplotlib is required for plotting when the "
'default backend "matplotlib" is selected.'
) from None

_backends["matplotlib"] = module

Expand Down
63 changes: 31 additions & 32 deletions pandas/tests/plotting/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,38 @@

import pandas

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

def test_matplotlib_backend_error():
msg = (
"matplotlib is required for plotting when the default backend "
'"matplotlib" is selected.'
)
try:
import matplotlib # noqa
except ImportError:
with pytest.raises(ImportError, match=msg):
pandas.set_option("plotting.backend", "matplotlib")

@pytest.fixture
def restore_backend():
"""Restore the plotting backend to matplotlib"""
pandas.set_option("plotting.backend", "matplotlib")
yield
pandas.set_option("plotting.backend", "matplotlib")


def test_backend_is_not_module():
msg = (
'"not_an_existing_module" does not seem to be an installed module. '
"A pandas plotting backend must be a module that can be imported"
)
msg = "Could not find plotting backend 'not_an_existing_module'."
with pytest.raises(ValueError, match=msg):
pandas.set_option("plotting.backend", "not_an_existing_module")

assert pandas.options.plotting.backend == "matplotlib"

def test_backend_is_correct(monkeypatch):
monkeypatch.setattr(
"pandas.core.config_init.importlib.import_module", lambda name: None
)
pandas.set_option("plotting.backend", "correct_backend")
assert pandas.get_option("plotting.backend") == "correct_backend"

# Restore backend for other tests (matplotlib can be not installed)
try:
pandas.set_option("plotting.backend", "matplotlib")
except ImportError:
pass
def test_backend_is_correct(monkeypatch, restore_backend):
monkeypatch.setitem(sys.modules, "pandas_dummy_backend", dummy_backend)

pandas.set_option("plotting.backend", "pandas_dummy_backend")
assert pandas.get_option("plotting.backend") == "pandas_dummy_backend"
assert (
pandas.plotting._core._get_plot_backend("pandas_dummy_backend") is dummy_backend
)


@td.skip_if_no_mpl
def test_register_entrypoint():
def test_register_entrypoint(restore_backend):

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


def test_register_import():
mod = types.ModuleType("my_backend2")
mod.plot = lambda *args, **kwargs: 1
sys.modules["my_backend2"] = mod
def test_setting_backend_without_plot_raises():
# GH-28163
module = types.ModuleType("pandas_plot_backend")
sys.modules["pandas_plot_backend"] = module

result = pandas.plotting._core._get_plot_backend("my_backend2")
assert result is mod
assert pandas.options.plotting.backend == "matplotlib"
with pytest.raises(
ValueError, match="Could not find plotting backend 'pandas_plot_backend'."
):
pandas.set_option("plotting.backend", "pandas_plot_backend")

assert pandas.options.plotting.backend == "matplotlib"


@td.skip_if_mpl
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/plotting/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_import_error_message():
# GH-19810
df = DataFrame({"A": [1, 2]})

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


Expand Down