diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 8e60267ed608..19113d399626 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -211,3 +211,24 @@ def ipython_in_subprocess(requested_backend_or_gui_framework, all_expected_backe ) assert proc.stdout.strip().endswith(f"'{expected_backend}'") + + +def is_ci_environment(): + # Common CI variables + ci_environment_variables = [ + 'CI', # Generic CI environment variable + 'CONTINUOUS_INTEGRATION', # Generic CI environment variable + 'TRAVIS', # Travis CI + 'CIRCLECI', # CircleCI + 'JENKINS', # Jenkins + 'GITLAB_CI', # GitLab CI + 'GITHUB_ACTIONS', # GitHub Actions + 'TEAMCITY_VERSION' # TeamCity + # Add other CI environment variables as needed + ] + + for env_var in ci_environment_variables: + if os.getenv(env_var): + return True + + return False diff --git a/lib/matplotlib/testing/__init__.pyi b/lib/matplotlib/testing/__init__.pyi index 1f52a8ccb8ee..6917b6a5a380 100644 --- a/lib/matplotlib/testing/__init__.pyi +++ b/lib/matplotlib/testing/__init__.pyi @@ -51,3 +51,4 @@ def ipython_in_subprocess( requested_backend_or_gui_framework: str, all_expected_backends: dict[tuple[int, int], str], ) -> None: ... +def is_ci_environment() -> bool: ... diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 6830e7d5c845..d624b5db0ac2 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -19,7 +19,7 @@ import matplotlib as mpl from matplotlib import _c_internal_utils from matplotlib.backend_tools import ToolToggleBase -from matplotlib.testing import subprocess_run_helper as _run_helper +from matplotlib.testing import subprocess_run_helper as _run_helper, is_ci_environment class _WaitForStringPopen(subprocess.Popen): @@ -110,27 +110,6 @@ def _get_testable_interactive_backends(): for env, marks in _get_available_interactive_backends()] -def is_ci_environment(): - # Common CI variables - ci_environment_variables = [ - 'CI', # Generic CI environment variable - 'CONTINUOUS_INTEGRATION', # Generic CI environment variable - 'TRAVIS', # Travis CI - 'CIRCLECI', # CircleCI - 'JENKINS', # Jenkins - 'GITLAB_CI', # GitLab CI - 'GITHUB_ACTIONS', # GitHub Actions - 'TEAMCITY_VERSION' # TeamCity - # Add other CI environment variables as needed - ] - - for env_var in ci_environment_variables: - if os.getenv(env_var): - return True - - return False - - # Reasonable safe values for slower CI/Remote and local architectures. _test_timeout = 120 if is_ci_environment() else 20 diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index 0cba4f392035..1474a67d28aa 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -1,5 +1,7 @@ from io import BytesIO import ast +import os +import sys import pickle import pickletools @@ -8,7 +10,7 @@ import matplotlib as mpl from matplotlib import cm -from matplotlib.testing import subprocess_run_helper +from matplotlib.testing import subprocess_run_helper, is_ci_environment from matplotlib.testing.decorators import check_figures_equal from matplotlib.dates import rrulewrapper from matplotlib.lines import VertexSelector @@ -307,3 +309,23 @@ def test_cycler(): ax = pickle.loads(pickle.dumps(ax)) l, = ax.plot([3, 4]) assert l.get_color() == "m" + + +# Run under an interactive backend to test that we don't try to pickle the +# (interactive and non-picklable) canvas. +def _test_axeswidget_interactive(): + ax = plt.figure().add_subplot() + pickle.dumps(mpl.widgets.Button(ax, "button")) + + +@pytest.mark.xfail( # https://github.com/actions/setup-python/issues/649 + ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and + sys.platform == 'darwin' and sys.version_info[:2] < (3, 11), + reason='Tk version mismatch on Azure macOS CI' + ) +def test_axeswidget_interactive(): + subprocess_run_helper( + _test_axeswidget_interactive, + timeout=120 if is_ci_environment() else 20, + extra_env={'MPLBACKEND': 'tkagg'} + ) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index ed130e6854f2..a298f3ae3d6a 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -90,22 +90,6 @@ def ignore(self, event): """ return not self.active - def _changed_canvas(self): - """ - Someone has switched the canvas on us! - - This happens if `savefig` needs to save to a format the previous - backend did not support (e.g. saving a figure using an Agg based - backend saved to a vector format). - - Returns - ------- - bool - True if the canvas has been changed. - - """ - return self.canvas is not self.ax.figure.canvas - class AxesWidget(Widget): """ @@ -131,9 +115,10 @@ class AxesWidget(Widget): def __init__(self, ax): self.ax = ax - self.canvas = ax.figure.canvas self._cids = [] + canvas = property(lambda self: self.ax.figure.canvas) + def connect_event(self, event, callback): """ Connect a callback function with an event. @@ -1100,7 +1085,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True, def _clear(self, event): """Internal event handler to clear the buttons.""" - if self.ignore(event) or self._changed_canvas(): + if self.ignore(event) or self.canvas.is_saving(): return self._background = self.canvas.copy_from_bbox(self.ax.bbox) self.ax.draw_artist(self._checks) @@ -1677,7 +1662,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *, def _clear(self, event): """Internal event handler to clear the buttons.""" - if self.ignore(event) or self._changed_canvas(): + if self.ignore(event) or self.canvas.is_saving(): return self._background = self.canvas.copy_from_bbox(self.ax.bbox) self.ax.draw_artist(self._buttons) @@ -1933,7 +1918,7 @@ def __init__(self, ax, *, horizOn=True, vertOn=True, useblit=False, def clear(self, event): """Internal event handler to clear the cursor.""" - if self.ignore(event) or self._changed_canvas(): + if self.ignore(event) or self.canvas.is_saving(): return if self.useblit: self.background = self.canvas.copy_from_bbox(self.ax.bbox) @@ -2573,9 +2558,7 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False, self.drag_from_anywhere = drag_from_anywhere self.ignore_event_outside = ignore_event_outside - # Reset canvas so that `new_axes` connects events. - self.canvas = None - self.new_axes(ax, _props=props) + self.new_axes(ax, _props=props, _init=True) # Setup handles self._handle_props = { @@ -2588,14 +2571,15 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False, self._active_handle = None - def new_axes(self, ax, *, _props=None): + def new_axes(self, ax, *, _props=None, _init=False): """Set SpanSelector to operate on a new Axes.""" - self.ax = ax - if self.canvas is not ax.figure.canvas: + reconnect = False + if _init or self.canvas is not ax.figure.canvas: if self.canvas is not None: self.disconnect_events() - - self.canvas = ax.figure.canvas + reconnect = True + self.ax = ax + if reconnect: self.connect_default_events() # Reset diff --git a/lib/matplotlib/widgets.pyi b/lib/matplotlib/widgets.pyi index c85ad2158ee7..58adf85aae60 100644 --- a/lib/matplotlib/widgets.pyi +++ b/lib/matplotlib/widgets.pyi @@ -33,8 +33,9 @@ class Widget: class AxesWidget(Widget): ax: Axes - canvas: FigureCanvasBase | None def __init__(self, ax: Axes) -> None: ... + @property + def canvas(self) -> FigureCanvasBase | None: ... def connect_event(self, event: Event, callback: Callable) -> None: ... def disconnect_events(self) -> None: ... @@ -310,7 +311,6 @@ class SpanSelector(_SelectorWidget): grab_range: float drag_from_anywhere: bool ignore_event_outside: bool - canvas: FigureCanvasBase | None def __init__( self, ax: Axes, @@ -330,7 +330,13 @@ class SpanSelector(_SelectorWidget): ignore_event_outside: bool = ..., snap_values: ArrayLike | None = ..., ) -> None: ... - def new_axes(self, ax: Axes, *, _props: dict[str, Any] | None = ...) -> None: ... + def new_axes( + self, + ax: Axes, + *, + _props: dict[str, Any] | None = ..., + _init: bool = ..., + ) -> None: ... def connect_default_events(self) -> None: ... @property def direction(self) -> Literal["horizontal", "vertical"]: ...
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: