Skip to content

Commit aec794a

Browse files
authoredDec 1, 2023
Merge pull request #17 from tacaswell/doc/rewrite
Doc/rewrite (and code changes inspired by trying to document it)
2 parents ea24bdd + 374c2af commit aec794a

File tree

10 files changed

+287
-164
lines changed

10 files changed

+287
-164
lines changed
 

Diff for: ‎.github/workflows/docs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ env:
88
99
jobs:
1010
build:
11-
runs-on: ubuntu-20.04
11+
runs-on: ubuntu-latest
1212
steps:
1313
- uses: actions/checkout@v2
1414
- name: Install Python dependencies

Diff for: ‎.github/workflows/testing.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ubuntu-latest
1313
strategy:
1414
matrix:
15-
python-version: ['3.7', '3.8', '3.9', '3.10']
15+
python-version: ['3.9', '3.10', '3.11', '3.12']
1616
fail-fast: false
1717
steps:
1818

Diff for: ‎docs/source/api.rst

+113-24
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,175 @@
1-
mpl gui
2-
=======
1+
mpl gui API Reference
2+
=====================
3+
4+
.. automodule:: mpl_gui
5+
:no-undoc-members:
36

4-
.. module:: mpl_gui
57

6-
Show
7-
----
88

9+
Select the backend
10+
------------------
911
.. autosummary::
1012
:toctree: _as_gen
11-
:nosignatures:
1213

13-
mpl_gui.show
14+
15+
mpl_gui.select_gui_toolkit
1416

1517

1618
Interactivity
1719
-------------
1820

1921
.. autosummary::
2022
:toctree: _as_gen
21-
:nosignatures:
23+
2224

2325
mpl_gui.ion
2426
mpl_gui.ioff
2527
mpl_gui.is_interactive
2628

2729

28-
Figure Fabrication
29-
------------------
30+
Unmanaged Figures
31+
-----------------
3032

31-
Un-managed
32-
++++++++++
33+
Figure Creation
34+
+++++++++++++++
3335

36+
These are not strictly necessary as they are only thin wrappers around creating
37+
a `matplotlib.figure.Figure` instance and creating children in one line.
3438

3539
.. autosummary::
3640
:toctree: _as_gen
37-
:nosignatures:
41+
42+
3843

3944
mpl_gui.figure
4045
mpl_gui.subplots
4146
mpl_gui.subplot_mosaic
4247

4348

49+
50+
Display
51+
+++++++
52+
4453
.. autosummary::
4554
:toctree: _as_gen
46-
:nosignatures:
4755

48-
mpl_gui.promote_figure
4956

50-
Managed
51-
+++++++
57+
58+
mpl_gui.display
59+
mpl_gui.demote_figure
60+
61+
62+
63+
Locally Managed Figures
64+
-----------------------
5265

5366

5467
.. autoclass:: mpl_gui.FigureRegistry
5568
:no-undoc-members:
5669
:show-inheritance:
5770

5871

72+
.. autoclass:: mpl_gui.FigureContext
73+
:no-undoc-members:
74+
:show-inheritance:
75+
76+
Create Figures and Axes
77+
+++++++++++++++++++++++
78+
5979
.. autosummary::
6080
:toctree: _as_gen
61-
:nosignatures:
81+
6282

6383
mpl_gui.FigureRegistry.figure
6484
mpl_gui.FigureRegistry.subplots
6585
mpl_gui.FigureRegistry.subplot_mosaic
86+
87+
88+
Access managed figures
89+
++++++++++++++++++++++
90+
91+
.. autosummary::
92+
:toctree: _as_gen
93+
94+
6695
mpl_gui.FigureRegistry.by_label
96+
mpl_gui.FigureRegistry.by_number
97+
mpl_gui.FigureRegistry.figures
98+
99+
100+
101+
Show and close managed Figures
102+
++++++++++++++++++++++++++++++
103+
104+
105+
.. autosummary::
106+
:toctree: _as_gen
107+
108+
67109
mpl_gui.FigureRegistry.show_all
68110
mpl_gui.FigureRegistry.close_all
111+
mpl_gui.FigureRegistry.show
112+
mpl_gui.FigureRegistry.close
69113

70114

71-
.. autoclass:: mpl_gui.FigureContext
115+
116+
117+
Globally managed
118+
----------------
119+
120+
121+
.. automodule:: mpl_gui.global_figures
72122
:no-undoc-members:
73-
:show-inheritance:
74123

