From bc0cdc7bd58095039f124516cd6ca88611509513 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 29 Jan 2022 19:34:46 -0500 Subject: [PATCH 01/51] BLD: add classifier --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d9caae6..bb74dbc 100644 --- a/setup.py +++ b/setup.py @@ -63,5 +63,6 @@ 'Development Status :: 2 - Pre-Alpha', 'Natural Language :: English', 'Programming Language :: Python :: 3', + 'Framework :: Matplotlib', ], ) From 7ec1b507c4ffaecc444013cb42ea0273dbf0fb51 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 10 Feb 2022 18:52:02 -0500 Subject: [PATCH 02/51] DOC: replace tacaswell -> matplotlib in setup.py --- CONTRIBUTING.rst | 4 ++-- README.rst | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1f14bbd..7bc112a 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -13,7 +13,7 @@ Types of Contributions Report Bugs ~~~~~~~~~~~ -Report bugs at https://github.com/tacaswell/mpl-gui/issues. +Report bugs at https://github.com/matplotlib/mpl-gui/issues. If you are reporting a bug, please include: @@ -42,7 +42,7 @@ or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ -The best way to send feedback is to file an issue at https://github.com/tacaswell/mpl-gui/issues. +The best way to send feedback is to file an issue at https://github.com/matplotlib/mpl-gui/issues. If you are proposing a feature: diff --git a/README.rst b/README.rst index 13e261d..3e29c5e 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ mpl-gui Prototype project for splitting pyplot in half * Free software: 3-clause BSD license -* Documentation: https://tacaswell.github.io/mpl-gui. +* Documentation: https://matplotlib.github.io/mpl-gui. Motivation ---------- diff --git a/setup.py b/setup.py index bb74dbc..a828056 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ long_description=readme, author="Thomas A Caswell", author_email='tcaswell@gmail.com', - url='https://github.com/tacaswell/mpl-gui', + url='https://github.com/matplotlib/mpl-gui', python_requires='>={}'.format('.'.join(str(n) for n in min_version)), packages=find_packages(exclude=['docs', 'tests']), entry_points={ From 29ccdf64f60e3b5910f1630b1c0fb02b85275f39 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 10 Feb 2022 19:34:52 -0500 Subject: [PATCH 03/51] DOC: tweak contributing.rst - refer to standard library venv - make use of requirements-dev.txt - remove tox (which we do not have a config for!) - remove py27 and old py3 from text --- CONTRIBUTING.rst | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 7bc112a..dc2840f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -61,33 +61,41 @@ Ready to contribute? Here's how to set up `mpl-gui` for local development. $ git clone git@github.com:your_name_here/mpl-gui.git -3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: +3. Install your local copy into a virtualenv. Using the standard-libary `venv`, see + [the Python documentation](https://docs.python.org/3/tutorial/venv.html#creating-virtual-environments) for details :: + + $ python -m venv mpl-gui-dev + $ source mpl-gui-dev/activate # unix + $ mpl-gui-dev\Scripts\activate.bat # windows + + or other virtual environment tool (e.g. conda, canopy) of choice. + +4. Install the development dependencies :: - $ mkvirtualenv mpl-gui $ cd mpl-gui/ - $ python setup.py develop + $ pip install -r requirements-dev.txt + $ pip install -v --no-build-isolation . -4. Create a branch for local development:: +5. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. -5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: +6. When you're done making changes, check that your changes pass flake8 and the tests:: $ flake8 mpl_gui tests - $ python setup.py test - $ tox + $ pytest - To get flake8 and tox, just pip install them into your virtualenv. + To get flake8 and pytest, pip install them into your virtualenv. -6. Commit your changes and push your branch to GitHub:: +8. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature -7. Submit a pull request through the GitHub website. +8. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- @@ -98,7 +106,4 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. -3. The pull request should work for Python 2.7, 3.3, 3.4, 3.5 and for PyPy. Check - https://travis-ci.org/tacaswell/mpl-gui/pull_requests - and make sure that the tests pass for all supported Python versions. - +3. The pull request should work for Python 3.7 and up. From 901e21da1f4db38c01c1c5c4a2f9618840b04ed2 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Fri, 11 Feb 2022 08:02:55 -0700 Subject: [PATCH 04/51] FIX: Map macosx to osx for ipython terminal gui toolkit The gui toolkit is called "osx" in ipython, so we need to map the Matplotlib's "macosx" name over to that within an ipython session. --- mpl_gui/_manage_backend.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mpl_gui/_manage_backend.py b/mpl_gui/_manage_backend.py index cd4a586..1c2aadc 100644 --- a/mpl_gui/_manage_backend.py +++ b/mpl_gui/_manage_backend.py @@ -166,6 +166,9 @@ def show_managers(cls, *, managers, block): # if so are we in an IPython session ip = mod_ipython.get_ipython() if ip: + # macosx -> osx mapping for the osx backend in ipython + if required_framework == "macosx": + required_framework = "osx" ip.enable_gui(required_framework) # remember to set the global variable From 22b0efab381cd050b082fd636b588c1d25d51762 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 23 Apr 2022 16:44:12 -0400 Subject: [PATCH 05/51] DOC: fix numbering Co-authored-by: Oscar Gustafsson --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index dc2840f..8ff1b90 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -89,7 +89,7 @@ Ready to contribute? Here's how to set up `mpl-gui` for local development. To get flake8 and pytest, pip install them into your virtualenv. -8. Commit your changes and push your branch to GitHub:: +7. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." From 711e72b89613befb26a86db2350ed0eea8b19047 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 30 May 2022 16:54:35 -0400 Subject: [PATCH 06/51] FIX: account for changes on Matplotlib main --- mpl_gui/tests/conftest.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/mpl_gui/tests/conftest.py b/mpl_gui/tests/conftest.py index 7d11b9f..dea8c2d 100644 --- a/mpl_gui/tests/conftest.py +++ b/mpl_gui/tests/conftest.py @@ -12,15 +12,6 @@ sys.modules["matplotlib.pyplot"] = None -class TestCanvas(FigureCanvasBase): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.call_info = {} - - def start_event_loop(self, timeout=0): - self.call_info["start_event_loop"] = {"timeout": timeout} - - class TestManger(FigureManagerBase): _active_managers = None @@ -35,6 +26,17 @@ def destroy(self): self.call_info["destroy"] = {} +class TestCanvas(FigureCanvasBase): + manager_class = TestManger + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.call_info = {} + + def start_event_loop(self, timeout=0): + self.call_info["start_event_loop"] = {"timeout": timeout} + + class TestShow(ShowBase): def mainloop(self): ... From 59bec65ed6c8368b5bcfffaf4e9a035a9be1e836 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 30 May 2022 17:34:44 -0400 Subject: [PATCH 07/51] DOC: comment out language in conf.py Setting it to None was causing sphinx warnings. --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b316225..8ed237a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -95,7 +95,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +# language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. From ea885dc70957cd43257e07dbc57055683651f576 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 20 Sep 2022 00:27:44 -0400 Subject: [PATCH 08/51] Update to 3.6 Sphinx theme --- docs/source/conf.py | 9 ++++++--- requirements-doc.txt | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 8ed237a..356e60a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -124,8 +124,12 @@ html_theme = "mpl_sphinx_theme" html_theme_options = { - "native_site": False, - "logo_link": "index", + "navbar_links": "server-stable", + "logo": { + "link": "index", + "image_light": "images/logo2.svg", + "image_dark": "images/logo_dark.svg", + }, # collapse_navigation in pydata-sphinx-theme is slow, so skipped for local # and CI builds https://github.com/pydata/pydata-sphinx-theme/pull/386 "collapse_navigation": not is_release_build, @@ -156,7 +160,6 @@ html_sidebars = { "**": [ "relations.html", # needs 'show_related': True theme option to display - "searchbox.html", ] } diff --git a/requirements-doc.txt b/requirements-doc.txt index 4896ed6..c796a23 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -2,7 +2,7 @@ ipython jinja2>3 matplotlib -mpl-sphinx-theme +mpl-sphinx-theme~=3.6.0 numpydoc sphinx sphinx-copybutton From db99ca427224c59af00464002e7334091e3edd35 Mon Sep 17 00:00:00 2001 From: LGTM Migrator Date: Wed, 9 Nov 2022 07:37:49 +0000 Subject: [PATCH 09/51] Add CodeQL workflow for GitHub code scanning --- .github/workflows/codeql.yml | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..ccaac44 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: "39 10 * * 5" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ python ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" From d44701e3302a1f00e6d51d85de365d42f7738006 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 27 Mar 2023 21:46:12 -0400 Subject: [PATCH 10/51] Update to mpl-sphinx-theme 3.7 --- docs/source/conf.py | 7 +------ requirements-doc.txt | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 356e60a..d94b651 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -124,12 +124,7 @@ html_theme = "mpl_sphinx_theme" html_theme_options = { - "navbar_links": "server-stable", - "logo": { - "link": "index", - "image_light": "images/logo2.svg", - "image_dark": "images/logo_dark.svg", - }, + "navbar_links": ("absolute", "server-stable"), # collapse_navigation in pydata-sphinx-theme is slow, so skipped for local # and CI builds https://github.com/pydata/pydata-sphinx-theme/pull/386 "collapse_navigation": not is_release_build, diff --git a/requirements-doc.txt b/requirements-doc.txt index c796a23..9288da8 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -2,7 +2,7 @@ ipython jinja2>3 matplotlib -mpl-sphinx-theme~=3.6.0 +mpl-sphinx-theme~=3.7.1 numpydoc sphinx sphinx-copybutton From 32dd7496a048250a5a2c26224218b3c7de572272 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 27 Mar 2023 23:48:10 -0400 Subject: [PATCH 11/51] Set release tag when publishing --- .github/workflows/docs.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2d81622..09235f6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,6 +2,9 @@ name: Docs on: [push, pull_request] +env: + IS_RELEASE: | + ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} jobs: build: @@ -14,9 +17,14 @@ jobs: - name: Install mpl-gui run: python -m pip install -v . - name: Build - run: make -Cdocs html + run: | + if [ "x${IS_RELEASE}" == "xtrue" ]; then + O="-t release" + fi + make -Cdocs html O="$O" + - name: Publish - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + if: ${{ env.IS_RELEASE == 'true' }} uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} From a76af262cf660dc827f06f4d9a18464d9bddaea1 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 14 Sep 2023 16:45:05 -0400 Subject: [PATCH 12/51] Update to mpl-sphinx-theme 3.8.0 --- requirements-doc.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-doc.txt b/requirements-doc.txt index 9288da8..1cedeab 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -2,7 +2,7 @@ ipython jinja2>3 matplotlib -mpl-sphinx-theme~=3.7.1 +mpl-sphinx-theme~=3.8.0 numpydoc sphinx sphinx-copybutton From 1c2e4cbcc6ee1e7e301f8167a12fe180c421e8fb Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 31 Jan 2022 01:07:39 -0500 Subject: [PATCH 13/51] ENH: add a module for pyplot like UX close #2 --- mpl_gui/registry.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 mpl_gui/registry.py diff --git a/mpl_gui/registry.py b/mpl_gui/registry.py new file mode 100644 index 0000000..d9aa720 --- /dev/null +++ b/mpl_gui/registry.py @@ -0,0 +1,30 @@ +"""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 + +_fr = _FigureRegistry() + +_fr_exports = [ + "figure", + "subplots", + "subplot_mosaic", + "by_label", + "show", + "show_all", + "close", + "close_all", +] + +for k in _fr_exports: + locals()[k] = getattr(_fr, k) + +# 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 aff3c3db254c4c9861ebd2be779177bd6cad4464 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 1 Feb 2022 18:42:08 -0500 Subject: [PATCH 14/51] API/ENH: mimic pyplot's "numbering" - make passing num required from promote_figure - make _creation.figure function naive to interactive mode - switch from list to dict keyed on Figures - go with pyplot style numbering - improve the clean up logic to try and not leak refs Closes #4 Co-authored-by: Jody Klymak --- mpl_gui/__init__.py | 132 ++++++++++++++++++++++++++------- mpl_gui/_creation.py | 11 --- mpl_gui/_manage_backend.py | 2 +- mpl_gui/_promotion.py | 9 ++- mpl_gui/registry.py | 9 +++ mpl_gui/tests/conftest.py | 15 +++- mpl_gui/tests/test_examples.py | 34 ++++++++- 7 files changed, 163 insertions(+), 49 deletions(-) diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index 8e737e3..999a216 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -12,9 +12,11 @@ to have smooth integration with the GUI event loop as with pyplot. """ -import logging +from collections import Counter import functools -from itertools import count +import logging +import warnings +import weakref from matplotlib.backend_bases import FigureCanvasBase as _FigureCanvasBase @@ -68,7 +70,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)) + managers.append(promote_figure(fig, num=None)) if block is None: block = not is_interactive() @@ -115,32 +117,41 @@ def __init__(self, *, block=None, timeout=0, prefix="Figure "): # settings stashed to set defaults on show self._timeout = timeout self._block = block - # Settings / state to control the default figure label - self._count = count() - self._prefix = prefix # the canonical location for storing the Figures this registry owns. - # any additional views must never include a figure not in the list but + # any additional views must never include a figure that is not a key but # may omit figures - self.figures = [] + self._fig_to_number = dict() + # Settings / state to control the default figure label + self._prefix = prefix + + @property + def figures(self): + return tuple(self._fig_to_number) def _register_fig(self, fig): # if the user closes the figure by any other mechanism, drop our # reference to it. This is important for getting a "pyplot" like user # experience - fig.canvas.mpl_connect( - "close_event", - lambda e: self.figures.remove(fig) if fig in self.figures else None, - ) - # hold a hard reference to the figure. - self.figures.append(fig) + def registry_cleanup(fig_wr): + fig = fig_wr() + if fig is not None: + if fig.canvas is not None: + fig.canvas.mpl_disconnect(cid) + self.close(fig) + + fig_wr = weakref.ref(fig) + cid = fig.canvas.mpl_connect("close_event", lambda e: registry_cleanup(fig_wr)) # Make sure we give the figure a quasi-unique label. We will never set # the same label twice, but will not over-ride any user label (but # empty string) on a Figure so if they provide duplicate labels, change # the labels under us, or provide a label that will be shadowed in the # future it will be what it is. - fignum = next(self._count) + fignum = max(self._fig_to_number.values(), default=-1) + 1 if fig.get_label() == "": fig.set_label(f"{self._prefix}{fignum:d}") + self._fig_to_number[fig] = fignum + if is_interactive(): + promote_figure(fig, num=fignum) return fig @property @@ -150,7 +161,27 @@ def by_label(self): If there are duplicate labels, newer figures will take precedence. """ - return {fig.get_label(): fig for fig in self.figures} + mapping = {fig.get_label(): fig for fig in self.figures} + if len(mapping) != len(self.figures): + counts = Counter(fig.get_label() for fig in self.figures) + multiples = {k: v for k, v in counts.items() if v > 1} + warnings.warn( + ( + f"There are repeated labels ({multiples!r}), but only the newest figure with that label can " + "be returned. " + ), + stacklevel=2, + ) + return mapping + + @property + def by_number(self): + """ + Return a dictionary of the current mapping number -> figures. + + """ + self._ensure_all_figures_promoted() + return {fig.canvas.manager.num: fig for fig in self.figures} @functools.wraps(figure) def figure(self, *args, **kwargs): @@ -167,6 +198,11 @@ def subplot_mosaic(self, *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]) + def show_all(self, *, block=None, timeout=None): """ Show all of the Figures that the FigureRegistry knows about. @@ -198,7 +234,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) # alias to easy pyplot compatibility @@ -219,20 +255,62 @@ def close_all(self): passing it to `show`. """ - for fig in self.figures: - if fig.canvas.manager is not None: - fig.canvas.manager.destroy() + for fig in list(self.figures): + self.close(fig) + + def close(self, val): + """ + Close (meaning destroy the UI) and forget a managed Figure. + + This will do two things: + + - 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. + + 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 + 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. + + Parameters + ---------- + val : 'all' or int or str or Figure + + - 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 + Figure is closed + - If it is a `Figure` instance, then that figure is closed + + """ + if val == "all": + return self.close_all() + # or do we want to close _all_ of the figures with a given label / number? + if isinstance(val, str): + fig = self.by_label[val] + elif isinstance(val, int): + fig = self.by_number[val] + else: + fig = val + if fig not in self.figures: + raise ValueError( + "Trying to close a figure not associated with this Registry." + ) + if fig.canvas.manager is not None: + fig.canvas.manager.destroy() # disconnect figure from canvas fig.canvas.figure = None # disconnect canvas from figure _FigureCanvasBase(figure=fig) - self.figures.clear() - - def close(self, val): - if val != "all": - # TODO close figures 1 at a time - raise RuntimeError("can only close them all") - self.close_all() + assert fig.canvas.manager is None + self._fig_to_number.pop(fig, None) class FigureContext(FigureRegistry): diff --git a/mpl_gui/_creation.py b/mpl_gui/_creation.py index 689b623..7b83ef7 100644 --- a/mpl_gui/_creation.py +++ b/mpl_gui/_creation.py @@ -1,9 +1,6 @@ """Helpers to create new Figures.""" -from matplotlib import is_interactive - from ._figure import Figure -from ._promotion import promote_figure def figure( @@ -15,8 +12,6 @@ def figure( edgecolor=None, # defaults to rc figure.edgecolor frameon=True, FigureClass=Figure, - clear=False, - auto_draw=True, **kwargs, ): """ @@ -75,8 +70,6 @@ def figure( frameon=frameon, **kwargs, ) - if is_interactive(): - promote_figure(fig, auto_draw=auto_draw) return fig @@ -216,10 +209,6 @@ def subplots( # Note that this is the same as plt.subplots(2, 2, sharex=True, sharey=True) - # Create figure number 10 with a single subplot - # and clears it if it already exists. - fig, ax = plt.subplots(num=10, clear=True) - """ fig = figure(**fig_kw) axs = fig.subplots( diff --git a/mpl_gui/_manage_backend.py b/mpl_gui/_manage_backend.py index 1c2aadc..1929b08 100644 --- a/mpl_gui/_manage_backend.py +++ b/mpl_gui/_manage_backend.py @@ -68,7 +68,7 @@ def select_gui_toolkit(newbackend=None): candidates = [best_guess] else: candidates = [] - candidates += ["macosx", "qt5agg", "gtk3agg", "tkagg", "wxagg"] + candidates += ["macosx", "qtagg", "gtk3agg", "tkagg", "wxagg"] # Don't try to fallback on the cairo-based backends as they each have # an additional dependency (pycairo) over the agg-based backend, and diff --git a/mpl_gui/_promotion.py b/mpl_gui/_promotion.py index b497a21..d2aa086 100644 --- a/mpl_gui/_promotion.py +++ b/mpl_gui/_promotion.py @@ -37,10 +37,9 @@ def _auto_draw_if_interactive(fig, val): fig.canvas.draw_idle() -def promote_figure(fig, *, auto_draw=True): +def promote_figure(fig, *, auto_draw=True, num): """Create a new figure manager instance.""" _backend_mod = current_backend_module() - if ( getattr(_backend_mod.FigureCanvas, "required_interactive_framework", None) and threading.current_thread() is not threading.main_thread() @@ -57,7 +56,10 @@ def promote_figure(fig, *, auto_draw=True): return fig.canvas.manager # TODO: do we want to make sure we poison / destroy / decouple the existing # canavs? - manager = _backend_mod.new_figure_manager_given_figure(next(_figure_count), fig) + next_num = next(_figure_count) + manager = _backend_mod.new_figure_manager_given_figure( + num if num is not None else next_num, fig + ) if fig.get_label(): manager.set_window_title(fig.get_label()) @@ -71,7 +73,6 @@ def promote_figure(fig, *, auto_draw=True): # HACK: the callback in backend_bases uses GCF.destroy which misses these # figures by design! def _destroy(event): - if event.key in mpl.rcParams["keymap.quit"]: # grab the manager off the event mgr = event.canvas.manager diff --git a/mpl_gui/registry.py b/mpl_gui/registry.py index d9aa720..e8919b2 100644 --- a/mpl_gui/registry.py +++ b/mpl_gui/registry.py @@ -20,6 +20,15 @@ for k in _fr_exports: locals()[k] = getattr(_fr, k) + +def get_figlabels(): + return list(_fr.by_label) + + +def get_fignums(): + return sorted(_fr.by_number) + + # if one must. `from foo import *` is a language miss-feature, but provide # sensible behavior anyway. __all__ = _fr_exports + [ diff --git a/mpl_gui/tests/conftest.py b/mpl_gui/tests/conftest.py index dea8c2d..e4d8d87 100644 --- a/mpl_gui/tests/conftest.py +++ b/mpl_gui/tests/conftest.py @@ -8,8 +8,19 @@ import sys -# make sure we do not sneakily get pyplot -sys.modules["matplotlib.pyplot"] = None +def pytest_configure(config): + # config is initialized here rather than in pytest.ini so that `pytest + # --pyargs matplotlib` (which would not find pytest.ini) works. The only + # entries in pytest.ini set minversion (which is checked earlier), + # testpaths/python_files, as they are required to properly find the tests + for key, value in [ + ("filterwarnings", "error"), + ]: + config.addinivalue_line(key, value) + + # make sure we do not sneakily get pyplot + assert sys.modules.get("matplotlib.pyplot") is None + sys.modules["matplotlib.pyplot"] = None class TestManger(FigureManagerBase): diff --git a/mpl_gui/tests/test_examples.py b/mpl_gui/tests/test_examples.py index c10e448..0306312 100644 --- a/mpl_gui/tests/test_examples.py +++ b/mpl_gui/tests/test_examples.py @@ -8,7 +8,6 @@ def test_no_pyplot(): - assert sys.modules.get("matplotlib.pyplot", None) is None @@ -75,7 +74,6 @@ class TestException(Exception): if forgiving: assert "start_event_loop" in fig.canvas.call_info else: - assert isinstance(fig.canvas, FigureCanvasBase) @@ -113,10 +111,12 @@ def test_labels_collision(): fr = mg.FigureRegistry(block=False) for j in range(5): fr.figure(label="aardvark") - assert list(fr.by_label) == ["aardvark"] + with pytest.warns(UserWarning, match="{'aardvark': 5}"): + assert list(fr.by_label) == ["aardvark"] assert len(fr.figures) == 5 assert len(set(fr.figures)) == 5 - assert fr.figures[-1] is fr.by_label["aardvark"] + with pytest.warns(UserWarning, match="{'aardvark': 5}"): + assert fr.figures[-1] is fr.by_label["aardvark"] fr.close_all() assert len(fr.by_label) == 0 @@ -138,3 +138,29 @@ def test_change_labels(): for j, f in enumerate(fr.by_label.values()): f.set_label(f"aardvark {j}") assert list(fr.by_label) == [f"aardvark {j}" for j in range(5)] + + +def test_close_one_at_a_time(): + fr = mg.FigureRegistry(block=False) + fig1 = fr.figure(label="a") + fig2 = fr.figure(label="b") + fig3 = fr.figure(label="c") + fr.figure(label="d") + assert len(fr.figures) == 4 + + fr.close(fig1) + assert len(fr.figures) == 3 + assert fig1 not in fr.figures + + fr.close(fig2.get_label()) + assert len(fr.figures) == 2 + assert fig2 not in fr.figures + + fr.show() + + fr.close(fig3.canvas.manager.num) + assert len(fr.figures) == 1 + assert fig3 not in fr.figures + + fr.close("all") + assert len(fr.figures) == 0 From 9574947a24e1542064335ca9672db27cd6d60f39 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 1 Nov 2023 15:06:45 -0400 Subject: [PATCH 15/51] 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 16/51] 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 17/51] 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 18/51] 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 19/51] 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 20/51] 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 21/51] 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 22/51] 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 23/51] 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 24/51] 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 25/51] 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 26/51] 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 27/51] 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 28/51] 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 29/51] 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 30/51] 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 31/51] 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 32/51] 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 33/51] 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 34/51] 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 From 201f6250b475488c10672450cbcd69a1246150b4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 16 May 2024 19:08:16 -0400 Subject: [PATCH 35/51] Update to mpl-sphinx-theme 3.9.0 --- requirements-doc.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-doc.txt b/requirements-doc.txt index 1cedeab..a234e53 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -2,7 +2,7 @@ ipython jinja2>3 matplotlib -mpl-sphinx-theme~=3.8.0 +mpl-sphinx-theme~=3.9.0 numpydoc sphinx sphinx-copybutton From 09e9a9d712f4bf0af672c839257b644dac790b62 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 17 Oct 2023 18:18:22 -0400 Subject: [PATCH 36/51] MNT: don't use else close on for-loop as we return inside --- mpl_gui/_manage_backend.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mpl_gui/_manage_backend.py b/mpl_gui/_manage_backend.py index 1929b08..6e51de2 100644 --- a/mpl_gui/_manage_backend.py +++ b/mpl_gui/_manage_backend.py @@ -79,10 +79,9 @@ def select_gui_toolkit(newbackend=None): except ImportError: continue - else: - # Switching to Agg should always succeed; if it doesn't, let the - # exception propagate out. - return select_gui_toolkit("agg") + # Switching to Agg should always succeed; if it doesn't, let the + # exception propagate out. + return select_gui_toolkit("agg") if isinstance(newbackend, str): # Backends are implemented as modules, but "inherit" default method From 7ad3c9ca1684199799b141a16308bc99636f5a0a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 17 Oct 2023 18:19:14 -0400 Subject: [PATCH 37/51] MNT: remove redundant config in docs conf.py --- docs/source/conf.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 8e7bafc..8453e3f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -108,10 +108,6 @@ # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - default_role = "obj" nitpicky = True From e9ba62019b09a8ed597901a7ce3144e78dd1a4d0 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 17 Oct 2023 18:23:09 -0400 Subject: [PATCH 38/51] MNT: use explict None return --- mpl_gui/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index 2dcb674..d36a205 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -306,7 +306,8 @@ def close(self, val): """ if val == "all": - return self.close_all() + self.close_all() + return # or do we want to close _all_ of the figures with a given label / number? if isinstance(val, str): fig = self.by_label[val] From 916a46dcf9e83e976933e9647a8911f6f1a3bb87 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 18 Oct 2023 14:10:13 -0400 Subject: [PATCH 39/51] MNT: more codeql warnings addressed --- mpl_gui/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mpl_gui/__init__.py b/mpl_gui/__init__.py index d36a205..278f505 100644 --- a/mpl_gui/__init__.py +++ b/mpl_gui/__init__.py @@ -250,7 +250,7 @@ def show_all(self, *, block=None, timeout=None): if timeout is None: timeout = self._timeout self._ensure_all_figures_promoted() - display(*self.figures, block=self._block, timeout=self._timeout) + display(*self.figures, block=self._block, timeout=timeout) # alias to easy pyplot compatibility show = show_all @@ -327,6 +327,7 @@ def close(self, val): _FigureCanvasBase(figure=fig) assert fig.canvas.manager is None self._fig_to_number.pop(fig, None) + return class FigureContext(FigureRegistry): From 8590c35450d65a65b7b717c60b3ef93a611a7d23 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 28 Dec 2024 21:10:53 -0500 Subject: [PATCH 40/51] DOC: account for new warnings in sphinx autosummary --- docs/source/api.rst | 64 ++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index f016221..90c7985 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -12,7 +12,7 @@ Select the backend :toctree: _as_gen - mpl_gui.select_gui_toolkit + select_gui_toolkit Interactivity @@ -22,9 +22,9 @@ Interactivity :toctree: _as_gen - mpl_gui.ion - mpl_gui.ioff - mpl_gui.is_interactive + ion + ioff + is_interactive Unmanaged Figures @@ -41,9 +41,9 @@ a `matplotlib.figure.Figure` instance and creating children in one line. - mpl_gui.figure - mpl_gui.subplots - mpl_gui.subplot_mosaic + figure + subplots + subplot_mosaic @@ -55,8 +55,8 @@ Display - mpl_gui.display - mpl_gui.demote_figure + display + demote_figure @@ -64,12 +64,12 @@ Locally Managed Figures ----------------------- -.. autoclass:: mpl_gui.FigureRegistry +.. autoclass:: FigureRegistry :no-undoc-members: :show-inheritance: -.. autoclass:: mpl_gui.FigureContext +.. autoclass:: FigureContext :no-undoc-members: :show-inheritance: @@ -80,9 +80,9 @@ Create Figures and Axes :toctree: _as_gen - mpl_gui.FigureRegistry.figure - mpl_gui.FigureRegistry.subplots - mpl_gui.FigureRegistry.subplot_mosaic + FigureRegistry.figure + FigureRegistry.subplots + FigureRegistry.subplot_mosaic Access managed figures @@ -92,9 +92,9 @@ Access managed figures :toctree: _as_gen - mpl_gui.FigureRegistry.by_label - mpl_gui.FigureRegistry.by_number - mpl_gui.FigureRegistry.figures + FigureRegistry.by_label + FigureRegistry.by_number + FigureRegistry.figures @@ -106,10 +106,10 @@ Show and close managed Figures :toctree: _as_gen - mpl_gui.FigureRegistry.show_all - mpl_gui.FigureRegistry.close_all - mpl_gui.FigureRegistry.show - mpl_gui.FigureRegistry.close + FigureRegistry.show_all + FigureRegistry.close_all + FigureRegistry.show + FigureRegistry.close @@ -130,9 +130,9 @@ Create Figures and Axes :toctree: _as_gen - mpl_gui.global_figures.figure - mpl_gui.global_figures.subplots - mpl_gui.global_figures.subplot_mosaic + figure + subplots + subplot_mosaic Access managed figures @@ -143,7 +143,7 @@ Access managed figures :toctree: _as_gen - mpl_gui.global_figures.by_label + by_label Show and close managed Figures @@ -156,10 +156,10 @@ Show and close managed Figures - mpl_gui.global_figures.show - mpl_gui.global_figures.show_all - mpl_gui.global_figures.close_all - mpl_gui.global_figures.close + show + show_all + close_all + close Interactivity @@ -170,6 +170,6 @@ Interactivity - mpl_gui.global_figures.ion - mpl_gui.global_figures.ioff - mpl_gui.global_figures.is_interactive + ion + ioff + is_interactive From 75cdfaf76037e1e4a478654ebbcb538f0ee8d99e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 28 Dec 2024 21:03:25 -0500 Subject: [PATCH 41/51] FIX: account for changes to private methods in mpl39 Also require at least mpl 3.9 --- mpl_gui/_manage_backend.py | 3 ++- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mpl_gui/_manage_backend.py b/mpl_gui/_manage_backend.py index 6e51de2..a9b62fc 100644 --- a/mpl_gui/_manage_backend.py +++ b/mpl_gui/_manage_backend.py @@ -5,6 +5,7 @@ from matplotlib import cbook, rcsetup from matplotlib import rcParams, rcParamsDefault +from matplotlib.backends.registry import backend_registry import matplotlib.backend_bases @@ -92,7 +93,7 @@ def select_gui_toolkit(newbackend=None): if newbackend.lower() == "tkagg": backend_name = f"mpl_gui._patched_backends.{newbackend.lower()}" else: - backend_name = cbook._backend_module_name(newbackend) + backend_name = backend_registry._backend_module_name(newbackend) mod = importlib.import_module(backend_name) if hasattr(mod, "Backend"): diff --git a/requirements.txt b/requirements.txt index 7260f92..188b1f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ # List required packages in this file, one per line. -matplotlib +matplotlib>3.9 From 968627a9336e592eddaecff6bccdb48fa56da72f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 28 Dec 2024 21:19:38 -0500 Subject: [PATCH 42/51] API: bump minimum Python to match SPEC0 --- .github/workflows/testing.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a016ff1..cbc76b9 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.9', '3.10', '3.11', '3.12'] + python-version: ['3.11', '3.12', '3.11'] fail-fast: false steps: diff --git a/setup.py b/setup.py index a828056..25aae60 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ # NOTE: This file must remain Python 2 compatible for the foreseeable future, # to ensure that we error out properly for people with outdated setuptools # and/or pip. -min_version = (3, 7) +min_version = (3, 11) if sys.version_info < min_version: error = """ mpl-gui does not support Python {0}.{1}. From a949e535d0f0165631b770031b76132b1414c376 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 17 Jul 2025 21:01:34 -0400 Subject: [PATCH 43/51] CI: pin actions by SHA This eliminates the possibility of a tag being changed under us. --- .github/workflows/docs.yml | 2 +- .github/workflows/testing.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e4c45e3..e3fb2d5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,7 +25,7 @@ jobs: - name: Publish if: ${{ env.IS_RELEASE == 'true' }} - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs/build/html diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index cbc76b9..9c05f89 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -41,4 +41,4 @@ jobs: coverage report - name: Upload code coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@29386c70ef20e286228c72b668a06fd0e8399192 # v1 From a85ffe33f6c47afa2fa9b56cbf20441f74aa3100 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 17 Jul 2025 23:05:26 -0400 Subject: [PATCH 44/51] CI: auto-fix via zizmor May include: - Avoids risky string interpolation. - Prevents checkout premissions from leaking --- .github/workflows/codeql.yml | 2 ++ .github/workflows/docs.yml | 2 ++ .github/workflows/testing.yml | 1 + 3 files changed, 5 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ccaac44..1ff335c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,6 +25,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e3fb2d5..ba2a5e8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + persist-credentials: false - name: Install Python dependencies run: pip install -r requirements-doc.txt diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 9c05f89..0b1143f 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -19,6 +19,7 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 From dfd22986ed536456758fd76986f34c0de9f13453 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 17 Jul 2025 23:18:25 -0400 Subject: [PATCH 45/51] CI: Restrict default permissions Reduces risk of arbitrary code is run by attacker. --- .github/workflows/docs.yml | 2 ++ .github/workflows/testing.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ba2a5e8..3c28072 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,6 @@ name: Docs +permissions: + contents: read on: [push, pull_request] diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0b1143f..cd6717e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -1,4 +1,6 @@ name: Unit Tests +permissions: + contents: read on: push: From 3260bf8fe4e792a02f57e53de8bc87df4480f0ab Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 18 Jul 2025 11:31:23 -0400 Subject: [PATCH 46/51] CI: add dependabot config file for GHA --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fc9f855 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # Location of your workflow files + schedule: + interval: "weekly" # Options: daily, weekly, monthly From 3cf5b6a210f0daf9737cffd08aac8776e3e95d14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:41:50 +0000 Subject: [PATCH 47/51] Bump github/codeql-action from 2 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1ff335c..8587121 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,15 +29,15 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.language }}" From 7540c39cbf50f2be96372fabcb99f3210c1618b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:41:54 +0000 Subject: [PATCH 48/51] Bump actions/checkout from 2 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/testing.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1ff335c..863ef86 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3c28072..961398d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -12,7 +12,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: persist-credentials: false - name: Install Python dependencies diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index cd6717e..3c392de 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false From 0a3fabd156ea2500021b0fe396414d109cedcc28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:41:56 +0000 Subject: [PATCH 49/51] Bump actions/setup-python from 2 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .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 cd6717e..55c09ca 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} From 422ffb97dae59bd00628ee9793f108937009549b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:42:00 +0000 Subject: [PATCH 50/51] Bump codecov/codecov-action from 1.5.2 to 5.4.3 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1.5.2 to 5.4.3. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/29386c70ef20e286228c72b668a06fd0e8399192...18283e04ce6e62d37312384ff67231eb8fd56d24) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 5.4.3 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .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 cd6717e..b7ae72e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -44,4 +44,4 @@ jobs: coverage report - name: Upload code coverage - uses: codecov/codecov-action@29386c70ef20e286228c72b668a06fd0e8399192 # v1 + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v1 From 7ce58636016b46f20a9e5b23673413f0df5f340f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 18 Jul 2025 15:26:25 -0400 Subject: [PATCH 51/51] CI: update version string --- .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 b7ae72e..c58ab14 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -44,4 +44,4 @@ jobs: coverage report - name: Upload code coverage - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v1 + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy