Skip to content

Validate plot backend when setting. #28164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 3, 2019
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