75124

76125

126+
Create Figures and Axes
127+
+++++++++++++++++++++++
77128

129+
.. autosummary::
130+
:toctree: _as_gen
131+
132+
133+
mpl_gui.global_figures.figure
134+
mpl_gui.global_figures.subplots
135+
mpl_gui.global_figures.subplot_mosaic
136+
137+
138+
Access managed figures
139+
++++++++++++++++++++++
78140

79141

80-
Select the backend
81-
------------------
82142
.. autosummary::
83143
:toctree: _as_gen
84-
:nosignatures:
85144

86-
mpl_gui.select_gui_toolkit
145+
146+
mpl_gui.global_figures.by_label
147+
148+
149+
Show and close managed Figures
150+
++++++++++++++++++++++++++++++
151+
152+
153+
.. autosummary::
154+
:toctree: _as_gen
155+
156+
157+
158+
159+
mpl_gui.global_figures.show
160+
mpl_gui.global_figures.show_all
161+
mpl_gui.global_figures.close_all
162+
mpl_gui.global_figures.close
163+
164+
165+
Interactivity
166+
+++++++++++++
167+
168+
.. autosummary::
169+
:toctree: _as_gen
170+
171+
172+
173+
mpl_gui.global_figures.ion
174+
mpl_gui.global_figures.ioff
175+
mpl_gui.global_figures.is_interactive

Diff for: ‎docs/source/conf.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@
129129
# and CI builds https://github.com/pydata/pydata-sphinx-theme/pull/386
130130
"collapse_navigation": not is_release_build,
131131
"show_prev_next": False,
132+
"navigation_with_keys": False,
133+
# "secondary_sidebar_items": "page-toc.html",
134+
"footer_start": ["copyright", "sphinx-version", "doc_version"],
132135
}
133136
include_analytics = is_release_build
134137
if include_analytics:
@@ -153,9 +156,6 @@
153156
# This is required for the alabaster theme
154157
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
155158
html_sidebars = {
156-
"**": [
157-
"relations.html", # needs 'show_related': True theme option to display
158-
]
159159
}
160160

161161

Diff for: ‎docs/source/index.rst

+94-81
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
=======================
77
mpl-gui Documentation
88
=======================
9+
.. highlight:: python
910

1011
.. toctree::
11-
:maxdepth: 2
12+
:maxdepth: 1
1213

1314
api
1415
release_history
@@ -30,18 +31,12 @@ The pyplot module current serves two critical, but unrelated functions:
3031
While it can be very convenient when working at the prompt, the state-full API
3132
can lead to brittle code that depends on the global state in confusing ways,
3233
particularly when used in library code. On the other hand,
33-
``matplotlib.pyplot`` does a very good job of hiding from the user the fact
34+
`matplotlib.pyplot` does a very good job of hiding from the user the fact
3435
that they are developing a GUI application and handling, along with IPython,
3536
many of the details involved in running a GUI application in parallel with
3637
Python.
3738

3839

39-
Examples
40-
========
41-
42-
.. highlight:: python
43-
44-
4540
If you want to be sure that this code does not secretly depend on pyplot run ::
4641

4742
import sys
@@ -51,10 +46,30 @@ If you want to be sure that this code does not secretly depend on pyplot run ::
5146
which will prevent pyplot from being imported!
5247

5348

54-
showing
55-
-------
5649

57-
The core of the API is `~.show` ::
50+
Selecting the GUI toolkit
51+
=========================
52+
53+
`mpl_gui` makes use of `Matplotlib backends
54+
<https://matplotlib.org/stable/users/explain/backends.html>`_ for actually
55+
providing the GUI bindings. Analagous to `matplotlib.use` and
56+
`matplotlib.pyplot.switch_backend` `mpl_gui` provides
57+
`mpl_gui.select_gui_toolkit` to select which GUI toolkit is used.
58+
`~mpl_gui.select_gui_toolkit` has the same fall-back behavior as
59+
`~matplotlib.pyplot` and stores its state in :rc:`backend`.
60+
61+
`mpl_gui` will
62+
consistently co-exist with `matplotlib.pyplot` managed Figures in the same
63+
process.
64+
65+
66+
67+
User Managed Figures
68+
====================
69+
70+
There are cases where having such a registry may be too much implicit state.
71+
For such cases the underlying tools that `.FigureRegistry` are built on are
72+
explicitly available ::
5873

