diff --git a/ipykernel/kernelapp.py b/ipykernel/kernelapp.py index 2e9edc0a6..9771c34dc 100644 --- a/ipykernel/kernelapp.py +++ b/ipykernel/kernelapp.py @@ -512,7 +512,7 @@ def init_gui_pylab(self): # but lower priority than anything else (mpl.use() for instance). # This only affects matplotlib >= 1.5 if not os.environ.get('MPLBACKEND'): - os.environ['MPLBACKEND'] = 'module://ipykernel.pylab.backend_inline' + os.environ['MPLBACKEND'] = 'module://matplotlib_inline.backend_inline' # Provide a wrapper for :meth:`InteractiveShellApp.init_gui_pylab` # to ensure that any exception is printed straight to stderr. diff --git a/ipykernel/pylab/backend_inline.py b/ipykernel/pylab/backend_inline.py index 36af62936..3b53c7629 100644 --- a/ipykernel/pylab/backend_inline.py +++ b/ipykernel/pylab/backend_inline.py @@ -3,245 +3,13 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -import matplotlib -from matplotlib.backends.backend_agg import ( - new_figure_manager, - FigureCanvasAgg, - new_figure_manager_given_figure, -) # analysis: ignore -from matplotlib import colors -from matplotlib._pylab_helpers import Gcf +import warnings -from IPython.core.getipython import get_ipython -from IPython.core.pylabtools import select_figure_formats -from IPython.display import display +from matplotlib_inline.backend_inline import * # analysis: ignore -from .config import InlineBackend - -def show(close=None, block=None): - """Show all figures as SVG/PNG payloads sent to the IPython clients. - - Parameters - ---------- - close : bool, optional - If true, a ``plt.close('all')`` call is automatically issued after - sending all the figures. If this is set, the figures will entirely - removed from the internal list of figures. - block : Not used. - The `block` parameter is a Matplotlib experimental parameter. - We accept it in the function signature for compatibility with other - backends. - """ - if close is None: - close = InlineBackend.instance().close_figures - try: - for figure_manager in Gcf.get_all_fig_managers(): - display( - figure_manager.canvas.figure, - metadata=_fetch_figure_metadata(figure_manager.canvas.figure) - ) - finally: - show._to_draw = [] - # only call close('all') if any to close - # close triggers gc.collect, which can be slow - if close and Gcf.get_all_fig_managers(): - matplotlib.pyplot.close('all') - - -# This flag will be reset by draw_if_interactive when called -show._draw_called = False -# list of figures to draw when flush_figures is called -show._to_draw = [] - - -def draw_if_interactive(): - """ - Is called after every pylab drawing command - """ - # signal that the current active figure should be sent at the end of - # execution. Also sets the _draw_called flag, signaling that there will be - # something to send. At the end of the code execution, a separate call to - # flush_figures() will act upon these values - manager = Gcf.get_active() - if manager is None: - return - fig = manager.canvas.figure - - # Hack: matplotlib FigureManager objects in interacive backends (at least - # in some of them) monkeypatch the figure object and add a .show() method - # to it. This applies the same monkeypatch in order to support user code - # that might expect `.show()` to be part of the official API of figure - # objects. - # For further reference: - # https://github.com/ipython/ipython/issues/1612 - # https://github.com/matplotlib/matplotlib/issues/835 - - if not hasattr(fig, 'show'): - # Queue up `fig` for display - fig.show = lambda *a: display(fig, metadata=_fetch_figure_metadata(fig)) - - # If matplotlib was manually set to non-interactive mode, this function - # should be a no-op (otherwise we'll generate duplicate plots, since a user - # who set ioff() manually expects to make separate draw/show calls). - if not matplotlib.is_interactive(): - return - - # ensure current figure will be drawn, and each subsequent call - # of draw_if_interactive() moves the active figure to ensure it is - # drawn last - try: - show._to_draw.remove(fig) - except ValueError: - # ensure it only appears in the draw list once - pass - # Queue up the figure for drawing in next show() call - show._to_draw.append(fig) - show._draw_called = True - - -def flush_figures(): - """Send all figures that changed - - This is meant to be called automatically and will call show() if, during - prior code execution, there had been any calls to draw_if_interactive. - - This function is meant to be used as a post_execute callback in IPython, - so user-caused errors are handled with showtraceback() instead of being - allowed to raise. If this function is not called from within IPython, - then these exceptions will raise. - """ - if not show._draw_called: - return - - if InlineBackend.instance().close_figures: - # ignore the tracking, just draw and close all figures - try: - return show(True) - except Exception as e: - # safely show traceback if in IPython, else raise - ip = get_ipython() - if ip is None: - raise e - else: - ip.showtraceback() - return - try: - # exclude any figures that were closed: - active = set([fm.canvas.figure for fm in Gcf.get_all_fig_managers()]) - for fig in [ fig for fig in show._to_draw if fig in active ]: - try: - display(fig, metadata=_fetch_figure_metadata(fig)) - except Exception as e: - # safely show traceback if in IPython, else raise - ip = get_ipython() - if ip is None: - raise e - else: - ip.showtraceback() - return - finally: - # clear flags for next round - show._to_draw = [] - show._draw_called = False - - -# Changes to matplotlib in version 1.2 requires a mpl backend to supply a default -# figurecanvas. This is set here to a Agg canvas -# See https://github.com/matplotlib/matplotlib/pull/1125 -FigureCanvas = FigureCanvasAgg - - -def configure_inline_support(shell, backend): - """Configure an IPython shell object for matplotlib use. - - Parameters - ---------- - shell : InteractiveShell instance - backend : matplotlib backend - """ - # If using our svg payload backend, register the post-execution - # function that will pick up the results for display. This can only be - # done with access to the real shell object. - - cfg = InlineBackend.instance(parent=shell) - cfg.shell = shell - if cfg not in shell.configurables: - shell.configurables.append(cfg) - - if backend == 'module://ipykernel.pylab.backend_inline': - shell.events.register('post_execute', flush_figures) - - # Save rcParams that will be overwrittern - shell._saved_rcParams = {} - for k in cfg.rc: - shell._saved_rcParams[k] = matplotlib.rcParams[k] - # load inline_rc - matplotlib.rcParams.update(cfg.rc) - new_backend_name = "inline" - else: - try: - shell.events.unregister('post_execute', flush_figures) - except ValueError: - pass - if hasattr(shell, '_saved_rcParams'): - matplotlib.rcParams.update(shell._saved_rcParams) - del shell._saved_rcParams - new_backend_name = "other" - - # only enable the formats once -> don't change the enabled formats (which the user may - # has changed) when getting another "%matplotlib inline" call. - # See https://github.com/ipython/ipykernel/issues/29 - cur_backend = getattr(configure_inline_support, "current_backend", "unset") - if new_backend_name != cur_backend: - # Setup the default figure format - select_figure_formats(shell, cfg.figure_formats, **cfg.print_figure_kwargs) - configure_inline_support.current_backend = new_backend_name - - -def _enable_matplotlib_integration(): - """Enable extra IPython matplotlib integration when we are loaded as the matplotlib backend.""" - from matplotlib import get_backend - ip = get_ipython() - backend = get_backend() - if ip and backend == 'module://%s' % __name__: - from IPython.core.pylabtools import activate_matplotlib - try: - activate_matplotlib(backend) - configure_inline_support(ip, backend) - except (ImportError, AttributeError): - # bugs may cause a circular import on Python 2 - def configure_once(*args): - activate_matplotlib(backend) - configure_inline_support(ip, backend) - ip.events.unregister('post_run_cell', configure_once) - ip.events.register('post_run_cell', configure_once) - -_enable_matplotlib_integration() - -def _fetch_figure_metadata(fig): - """Get some metadata to help with displaying a figure.""" - # determine if a background is needed for legibility - if _is_transparent(fig.get_facecolor()): - # the background is transparent - ticksLight = _is_light([label.get_color() - for axes in fig.axes - for axis in (axes.xaxis, axes.yaxis) - for label in axis.get_ticklabels()]) - if ticksLight.size and (ticksLight == ticksLight[0]).all(): - # there are one or more tick labels, all with the same lightness - return {'needs_background': 'dark' if ticksLight[0] else 'light'} - - return None - -def _is_light(color): - """Determines if a color (or each of a sequence of colors) is light (as - opposed to dark). Based on ITU BT.601 luminance formula (see - https://stackoverflow.com/a/596241).""" - rgbaArr = colors.to_rgba_array(color) - return rgbaArr[:,:3].dot((.299, .587, .114)) > .5 - -def _is_transparent(color): - """Determine transparency from alpha.""" - rgba = colors.to_rgba(color) - return rgba[3] < .5 +warnings.warn( + "`ipykernel.pylab.backend_inline` is deprecated, directly " + "use `matplotlib_inline.backend_inline`", + DeprecationWarning +) diff --git a/ipykernel/pylab/config.py b/ipykernel/pylab/config.py index 249389fab..7846cb89e 100644 --- a/ipykernel/pylab/config.py +++ b/ipykernel/pylab/config.py @@ -2,109 +2,14 @@ This module does not import anything from matplotlib. """ -#----------------------------------------------------------------------------- -# Copyright (C) 2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +import warnings -from traitlets.config.configurable import SingletonConfigurable -from traitlets import ( - Dict, Instance, Set, Bool, TraitError, Unicode -) - -#----------------------------------------------------------------------------- -# Configurable for inline backend options -#----------------------------------------------------------------------------- - -def pil_available(): - """Test if PIL/Pillow is available""" - out = False - try: - from PIL import Image - out = True - except: - pass - return out - -# inherit from InlineBackendConfig for deprecation purposes -class InlineBackendConfig(SingletonConfigurable): - pass - -class InlineBackend(InlineBackendConfig): - """An object to store configuration of the inline backend.""" - - # The typical default figure size is too large for inline use, - # so we shrink the figure size to 6x4, and tweak fonts to - # make that fit. - rc = Dict({'figure.figsize': (6.0,4.0), - # play nicely with white background in the Qt and notebook frontend - 'figure.facecolor': (1,1,1,0), - 'figure.edgecolor': (1,1,1,0), - # 12pt labels get cutoff on 6x4 logplots, so use 10pt. - 'font.size': 10, - # 72 dpi matches SVG/qtconsole - # this only affects PNG export, as SVG has no dpi setting - 'figure.dpi': 72, - # 10pt still needs a little more room on the xlabel: - 'figure.subplot.bottom' : .125 - }, - help="""Subset of matplotlib rcParams that should be different for the - inline backend.""" - ).tag(config=True) - - figure_formats = Set({'png'}, - help="""A set of figure formats to enable: 'png', - 'retina', 'jpeg', 'svg', 'pdf'.""").tag(config=True) - - def _update_figure_formatters(self): - if self.shell is not None: - from IPython.core.pylabtools import select_figure_formats - select_figure_formats(self.shell, self.figure_formats, **self.print_figure_kwargs) +from matplotlib_inline.config import * # analysis: ignore - def _figure_formats_changed(self, name, old, new): - if 'jpg' in new or 'jpeg' in new: - if not pil_available(): - raise TraitError("Requires PIL/Pillow for JPG figures") - self._update_figure_formatters() - - figure_format = Unicode(help="""The figure format to enable (deprecated - use `figure_formats` instead)""").tag(config=True) - - def _figure_format_changed(self, name, old, new): - if new: - self.figure_formats = {new} - - print_figure_kwargs = Dict({'bbox_inches' : 'tight'}, - help="""Extra kwargs to be passed to fig.canvas.print_figure. - - Logical examples include: bbox_inches, quality (for jpeg figures), etc. - """ - ).tag(config=True) - _print_figure_kwargs_changed = _update_figure_formatters - - close_figures = Bool(True, - help="""Close all figures at the end of each cell. - - When True, ensures that each cell starts with no active figures, but it - also means that one must keep track of references in order to edit or - redraw figures in subsequent cells. This mode is ideal for the notebook, - where residual plots from other cells might be surprising. - - When False, one must call figure() to create new figures. This means - that gcf() and getfigs() can reference figures created in other cells, - and the active figure can continue to be edited with pylab/pyplot - methods that reference the current active figure. This mode facilitates - iterative editing of figures, and behaves most consistently with - other matplotlib backends, but figure barriers between cells must - be explicit. - """).tag(config=True) - - shell = Instance('IPython.core.interactiveshell.InteractiveShellABC', - allow_none=True) +warnings.warn( + "`ipykernel.pylab.config` is deprecated, directly " + "use `matplotlib_inline.config`", + DeprecationWarning +) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 14b6feaed..f41bcfb70 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -621,15 +621,6 @@ def init_magics(self): self.register_magics(KernelMagics) self.magics_manager.register_alias('ed', 'edit') - def enable_matplotlib(self, gui=None): - gui, backend = super(ZMQInteractiveShell, self).enable_matplotlib(gui) - - from ipykernel.pylab.backend_inline import configure_inline_support - - configure_inline_support(self, backend) - - return gui, backend - def init_virtualenv(self): # Overridden not to do virtualenv detection, because it's probably # not appropriate in a kernel. To use a kernel in a virtualenv, install diff --git a/setup.py b/setup.py index efb25c035..b84d58bf1 100644 --- a/setup.py +++ b/setup.py @@ -76,10 +76,11 @@ def run(self): install_requires=[ 'importlib-metadata<4;python_version<"3.8.0"', 'debugpy>=1.0.0', - 'ipython>=7.21.0', + 'ipython>=7.23.0', 'traitlets>=4.1.0', 'jupyter_client', 'tornado>=4.2', + 'matplotlib-inline>=0.1.0,<0.2.0' 'appnope;platform_system=="Darwin"', ], extras_require={