diff --git a/.gitignore b/.gitignore index 94be9995..d36f9bee 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ js/*.tgz # OS X .DS_Store + +# vscode +.vscode/ diff --git a/ipympl/backend_nbagg.py b/ipympl/backend_nbagg.py index 99e12a87..d665f218 100644 --- a/ipympl/backend_nbagg.py +++ b/ipympl/backend_nbagg.py @@ -13,13 +13,13 @@ ) from matplotlib import rcParams -from matplotlib.figure import Figure from matplotlib import is_interactive from matplotlib.backends.backend_webagg_core import (FigureManagerWebAgg, FigureCanvasWebAggCore, NavigationToolbar2WebAgg, TimerTornado) -from matplotlib.backend_bases import ShowBase, NavigationToolbar2, cursors +from matplotlib.backend_bases import NavigationToolbar2, cursors, _Backend +from matplotlib._pylab_helpers import Gcf from ._version import js_semver @@ -32,43 +32,6 @@ } -class Show(ShowBase): - - def __call__(self, block=None): - from matplotlib._pylab_helpers import Gcf - - managers = Gcf.get_all_fig_managers() - if not managers: - return - - interactive = is_interactive() - - for manager in managers: - manager.show() - - # plt.figure adds an event which puts the figure in focus - # in the activeQue. Disable this behaviour, as it results in - # figures being put as the active figure after they have been - # shown, even in non-interactive mode. - if hasattr(manager, '_cidgcf'): - manager.canvas.mpl_disconnect(manager._cidgcf) - - if not interactive and manager in Gcf._activeQue: - Gcf._activeQue.remove(manager) - - -show = Show() - - -def draw_if_interactive(): - import matplotlib._pylab_helpers as pylab_helpers - - if is_interactive(): - manager = pylab_helpers.Gcf.get_active() - if manager is not None: - manager.show() - - def connection_info(): """ Return a string showing the figure and connection status for @@ -260,33 +223,47 @@ def destroy(self): self.canvas.close() -def new_figure_manager(num, *args, **kwargs): - """ - Create a new figure manager instance - """ - figure_class = kwargs.pop('FigureClass', Figure) - this_fig = figure_class(*args, **kwargs) - return new_figure_manager_given_figure(num, this_fig) +@_Backend.export +class _Backend_ipympl(_Backend): + FigureCanvas = Canvas + FigureManager = FigureManager + @staticmethod + def new_figure_manager_given_figure(num, figure): + canvas = Canvas(figure) + if 'nbagg.transparent' in rcParams and rcParams['nbagg.transparent']: + figure.patch.set_alpha(0) + manager = FigureManager(canvas, num) + if is_interactive(): + manager.show() + figure.canvas.draw_idle() -def new_figure_manager_given_figure(num, figure): - """ - Create a new figure manager instance for the given figure. - """ - from matplotlib._pylab_helpers import Gcf + def destroy(event): + canvas.mpl_disconnect(cid) + Gcf.destroy(manager) - def closer(event): - Gcf.destroy(num) + cid = canvas.mpl_connect('close_event', destroy) + return manager - canvas = Canvas(figure) - if 'nbagg.transparent' in rcParams and rcParams['nbagg.transparent']: - figure.patch.set_alpha(0) - manager = FigureManager(canvas, num) + @staticmethod + def show(block=None): + # TODO: something to do when keyword block==False ? - if is_interactive(): - manager.show() - figure.canvas.draw_idle() + managers = Gcf.get_all_fig_managers() + if not managers: + return + + interactive = is_interactive() + + for manager in managers: + manager.show() - canvas.mpl_connect('close_event', closer) + # plt.figure adds an event which makes the figure in focus the + # active one. Disable this behaviour, as it results in + # figures being put as the active figure after they have been + # shown, even in non-interactive mode. + if hasattr(manager, '_cidgcf'): + manager.canvas.mpl_disconnect(manager._cidgcf) - return manager + if not interactive: + Gcf.figs.pop(manager.num, None) diff --git a/pyproject.toml b/pyproject.toml index 7a106586..498efe4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,15 @@ [build-system] requires = ["jupyter_packaging~=0.7.0", "jupyterlab>=3.0.0,==3.*", "setuptools>=40.8.0", "wheel"] build-backend = "setuptools.build_meta" + + +[tool.pytest.ini_options] +testpaths = [ + "test-notebooks", + "examples", +] +norecursedirs = [ + "node_modules", + ".ipynb_checkpoints", +] +addopts = "--nbval --current-env" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 24ff9b7b..00000000 --- a/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -testpaths = examples -norecursedirs = node_modules .ipynb_checkpoints -addopts = --nbval --current-env diff --git a/test-notebooks/UAT.ipynb b/test-notebooks/UAT.ipynb new file mode 100644 index 00000000..40e2eb98 --- /dev/null +++ b/test-notebooks/UAT.ipynb @@ -0,0 +1,92 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "antique-treasure", + "metadata": {}, + "source": [ + "# User Automated Testing\n", + "\n", + "A notebook to run manually to catch issues with the backend." + ] + }, + { + "cell_type": "markdown", + "id": "binary-speaker", + "metadata": {}, + "source": [ + "# 1. Checking plt.show()\n", + "\n", + "The implementation of plt.show relies on the private `matplotlib._pylab_helpers.Gcf` which can break (https://github.com/matplotlib/ipympl/issues/303). The cell should run without any errors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "agricultural-whole", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib ipympl\n", + "import matplotlib.pyplot as plt\n", + "plt.ioff()\n", + "fig = plt.figure()\n", + "plt.plot([0,1],[0,1])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "secure-consistency", + "metadata": {}, + "source": [ + "# 2 plt.close()\n", + "\n", + "Calling `plt.close()` should prevent further interactions with the figure - so panning and resizing should no longer work after the second cell in this test." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hollow-front", + "metadata": {}, + "outputs": [], + "source": [ + "plt.ion()\n", + "plt.figure()\n", + "plt.plot([0,1],[0,1])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "classical-pavilion", + "metadata": {}, + "outputs": [], + "source": [ + "plt.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}