5974
import mpl_gui as mg
6075
from matplotlib.figure import Figure
@@ -63,20 +78,16 @@ The core of the API is `~.show` ::
6378

6479
fig2 = Figure()
6580

66-
mg.show([fig1, fig2])
81+
mg.display(fig1, fig2)
6782

6883

6984
which will show both figures and block until they are closed. As part of the
7085
"showing" process, the correct GUI objects will be created, put on the
7186
screen, and the event loop for the host GUI framework is run.
7287

73-
74-
blocking (or not)
75-
+++++++++++++++++
76-
7788
Similar to `plt.ion<matplotlib.pyplot.ion>` and
7889
`plt.ioff<matplotlib.pyplot.ioff>`, we provide `mg.ion()<mpl_gui.ion>` and
79-
`mg.ioff()<mpl_gui.ioff>` which have identical semantics. Thus ::
90+
`mg.ioff()<mpl_gui.ioff>` which have identical semantics. Thus ::
8091

8192
import mpl_gui as mg
8293
from matplotlib.figure import Figure
@@ -85,59 +96,35 @@ Similar to `plt.ion<matplotlib.pyplot.ion>` and
8596
print(mg.is_interactive())
8697
fig = Figure()
8798

88-
mg.show([fig]) # will not block
99+
mg.display([fig]) # will not block
89100

90101
mg.ioff()
91102
print(mg.is_interactive())
92-
mg.show([fig]) # will block!
103+
mg.display(fig) # will block!
93104

94105

95106
As with `plt.show<matplotlib.pyplot.show>`, you can explicitly control the
96-
blocking behavior of `mg.show<.show>` via the *block* keyword argument ::
107+
blocking behavior of `mg.display<mpl_gui.display>` via the *block* keyword argument ::
97108

98109
import mpl_gui as mg
99110
from matplotlib.figure import Figure
100111

101112
fig = Figure(label='control blocking')
102113

103-
mg.show([fig], block=False) # will never block
104-
mg.show([fig], block=True) # will always block
114+
mg.display(fig, block=False) # will never block
115+
mg.display(fig, block=True) # will always block
105116

106117

107118
The interactive state is shared Matplotlib and can also be controlled with
108119
`matplotlib.interactive` and queried via `matplotlib.is_interactive`.
109120

110121

111-
Figure and Axes Creation
112-
------------------------
113-
114-
In analogy with `matplotlib.pyplot` we also provide `~mpl_gui.figure`,
115-
`~mpl_gui.subplots` and `~mpl_gui.subplot_mosaic` ::
116122

117-
import mpl_gui as mg
118-
fig1 = mg.figure()
119-
fig2, axs = mg.subplots(2, 2)
120-
fig3, axd = mg.subplot_mosaic('AA\nBC')
121-
122-
mg.show([fig1, fig2, fig3])
123-
124-
If `mpl_gui` is in "interactive mode", `mpl_gui.figure`, `mpl_gui.subplots` and
125-
`mpl_gui.subplot_mosaic` will automatically put the new Figure in a window on
126-
the screen (but not run the event loop).
127-
128-
129-
130-
FigureRegistry
131-
--------------
123+
Locally Managed Figures
124+
=======================
132125

133-
In the above examples it is the responsibility of the user to keep track of the
134-
`~matplotlib.figure.Figure` instances that are created. If the user does not keep a hard
135-
reference to the ``fig`` object, either directly or indirectly through its
136-
children, then it will be garbage collected like any other Python object.
137-
While this can be advantageous in some cases (such as scripts or functions that
138-
create many transient figures). It loses the convenience of
139-
`matplotlib.pyplot` keeping track of the instances for you. To this end we
140-
also have provided `.FigureRegistry` ::
126+
To avoid the issues with global state the objects you can create a local `.FigureRegistry`.
127+
It keeps much of the convenience of the ``pyplot`` API but without the risk of global state ::
141128

142129
import mpl_gui as mg
143130

@@ -153,60 +140,86 @@ also have provided `.FigureRegistry` ::
153140
fr.close_all() # will close all three figures
154141
fr.close('all') # alias for pyplot compatibility
155142

156-
Thus, if you are only using this restricted set of the pyplot API then you can change ::
157143

