Skip to content

Commit

Permalink
Backport PR pandas-dev#27488: API: Add entrypoint for plotting
Browse files Browse the repository at this point in the history
  • Loading branch information
TomAugspurger authored and MeeseeksDev[bot] committed Jul 25, 2019
1 parent f1e80fd commit 5d2d653
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 7 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ lint-diff:
git diff upstream/master --name-only -- "*.py" | xargs flake8

black:
black . --exclude '(asv_bench/env|\.egg|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist)'
black . --exclude '(asv_bench/env|\.egg|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|setup.py)'

develop: build
python setup.py develop
Expand Down
2 changes: 1 addition & 1 deletion ci/code_checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ if [[ -z "$CHECK" || "$CHECK" == "lint" ]]; then
black --version

MSG='Checking black formatting' ; echo $MSG
black . --check --exclude '(asv_bench/env|\.egg|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist)'
black . --check --exclude '(asv_bench/env|\.egg|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|setup.py)'
RET=$(($RET + $?)) ; echo $MSG "DONE"

# `setup.cfg` contains the list of error codes that are being ignored in flake8
Expand Down
17 changes: 17 additions & 0 deletions doc/source/development/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -441,5 +441,22 @@ This would be more or less equivalent to:
The backend module can then use other visualization tools (Bokeh, Altair,...)
to generate the plots.

Libraries implementing the plotting backend should use `entry points <https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins>`__
to make their backend discoverable to pandas. The key is ``"pandas_plotting_backends"``. For example, pandas
registers the default "matplotlib" backend as follows.

.. code-block:: python
# in setup.py
setup( # noqa: F821
...,
entry_points={
"pandas_plotting_backends": [
"matplotlib = pandas:plotting._matplotlib",
],
},
)
More information on how to implement a third-party plotting backend can be found at
https://github.com/pandas-dev/pandas/blob/master/pandas/plotting/__init__.py#L1.
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v0.25.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ I/O
Plotting
^^^^^^^^

-
- Added a pandas_plotting_backends entrypoint group for registering plot backends. See :ref:`extending.plotting-backends` for more (:issue:`26747`).
-
-

Expand Down
66 changes: 62 additions & 4 deletions pandas/plotting/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1533,6 +1533,53 @@ def hexbin(self, x, y, C=None, reduce_C_function=None, gridsize=None, **kwargs):
return self(kind="hexbin", x=x, y=y, C=C, **kwargs)


_backends = {}


def _find_backend(backend: str):
"""
Find a pandas plotting backend>
Parameters
----------
backend : str
The identifier for the backend. Either an entrypoint item registered
with pkg_resources, or a module name.
Notes
-----
Modifies _backends with imported backends as a side effect.
Returns
-------
types.ModuleType
The imported backend.
"""
import pkg_resources # Delay import for performance.

for entry_point in pkg_resources.iter_entry_points("pandas_plotting_backends"):
if entry_point.name == "matplotlib":
# matplotlib is an optional dependency. When
# missing, this would raise.
continue
_backends[entry_point.name] = entry_point.load()

try:
return _backends[backend]
except KeyError:
# Fall back to unregisted, module name approach.
try:
module = importlib.import_module(backend)
except ImportError:
# We re-raise later on.
pass
else:
_backends[backend] = module
return module

raise ValueError("No backend {}".format(backend))


def _get_plot_backend(backend=None):
"""
Return the plotting backend to use (e.g. `pandas.plotting._matplotlib`).
Expand All @@ -1546,7 +1593,18 @@ def _get_plot_backend(backend=None):
The backend is imported lazily, as matplotlib is a soft dependency, and
pandas can be used without it being installed.
"""
backend_str = backend or pandas.get_option("plotting.backend")
if backend_str == "matplotlib":
backend_str = "pandas.plotting._matplotlib"
return importlib.import_module(backend_str)
backend = backend or pandas.get_option("plotting.backend")

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

_backends["matplotlib"] = module

if backend in _backends:
return _backends[backend]

module = _find_backend(backend)
_backends[backend] = module
return module
47 changes: 47 additions & 0 deletions pandas/tests/plotting/test_backend.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import sys
import types

import pkg_resources
import pytest

import pandas.util._test_decorators as td

import pandas


Expand Down Expand Up @@ -36,3 +42,44 @@ def test_backend_is_correct(monkeypatch):
pandas.set_option("plotting.backend", "matplotlib")
except ImportError:
pass


@td.skip_if_no_mpl
def test_register_entrypoint():
mod = types.ModuleType("my_backend")
mod.plot = lambda *args, **kwargs: 1

backends = pkg_resources.get_entry_map("pandas")
my_entrypoint = pkg_resources.EntryPoint(
"pandas_plotting_backend",
mod.__name__,
dist=pkg_resources.get_distribution("pandas"),
)
backends["pandas_plotting_backends"]["my_backend"] = my_entrypoint
# TODO: the docs recommend importlib.util.module_from_spec. But this works for now.
sys.modules["my_backend"] = mod

result = pandas.plotting._core._get_plot_backend("my_backend")
assert result is mod

# TODO: https://github.com/pandas-dev/pandas/issues/27517
# Remove the td.skip_if_no_mpl
with pandas.option_context("plotting.backend", "my_backend"):
result = pandas.plotting._core._get_plot_backend()

assert result is mod


def test_register_import():
mod = types.ModuleType("my_backend2")
mod.plot = lambda *args, **kwargs: 1
sys.modules["my_backend2"] = mod

result = pandas.plotting._core._get_plot_backend("my_backend2")
assert result is mod


@td.skip_if_mpl
def test_no_matplotlib_ok():
with pytest.raises(ImportError):
pandas.plotting._core._get_plot_backend("matplotlib")
5 changes: 5 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,5 +830,10 @@ def srcpath(name=None, suffix=".pyx", subdir="src"):
"hypothesis>=3.58",
]
},
entry_points={
"pandas_plotting_backends": [
"matplotlib = pandas:plotting._matplotlib",
],
},
**setuptools_kwargs
)

0 comments on commit 5d2d653

Please sign in to comment.