Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit aad2a58

Browse files
authored
Merge pull request #28430 from anntzer/pw
Fix pickling of AxesWidgets.
2 parents 0ad8ee1 + f6bb9ee commit aad2a58

File tree

6 files changed

+67
-54
lines changed

6 files changed

+67
-54
lines changed

lib/matplotlib/testing/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,24 @@ def ipython_in_subprocess(requested_backend_or_gui_framework, all_expected_backe
211211
)
212212

213213
assert proc.stdout.strip().endswith(f"'{expected_backend}'")
214+
215+
216+
def is_ci_environment():
217+
# Common CI variables
218+
ci_environment_variables = [
219+
'CI', # Generic CI environment variable
220+
'CONTINUOUS_INTEGRATION', # Generic CI environment variable
221+
'TRAVIS', # Travis CI
222+
'CIRCLECI', # CircleCI
223+
'JENKINS', # Jenkins
224+
'GITLAB_CI', # GitLab CI
225+
'GITHUB_ACTIONS', # GitHub Actions
226+
'TEAMCITY_VERSION' # TeamCity
227+
# Add other CI environment variables as needed
228+
]
229+
230+
for env_var in ci_environment_variables:
231+
if os.getenv(env_var):
232+
return True
233+
234+
return False

lib/matplotlib/testing/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ def ipython_in_subprocess(
5151
requested_backend_or_gui_framework: str,
5252
all_expected_backends: dict[tuple[int, int], str],
5353
) -> None: ...
54+
def is_ci_environment() -> bool: ...

lib/matplotlib/tests/test_backends_interactive.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import matplotlib as mpl
2020
from matplotlib import _c_internal_utils
2121
from matplotlib.backend_tools import ToolToggleBase
22-
from matplotlib.testing import subprocess_run_helper as _run_helper
22+
from matplotlib.testing import subprocess_run_helper as _run_helper, is_ci_environment
2323

2424

2525
class _WaitForStringPopen(subprocess.Popen):
@@ -110,27 +110,6 @@ def _get_testable_interactive_backends():
110110
for env, marks in _get_available_interactive_backends()]
111111

112112

113-
def is_ci_environment():
114-
# Common CI variables
115-
ci_environment_variables = [
116-
'CI', # Generic CI environment variable
117-
'CONTINUOUS_INTEGRATION', # Generic CI environment variable
118-
'TRAVIS', # Travis CI
119-
'CIRCLECI', # CircleCI
120-
'JENKINS', # Jenkins
121-
'GITLAB_CI', # GitLab CI
122-
'GITHUB_ACTIONS', # GitHub Actions
123-
'TEAMCITY_VERSION' # TeamCity
124-
# Add other CI environment variables as needed
125-
]
126-
127-
for env_var in ci_environment_variables:
128-
if os.getenv(env_var):
129-
return True
130-
131-
return False
132-
133-
134113
# Reasonable safe values for slower CI/Remote and local architectures.
135114
_test_timeout = 120 if is_ci_environment() else 20
136115

lib/matplotlib/tests/test_pickle.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from io import BytesIO
22
import ast
3+
import os
4+
import sys
35
import pickle
46
import pickletools
57

@@ -8,7 +10,7 @@
810