158-
import matplotlib.pyplot as plt
159-
160-
to ::
161-
162-
import mpl_gui as mg
163-
plt = mg.FigureRegistry()
164-
165-
and have a (mostly) drop-in replacement.
166-
167-
Additionally, there is a `.FigureRegistry.by_label` accessory that returns
168-
a dictionary mapping the Figures' labels to each Figure ::
144+
Additionally, there are the `.FigureRegistry.by_label`, `.FigureRegistry.by_number`,
145+
`.FigureRegistry.figures` accessors that returns a dictionary mapping the
146+
Figures' labels to each Figure, the figures number to Figure, and a tuple of known Figures::
169147

170148
import mpl_gui as mg
171149

172150
fr = mg.FigureRegistry()
173151

174152
figA = fr.figure(label='A')
175-
figB = fr.subplots(2, 2, label='B')
153+
figB, axs = fr.subplots(2, 2, label='B')
176154

177155
fr.by_label['A'] is figA
178156
fr.by_label['B'] is figB
179157

180-
FigureContext
181-
-------------
158+
fr.by_number[0] is figA
159+
fr.by_number[1] is figB
160+
161+
fr.figures == (figA, figB)
162+
163+
fr.show()
164+
165+
The `.FigureRegistry` is local state so that if the user drops all references
166+
to it it will be eligible for garbage collection. If there are no other
167+
references to the ``Figure`` objects it is likely that they may be closed when
168+
the garbage collector runs!
169+
182170

183171
A very common use case is to make several figures and then show them all
184-
together at the end. To facilitate this we provide a sub-class of
185-
`.FigureRegistry` that can be used as a context manager that (locally) keeps
172+
together at the end. To facilitate this we provide a `.FigureContext` that is
173+
a `.FigureRegistry` that can be used as a context manager that (locally) keeps
186174
track of the created figures and shows them on exit ::
187175

188176
import mpl_gui as mg
189177

190-
with mg.FigureContext() as fc:
178+
with mg.FigureContext(block=None) as fc:
191179
fc.subplot_mosaic('AA\nBC')
192180
fc.figure()
193181
fc.subplots(2, 2)
194182

195183

196184
This will create 3 figures and block on ``__exit__``. The blocking
197-
behavior depends on ``mg.is_interacitve()`` (and follow the behavior of
198-
``mg.show`` or can explicitly controlled via the *block* keyword argument).
185+
behavior depends on `~mpl_gui.is_interactive()` (and follow the behavior of
186+
`.display` and `.FigureRegistry.show` can explicitly controlled via the *block* keyword argument).
199187

188+
The `.global_figures` module is implemented by having a singleton `.FigureRegistry`
189+
at the module level.
200190

201-
Selecting the GUI toolkit
202-
-------------------------
203191

204-
`mpl_gui` makes use of `Matplotlib backends
205-
<https://matplotlib.org/stable/users/explain/backends.html>`_ for actually
206-
providing the GUI bindings. Analagous to `matplotlib.use` and
207-
`matplotlib.pyplot.switch_backend` `mpl_gui` provides
208-
`mpl_gui.select_gui_toolkit` to select which GUI toolkit is used.
209-
`~mpl_gui.select_gui_toolkit` has the same fall-back behavior as
210-
`~matplotlib.pyplot` and stores its state in :rc:`backend`. `mpl_gui` will
211-
consistently co-exist with `matplotlib.pyplot` managed Figures in the same
212-
process.
192+
193+
194+
Globally Managed Figures
195+
========================
196+
197+
198+
The `mpl_gui.global_figures` module provides a direct analogy to the
199+
`matplotlib.pyplot` behavior of having a global registry of figures. Thus, any
200+
figures created via the functions in `.global_figures` will remain alive until they
201+
have been cleared from the registry (and the user has dropped all other
202+
references). While it can be convenient, it carries with it the risk inherent
203+
in any use of global state.
204+
205+
The `matplotlib.pyplot` API related to figure creation, showing, and closing is a drop-in replacement:
206+
207+
::
208+
209+
import mpl_gui.global_figures as gfigs
210+
211+
fig = gfigs.figure()
212+
fig, ax = gfigs.subplots()
213+
fig, axd = gfigs.subplot_mosaic('AA\nCD')
214+
215+
gfigs.show(block=True) # blocks until all figures are closed
216+
gfigs.show(block=True, timeout=1000) # blocks for up to 1s or all figures are closed
217+
gfigs.show(block=False) # does not block
218+
gfigs.show() # depends on if in "interacitve mode"
219+
220+
gfigs.ion() # turn on interactive mode
221+
gfigs.ioff() # turn off interactive mode
222+
gfigs.is_interactive() # query interactive state
223+
224+
gfigs.close('all') # close all open figures
225+
gfigs.close(fig) # close a particular figure

