From 9574947a24e1542064335ca9672db27cd6d60f39 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 15:06:45 -0400 Subject: [PATCH 01/20] DOC: tweak sidebar in theme --- docs/source/conf.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index d94b651..410d456 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -51,6 +51,7 @@ "matplotlib.sphinxext.plot_directive", "numpydoc", "sphinx_copybutton", + 'sphinx_design', ] # Configuration options for plot_directive. See: @@ -129,6 +130,9 @@ # and CI builds https://github.com/pydata/pydata-sphinx-theme/pull/386 "collapse_navigation": not is_release_build, "show_prev_next": False, + "navigation_with_keys": False, + # "secondary_sidebar_items": "page-toc.html", + "footer_start": ["copyright", "sphinx-version", "doc_version"], } include_analytics = is_release_build if include_analytics: @@ -153,9 +157,6 @@ # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { - "**": [ - "relations.html", # needs 'show_related': True theme option to display - ] } From edafd11a02e332307bd793e04ba6b95eaaf0620b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 15:07:14 -0400 Subject: [PATCH 02/20] API: remove select_gui_toolkit from mpl_gui.registry Use the top level one still, no need to put it in multiple places --- mpl_gui/registry.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mpl_gui/registry.py b/mpl_gui/registry.py index e8919b2..e3958b3 100644 --- a/mpl_gui/registry.py +++ b/mpl_gui/registry.py @@ -1,8 +1,11 @@ """Reproduces the module-level pyplot UX for Figure management.""" from . import FigureRegistry as _FigureRegistry -from ._manage_backend import select_gui_toolkit -from ._manage_interactive import ion, ioff, is_interactive +from ._manage_interactive import ( + ion as ion, + ioff as ioff, + is_interactive as is_interactive, +) _fr = _FigureRegistry() @@ -32,7 +35,6 @@ def get_fignums(): # if one must. `from foo import *` is a language miss-feature, but provide # sensible behavior anyway. __all__ = _fr_exports + [ - "select_gui_toolkit", "ion", "ioff", "is_interactive", From d929395141787d61c2f756053970eefd6ec58ad9 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 15:08:36 -0400 Subject: [PATCH 03/20] API: remove helper fabrication functions from top-level --- docs/source/api.rst | 18 ------------------ mpl_gui/__init__.py | 27 ++++++++++++++++----------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index b3f007d..025d61c 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -28,24 +28,6 @@ Interactivity Figure Fabrication ------------------ -Un-managed -++++++++++ - - -.. autosummary:: - :toctree: _as_gen - :nosignatures: - - mpl_gui.figure - mpl_gui.subplots - mpl_gui.subplot_mosaic - - -.. autosummary:: - :toctree: _as_gen - :nosignatures: - - mpl_gui.promote_figure Managed +++++++ diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index 999a216..3108c9e 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -25,8 +25,13 @@ from ._manage_interactive import ion, ioff, is_interactive # noqa: F401 from ._manage_backend import select_gui_toolkit # noqa: F401 from ._manage_backend import current_backend_module as _cbm -from ._promotion import promote_figure as promote_figure -from ._creation import figure, subplots, subplot_mosaic # noqa: F401 +from ._promotion import promote_figure as _promote_figure +from ._creation import ( + figure as _figure, + subplots as _subplots, + subplot_mosaic as _subplot_mosaic, +) + from ._version import get_versions @@ -70,7 +75,7 @@ def show(figs, *, block=None, timeout=0): if fig.canvas.manager is not None: managers.append(fig.canvas.manager) else: - managers.append(promote_figure(fig, num=None)) + managers.append(_promote_figure(fig, num=None)) if block is None: block = not is_interactive() @@ -151,7 +156,7 @@ def registry_cleanup(fig_wr): fig.set_label(f"{self._prefix}{fignum:d}") self._fig_to_number[fig] = fignum if is_interactive(): - promote_figure(fig, num=fignum) + _promote_figure(fig, num=fignum) return fig @property @@ -183,25 +188,25 @@ def by_number(self): self._ensure_all_figures_promoted() return {fig.canvas.manager.num: fig for fig in self.figures} - @functools.wraps(figure) + @functools.wraps(_figure) def figure(self, *args, **kwargs): - fig = figure(*args, **kwargs) + fig = _figure(*args, **kwargs) return self._register_fig(fig) - @functools.wraps(subplots) + @functools.wraps(_subplots) def subplots(self, *args, **kwargs): - fig, axs = subplots(*args, **kwargs) + fig, axs = _subplots(*args, **kwargs) return self._register_fig(fig), axs - @functools.wraps(subplot_mosaic) + @functools.wraps(_subplot_mosaic) def subplot_mosaic(self, *args, **kwargs): - fig, axd = subplot_mosaic(*args, **kwargs) + fig, axd = _subplot_mosaic(*args, **kwargs) return self._register_fig(fig), axd def _ensure_all_figures_promoted(self): for f in self.figures: if f.canvas.manager is None: - promote_figure(f, num=self._fig_to_number[f]) + _promote_figure(f, num=self._fig_to_number[f]) def show_all(self, *, block=None, timeout=None): """ From 8fd22355b0c820c7a5725350c6115fda881c841f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 15:35:00 -0400 Subject: [PATCH 04/20] DOC: add missing entry in docstring --- mpl_gui/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index 3108c9e..0736f7b 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -65,6 +65,9 @@ def show(figs, *, block=None, timeout=0): Defaults to True in non-interactive mode and to False in interactive mode (see `.is_interactive`). + timeout : float, optional + How long to run the event loop in msec if blocking. + """ # TODO handle single figure From 3130568e4fe1b36e69e5162ea54f1c0bab88c022 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 15:35:22 -0400 Subject: [PATCH 05/20] DOC: if xrefs --- mpl_gui/__init__.py | 2 +- mpl_gui/_manage_interactive.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index 0736f7b..40d54ee 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -260,7 +260,7 @@ def close_all(self): 4. drops its hard reference to the Figure If the user still holds a reference to the Figure it can be revived by - passing it to `show`. + passing it to `mpl_gui.show`. """ for fig in list(self.figures): diff --git a/mpl_gui/_manage_interactive.py b/mpl_gui/_manage_interactive.py index e66e682..fb55aa2 100644 --- a/mpl_gui/_manage_interactive.py +++ b/mpl_gui/_manage_interactive.py @@ -28,7 +28,7 @@ def is_interactive(): -------- ion : Enable interactive mode. ioff : Disable interactive mode. - show : Show all figures (and maybe block). + mpl_gui.show : Show all figures (and maybe block). """ return _is_interact() @@ -89,7 +89,7 @@ def ioff(): -------- ion : Enable interactive mode. is_interactive : Whether interactive mode is enabled. - show : Show all figures (and maybe block). + mpl_gui.show : Show all figures (and maybe block). Notes ----- @@ -124,7 +124,7 @@ def ion(): -------- ioff : Disable interactive mode. is_interactive : Whether interactive mode is enabled. - show : Show all figures (and maybe block). + mpl_gui.show : Show all figures (and maybe block). Notes ----- From 83f5d0105b94532e5871268ffc58415fd6eaf484 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 15:35:37 -0400 Subject: [PATCH 06/20] MNT: us alias imports to start down the path of adding typing --- mpl_gui/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index 40d54ee..fb101c1 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -22,8 +22,12 @@ from ._figure import Figure # noqa: F401 -from ._manage_interactive import ion, ioff, is_interactive # noqa: F401 -from ._manage_backend import select_gui_toolkit # noqa: F401 +from ._manage_interactive import ( # noqa: F401 + ion as ion, + ioff as ioff, + is_interactive as is_interactive, +) +from ._manage_backend import select_gui_toolkit as select_gui_toolkit # noqa: F401 from ._manage_backend import current_backend_module as _cbm from ._promotion import promote_figure as _promote_figure from ._creation import ( From deede04ef85675dfc2ad93e0f01ed6c40c7cffde Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 15:37:02 -0400 Subject: [PATCH 07/20] DOC: major re-write/re-organization of docs --- docs/source/api.rst | 111 +++++++++++++++++++--- docs/source/index.rst | 210 ++++++++++++++++++++++-------------------- 2 files changed, 208 insertions(+), 113 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 025d61c..4cc31f7 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,7 +1,19 @@ -mpl gui -======= +mpl gui API Reference +===================== + +.. automodule:: mpl_gui + :no-undoc-members: + + + +Select the backend +------------------ +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + mpl_gui.select_gui_toolkit -.. module:: mpl_gui Show ---- @@ -10,6 +22,7 @@ Show :toctree: _as_gen :nosignatures: + mpl_gui.show @@ -25,18 +38,21 @@ Interactivity mpl_gui.is_interactive -Figure Fabrication ------------------- +Locally Managed Figures +----------------------- -Managed -+++++++ +.. autoclass:: mpl_gui.FigureRegistry + :no-undoc-members: + :show-inheritance: -.. autoclass:: mpl_gui.FigureRegistry +.. autoclass:: mpl_gui.FigureContext :no-undoc-members: :show-inheritance: +Create Figures and Axes ++++++++++++++++++++++++ .. autosummary:: :toctree: _as_gen @@ -45,24 +61,93 @@ Managed mpl_gui.FigureRegistry.figure mpl_gui.FigureRegistry.subplots mpl_gui.FigureRegistry.subplot_mosaic + + +Access managed figures +++++++++++++++++++++++ + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + mpl_gui.FigureRegistry.by_label + mpl_gui.FigureRegistry.by_number + mpl_gui.FigureRegistry.figures + + + +Show and close managed Figures +++++++++++++++++++++++++++++++ + + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + mpl_gui.FigureRegistry.show_all mpl_gui.FigureRegistry.close_all + mpl_gui.FigureRegistry.show + mpl_gui.FigureRegistry.close -.. autoclass:: mpl_gui.FigureContext + + +Globally managed +---------------- + + +.. automodule:: mpl_gui.registry :no-undoc-members: - :show-inheritance: +Create Figures and Axes ++++++++++++++++++++++++ +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + mpl_gui.registry.figure + mpl_gui.registry.subplots + mpl_gui.registry.subplot_mosaic + + +Access managed figures +++++++++++++++++++++++ -Select the backend ------------------- .. autosummary:: :toctree: _as_gen :nosignatures: - mpl_gui.select_gui_toolkit + mpl_gui.registry.by_label + + +Show and close managed Figures +++++++++++++++++++++++++++++++ + + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + + + mpl_gui.registry.show + mpl_gui.registry.show_all + mpl_gui.registry.close_all + mpl_gui.registry.close + + +Interactivity ++++++++++++++ + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + + mpl_gui.registry.ion + mpl_gui.registry.ioff + mpl_gui.registry.is_interactive diff --git a/docs/source/index.rst b/docs/source/index.rst index 8844303..40dac47 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,9 +6,10 @@ ======================= mpl-gui Documentation ======================= +.. highlight:: python .. toctree:: - :maxdepth: 2 + :maxdepth: 1 api release_history @@ -30,18 +31,12 @@ The pyplot module current serves two critical, but unrelated functions: While it can be very convenient when working at the prompt, the state-full API can lead to brittle code that depends on the global state in confusing ways, particularly when used in library code. On the other hand, -``matplotlib.pyplot`` does a very good job of hiding from the user the fact +`matplotlib.pyplot` does a very good job of hiding from the user the fact that they are developing a GUI application and handling, along with IPython, many of the details involved in running a GUI application in parallel with Python. -Examples -======== - -.. highlight:: python - - If you want to be sure that this code does not secretly depend on pyplot run :: import sys @@ -51,155 +46,168 @@ If you want to be sure that this code does not secretly depend on pyplot run :: which will prevent pyplot from being imported! -showing -------- +Globally Managed Figures +======================== -The core of the API is `~.show` :: - import mpl_gui as mg - from matplotlib.figure import Figure +The `mpl_gui.registry` module provides a direct analogy to the +`matplotlib.pyplot` behavior of having a global registry of figures. Thus, any +figures created via the functions in `.registry` will remain alive until they +have been cleared from the registry (and the user has dropped all other +references). While it can be convenient, it carries with it the risk inherent +in any use of global state. - fig1 = Figure(label='A Label!') +The `matplotlib.pyplot` API related to figure creation, showing, and closing is a drop-in replacement: - fig2 = Figure() +:: - mg.show([fig1, fig2]) + import mpl_gui.registry as reg + fig = reg.figure() + fig, ax = reg.subplots() + fig, axd = reg.subplot_mosaic('AA\nCD') -which will show both figures and block until they are closed. As part of the -"showing" process, the correct GUI objects will be created, put on the -screen, and the event loop for the host GUI framework is run. + reg.show(block=True) # blocks until all figures are closed + reg.show(block=True, timeout=1000) # blocks for up to 1s or all figures are closed + reg.show(block=False) # does not block + reg.show() # depends on if in "interacitve mode" + reg.ion() # turn on interactive mode + reg.ioff() # turn off interactive mode + reg.is_interactive() # query interactive state -blocking (or not) -+++++++++++++++++ + reg.close('all') # close all open figures + reg.close(fig) # close a particular figure -Similar to `plt.ion` and -`plt.ioff`, we provide `mg.ion()` and -`mg.ioff()` which have identical semantics. Thus :: - - import mpl_gui as mg - from matplotlib.figure import Figure - mg.ion() - print(mg.is_interactive()) - fig = Figure() - mg.show([fig]) # will not block +Locally Managed Figures +======================= - mg.ioff() - print(mg.is_interactive()) - mg.show([fig]) # will block! +To avoid the issues with global state the objects you can create a local `.FigureRegistry`. +It keeps much of the convenience of the ``pyplot`` API but without the risk of global state :: + import mpl_gui as mg -As with `plt.show`, you can explicitly control the -blocking behavior of `mg.show<.show>` via the *block* keyword argument :: + fr = mg.FigureRegistry() - import mpl_gui as mg - from matplotlib.figure import Figure + fr.figure() + fr.subplots(2, 2) + fr.subplot_mosaic('AA\nBC') - fig = Figure(label='control blocking') + fr.show_all() # will show all three figures + fr.show() # alias for pyplot compatibility - mg.show([fig], block=False) # will never block - mg.show([fig], block=True) # will always block + fr.close_all() # will close all three figures + fr.close('all') # alias for pyplot compatibility -The interactive state is shared Matplotlib and can also be controlled with -`matplotlib.interactive` and queried via `matplotlib.is_interactive`. +Additionally, there are the `.FigureRegistry.by_label`, `.FigureRegistry.by_number`, +`.FigureRegistry.figures` accessors that returns a dictionary mapping the +Figures' labels to each Figure, the figures number to Figure, and a tuple of known Figures:: + import mpl_gui as mg -Figure and Axes Creation ------------------------- + fr = mg.FigureRegistry() -In analogy with `matplotlib.pyplot` we also provide `~mpl_gui.figure`, -`~mpl_gui.subplots` and `~mpl_gui.subplot_mosaic` :: + figA = fr.figure(label='A') + figB, axs = fr.subplots(2, 2, label='B') - import mpl_gui as mg - fig1 = mg.figure() - fig2, axs = mg.subplots(2, 2) - fig3, axd = mg.subplot_mosaic('AA\nBC') + fr.by_label['A'] is figA + fr.by_label['B'] is figB - mg.show([fig1, fig2, fig3]) + fr.by_number[0] is figA + fr.by_number[1] is figB -If `mpl_gui` is in "interactive mode", `mpl_gui.figure`, `mpl_gui.subplots` and -`mpl_gui.subplot_mosaic` will automatically put the new Figure in a window on -the screen (but not run the event loop). + fr.figures == (figA, figB) + fr.show() +The `.FigureRegistry` is local state so that if the user drops all references +to it it will be eligible for garbage collection. If there are no other +references to the ``Figure`` objects it is likely that they may be closed when +the garbage collector runs! -FigureRegistry --------------- -In the above examples it is the responsibility of the user to keep track of the -`~matplotlib.figure.Figure` instances that are created. If the user does not keep a hard -reference to the ``fig`` object, either directly or indirectly through its -children, then it will be garbage collected like any other Python object. -While this can be advantageous in some cases (such as scripts or functions that -create many transient figures). It loses the convenience of -`matplotlib.pyplot` keeping track of the instances for you. To this end we -also have provided `.FigureRegistry` :: +A very common use case is to make several figures and then show them all +together at the end. To facilitate this we provide a `.FigureContext` that is +a `.FigureRegistry` that can be used as a context manager that (locally) keeps +track of the created figures and shows them on exit :: import mpl_gui as mg - fr = mg.FigureRegistry() + with mg.FigureContext(block=None) as fc: + fc.subplot_mosaic('AA\nBC') + fc.figure() + fc.subplots(2, 2) - fr.figure() - fr.subplots(2, 2) - fr.subplot_mosaic('AA\nBC') - fr.show_all() # will show all three figures - fr.show() # alias for pyplot compatibility +This will create 3 figures and block on ``__exit__``. The blocking +behavior depends on ``mg.is_interacitve()`` (and follow the behavior of +``mg.show`` or can explicitly controlled via the *block* keyword argument). - fr.close_all() # will close all three figures - fr.close('all') # alias for pyplot compatibility +The `.registry` module is implemented by having a singleton `.FigureRegistry` +at the module level. -Thus, if you are only using this restricted set of the pyplot API then you can change :: - import matplotlib.pyplot as plt +User Managed Figures +==================== -to :: +There are cases where having such a registry may be too much implicit state. +For such cases the underlying tools that `.FigureRegistry` are built on are +explicitly available :: import mpl_gui as mg - plt = mg.FigureRegistry() + from matplotlib.figure import Figure -and have a (mostly) drop-in replacement. + fig1 = Figure(label='A Label!') -Additionally, there is a `.FigureRegistry.by_label` accessory that returns -a dictionary mapping the Figures' labels to each Figure :: + fig2 = Figure() + + mg.show([fig1, fig2]) + + +which will show both figures and block until they are closed. As part of the +"showing" process, the correct GUI objects will be created, put on the +screen, and the event loop for the host GUI framework is run. + +Similar to `plt.ion` and +`plt.ioff`, we provide `mg.ion()` and +`mg.ioff()` which have identical semantics. Thus :: import mpl_gui as mg + from matplotlib.figure import Figure - fr = mg.FigureRegistry() + mg.ion() + print(mg.is_interactive()) + fig = Figure() - figA = fr.figure(label='A') - figB = fr.subplots(2, 2, label='B') + mg.show([fig]) # will not block - fr.by_label['A'] is figA - fr.by_label['B'] is figB + mg.ioff() + print(mg.is_interactive()) + mg.show([fig]) # will block! -FigureContext -------------- -A very common use case is to make several figures and then show them all -together at the end. To facilitate this we provide a sub-class of -`.FigureRegistry` that can be used as a context manager that (locally) keeps -track of the created figures and shows them on exit :: +As with `plt.show`, you can explicitly control the +blocking behavior of `mg.show` via the *block* keyword argument :: import mpl_gui as mg + from matplotlib.figure import Figure - with mg.FigureContext() as fc: - fc.subplot_mosaic('AA\nBC') - fc.figure() - fc.subplots(2, 2) + fig = Figure(label='control blocking') + mg.show([fig], block=False) # will never block + mg.show([fig], block=True) # will always block + + +The interactive state is shared Matplotlib and can also be controlled with +`matplotlib.interactive` and queried via `matplotlib.is_interactive`. -This will create 3 figures and block on ``__exit__``. The blocking -behavior depends on ``mg.is_interacitve()`` (and follow the behavior of -``mg.show`` or can explicitly controlled via the *block* keyword argument). Selecting the GUI toolkit -------------------------- +========================= `mpl_gui` makes use of `Matplotlib backends `_ for actually @@ -207,6 +215,8 @@ providing the GUI bindings. Analagous to `matplotlib.use` and `matplotlib.pyplot.switch_backend` `mpl_gui` provides `mpl_gui.select_gui_toolkit` to select which GUI toolkit is used. `~mpl_gui.select_gui_toolkit` has the same fall-back behavior as -`~matplotlib.pyplot` and stores its state in :rc:`backend`. `mpl_gui` will +`~matplotlib.pyplot` and stores its state in :rc:`backend`. + +`mpl_gui` will consistently co-exist with `matplotlib.pyplot` managed Figures in the same process. From 63ed7e83a146f35007ef9840acf06a30bb07a1bb Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 15:42:31 -0400 Subject: [PATCH 08/20] DOC: remove sphinx_design We are not using it. --- docs/source/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 410d456..8e7bafc 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -51,7 +51,6 @@ "matplotlib.sphinxext.plot_directive", "numpydoc", "sphinx_copybutton", - 'sphinx_design', ] # Configuration options for plot_directive. See: From a122e06d34eef75af4d9049e18143d69d0c11223 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 16:09:01 -0400 Subject: [PATCH 09/20] DOC: fix xrefs --- mpl_gui/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index fb101c1..085366b 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -279,16 +279,16 @@ def close(self, val): - start the destruction process of an UI (the event loop may need to run to complete this process and if the user is holding hard references to any of the UI elements they may remain alive). - - Remove the `Figure` from this Registry. + - Remove the `~matplotlib.figure.Figure` from this Registry. We will no longer have any hard references to the Figure, but if - the user does the `Figure` (and its components) will not be garbage + the user does the `~matplotlib.figure.Figure` (and its components) will not be garbage collected. Due to the circular references in Matplotlib these objects may not be collected until the full cyclic garbage collection runs. - If the user still has a reference to the `Figure` they can re-show the - figure via `show`, but the `FigureRegistry` will not be aware of it. + If the user still has a reference to the `~matplotlib.figure.Figure` they can re-show the + figure via `show`, but the `.FigureRegistry` will not be aware of it. Parameters ---------- @@ -297,9 +297,9 @@ def close(self, val): - The special case of 'all' closes all open Figures - If any other string is passed, it is interpreted as a key in `by_label` and that Figure is closed - - If an integer it is interpreted as a key in `by_number` and that + - If an integer it is interpreted as a key in `.FigureRegistry.by_number` and that Figure is closed - - If it is a `Figure` instance, then that figure is closed + - If it is a `~matplotlib.figure.Figure` instance, then that figure is closed """ if val == "all": From 8fe8d0ad4f27e2d18043ead674a10b25506c59d6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 16:13:18 -0400 Subject: [PATCH 10/20] TST: fix tests --- mpl_gui/tests/test_examples.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/mpl_gui/tests/test_examples.py b/mpl_gui/tests/test_examples.py index 0306312..824ee7e 100644 --- a/mpl_gui/tests/test_examples.py +++ b/mpl_gui/tests/test_examples.py @@ -18,12 +18,6 @@ def test_promotion(): assert fig.canvas.manager is not None -def test_smoke_test_creation(): - mg.figure() - mg.subplots() - mg.subplot_mosaic("A\nB") - - def test_smoke_test_context(): with mg.FigureContext(block=False) as fc: fc.figure() @@ -34,7 +28,8 @@ def test_smoke_test_context(): def test_ion(): with mg.ion(): assert mg.is_interactive() - fig, ax = mg.subplots() + fig = mg.Figure() + ax = fig.subplots() (ln,) = ax.plot(range(5)) ln.set_color("k") mg.show([fig], timeout=1) From 65965ba5eb47bba19fa6daa18a6b294bee8b15e6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 16:15:50 -0400 Subject: [PATCH 11/20] TST: change supported Python versions --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index b01b4bb..a016ff1 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12'] fail-fast: false steps: From ac05a410ca47a63974f0547835daede2c6adf9d5 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 16:40:19 -0400 Subject: [PATCH 12/20] API: make mg.show take positional variadic for input Figures --- docs/source/index.rst | 8 ++++---- mpl_gui/__init__.py | 8 ++++---- mpl_gui/tests/test_examples.py | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 40dac47..4968fc1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -164,7 +164,7 @@ explicitly available :: fig2 = Figure() - mg.show([fig1, fig2]) + mg.show(fig1, fig2) which will show both figures and block until they are closed. As part of the @@ -186,7 +186,7 @@ Similar to `plt.ion` and mg.ioff() print(mg.is_interactive()) - mg.show([fig]) # will block! + mg.show(fig) # will block! As with `plt.show`, you can explicitly control the @@ -197,8 +197,8 @@ blocking behavior of `mg.show` via the *block* keyword argument :: fig = Figure(label='control blocking') - mg.show([fig], block=False) # will never block - mg.show([fig], block=True) # will always block + mg.show(fig, block=False) # will never block + mg.show(fig, block=True) # will always block The interactive state is shared Matplotlib and can also be controlled with diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index 085366b..a46d998 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -46,13 +46,13 @@ _log = logging.getLogger(__name__) -def show(figs, *, block=None, timeout=0): +def show(*figs, block=None, timeout=0): """ Show the figures and maybe block. Parameters ---------- - figs : List[Figure] + *figs : Figure The figures to show. If they do not currently have a GUI aware canvas + manager attached they will be promoted. @@ -247,7 +247,7 @@ def show_all(self, *, block=None, timeout=None): if timeout is None: timeout = self._timeout self._ensure_all_figures_promoted() - show(self.figures, block=self._block, timeout=self._timeout) + show(*self.figures, block=self._block, timeout=self._timeout) # alias to easy pyplot compatibility show = show_all @@ -368,7 +368,7 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): if exc_value is not None and not self._forgive_failure: return - show(self.figures, block=self._block, timeout=self._timeout) + show(*self.figures, block=self._block, timeout=self._timeout) # from mpl_gui import * # is a langauge miss-feature diff --git a/mpl_gui/tests/test_examples.py b/mpl_gui/tests/test_examples.py index 824ee7e..ddea476 100644 --- a/mpl_gui/tests/test_examples.py +++ b/mpl_gui/tests/test_examples.py @@ -14,7 +14,7 @@ def test_no_pyplot(): def test_promotion(): fig = mg.Figure(label="test") assert fig.canvas.manager is None - mg.show([fig], block=False) + mg.show(*[fig], block=False) assert fig.canvas.manager is not None @@ -32,7 +32,7 @@ def test_ion(): ax = fig.subplots() (ln,) = ax.plot(range(5)) ln.set_color("k") - mg.show([fig], timeout=1) + mg.show(*[fig], timeout=1) assert "start_event_loop" not in fig.canvas.call_info @@ -43,7 +43,7 @@ def test_ioff(): def test_timeout(): fig = mg.Figure() - mg.show([fig], block=True, timeout=1) + mg.show(*[fig], block=True, timeout=1) assert "start_event_loop" in fig.canvas.call_info @@ -89,7 +89,7 @@ def test_close_all(): # test revive old_canvas = fig.canvas - mg.show([fig]) + mg.show(fig) assert fig.canvas is not old_canvas From fecb3c0add2c1a2edc324d4d8d63b6b1e5d6b788 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 16:46:27 -0400 Subject: [PATCH 13/20] API: rename mg.show -> mg.display Having a conversation about this it was impossible to keep straight with the two meanings of `show`. closes #16 --- docs/source/api.rst | 6 +++--- docs/source/index.rst | 16 ++++++++-------- mpl_gui/__init__.py | 8 ++++---- mpl_gui/_manage_interactive.py | 10 +++++----- mpl_gui/tests/test_examples.py | 8 ++++---- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 4cc31f7..304eb6b 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -15,15 +15,15 @@ Select the backend mpl_gui.select_gui_toolkit -Show ----- +Display +------- .. autosummary:: :toctree: _as_gen :nosignatures: - mpl_gui.show + mpl_gui.display Interactivity diff --git a/docs/source/index.rst b/docs/source/index.rst index 4968fc1..2157ceb 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -143,8 +143,8 @@ track of the created figures and shows them on exit :: This will create 3 figures and block on ``__exit__``. The blocking -behavior depends on ``mg.is_interacitve()`` (and follow the behavior of -``mg.show`` or can explicitly controlled via the *block* keyword argument). +behavior depends on `~mpl_gui.is_interactive()` (and follow the behavior of +`.display` and `.FigureRegistry.show` can explicitly controlled via the *block* keyword argument). The `.registry` module is implemented by having a singleton `.FigureRegistry` at the module level. @@ -164,7 +164,7 @@ explicitly available :: fig2 = Figure() - mg.show(fig1, fig2) + mg.display(fig1, fig2) which will show both figures and block until they are closed. As part of the @@ -182,23 +182,23 @@ Similar to `plt.ion` and print(mg.is_interactive()) fig = Figure() - mg.show([fig]) # will not block + mg.display([fig]) # will not block mg.ioff() print(mg.is_interactive()) - mg.show(fig) # will block! + mg.display(fig) # will block! As with `plt.show`, you can explicitly control the -blocking behavior of `mg.show` via the *block* keyword argument :: +blocking behavior of `mg.display` via the *block* keyword argument :: import mpl_gui as mg from matplotlib.figure import Figure fig = Figure(label='control blocking') - mg.show(fig, block=False) # will never block - mg.show(fig, block=True) # will always block + mg.display(fig, block=False) # will never block + mg.display(fig, block=True) # will always block The interactive state is shared Matplotlib and can also be controlled with diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index a46d998..a0e2e99 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -46,7 +46,7 @@ _log = logging.getLogger(__name__) -def show(*figs, block=None, timeout=0): +def display(*figs, block=None, timeout=0): """ Show the figures and maybe block. @@ -247,7 +247,7 @@ def show_all(self, *, block=None, timeout=None): if timeout is None: timeout = self._timeout self._ensure_all_figures_promoted() - show(*self.figures, block=self._block, timeout=self._timeout) + display(*self.figures, block=self._block, timeout=self._timeout) # alias to easy pyplot compatibility show = show_all @@ -264,7 +264,7 @@ def close_all(self): 4. drops its hard reference to the Figure If the user still holds a reference to the Figure it can be revived by - passing it to `mpl_gui.show`. + passing it to `mpl_gui.display`. """ for fig in list(self.figures): @@ -368,7 +368,7 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): if exc_value is not None and not self._forgive_failure: return - show(*self.figures, block=self._block, timeout=self._timeout) + display(*self.figures, block=self._block, timeout=self._timeout) # from mpl_gui import * # is a langauge miss-feature diff --git a/mpl_gui/_manage_interactive.py b/mpl_gui/_manage_interactive.py index fb55aa2..129f111 100644 --- a/mpl_gui/_manage_interactive.py +++ b/mpl_gui/_manage_interactive.py @@ -14,21 +14,21 @@ def is_interactive(): - newly created figures will be shown immediately; - figures will automatically redraw on change; - - `mpl_gui.show` will not block by default. + - `.display` will not block by default. - `mpl_gui.FigureContext` will not block on ``__exit__`` by default. In non-interactive mode: - newly created figures and changes to figures will not be reflected until explicitly asked to be; - - `mpl_gui.show` will block by default. + - `.display` will block by default. - `mpl_gui.FigureContext` will block on ``__exit__`` by default. See Also -------- ion : Enable interactive mode. ioff : Disable interactive mode. - mpl_gui.show : Show all figures (and maybe block). + mpl_gui.display : Show all figures (and maybe block). """ return _is_interact() @@ -89,7 +89,7 @@ def ioff(): -------- ion : Enable interactive mode. is_interactive : Whether interactive mode is enabled. - mpl_gui.show : Show all figures (and maybe block). + mpl_gui.display : Show all figures (and maybe block). Notes ----- @@ -124,7 +124,7 @@ def ion(): -------- ioff : Disable interactive mode. is_interactive : Whether interactive mode is enabled. - mpl_gui.show : Show all figures (and maybe block). + mpl_gui.display : Show all figures (and maybe block). Notes ----- diff --git a/mpl_gui/tests/test_examples.py b/mpl_gui/tests/test_examples.py index ddea476..56740d8 100644 --- a/mpl_gui/tests/test_examples.py +++ b/mpl_gui/tests/test_examples.py @@ -14,7 +14,7 @@ def test_no_pyplot(): def test_promotion(): fig = mg.Figure(label="test") assert fig.canvas.manager is None - mg.show(*[fig], block=False) + mg.display(*[fig], block=False) assert fig.canvas.manager is not None @@ -32,7 +32,7 @@ def test_ion(): ax = fig.subplots() (ln,) = ax.plot(range(5)) ln.set_color("k") - mg.show(*[fig], timeout=1) + mg.display(*[fig], timeout=1) assert "start_event_loop" not in fig.canvas.call_info @@ -43,7 +43,7 @@ def test_ioff(): def test_timeout(): fig = mg.Figure() - mg.show(*[fig], block=True, timeout=1) + mg.display(*[fig], block=True, timeout=1) assert "start_event_loop" in fig.canvas.call_info @@ -89,7 +89,7 @@ def test_close_all(): # test revive old_canvas = fig.canvas - mg.show(fig) + mg.display(fig) assert fig.canvas is not old_canvas From e59811f6df8b2d2b1ccfad3ab30bfedc894a50a3 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 2 Nov 2023 11:33:49 -0400 Subject: [PATCH 14/20] API: rename registry module to global_figures --- docs/source/api.rst | 24 ++++++++-------- docs/source/index.rst | 32 +++++++++++----------- mpl_gui/{registry.py => global_figures.py} | 0 3 files changed, 28 insertions(+), 28 deletions(-) rename mpl_gui/{registry.py => global_figures.py} (100%) diff --git a/docs/source/api.rst b/docs/source/api.rst index 304eb6b..2fdec98 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -96,7 +96,7 @@ Globally managed ---------------- -.. automodule:: mpl_gui.registry +.. automodule:: mpl_gui.global_figures :no-undoc-members: @@ -108,9 +108,9 @@ Create Figures and Axes :toctree: _as_gen :nosignatures: - mpl_gui.registry.figure - mpl_gui.registry.subplots - mpl_gui.registry.subplot_mosaic + mpl_gui.global_figures.figure + mpl_gui.global_figures.subplots + mpl_gui.global_figures.subplot_mosaic Access managed figures @@ -121,7 +121,7 @@ Access managed figures :toctree: _as_gen :nosignatures: - mpl_gui.registry.by_label + mpl_gui.global_figures.by_label Show and close managed Figures @@ -134,10 +134,10 @@ Show and close managed Figures - mpl_gui.registry.show - mpl_gui.registry.show_all - mpl_gui.registry.close_all - mpl_gui.registry.close + mpl_gui.global_figures.show + mpl_gui.global_figures.show_all + mpl_gui.global_figures.close_all + mpl_gui.global_figures.close Interactivity @@ -148,6 +148,6 @@ Interactivity :nosignatures: - mpl_gui.registry.ion - mpl_gui.registry.ioff - mpl_gui.registry.is_interactive + mpl_gui.global_figures.ion + mpl_gui.global_figures.ioff + mpl_gui.global_figures.is_interactive diff --git a/docs/source/index.rst b/docs/source/index.rst index 2157ceb..0e39906 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -50,9 +50,9 @@ Globally Managed Figures ======================== -The `mpl_gui.registry` module provides a direct analogy to the +The `mpl_gui.global_figures` module provides a direct analogy to the `matplotlib.pyplot` behavior of having a global registry of figures. Thus, any -figures created via the functions in `.registry` will remain alive until they +figures created via the functions in `.global_figures` will remain alive until they have been cleared from the registry (and the user has dropped all other references). While it can be convenient, it carries with it the risk inherent in any use of global state. @@ -61,23 +61,23 @@ The `matplotlib.pyplot` API related to figure creation, showing, and closing is :: - import mpl_gui.registry as reg + import mpl_gui.global_figures as gfigs - fig = reg.figure() - fig, ax = reg.subplots() - fig, axd = reg.subplot_mosaic('AA\nCD') + fig = gfigs.figure() + fig, ax = gfigs.subplots() + fig, axd = gfigs.subplot_mosaic('AA\nCD') - reg.show(block=True) # blocks until all figures are closed - reg.show(block=True, timeout=1000) # blocks for up to 1s or all figures are closed - reg.show(block=False) # does not block - reg.show() # depends on if in "interacitve mode" + gfigs.show(block=True) # blocks until all figures are closed + gfigs.show(block=True, timeout=1000) # blocks for up to 1s or all figures are closed + gfigs.show(block=False) # does not block + gfigs.show() # depends on if in "interacitve mode" - reg.ion() # turn on interactive mode - reg.ioff() # turn off interactive mode - reg.is_interactive() # query interactive state + gfigs.ion() # turn on interactive mode + gfigs.ioff() # turn off interactive mode + gfigs.is_interactive() # query interactive state - reg.close('all') # close all open figures - reg.close(fig) # close a particular figure + gfigs.close('all') # close all open figures + gfigs.close(fig) # close a particular figure @@ -146,7 +146,7 @@ This will create 3 figures and block on ``__exit__``. The blocking behavior depends on `~mpl_gui.is_interactive()` (and follow the behavior of `.display` and `.FigureRegistry.show` can explicitly controlled via the *block* keyword argument). -The `.registry` module is implemented by having a singleton `.FigureRegistry` +The `.global_figures` module is implemented by having a singleton `.FigureRegistry` at the module level. diff --git a/mpl_gui/registry.py b/mpl_gui/global_figures.py similarity index 100% rename from mpl_gui/registry.py rename to mpl_gui/global_figures.py From 8e53cabf84c51295135957604e9f899e634d9867 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 8 Nov 2023 12:28:14 -0500 Subject: [PATCH 15/20] CI: use latest ubuntu --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 09235f6..e4c45e3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,7 +8,7 @@ env: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install Python dependencies From ac06b2aa7e1d3e9e6f64202f260b27871fd489ae Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 8 Nov 2023 12:44:39 -0500 Subject: [PATCH 16/20] FIX: do not be more aggressive about cleanup on 'q' that close We were more aggressively cleaning up the Figure via the 'q' hot key than via closing via Window Manager UI. --- mpl_gui/_promotion.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/mpl_gui/_promotion.py b/mpl_gui/_promotion.py index d2aa086..d3f3388 100644 --- a/mpl_gui/_promotion.py +++ b/mpl_gui/_promotion.py @@ -87,13 +87,6 @@ def _destroy(event): mgr._destroy_cid = None # close the window mgr.destroy() - # disconnect the manager from the canvas - fig.canvas.manager = None - # reset the dpi - fig.dpi = getattr(fig, "_original_dpi", fig.dpi) - # Go back to "base" canvas - # (this sets state on fig in the canvas init) - FigureCanvasBase(fig) manager._destroy_cid = fig.canvas.mpl_connect("key_press_event", _destroy) From c60c60e17f238c11b8a24a7f6b6be8cf384d6b9e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 9 Nov 2023 00:01:29 -0500 Subject: [PATCH 17/20] MNT: simplify close handler --- mpl_gui/_promotion.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/mpl_gui/_promotion.py b/mpl_gui/_promotion.py index d3f3388..cb21c2f 100644 --- a/mpl_gui/_promotion.py +++ b/mpl_gui/_promotion.py @@ -72,22 +72,17 @@ def promote_figure(fig, *, auto_draw=True, num): # HACK: the callback in backend_bases uses GCF.destroy which misses these # figures by design! - def _destroy(event): + def _destroy_on_hotkey(event): if event.key in mpl.rcParams["keymap.quit"]: # grab the manager off the event mgr = event.canvas.manager if mgr is None: - raise RuntimeError("Should never be here, please report a bug") - fig = event.canvas.figure - # remove this callback. Callbacks lives on the Figure so survive - # the canvas being replaced. - old_cid = getattr(mgr, "_destroy_cid", None) - if old_cid is not None: - fig.canvas.mpl_disconnect(old_cid) - mgr._destroy_cid = None + raise RuntimeError("Should never be here, please report a bug.") # close the window mgr.destroy() - manager._destroy_cid = fig.canvas.mpl_connect("key_press_event", _destroy) + # remove this callback. Callbacks live on the Figure so survive the canvas + # being replaced. + fig.canvas.mpl_connect("key_press_event", _destroy_on_hotkey) return manager From 7f5a885633c50349afd040386b1061843a69c0c4 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 9 Nov 2023 14:58:39 -0500 Subject: [PATCH 18/20] DOC: re-order to put un-managed first --- docs/source/index.rst | 167 +++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 82 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 0e39906..72bbf95 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -46,38 +46,77 @@ If you want to be sure that this code does not secretly depend on pyplot run :: which will prevent pyplot from being imported! -Globally Managed Figures -======================== +Selecting the GUI toolkit +========================= -The `mpl_gui.global_figures` module provides a direct analogy to the -`matplotlib.pyplot` behavior of having a global registry of figures. Thus, any -figures created via the functions in `.global_figures` will remain alive until they -have been cleared from the registry (and the user has dropped all other -references). While it can be convenient, it carries with it the risk inherent -in any use of global state. +`mpl_gui` makes use of `Matplotlib backends +`_ for actually +providing the GUI bindings. Analagous to `matplotlib.use` and +`matplotlib.pyplot.switch_backend` `mpl_gui` provides +`mpl_gui.select_gui_toolkit` to select which GUI toolkit is used. +`~mpl_gui.select_gui_toolkit` has the same fall-back behavior as +`~matplotlib.pyplot` and stores its state in :rc:`backend`. -The `matplotlib.pyplot` API related to figure creation, showing, and closing is a drop-in replacement: +`mpl_gui` will +consistently co-exist with `matplotlib.pyplot` managed Figures in the same +process. -:: - import mpl_gui.global_figures as gfigs - fig = gfigs.figure() - fig, ax = gfigs.subplots() - fig, axd = gfigs.subplot_mosaic('AA\nCD') +User Managed Figures +==================== - gfigs.show(block=True) # blocks until all figures are closed - gfigs.show(block=True, timeout=1000) # blocks for up to 1s or all figures are closed - gfigs.show(block=False) # does not block - gfigs.show() # depends on if in "interacitve mode" +There are cases where having such a registry may be too much implicit state. +For such cases the underlying tools that `.FigureRegistry` are built on are +explicitly available :: - gfigs.ion() # turn on interactive mode - gfigs.ioff() # turn off interactive mode - gfigs.is_interactive() # query interactive state + import mpl_gui as mg + from matplotlib.figure import Figure - gfigs.close('all') # close all open figures - gfigs.close(fig) # close a particular figure + fig1 = Figure(label='A Label!') + + fig2 = Figure() + + mg.display(fig1, fig2) + + +which will show both figures and block until they are closed. As part of the +"showing" process, the correct GUI objects will be created, put on the +screen, and the event loop for the host GUI framework is run. + +Similar to `plt.ion` and +`plt.ioff`, we provide `mg.ion()` and +`mg.ioff()` which have identical semantics. Thus :: + + import mpl_gui as mg + from matplotlib.figure import Figure + + mg.ion() + print(mg.is_interactive()) + fig = Figure() + + mg.display([fig]) # will not block + + mg.ioff() + print(mg.is_interactive()) + mg.display(fig) # will block! + + +As with `plt.show`, you can explicitly control the +blocking behavior of `mg.display` via the *block* keyword argument :: + + import mpl_gui as mg + from matplotlib.figure import Figure + + fig = Figure(label='control blocking') + + mg.display(fig, block=False) # will never block + mg.display(fig, block=True) # will always block + + +The interactive state is shared Matplotlib and can also be controlled with +`matplotlib.interactive` and queried via `matplotlib.is_interactive`. @@ -150,73 +189,37 @@ The `.global_figures` module is implemented by having a singleton `.FigureRegist at the module level. -User Managed Figures -==================== - -There are cases where having such a registry may be too much implicit state. -For such cases the underlying tools that `.FigureRegistry` are built on are -explicitly available :: - - import mpl_gui as mg - from matplotlib.figure import Figure - - fig1 = Figure(label='A Label!') - - fig2 = Figure() - - mg.display(fig1, fig2) - - -which will show both figures and block until they are closed. As part of the -"showing" process, the correct GUI objects will be created, put on the -screen, and the event loop for the host GUI framework is run. - -Similar to `plt.ion` and -`plt.ioff`, we provide `mg.ion()` and -`mg.ioff()` which have identical semantics. Thus :: - - import mpl_gui as mg - from matplotlib.figure import Figure - mg.ion() - print(mg.is_interactive()) - fig = Figure() - mg.display([fig]) # will not block - - mg.ioff() - print(mg.is_interactive()) - mg.display(fig) # will block! - - -As with `plt.show`, you can explicitly control the -blocking behavior of `mg.display` via the *block* keyword argument :: - - import mpl_gui as mg - from matplotlib.figure import Figure +Globally Managed Figures +======================== - fig = Figure(label='control blocking') - mg.display(fig, block=False) # will never block - mg.display(fig, block=True) # will always block +The `mpl_gui.global_figures` module provides a direct analogy to the +`matplotlib.pyplot` behavior of having a global registry of figures. Thus, any +figures created via the functions in `.global_figures` will remain alive until they +have been cleared from the registry (and the user has dropped all other +references). While it can be convenient, it carries with it the risk inherent +in any use of global state. +The `matplotlib.pyplot` API related to figure creation, showing, and closing is a drop-in replacement: -The interactive state is shared Matplotlib and can also be controlled with -`matplotlib.interactive` and queried via `matplotlib.is_interactive`. +:: + import mpl_gui.global_figures as gfigs + fig = gfigs.figure() + fig, ax = gfigs.subplots() + fig, axd = gfigs.subplot_mosaic('AA\nCD') -Selecting the GUI toolkit -========================= + gfigs.show(block=True) # blocks until all figures are closed + gfigs.show(block=True, timeout=1000) # blocks for up to 1s or all figures are closed + gfigs.show(block=False) # does not block + gfigs.show() # depends on if in "interacitve mode" -`mpl_gui` makes use of `Matplotlib backends -`_ for actually -providing the GUI bindings. Analagous to `matplotlib.use` and -`matplotlib.pyplot.switch_backend` `mpl_gui` provides -`mpl_gui.select_gui_toolkit` to select which GUI toolkit is used. -`~mpl_gui.select_gui_toolkit` has the same fall-back behavior as -`~matplotlib.pyplot` and stores its state in :rc:`backend`. + gfigs.ion() # turn on interactive mode + gfigs.ioff() # turn off interactive mode + gfigs.is_interactive() # query interactive state -`mpl_gui` will -consistently co-exist with `matplotlib.pyplot` managed Figures in the same -process. + gfigs.close('all') # close all open figures + gfigs.close(fig) # close a particular figure From 7d017c930eacd86dc2296f9e41e809eb64a93911 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 9 Nov 2023 14:58:53 -0500 Subject: [PATCH 19/20] DOC/API: put the top-level helpers back --- docs/source/api.rst | 58 +++++++++++++++++++++++++++++++-------------- mpl_gui/__init__.py | 23 ++++++++++-------- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 2fdec98..f016221 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -10,32 +10,54 @@ Select the backend ------------------ .. autosummary:: :toctree: _as_gen - :nosignatures: + mpl_gui.select_gui_toolkit -Display -------- +Interactivity +------------- .. autosummary:: :toctree: _as_gen - :nosignatures: - mpl_gui.display + mpl_gui.ion + mpl_gui.ioff + mpl_gui.is_interactive -Interactivity -------------- +Unmanaged Figures +----------------- + +Figure Creation ++++++++++++++++ + +These are not strictly necessary as they are only thin wrappers around creating +a `matplotlib.figure.Figure` instance and creating children in one line. .. autosummary:: :toctree: _as_gen - :nosignatures: - mpl_gui.ion - mpl_gui.ioff - mpl_gui.is_interactive + + + mpl_gui.figure + mpl_gui.subplots + mpl_gui.subplot_mosaic + + + +Display ++++++++ + +.. autosummary:: + :toctree: _as_gen + + + + mpl_gui.display + mpl_gui.demote_figure + Locally Managed Figures @@ -56,7 +78,7 @@ Create Figures and Axes .. autosummary:: :toctree: _as_gen - :nosignatures: + mpl_gui.FigureRegistry.figure mpl_gui.FigureRegistry.subplots @@ -68,7 +90,7 @@ Access managed figures .. autosummary:: :toctree: _as_gen - :nosignatures: + mpl_gui.FigureRegistry.by_label mpl_gui.FigureRegistry.by_number @@ -82,7 +104,7 @@ Show and close managed Figures .. autosummary:: :toctree: _as_gen - :nosignatures: + mpl_gui.FigureRegistry.show_all mpl_gui.FigureRegistry.close_all @@ -106,7 +128,7 @@ Create Figures and Axes .. autosummary:: :toctree: _as_gen - :nosignatures: + mpl_gui.global_figures.figure mpl_gui.global_figures.subplots @@ -119,7 +141,7 @@ Access managed figures .. autosummary:: :toctree: _as_gen - :nosignatures: + mpl_gui.global_figures.by_label @@ -130,7 +152,7 @@ Show and close managed Figures .. autosummary:: :toctree: _as_gen - :nosignatures: + @@ -145,7 +167,7 @@ Interactivity .. autosummary:: :toctree: _as_gen - :nosignatures: + mpl_gui.global_figures.ion diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index a0e2e99..2dcb674 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -29,11 +29,14 @@ ) from ._manage_backend import select_gui_toolkit as select_gui_toolkit # noqa: F401 from ._manage_backend import current_backend_module as _cbm -from ._promotion import promote_figure as _promote_figure +from ._promotion import ( + promote_figure as _promote_figure, + demote_figure as demote_figure, +) from ._creation import ( - figure as _figure, - subplots as _subplots, - subplot_mosaic as _subplot_mosaic, + figure as figure, + subplots as subplots, + subplot_mosaic as subplot_mosaic, ) @@ -195,19 +198,19 @@ def by_number(self): self._ensure_all_figures_promoted() return {fig.canvas.manager.num: fig for fig in self.figures} - @functools.wraps(_figure) + @functools.wraps(figure) def figure(self, *args, **kwargs): - fig = _figure(*args, **kwargs) + fig = figure(*args, **kwargs) return self._register_fig(fig) - @functools.wraps(_subplots) + @functools.wraps(subplots) def subplots(self, *args, **kwargs): - fig, axs = _subplots(*args, **kwargs) + fig, axs = subplots(*args, **kwargs) return self._register_fig(fig), axs - @functools.wraps(_subplot_mosaic) + @functools.wraps(subplot_mosaic) def subplot_mosaic(self, *args, **kwargs): - fig, axd = _subplot_mosaic(*args, **kwargs) + fig, axd = subplot_mosaic(*args, **kwargs) return self._register_fig(fig), axd def _ensure_all_figures_promoted(self): From 374c2afc1f7e07ab2d4dcec1b9c70370a4a375c0 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 9 Nov 2023 14:59:08 -0500 Subject: [PATCH 20/20] ENH: add top-level function to fully demote a figure --- mpl_gui/_promotion.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/mpl_gui/_promotion.py b/mpl_gui/_promotion.py index cb21c2f..4fc3751 100644 --- a/mpl_gui/_promotion.py +++ b/mpl_gui/_promotion.py @@ -83,6 +83,27 @@ def _destroy_on_hotkey(event): # remove this callback. Callbacks live on the Figure so survive the canvas # being replaced. - fig.canvas.mpl_connect("key_press_event", _destroy_on_hotkey) + fig._destroy_cid = fig.canvas.mpl_connect("key_press_event", _destroy_on_hotkey) return manager + + +def demote_figure(fig): + """Fully clear all GUI elements from the `~matplotlib.figure.Figure`. + + The opposite of what is done during `mpl_gui.display`. + + Parameters + ---------- + fig : matplotlib.figure.Figure + + """ + fig.canvas.destroy() + fig.canvas.manager = None + original_dpi = getattr(fig, "_original_dpi", fig.dpi) + if (cid := getattr(fig, '_destroy_cid', None)) is not None: + fig.canvas.mpl_disconnect(cid) + FigureCanvasBase(fig) + fig.dpi = original_dpi + + return fig