911
import matplotlib as mpl
1012
from matplotlib import cm
11-
from matplotlib.testing import subprocess_run_helper
13+
from matplotlib.testing import subprocess_run_helper, is_ci_environment
1214
from matplotlib.testing.decorators import check_figures_equal
1315
from matplotlib.dates import rrulewrapper
1416
from matplotlib.lines import VertexSelector
@@ -307,3 +309,23 @@ def test_cycler():
307309
ax = pickle.loads(pickle.dumps(ax))
308310
l, = ax.plot([3, 4])
309311
assert l.get_color() == "m"
312+
313+
314+
# Run under an interactive backend to test that we don't try to pickle the
315+
# (interactive and non-picklable) canvas.
316+
def _test_axeswidget_interactive():
317+
ax = plt.figure().add_subplot()
318+
pickle.dumps(mpl.widgets.Button(ax, "button"))
319+
320+
321+
@pytest.mark.xfail( # https://github.com/actions/setup-python/issues/649
322+
('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
323+
sys.platform == 'darwin' and sys.version_info[:2] < (3, 11),
324+
reason='Tk version mismatch on Azure macOS CI'
325+
)
326+
def test_axeswidget_interactive():
327+
subprocess_run_helper(
328+
_test_axeswidget_interactive,
329+
timeout=120 if is_ci_environment() else 20,
330+
extra_env={'MPLBACKEND': 'tkagg'}
331+
)

lib/matplotlib/widgets.py

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,6 @@ def ignore(self, event):
9090
"""
9191
return not self.active
9292

93-
def _changed_canvas(self):
94-
"""
95-
Someone has switched the canvas on us!
96-
97-
This happens if `savefig` needs to save to a format the previous
98-
backend did not support (e.g. saving a figure using an Agg based
99-
backend saved to a vector format).
100-
101-
Returns
102-
-------
103-
bool
104-
True if the canvas has been changed.
105-
106-
"""
107-
return self.canvas is not self.ax.figure.canvas
108-
10993

11094
class AxesWidget(Widget):
11195
"""
@@ -131,9 +115,10 @@ class AxesWidget(Widget):
131115

132116
def __init__(self, ax):
133117
self.ax = ax
134-
self.canvas = ax.figure.canvas
135118
self._cids = []
136119

120+
canvas = property(lambda self: self.ax.figure.canvas)
121+
137122
def connect_event(self, event, callback):
138123
"""
139124
Connect a callback function with an event.
@@ -1100,7 +1085,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
11001085

11011086
def _clear(self, event):
11021087
"""Internal event handler to clear the buttons."""
1103-
if self.ignore(event) or self._changed_canvas():
1088+
if self.ignore(event) or self.canvas.is_saving():
11041089
return
11051090
self._background = self.canvas.copy_from_bbox(self.ax.bbox)
11061091
self.ax.draw_artist(self._checks)
@@ -1677,7 +1662,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16771662

16781663
def _clear(self, event):
16791664
"""Internal event handler to clear the buttons."""
1680-
if self.ignore(event) or self._changed_canvas():
1665+
if self.ignore(event) or self.canvas.is_saving():
16811666
return
16821667
self._background = self.canvas.copy_from_bbox(self.ax.bbox)
16831668
self.ax.draw_artist(self._buttons)
@@ -1933,7 +1918,7 @@ def __init__(self, ax, *, horizOn=True, vertOn=True, useblit=False,
19331918

19341919
def clear(self, event):
19351920
"""Internal event handler to clear the cursor."""
1936-
if self.ignore(event) or self._changed_canvas():
1921+
if self.ignore(event) or self.canvas.is_saving():
19371922
return
19381923
if self.useblit:
19391924
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
@@ -2573,9 +2558,7 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False,
25732558
self.drag_from_anywhere = drag_from_anywhere
25742559
self.ignore_event_outside = ignore_event_outside
25752560

2576-
# Reset canvas so that `new_axes` connects events.
2577-
self.canvas = None
2578-
self.new_axes(ax, _props=props)
2561+
self.new_axes(ax, _props=props, _init=True)
25792562

25802563
# Setup handles
25812564
self._handle_props = {
@@ -2588,14 +2571,15 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False,
25882571

25892572
self._active_handle = None
25902573

2591-
def new_axes(self, ax, *, _props=None):
2574+
def new_axes(self, ax, *, _props=None, _init=False):
25922575
"""Set SpanSelector to operate on a new Axes."""
2593-
self.ax = ax
2594-
if self.canvas is not ax.figure.canvas:
2576+
reconnect = False
2577+
if _init or self.canvas is not ax.figure.canvas:
25952578
if self.canvas is not None:
25962579
self.disconnect_events()
2597-
2598-
self.canvas = ax.figure.canvas
2580+
reconnect = True
2581+
self.ax = ax
2582+
if reconnect:
25992583
self.connect_default_events()
26002584

26012585
# Reset

lib/matplotlib/widgets.pyi

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ class Widget:
3333

3434
class AxesWidget(Widget):
3535
ax: Axes
36-
canvas: FigureCanvasBase | None
3736
def __init__(self, ax: Axes) -> None: ...
37+
@property
38+
def canvas(self) -> FigureCanvasBase | None: ...
3839
def connect_event(self, event: Event, callback: Callable) -> None: ...
3940
def disconnect_events(self) -> None: ...
4041

@@ -310,7 +311,6 @@ class SpanSelector(_SelectorWidget):
310311
grab_range: float
311312
drag_from_anywhere: bool
312313
ignore_event_outside: bool
313-
canvas: FigureCanvasBase | None
314314
def __init__(
315315
self,
316316
ax: Axes,
@@ -330,7 +330,13 @@ class SpanSelector(_SelectorWidget):
330330
ignore_event_outside: bool = ...,
331331
snap_values: ArrayLike | None = ...,
332332
) -> None: ...
333-
def new_axes(self, ax: Axes, *, _props: dict[str, Any] | None = ...) -> None: ...
333+
def new_axes(
334+
self,
335+
ax: Axes,
336+
*,
337+
_props: dict[str, Any] | None = ...,
338+
_init: bool = ...,
339+
) -> None: ...
334340
def connect_default_events(self) -> None: ...
335341
@property
336342
def direction(self) -> Literal["horizontal", "vertical"]: ...

0 commit comments

Comments
 (0)