Diff for: ‎mpl_gui/__init__.py

+33-18
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,23 @@
2222

2323
from ._figure import Figure # noqa: F401
2424

25-
from ._manage_interactive import ion, ioff, is_interactive # noqa: F401
26-
from ._manage_backend import select_gui_toolkit # noqa: F401
25+
from ._manage_interactive import ( # noqa: F401
26+
ion as ion,
27+
ioff as ioff,
28+
is_interactive as is_interactive,
29+
)
30+
from ._manage_backend import select_gui_toolkit as select_gui_toolkit # noqa: F401
2731
from ._manage_backend import current_backend_module as _cbm
28-
from ._promotion import promote_figure as promote_figure
29-
from ._creation import figure, subplots, subplot_mosaic # noqa: F401
32+
from ._promotion import (
33+
promote_figure as _promote_figure,
34+
demote_figure as demote_figure,
35+
)
36+
from ._creation import (
37+
figure as figure,
38+
subplots as subplots,
39+
subplot_mosaic as subplot_mosaic,
40+
)
41+
3042

3143
from ._version import get_versions
3244

@@ -37,13 +49,13 @@
3749
_log = logging.getLogger(__name__)
3850

3951

40-
def show(figs, *, block=None, timeout=0):
52+
def display(*figs, block=None, timeout=0):
4153
"""
4254
Show the figures and maybe block.
4355
4456
Parameters
4557
----------
46-
figs : List[Figure]
58+
*figs : Figure
4759
The figures to show. If they do not currently have a GUI aware
4860
canvas + manager attached they will be promoted.
4961
@@ -60,6 +72,9 @@ def show(figs, *, block=None, timeout=0):
6072
Defaults to True in non-interactive mode and to False in interactive
6173
mode (see `.is_interactive`).
6274
75+
timeout : float, optional
76+
How long to run the event loop in msec if blocking.
77+
6378
"""
6479
# TODO handle single figure
6580

@@ -70,7 +85,7 @@ def show(figs, *, block=None, timeout=0):
7085
if fig.canvas.manager is not None:
7186
managers.append(fig.canvas.manager)
7287
else:
73-
managers.append(promote_figure(fig, num=None))
88+
managers.append(_promote_figure(fig, num=None))
7489

7590
if block is None:
7691
block = not is_interactive()
@@ -151,7 +166,7 @@ def registry_cleanup(fig_wr):
151166
fig.set_label(f"{self._prefix}{fignum:d}")
152167
self._fig_to_number[fig] = fignum
153168
if is_interactive():
154-
promote_figure(fig, num=fignum)
169+
_promote_figure(fig, num=fignum)
155170
return fig
156171

157172
@property
@@ -201,7 +216,7 @@ def subplot_mosaic(self, *args, **kwargs):
201216
def _ensure_all_figures_promoted(self):
202217
for f in self.figures:
203218
if f.canvas.manager is None:
204-
promote_figure(f, num=self._fig_to_number[f])
219+
_promote_figure(f, num=self._fig_to_number[f])
205220

