Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Move Matplotlib backend mapping to Matplotlib #14371

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 8 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ jobs:
- os: macos-latest
python-version: "pypy-3.10"
deps: test
# Temporary CI run to use entry point compatible code in matplotlib-inline.
- os: ubuntu-latest
python-version: "3.12"
deps: test_extra
want-latest-entry-point-code: true

steps:
- uses: actions/checkout@v3
Expand Down Expand Up @@ -82,6 +87,16 @@ jobs:
- name: Check manifest
if: runner.os != 'Windows' # setup.py does not support sdist on Windows
run: check-manifest

- name: Install entry point compatible code (TEMPORARY)
if: matrix.want-latest-entry-point-code
run: |
python -m pip list
# Not installing matplotlib's entry point code as building matplotlib from source is complex.
# Rely upon matplotlib to test all the latest entry point branches together.
python -m pip install --upgrade git+https://github.com/ipython/matplotlib-inline.git@main
python -m pip list

- name: pytest
env:
COLUMNS: 120
Expand Down
2 changes: 1 addition & 1 deletion IPython/core/display_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def display(
display_id=None,
raw=False,
clear=False,
**kwargs
**kwargs,
):
"""Display a Python object in all frontends.

Expand Down
2 changes: 1 addition & 1 deletion IPython/core/interactiveshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -3657,7 +3657,7 @@ def enable_matplotlib(self, gui=None):
from IPython.core import pylabtools as pt
gui, backend = pt.find_gui_and_backend(gui, self.pylab_gui_select)

if gui != 'inline':
if gui != None:
# If we have our first gui selection, store it
if self.pylab_gui_select is None:
self.pylab_gui_select = gui
Expand Down
16 changes: 10 additions & 6 deletions IPython/core/magics/pylab.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@
from IPython.core.magic import Magics, magics_class, line_magic
from IPython.testing.skipdoctest import skip_doctest
from warnings import warn
from IPython.core.pylabtools import backends

#-----------------------------------------------------------------------------
# Magic implementation classes
#-----------------------------------------------------------------------------

magic_gui_arg = magic_arguments.argument(
'gui', nargs='?',
help="""Name of the matplotlib backend to use %s.
"gui",
nargs="?",
help="""Name of the matplotlib backend to use such as 'qt' or 'widget'.
If given, the corresponding matplotlib backend is used,
otherwise it will be matplotlib's default
(which you can set in your matplotlib config file).
""" % str(tuple(sorted(backends.keys())))
""",
)


Expand Down Expand Up @@ -93,8 +93,12 @@ def matplotlib(self, line=''):
"""
args = magic_arguments.parse_argstring(self.matplotlib, line)
if args.list:
backends_list = list(backends.keys())
print("Available matplotlib backends: %s" % backends_list)
from IPython.core.pylabtools import _list_matplotlib_backends_and_gui_loops

print(
"Available matplotlib backends: %s"
% _list_matplotlib_backends_and_gui_loops()
)
else:
gui, backend = self.shell.enable_matplotlib(args.gui.lower() if isinstance(args.gui, str) else args.gui)
self._show_matplotlib_backend(args.gui, backend)
Expand Down
127 changes: 106 additions & 21 deletions IPython/core/pylabtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
from IPython.core.display import _pngxy
from IPython.utils.decorators import flag_calls

