Skip to content

Commit e9a60bb

Browse files
TomAugspurgerjreback
authored andcommitted
API: Add entrypoint for plotting (#27488)
1 parent 8993fac commit e9a60bb

File tree

7 files changed

+134
-7
lines changed

7 files changed

+134
-7
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ lint-diff:
1515
git diff upstream/master --name-only -- "*.py" | xargs flake8
1616

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

2020
develop: build
2121
python setup.py develop

ci/code_checks.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ if [[ -z "$CHECK" || "$CHECK" == "lint" ]]; then
5656
black --version
5757

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

6262
# `setup.cfg` contains the list of error codes that are being ignored in flake8

doc/source/development/extending.rst

+17
Original file line numberDiff line numberDiff line change
@@ -441,5 +441,22 @@ This would be more or less equivalent to:
441441
The backend module can then use other visualization tools (Bokeh, Altair,...)
442442
to generate the plots.
443443

444+
Libraries implementing the plotting backend should use `entry points <https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins>`__
445+
to make their backend discoverable to pandas. The key is ``"pandas_plotting_backends"``. For example, pandas
446+
registers the default "matplotlib" backend as follows.
447+
448+
.. code-block:: python
449+
450+
# in setup.py
451+
setup( # noqa: F821
452+
...,
453+
entry_points={
454+
"pandas_plotting_backends": [
455+
"matplotlib = pandas:plotting._matplotlib",
456+
],
457+
},
458+
)
459+
460+
444461
More information on how to implement a third-party plotting backend can be found at
445462
https://github.com/pandas-dev/pandas/blob/master/pandas/plotting/__init__.py#L1.

doc/source/whatsnew/v0.25.1.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ I/O
114114
Plotting
115115
^^^^^^^^
116116

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

pandas/plotting/_core.py

+62-4
Original file line numberDiff line numberDiff line change
@@ -1533,6 +1533,53 @@ def hexbin(self, x, y, C=None, reduce_C_function=None, gridsize=None, **kwargs):
15331533
return self(kind="hexbin", x=x, y=y, C=C, **kwargs)
15341534

15351535

1536+
_backends = {}
1537+
1538+
1539+
def _find_backend(backend: str):
1540+
"""
1541+
Find a pandas plotting backend>
1542+
1543+
Parameters
1544+
----------
1545+
backend : str
1546+
The identifier for the backend. Either an entrypoint item registered
1547+
with pkg_resources, or a module name.
1548+
1549+
Notes
1550+
-----
1551+
Modifies _backends with imported backends as a side effect.
1552+
1553+
Returns
1554+
-------
1555+
types.ModuleType
1556+
The imported backend.
1557+
"""
1558+
import pkg_resources # Delay import for performance.
1559+
1560+
for entry_point in pkg_resources.iter_entry_points("pandas_plotting_backends"):
1561+
if entry_point.name == "matplotlib":
1562+
# matplotlib is an optional dependency. When
1563+
# missing, this would raise.
1564+
continue
1565+
_backends[entry_point.name] = entry_point.load()
1566+
1567+
try:
1568+
return _backends[backend]
1569+
except KeyError:
1570+
# Fall back to unregisted, module name approach.
1571+
try:
1572+
module = importlib.import_module(backend)
1573+
except ImportError:
1574+
# We re-raise later on.
1575+
pass
1576+
else:
1577+
_backends[backend] = module
1578+
return module
1579+
1580+
raise ValueError("No backend {}".format(backend))
1581+
1582+
15361583
def _get_plot_backend(backend=None):
15371584
"""
15381585
Return the plotting backend to use (e.g. `pandas.plotting._matplotlib`).
@@ -1546,7 +1593,18 @@ def _get_plot_backend(backend=None):
15461593
The backend is imported lazily, as matplotlib is a soft dependency, and
15471594
pandas can be used without it being installed.
15481595
"""
1549-
backend_str = backend or pandas.get_option("plotting.backend")
1550-
if backend_str == "matplotlib":
1551-
backend_str = "pandas.plotting._matplotlib"
1552-
return importlib.import_module(backend_str)
1596+
backend = backend or pandas.get_option("plotting.backend")
1597+
1598+
if backend == "matplotlib":
1599+
# Because matplotlib is an optional dependency and first-party backend,
1600+
# we need to attempt an import here to raise an ImportError if needed.
1601+
import pandas.plotting._matplotlib as module
1602+
1603+
_backends["matplotlib"] = module
1604+
1605+
if backend in _backends:
1606+
return _backends[backend]
1607+
1608+
module = _find_backend(backend)
1609+
_backends[backend] = module
1610+
return module

pandas/tests/plotting/test_backend.py

+47
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
import sys
2+
import types
3+
4+
import pkg_resources
15
import pytest
26

7+
import pandas.util._test_decorators as td
8+
39
import pandas
410

511

@@ -36,3 +42,44 @@ def test_backend_is_correct(monkeypatch):
3642
pandas.set_option("plotting.backend", "matplotlib")
3743
except ImportError:
3844
pass
45+
46+
47+
@td.skip_if_no_mpl
48+
def test_register_entrypoint():
49+
mod = types.ModuleType("my_backend")
50+
mod.plot = lambda *args, **kwargs: 1
51+
52+
backends = pkg_resources.get_entry_map("pandas")
53+
my_entrypoint = pkg_resources.EntryPoint(
54+
"pandas_plotting_backend",
55+
mod.__name__,
56+
dist=pkg_resources.get_distribution("pandas"),
57+
)
58+
backends["pandas_plotting_backends"]["my_backend"] = my_entrypoint
59+
# TODO: the docs recommend importlib.util.module_from_spec. But this works for now.
60+
sys.modules["my_backend"] = mod
61+
62+
result = pandas.plotting._core._get_plot_backend("my_backend")
63+
assert result is mod
64+
65+
# TODO: https://github.com/pandas-dev/pandas/issues/27517
66+
# Remove the td.skip_if_no_mpl
67+
with pandas.option_context("plotting.backend", "my_backend"):
68+
result = pandas.plotting._core._get_plot_backend()
69+
70+
assert result is mod
71+
72+
73+
def test_register_import():
74+
mod = types.ModuleType("my_backend2")
75+
mod.plot = lambda *args, **kwargs: 1
76+
sys.modules["my_backend2"] = mod
77+
78+
result = pandas.plotting._core._get_plot_backend("my_backend2")
79+
assert result is mod
80+
81+
82+
@td.skip_if_mpl
83+
def test_no_matplotlib_ok():
84+
with pytest.raises(ImportError):
85+
pandas.plotting._core._get_plot_backend("matplotlib")

setup.py

+5
Original file line numberDiff line numberDiff line change
@@ -830,5 +830,10 @@ def srcpath(name=None, suffix=".pyx", subdir="src"):
830830
"hypothesis>=3.58",
831831
]
832832
},
833+
entry_points={
834+
"pandas_plotting_backends": [
835+
"matplotlib = pandas:plotting._matplotlib",
836+
],
837+
},
833838
**setuptools_kwargs
834839
)

0 commit comments

Comments
 (0)