206221
def show_all(self, *, block=None, timeout=None):
207222
"""
@@ -235,7 +250,7 @@ def show_all(self, *, block=None, timeout=None):
235250
if timeout is None:
236251
timeout = self._timeout
237252
self._ensure_all_figures_promoted()
238-
show(self.figures, block=self._block, timeout=self._timeout)
253+
display(*self.figures, block=self._block, timeout=self._timeout)
239254

240255
# alias to easy pyplot compatibility
241256
show = show_all
@@ -252,7 +267,7 @@ def close_all(self):
252267
4. drops its hard reference to the Figure
253268
254269
If the user still holds a reference to the Figure it can be revived by
255-
passing it to `show`.
270+
passing it to `mpl_gui.display`.
256271
257272
"""
258273
for fig in list(self.figures):
@@ -267,16 +282,16 @@ def close(self, val):
267282
- start the destruction process of an UI (the event loop may need to
268283
run to complete this process and if the user is holding hard
269284
references to any of the UI elements they may remain alive).
270-
- Remove the `Figure` from this Registry.
285+
- Remove the `~matplotlib.figure.Figure` from this Registry.
271286
272287
We will no longer have any hard references to the Figure, but if
273-
the user does the `Figure` (and its components) will not be garbage
288+
the user does the `~matplotlib.figure.Figure` (and its components) will not be garbage
274289
collected. Due to the circular references in Matplotlib these
275290
objects may not be collected until the full cyclic garbage collection
276291
runs.
277292
278-
If the user still has a reference to the `Figure` they can re-show the
279-
figure via `show`, but the `FigureRegistry` will not be aware of it.
293+
If the user still has a reference to the `~matplotlib.figure.Figure` they can re-show the
294+
figure via `show`, but the `.FigureRegistry` will not be aware of it.
280295
281296
Parameters
282297
----------
@@ -285,9 +300,9 @@ def close(self, val):
285300
- The special case of 'all' closes all open Figures
286301
- If any other string is passed, it is interpreted as a key in
287302
`by_label` and that Figure is closed
288-
- If an integer it is interpreted as a key in `by_number` and that
303+
- If an integer it is interpreted as a key in `.FigureRegistry.by_number` and that
289304
Figure is closed
290-
- If it is a `Figure` instance, then that figure is closed
305+
- If it is a `~matplotlib.figure.Figure` instance, then that figure is closed
291306
292307
"""
293308
if val == "all":
@@ -356,7 +371,7 @@ def __enter__(self):
356371
def __exit__(self, exc_type, exc_value, traceback):
357372
if exc_value is not None and not self._forgive_failure:
358373
return
359-
show(self.figures, block=self._block, timeout=self._timeout)
374+
display(*self.figures, block=self._block, timeout=self._timeout)
360375

361376

362377
# from mpl_gui import * # is a langauge miss-feature

Diff for: ‎mpl_gui/_manage_interactive.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ def is_interactive():
1414
1515
- newly created figures will be shown immediately;
1616
- figures will automatically redraw on change;
17-
- `mpl_gui.show` will not block by default.
17+
- `.display` will not block by default.
1818
- `mpl_gui.FigureContext` will not block on ``__exit__`` by default.
1919
2020
In non-interactive mode:
2121
2222
- newly created figures and changes to figures will not be reflected until
2323
explicitly asked to be;
24-
- `mpl_gui.show` will block by default.
24+
- `.display` will block by default.
2525
- `mpl_gui.FigureContext` will block on ``__exit__`` by default.
2626
2727
See Also
2828
--------
2929
ion : Enable interactive mode.
3030
ioff : Disable interactive mode.
31-
show : Show all figures (and maybe block).
31+
mpl_gui.display : Show all figures (and maybe block).
3232
"""
3333
return _is_interact()
3434

@@ -89,7 +89,7 @@ def ioff():
8989
--------
9090
ion : Enable interactive mode.
9191
is_interactive : Whether interactive mode is enabled.
92-
show : Show all figures (and maybe block).
92+
mpl_gui.display : Show all figures (and maybe block).
9393
9494
Notes
9595
-----
@@ -124,7 +124,7 @@ def ion():
124124
--------
125125
ioff : Disable interactive mode.
126126
is_interactive : Whether interactive mode is enabled.
127-
show : Show all figures (and maybe block).
127+
mpl_gui.display : Show all figures (and maybe block).
128128
129129
Notes
130130
-----

Diff for: ‎mpl_gui/_promotion.py

+26-17
Original file line numberDiff line numberDiff line change
@@ -72,29 +72,38 @@ def promote_figure(fig, *, auto_draw=True, num):
7272

7373
# HACK: the callback in backend_bases uses GCF.destroy which misses these
7474
# figures by design!
75-
def _destroy(event):
75+
def _destroy_on_hotkey(event):
7676
if event.key in mpl.rcParams["keymap.quit"]:
7777
# grab the manager off the event
7878
mgr = event.canvas.manager
7979
if mgr is None:
80-
raise RuntimeError("Should never be here, please report a bug")
81-
fig = event.canvas.figure
82-
# remove this callback. Callbacks lives on the Figure so survive
83-
# the canvas being replaced.
84-
old_cid = getattr(mgr, "_destroy_cid", None)
85-
if old_cid is not None:
86-
fig.canvas.mpl_disconnect(old_cid)
87-
mgr._destroy_cid = None
80+
raise RuntimeError("Should never be here, please report a bug.")
8881
# close the window
8982
mgr.destroy()
90-
# disconnect the manager from the canvas
91-
fig.canvas.manager = None
92-
# reset the dpi
93-
fig.dpi = getattr(fig, "_original_dpi", fig.dpi)
94-
# Go back to "base" canvas
95-
# (this sets state on fig in the canvas init)
96-
FigureCanvasBase(fig)
9783