# If user specifies a GUI, that dictates the backend, otherwise we read the
# user's mpl default from the mpl rc structure
backends = {

# Matplotlib backend resolution functionality moved from IPython to Matplotlib
# in IPython 8.24 and Matplotlib 3.9.1. Need to keep `backends` and `backend2gui`
# here for earlier Matplotlib and for external backend libraries such as
# mplcairo that might rely upon it.
_deprecated_backends = {
"tk": "TkAgg",
"gtk": "GTKAgg",
"gtk3": "GTK3Agg",
Expand All @@ -41,29 +44,44 @@
# GUI support to activate based on the desired matplotlib backend. For the
# most part it's just a reverse of the above dict, but we also need to add a
# few others that map to the same GUI manually:
backend2gui = dict(zip(backends.values(), backends.keys()))
_deprecated_backend2gui = dict(
zip(_deprecated_backends.values(), _deprecated_backends.keys())
)
# In the reverse mapping, there are a few extra valid matplotlib backends that
# map to the same GUI support
backend2gui["GTK"] = backend2gui["GTKCairo"] = "gtk"
backend2gui["GTK3Cairo"] = "gtk3"
backend2gui["GTK4Cairo"] = "gtk4"
backend2gui["WX"] = "wx"
backend2gui["CocoaAgg"] = "osx"
_deprecated_backend2gui["GTK"] = _deprecated_backend2gui["GTKCairo"] = "gtk"
_deprecated_backend2gui["GTK3Cairo"] = "gtk3"
_deprecated_backend2gui["GTK4Cairo"] = "gtk4"
_deprecated_backend2gui["WX"] = "wx"
_deprecated_backend2gui["CocoaAgg"] = "osx"
# There needs to be a hysteresis here as the new QtAgg Matplotlib backend
# supports either Qt5 or Qt6 and the IPython qt event loop support Qt4, Qt5,
# and Qt6.
backend2gui["QtAgg"] = "qt"
backend2gui["Qt4Agg"] = "qt4"
backend2gui["Qt5Agg"] = "qt5"
_deprecated_backend2gui["QtAgg"] = "qt"
_deprecated_backend2gui["Qt4Agg"] = "qt4"
_deprecated_backend2gui["Qt5Agg"] = "qt5"

# And some backends that don't need GUI integration
del backend2gui["nbAgg"]
del backend2gui["agg"]
del backend2gui["svg"]
del backend2gui["pdf"]
del backend2gui["ps"]
del backend2gui["module://matplotlib_inline.backend_inline"]
del backend2gui["module://ipympl.backend_nbagg"]
del _deprecated_backend2gui["nbAgg"]
del _deprecated_backend2gui["agg"]
del _deprecated_backend2gui["svg"]
del _deprecated_backend2gui["pdf"]
del _deprecated_backend2gui["ps"]
del _deprecated_backend2gui["module://matplotlib_inline.backend_inline"]
del _deprecated_backend2gui["module://ipympl.backend_nbagg"]


# Deprecated attributes backends and backend2gui mostly following PEP 562.
def __getattr__(name):
if name in ("backends", "backend2gui"):
warnings.warn(
f"{name} is deprecated since IPython 8.24, backends are managed "
"in matplotlib and can be externally registered.",
DeprecationWarning,
)
return globals()[f"_deprecated_{name}"]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


#-----------------------------------------------------------------------------
# Matplotlib utilities
Expand Down Expand Up @@ -267,7 +285,7 @@ def select_figure_formats(shell, formats, **kwargs):

[ f.pop(Figure, None) for f in shell.display_formatter.formatters.values() ]
mplbackend = matplotlib.get_backend().lower()
if mplbackend == 'nbagg' or mplbackend == 'module://ipympl.backend_nbagg':
if mplbackend in ("nbagg", "ipympl", "widget", "module://ipympl.backend_nbagg"):
formatter = shell.display_formatter.ipython_display_formatter
formatter.for_type(Figure, _reshow_nbagg_figure)

Expand Down Expand Up @@ -319,7 +337,23 @@ def find_gui_and_backend(gui=None, gui_select=None):

import matplotlib

has_unified_qt_backend = getattr(matplotlib, "__version_info__", (0, 0)) >= (3, 5)
if _matplotlib_manages_backends():
backend_registry = matplotlib.backends.registry.backend_registry
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that need explicit import of matplotlib.backends ? and/or other hasattr checks ?

In [1]: import matplotlib

In [2]: matplotlib.backends
...
AttributeError: module 'matplotlib' has no attribute 'backends'

In [3]: import matplotlib.backends

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _matplotlib_manages_backends() call checks that the version of Matplotlib is late enough to include the required backed_registry functions, so the import should always work. But now you have drawn my attention to this I realise it would be better to be explicit and use hasattr checks for backend_registry and its function resolve_gui_or_backend. This would avoid us having to have a hardcoded Matplotlib version and would allow us to merge this work before we are entirely sure what version of Matplotlib will include the corresponding changes.


# gui argument may be a gui event loop or may be a backend name.
if gui in ("auto", None):
backend = matplotlib.rcParamsOrig["backend"]
backend, gui = backend_registry.resolve_backend(backend)
else:
backend, gui = backend_registry.resolve_gui_or_backend(gui)

return gui, backend

# Fallback to previous behaviour (Matplotlib < 3.9)
mpl_version_info = getattr(matplotlib, "__version_info__", (0, 0))
has_unified_qt_backend = mpl_version_info >= (3, 5)

from IPython.core.pylabtools import backends

backends_ = dict(backends)
if not has_unified_qt_backend:
Expand All @@ -338,6 +372,7 @@ def find_gui_and_backend(gui=None, gui_select=None):
backend = matplotlib.rcParamsOrig['backend']
# In this case, we need to find what the appropriate gui selection call
# should be for IPython, so we can activate inputhook accordingly
from IPython.core.pylabtools import backend2gui
gui = backend2gui.get(backend, None)

# If we have already had a gui active, we need it and inline are the
Expand All @@ -346,6 +381,11 @@ def find_gui_and_backend(gui=None, gui_select=None):
gui = gui_select
backend = backends_[gui]

# Matplotlib before _matplotlib_manages_backends() can return "inline" for
# no gui event loop rather than the None that IPython >= 8.24.0 expects.
if gui == "inline":
gui = None

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we emit a warning here, to make sure that things like backend_to_gui never return "inline" or similar ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only get to this code if we are using "old" Matplotlib (i.e. before the backend_registry changes), in which case we will have gui == "inline" for the inline backend. There will already have been a DeprecationWarning issued for accessing backend2gui (a few lines above) and a second warning here might be more confusing than helpful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add an extra comment in the code here though.

return gui, backend


Expand Down Expand Up @@ -431,3 +471,48 @@ def configure_inline_support(shell, backend):
)

configure_inline_support_orig(shell, backend)


# Determine if Matplotlib manages backends only if needed, and cache result.
# Do not read this directly, instead use _matplotlib_manages_backends().
_matplotlib_manages_backends_value: bool | None = None


def _matplotlib_manages_backends() -> bool:
"""Return True if Matplotlib manages backends, False otherwise.

If it returns True, the caller can be sure that
matplotlib.backends.registry.backend_registry is available along with
member functions resolve_gui_or_backend, resolve_backend, list_all, and
list_gui_frameworks.
"""
global _matplotlib_manages_backends_value
if _matplotlib_manages_backends_value is None:
try:
from matplotlib.backends.registry import backend_registry

_matplotlib_manages_backends_value = hasattr(
backend_registry, "resolve_gui_or_backend"
)
except ImportError:
_matplotlib_manages_backends_value = False

return _matplotlib_manages_backends_value


def _list_matplotlib_backends_and_gui_loops() -> list[str]:
"""Return list of all Matplotlib backends and GUI event loops.

This is the list returned by
%matplotlib --list
"""
if _matplotlib_manages_backends():
from matplotlib.backends.registry import backend_registry

ret = backend_registry.list_all() + backend_registry.list_gui_frameworks()
else:
from IPython.core import pylabtools

ret = list(pylabtools.backends.keys())

return sorted(["auto"] + ret)
Loading