98-
manager._destroy_cid = fig.canvas.mpl_connect("key_press_event", _destroy)
84+
# remove this callback. Callbacks live on the Figure so survive the canvas
85+
# being replaced.
86+
fig._destroy_cid = fig.canvas.mpl_connect("key_press_event", _destroy_on_hotkey)
9987

10088
return manager
89+
90+
91+
def demote_figure(fig):
92+
"""Fully clear all GUI elements from the `~matplotlib.figure.Figure`.
93+
94+
The opposite of what is done during `mpl_gui.display`.
95+
96+
Parameters
97+
----------
98+
fig : matplotlib.figure.Figure
99+
100+
"""
101+
fig.canvas.destroy()
102+
fig.canvas.manager = None
103+
original_dpi = getattr(fig, "_original_dpi", fig.dpi)
104+
if (cid := getattr(fig, '_destroy_cid', None)) is not None:
105+
fig.canvas.mpl_disconnect(cid)
106+
FigureCanvasBase(fig)
107+
fig.dpi = original_dpi
108+
109+
return fig

Diff for: ‎mpl_gui/registry.py renamed to ‎mpl_gui/global_figures.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""Reproduces the module-level pyplot UX for Figure management."""
22

33
from . import FigureRegistry as _FigureRegistry
4-
from ._manage_backend import select_gui_toolkit
5-
from ._manage_interactive import ion, ioff, is_interactive
4+
from ._manage_interactive import (
5+
ion as ion,
6+
ioff as ioff,
7+
is_interactive as is_interactive,
8+
)
69

710
_fr = _FigureRegistry()
811

@@ -32,7 +35,6 @@ def get_fignums():
3235
# if one must. `from foo import *` is a language miss-feature, but provide
3336
# sensible behavior anyway.
3437
__all__ = _fr_exports + [
35-
"select_gui_toolkit",
3638
"ion",
3739
"ioff",
3840
"is_interactive",

Diff for: ‎mpl_gui/tests/test_examples.py

+6-11
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,10 @@ def test_no_pyplot():
1414
def test_promotion():
1515
fig = mg.Figure(label="test")
1616
assert fig.canvas.manager is None
17-
mg.show([fig], block=False)
17+
mg.display(*[fig], block=False)
1818
assert fig.canvas.manager is not None
1919

2020

21-
def test_smoke_test_creation():
22-
mg.figure()
23-
mg.subplots()
24-
mg.subplot_mosaic("A\nB")
25-
26-
2721
def test_smoke_test_context():
2822
with mg.FigureContext(block=False) as fc:
2923
fc.figure()
@@ -34,10 +28,11 @@ def test_smoke_test_context():
3428
def test_ion():
3529
with mg.ion():
3630
assert mg.is_interactive()
37-
fig, ax = mg.subplots()
31+
fig = mg.Figure()
32+
ax = fig.subplots()
3833
(ln,) = ax.plot(range(5))
3934
ln.set_color("k")
40-
mg.show([fig], timeout=1)
35+
mg.display(*[fig], timeout=1)
4136
assert "start_event_loop" not in fig.canvas.call_info
4237

4338

@@ -48,7 +43,7 @@ def test_ioff():
4843

4944
def test_timeout():
5045
fig = mg.Figure()
51-
mg.show([fig], block=True, timeout=1)
46+
mg.display(*[fig], block=True, timeout=1)
5247
assert "start_event_loop" in fig.canvas.call_info
5348

5449

@@ -94,7 +89,7 @@ def test_close_all():
9489

9590
# test revive
9691
old_canvas = fig.canvas
97-
mg.show([fig])
92+
mg.display(fig)
9893
assert fig.canvas is not old_canvas
9994

10095

0 commit comments

Comments
 (0)
Please sign in to comment.