From e04a9fa38343c8b90bac10f8cd6f08a474f89130 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:16:45 +0200 Subject: [PATCH 01/11] DOC: Use warnings instead of exceptions in gallery order Exceptions cause a hard exit of sphinx, which is undesirable. Incorrect order specification should not break the build. It also make fixes harder since one one gets the first ordering issue and possibly would have to do multiple builds to see and fix multiple ordering issues. --- doc/sphinxext/gallery_order.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/doc/sphinxext/gallery_order.py b/doc/sphinxext/gallery_order.py index 140280344e81..048a856c69e0 100644 --- a/doc/sphinxext/gallery_order.py +++ b/doc/sphinxext/gallery_order.py @@ -7,6 +7,9 @@ from pathlib import Path from sphinx_gallery.sorting import ExplicitOrder +from sphinx.util import logging as sphinx_logging + +logger = sphinx_logging.getLogger(__name__) # Gallery sections shall be displayed in the following order. # Non-matching sections are inserted at the unsorted position @@ -98,7 +101,7 @@ class MplFileExplicitOrder(ExplicitOrder): Use this if you want to ensure that a full order is intentionally maintained. """ def __init__(self, src_dir): - ordered_list = self.read_gallery_order(Path(src_dir)) or [] + ordered_list = self.read_gallery_order(Path(src_dir).resolve()) or [] super().__init__(ordered_list) @staticmethod @@ -135,13 +138,14 @@ def read_gallery_order(src_dir: Path): non_existing_examples = listed_examples - existing_examples missing_examples = existing_examples - listed_examples + rel_txt_path = gallery_order_txt.relative_to(gallery_order_txt.parents[3]) if non_existing_examples: - raise ValueError( - f"The following examples listed in {gallery_order_txt} do not exist: " + logger.warning( + f"The following examples listed in {rel_txt_path} do not exist: " f"{', '.join(non_existing_examples)}") if placeholder_index is None and missing_examples: - raise ValueError( - f"The following examples are not listed in {gallery_order_txt}. " + logger.warning( + f"The following examples are not listed in {rel_txt_path}. " f"Either include them or add a '*' to indicate where not listed " f"examples should be placed: " f"{', '.join(missing_examples)}" From 450daa4a2fccc85d3f5cc193098ddb13d295cd8f Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 5 Jun 2026 08:02:52 +0200 Subject: [PATCH 02/11] DOC: reorder statistics examples --- .../examples/statistics/gallery_order.txt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 galleries/examples/statistics/gallery_order.txt diff --git a/galleries/examples/statistics/gallery_order.txt b/galleries/examples/statistics/gallery_order.txt new file mode 100644 index 000000000000..ff63226ba2fb --- /dev/null +++ b/galleries/examples/statistics/gallery_order.txt @@ -0,0 +1,36 @@ +# Explicit example order. See https://matplotlib.org/devdocs/devel/document.html#order-examples + +# errorbars +errorbar +errorbar_features +errorbar_limits +errorbars_and_boxes +confidence_ellipse + +# histograms +hist +histogram_histtypes +histogram_normalization +histogram_multihist +multiple_histograms_side_by_side +histogram_bihistogram +histogram_cumulative +hexbin_demo +time_series_histogram + +# boxplots +boxplot_demo +boxplot +boxplot_color +bxp +boxplot_vs_violin + +# violinplots +violinplot +customized_violin + +# signal processing +xcorr_acorr_demo +psd_demo +csd_demo +cohere From 899e5a6b103a4c5bc954b1ec73c78ed6cbbb7b48 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:00:46 +0200 Subject: [PATCH 03/11] DOC: reorder ticks examples --- galleries/examples/ticks/gallery_order.txt | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 galleries/examples/ticks/gallery_order.txt diff --git a/galleries/examples/ticks/gallery_order.txt b/galleries/examples/ticks/gallery_order.txt new file mode 100644 index 000000000000..251cd9971aa3 --- /dev/null +++ b/galleries/examples/ticks/gallery_order.txt @@ -0,0 +1,34 @@ +# Explicit example order. See https://matplotlib.org/devdocs/devel/document.html#order-examples + +# positioning and alignment +auto_ticks +tick-locators +major_minor_demo +align_ticklabels +ticks_top_right +centered_ticklabels +ticklabels_rotation +multilevel_ticks + +# formatting +scalarformatter +tick-formatters +custom_ticker1 +dollar_ticks +engineering_formatter +engformatter_offset +tick_labels_from_values + +# date +date +date_formatters_locators +date_concise_formatter +date_demo_convert +date_demo_rrule +date_index_formatter +date_precision_and_epochs + +# other +colorbar_tick_labelling_demo +fig_axes_customize_simple +ticks_too_many From 80bae6fb0941af99c23951ca87b5f79c54be9495 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:44:17 +0200 Subject: [PATCH 04/11] DOC: reorder lines, bars, and markers examples --- .../lines_bars_and_markers/gallery_order.txt | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 galleries/examples/lines_bars_and_markers/gallery_order.txt diff --git a/galleries/examples/lines_bars_and_markers/gallery_order.txt b/galleries/examples/lines_bars_and_markers/gallery_order.txt new file mode 100644 index 000000000000..1e108847e868 --- /dev/null +++ b/galleries/examples/lines_bars_and_markers/gallery_order.txt @@ -0,0 +1,59 @@ +# Explicit example order. See https://matplotlib.org/devdocs/devel/document.html#order-examples + +# lines +simple_plot +linestyles +line_demo_dash_control +markevery_demo +joinstyle +capstyle +marker_reference +axline +multicolored_line +lines_with_ticks_demo + +# bars +barchart +bar_label_demo +bar_stacked +bar_colors +hat_graph +gradient_bar +barh +horizontal_barchart_distribution +broken_barh + +# scatter +scatter_demo2 +multivariate_marker_plot +scatter_star_poly +scatter_with_legend +scatter_masked +scatter_hist + +# step and stairs +stairs_demo +step_demo + +# filled +fill +fill_between_alpha +fill_between_demo +fill_betweenx_demo +stackplot_demo + +# sticks +eventcollection_demo +eventplot_demo +timeline +vline_hline_demo +span_regions +stem_plot + +# other +errorbar_limits_simple +errorbar_subsample +categorical_variables +masked_demo +curve_error_band +spectrum_demo From 3351c76c190224e950d9e659c8e4ebf02ae3111e Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:10:03 +0200 Subject: [PATCH 05/11] DOC: reorder images examples --- .../gallery_order.txt | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 galleries/examples/images_contours_and_fields/gallery_order.txt diff --git a/galleries/examples/images_contours_and_fields/gallery_order.txt b/galleries/examples/images_contours_and_fields/gallery_order.txt new file mode 100644 index 000000000000..371e8c1624bf --- /dev/null +++ b/galleries/examples/images_contours_and_fields/gallery_order.txt @@ -0,0 +1,69 @@ +# Explicit example order. See https://matplotlib.org/devdocs/devel/document.html#order-examples + +# image +image_demo +image_masked +multi_image +image_zcoord + +# interpolation, transparency, and blending +image_antialiasing +interpolation_methods +layer_images +image_transparency_blend +shading_example + +# positioning and clipping +image_clip_path +image_exact_placement + +# quadrilateral +pcolor_demo +pcolormesh_levels +pcolormesh_grids +image_nonuniform +quadmesh_demo + +# triangular +triplot_demo +tripcolor_demo +tricontour_demo +tricontour_smooth_user +tricontour_smooth_delaunay +trigradient_demo +triinterp_demo + +# contour +irregulardatagrid +contour_demo +contour_label_demo +contourf_demo +contour_image +contourf_log +contourf_hatching +contour_corner_mask +contours_in_optimization_demo + +# quiver +quiver_simple_demo +quiver_demo + +# barbs and streamlines +barb_demo +plot_streamplot + +# colormapping +colormap_interactive_adjustment +colormap_normalizations +colormap_normalizations_symlognorm +image_annotated_heatmap + +# other +affine_image +matshow +spy_demos +specgram_demo +barcode_demo +demo_bboximage +figimage_demo +watermark_image From 8b4eb2f54d0ec7f5a2f1194438cbf2e923b596b0 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:36:01 +0200 Subject: [PATCH 06/11] DOC: reorder subplots, axes, and figures examples --- .../gallery_order.txt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 galleries/examples/subplots_axes_and_figures/gallery_order.txt diff --git a/galleries/examples/subplots_axes_and_figures/gallery_order.txt b/galleries/examples/subplots_axes_and_figures/gallery_order.txt new file mode 100644 index 000000000000..5ec9bf1a0d00 --- /dev/null +++ b/galleries/examples/subplots_axes_and_figures/gallery_order.txt @@ -0,0 +1,48 @@ +# Explicit example order. See https://matplotlib.org/devdocs/devel/document.html#order-examples + +# subplots +subplot +subplots_demo +axes_demo +gridspec_customization +gridspec_multicolumn +gridspec_nested +gridspec_and_subplots +subplot2grid +demo_constrained_layout +demo_tight_layout +subplots_adjust +auto_subplots_adjust +ganged_plots + +axes_margins +axes_box_aspect +axis_equal_demo + +# figure +multiple_figs_demo +figure_size_units +subfigures +custom_figure_class + +# shared, twined, and secondary axes +shared_axis_demo +invert_axes +two_scales +fahrenheit_celsius_scales +multiple_yaxis_with_spines +twin_axes_zorder +secondary_axis + +# titles and labels +align_labels_demo +axis_labels_demo +axes_props +figure_title + +# other +axes_zoom_effect +zoom_inset_axes +broken_axis +geo_demo +axhspan_demo From ddfdbb75546f1c3c3f0e44c1885aa15a2e47d444 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:58:01 +0200 Subject: [PATCH 07/11] DOC: Add missing gallery order for Plot types > Gridded data (#31834) Forgot to add this file in #31714. This maintains the order that has been there before. --- galleries/plot_types/arrays/gallery_order.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 galleries/plot_types/arrays/gallery_order.txt diff --git a/galleries/plot_types/arrays/gallery_order.txt b/galleries/plot_types/arrays/gallery_order.txt new file mode 100644 index 000000000000..0cd5a8007064 --- /dev/null +++ b/galleries/plot_types/arrays/gallery_order.txt @@ -0,0 +1,8 @@ +# Explicit example order. See https://matplotlib.org/devdocs/devel/document.html#order-examples +imshow +pcolormesh +contour +contourf +barbs +quiver +streamplot From 80dc791b8f8dd87af7ae6cb145f371ef2619a063 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 5 Jun 2026 12:58:19 -0400 Subject: [PATCH 08/11] Partially revert "Fix test_RcParams_class on Python 3.15" (#31825) This partially reverts commit 43b79764d5a4668d4cce7ccc32b1e047568f8815, as the change has been [reverted in Python 3.15 b2](https://github.com/python/cpython/pull/150249). I have left in the shortening of the lists, so that lines are shorter and have less chance of being wrapped in the future. --- lib/matplotlib/tests/test_rcparams.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 22295f102a45..dc36392ab676 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -79,19 +79,12 @@ def test_RcParams_class(): 'font.size': 12}) expected_repr = """ -RcParams({ - 'font.cursive': ['Zapf Chancery', 'cursive'], +RcParams({'font.cursive': ['Zapf Chancery', 'cursive'], 'font.family': ['sans-serif'], 'font.size': 12.0, - 'font.weight': 'normal', - })""".lstrip() + 'font.weight': 'normal'})""".lstrip() actual_repr = repr(rc) - if sys.version_info[:2] < (3, 15): - actual_repr = (actual_repr - .replace("{'", "{\n '") - .replace("'}", "',\n }")) - assert actual_repr == expected_repr expected_str = """ From eb47c013528409fae59080f38fcc7db55437ff31 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:37:51 +0200 Subject: [PATCH 09/11] DOC: reorder text, labels and annotations examples (#31838) * DOC: reorder text, labels and annotations examples * Apply suggestions from code review Co-authored-by: Elliott Sales de Andrade --------- Co-authored-by: Elliott Sales de Andrade --- .../gallery_order.txt | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 galleries/examples/text_labels_and_annotations/gallery_order.txt diff --git a/galleries/examples/text_labels_and_annotations/gallery_order.txt b/galleries/examples/text_labels_and_annotations/gallery_order.txt new file mode 100644 index 000000000000..2620097be023 --- /dev/null +++ b/galleries/examples/text_labels_and_annotations/gallery_order.txt @@ -0,0 +1,54 @@ +# Explicit example order. See https://matplotlib.org/devdocs/devel/document.html#order-examples + +# text formatting +text_commands +text_fontdict +text_alignment +multiline +autowrap +demo_text_rotation_mode +text_rotation_relative_to_line +rainbow_text +demo_text_path + +# annotations +annotation_basic +annotation_demo +annotation_polar +angles_on_bracket_arrows +fancyarrow_demo +fancytextbox_demo +placing_text_boxes +label_subplots +demo_annotation_box +arrow_demo +angle_annotation + +# legend +legend_demo +legend +custom_legends +figlegend_demo + +# demos +accented_text +dfrac_demo +font_family_rc +font_file +font_table +fonts_demo +fonts_demo_kw +mathtext_asarray +mathtext_demo +mathtext_examples +mathtext_fontfamily_example +stix_fonts_demo +tex_demo +unicode_minus +usetex_baseline_test +usetex_fonteffects + +# other +line_with_text +titles_demo +watermark_text From b3bd035d16a72a2fddf61394794bf2ec71c07713 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 20 Apr 2026 19:33:41 +0100 Subject: [PATCH 10/11] DOC: Build against 3.11.0 of mpl-sphinx-theme --- environment.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/environment.yml b/environment.yml index 068bb10588db..27624759ea86 100644 --- a/environment.yml +++ b/environment.yml @@ -37,7 +37,7 @@ dependencies: - ipywidgets - numpydoc>=1.0 - packaging>=20 - - pydata-sphinx-theme=0.16.1 # required by mpl-sphinx-theme=3.10 + - pydata-sphinx-theme=0.18.0 # required by mpl-sphinx-theme=3.11 - pyyaml - sphinx>=3.0.0,!=6.1.2 - sphinx-copybutton @@ -49,7 +49,7 @@ dependencies: - pikepdf - pip - pip: - - mpl-sphinx-theme~=3.10.0 + - mpl-sphinx-theme~=3.11.0 - sphinxcontrib-svg2pdfconverter>=1.1.0 - sphinxcontrib-video>=0.2.1 # testing diff --git a/pyproject.toml b/pyproject.toml index 1a5d42c1b782..cf0cde26cdce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,7 +112,7 @@ doc = [ "ipywidgets", "ipykernel", "numpydoc>=1.0", - "mpl-sphinx-theme~=3.10.0", + "mpl-sphinx-theme~=3.11.0", "pyyaml", "PyStemmer", "sphinxcontrib-svg2pdfconverter>=1.1.0", From 1e6bcafd1075effae6ef2469a17936f3b56e10f9 Mon Sep 17 00:00:00 2001 From: Ricci Adams <1097294+iccir@users.noreply.github.com> Date: Fri, 5 Jun 2026 19:08:48 -0700 Subject: [PATCH 11/11] FIX: memory management within macos backend Fix several MacOS memory management bugs. Use more auto release pools on every transition from Python to objc. Add error handling to turn objc exceptions into Python exceptions within these blocks. --- src/_macosx.m | 330 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 231 insertions(+), 99 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index 62daf43abe1b..0de0540018a7 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -21,6 +21,37 @@ #define WINDOW_CLOSING 3 +/* When calling into Objective-C from Python, wrap the calls with + BEGIN_OBJC_ENTRY and END_OBJC_ENTRY. This will set up an autorelease + pool as well as catch any Obj-C exceptions thrown. These macros + should be used for any call exposed to Python via the external module + interface. + + To avoid undefined behavior, each END_OBJC_ENTRY should be followed + by a return statement which handles the rare case when an Objective-C + exception was thrown. + + As a convenience, the RETURN_NULL_OR_NONE macro can be used for functions + that return a PyObject* */ +#define BEGIN_OBJC_ENTRY \ + @autoreleasepool { @try { + +#define END_OBJC_ENTRY \ + } @catch (NSException *e) { errSetException(e); } } + +#define RETURN_NULL_OR_NONE \ + if (PyErr_Occurred()) { \ + return NULL; \ + } else { \ + Py_RETURN_NONE; \ + } + + +/* Variable for our delegate since it has a +1 reference count. + Not needed under manual reference count, but standard practice + under ARC. */ +static id appDelegate = nil; + /* Keep track of number of windows present Needed to know when to stop the NSApp */ static long FigureWindowCount = 0; @@ -43,6 +74,11 @@ // Global variable to store the original SIGINT handler static PyOS_sighandler_t originalSigintAction = NULL; +// Convert an Objective-C exception into a Python RuntimeError +static void errSetException(NSException *exception) { + PyErr_SetString(PyExc_RuntimeError, [[exception reason] UTF8String]); +} + // Stop the current app's run loop, sending an event to ensure it actually stops static void stopWithEvent() { [NSApp stop: nil]; @@ -73,57 +109,64 @@ static void handleSigint(int signal) { // It is used in the input hook as well as wrapped in a version callable from Python. static void flushEvents() { while (true) { - NSEvent* event = [NSApp nextEventMatchingMask: NSEventMaskAny - untilDate: [NSDate distantPast] - inMode: NSDefaultRunLoopMode - dequeue: YES]; - if (!event) { - break; + @autoreleasepool { + NSEvent* event = [NSApp nextEventMatchingMask: NSEventMaskAny + untilDate: [NSDate distantPast] + inMode: NSDefaultRunLoopMode + dequeue: YES]; + if (!event) { + break; + } + [NSApp sendEvent:event]; } - [NSApp sendEvent:event]; } } static int wait_for_stdin() { + BEGIN_OBJC_ENTRY + // Short circuit if no windows are active // Rely on Python's input handling to manage CPU usage // This queries the NSApp, rather than using our FigureWindowCount because that is decremented when events still // need to be processed to properly close the windows. - if (![[NSApp windows] count]) { - flushEvents(); - return 1; + @autoreleasepool { + if (![[NSApp windows] count]) { + flushEvents(); + return 1; + } } - @autoreleasepool { - // Set up a SIGINT handler to interrupt the event loop if ctrl+c comes in too - originalSigintAction = PyOS_setsig(SIGINT, handleSigint); + // Set up a SIGINT handler to interrupt the event loop if ctrl+c comes in too + originalSigintAction = PyOS_setsig(SIGINT, handleSigint); - // Create an NSFileHandle for standard input - NSFileHandle *stdinHandle = [NSFileHandle fileHandleWithStandardInput]; + // Create an NSFileHandle for standard input + NSFileHandle *stdinHandle = [NSFileHandle fileHandleWithStandardInput]; - // Register for data available notifications on standard input - id notificationID = [[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification - object: stdinHandle - queue: [NSOperationQueue mainQueue] // Use the main queue - usingBlock: ^(NSNotification *notification) {stopWithEvent();} - ]; + // Register for data available notifications on standard input + id notificationID = [[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification + object: stdinHandle + queue: [NSOperationQueue mainQueue] // Use the main queue + usingBlock: ^(NSNotification *notification) {stopWithEvent();} + ]; - // Wait in the background for anything that happens to stdin - [stdinHandle waitForDataInBackgroundAndNotify]; + // Wait in the background for anything that happens to stdin + [stdinHandle waitForDataInBackgroundAndNotify]; - // Run the application's event loop, which will be interrupted on stdin or SIGINT - [NSApp run]; + // Run the application's event loop, which will be interrupted on stdin or SIGINT + [NSApp run]; - // Remove the input handler as an observer - [[NSNotificationCenter defaultCenter] removeObserver: notificationID]; + // Remove the input handler as an observer + [[NSNotificationCenter defaultCenter] removeObserver: notificationID]; - // Restore the original SIGINT handler upon exiting the function - PyOS_setsig(SIGINT, originalSigintAction); + // Restore the original SIGINT handler upon exiting the function + PyOS_setsig(SIGINT, originalSigintAction); - return 1; - } + return 1; + + END_OBJC_ENTRY + return 0; } /* ---------------------------- Cocoa classes ---------------------------- */ @@ -225,7 +268,8 @@ static void lazy_init(void) { NSApp = [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; - [NSApp setDelegate: [[[MatplotlibAppDelegate alloc] init] autorelease]]; + appDelegate = [[MatplotlibAppDelegate alloc] init]; + [NSApp setDelegate:appDelegate]; // Run our own event loop while waiting for stdin on the Python side // this is needed to keep the application responsive while waiting for input @@ -235,21 +279,26 @@ static void lazy_init(void) { static PyObject* event_loop_is_running(PyObject* self) { + BEGIN_OBJC_ENTRY + if (backend_inited) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } + + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* wake_on_fd_write(PyObject* unused, PyObject* args) { + BEGIN_OBJC_ENTRY int fd; if (!PyArg_ParseTuple(args, "i", &fd)) { return NULL; } NSFileHandle* fh = [[NSFileHandle alloc] initWithFileDescriptor: fd]; - [fh waitForDataInBackgroundAndNotify]; - [[NSNotificationCenter defaultCenter] + __block id notificationID = [[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification object: fh queue: nil @@ -257,15 +306,21 @@ static void lazy_init(void) { PyGILState_STATE gstate = PyGILState_Ensure(); PyErr_CheckSignals(); PyGILState_Release(gstate); + [fh release]; + [[NSNotificationCenter defaultCenter] removeObserver:notificationID]; }]; - Py_RETURN_NONE; + [fh waitForDataInBackgroundAndNotify]; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* stop(PyObject* self, PyObject* _ /* ignored */) { + BEGIN_OBJC_ENTRY stopWithEvent(); - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static CGFloat _get_device_scale(CGContextRef cr) @@ -343,16 +398,22 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) static PyObject* FigureCanvas_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + BEGIN_OBJC_ENTRY + lazy_init(); FigureCanvas *self = (FigureCanvas*)type->tp_alloc(type, 0); if (!self) { return NULL; } self->view = [View alloc]; return (PyObject*)self; + + END_OBJC_ENTRY + return NULL; } static int FigureCanvas_init(FigureCanvas *self, PyObject *args, PyObject *kwds) { + BEGIN_OBJC_ENTRY if (!self->view) { PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); return -1; @@ -392,14 +453,18 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) Py_XDECREF(super_init); Py_XDECREF(init_res); Py_XDECREF(wh); + + END_OBJC_ENTRY return PyErr_Occurred() ? -1 : 0; } static void FigureCanvas_dealloc(FigureCanvas* self) { + BEGIN_OBJC_ENTRY [self->view setCanvas: NULL]; [self->view release]; + END_OBJC_ENTRY Py_TYPE(self)->tp_free((PyObject*)self); } @@ -413,13 +478,16 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) static PyObject* FigureCanvas_update(FigureCanvas* self) { + BEGIN_OBJC_ENTRY [self->view setNeedsDisplay: YES]; - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE; } static PyObject* FigureCanvas_flush_events(FigureCanvas* self) { + BEGIN_OBJC_ENTRY // We run the app, matching any events that are waiting in the queue // to process, breaking out of the loop when no events remain and // displaying the canvas if needed. @@ -430,12 +498,14 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) Py_END_ALLOW_THREADS [self->view displayIfNeeded]; - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureCanvas_set_cursor(PyObject* unused, PyObject* args) { + BEGIN_OBJC_ENTRY int i; if (!PyArg_ParseTuple(args, "i", &i)) { return NULL; } switch (i) { @@ -455,12 +525,14 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) case 7: [[NSCursor resizeUpDownCursor] set]; break; default: return NULL; } - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureCanvas_set_rubberband(FigureCanvas* self, PyObject *args) { + BEGIN_OBJC_ENTRY View* view = self->view; if (!view) { PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); @@ -477,19 +549,23 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) NSRect rubberband = NSMakeRect(x0 < x1 ? x0 : x1, y0 < y1 ? y0 : y1, abs(x1 - x0), abs(y1 - y0)); [view setRubberband: rubberband]; - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureCanvas_remove_rubberband(FigureCanvas* self) { + BEGIN_OBJC_ENTRY [self->view removeRubberband]; - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureCanvas__start_event_loop(FigureCanvas* self, PyObject* args, PyObject* keywords) { + BEGIN_OBJC_ENTRY float timeout = 0.0; static char* kwlist[] = {"timeout", NULL}; @@ -502,23 +578,27 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) NSDate* date = (timeout > 0.0) ? [NSDate dateWithTimeIntervalSinceNow: timeout] : [NSDate distantFuture]; - while (true) - { NSEvent* event = [NSApp nextEventMatchingMask: NSEventMaskAny - untilDate: date - inMode: NSDefaultRunLoopMode - dequeue: YES]; - if (!event || [event type]==NSEventTypeApplicationDefined) { break; } - [NSApp sendEvent: event]; + while (true) { + @autoreleasepool { + NSEvent* event = [NSApp nextEventMatchingMask: NSEventMaskAny + untilDate: date + inMode: NSDefaultRunLoopMode + dequeue: YES]; + if (!event || [event type]==NSEventTypeApplicationDefined) { break; } + [NSApp sendEvent: event]; + } } Py_END_ALLOW_THREADS - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureCanvas_stop_event_loop(FigureCanvas* self) { + BEGIN_OBJC_ENTRY // +[NSEvent otherEventWithType:...] is declared nullable but will not return // nil for these constant, valid arguments; guard defensively anyway. NSEvent* event = [NSEvent otherEventWithType: NSEventTypeApplicationDefined @@ -533,7 +613,8 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) if (event) { [NSApp postEvent: event atStart: true]; } - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyTypeObject FigureCanvasType = { @@ -591,6 +672,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) static PyObject* FigureManager_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + BEGIN_OBJC_ENTRY if (![NSThread isMainThread]) { PyErr_SetString( PyExc_RuntimeError, @@ -612,11 +694,14 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) self->window = window; ++FigureWindowCount; return (PyObject*)self; + END_OBJC_ENTRY + return NULL; } static int FigureManager_init(FigureManager *self, PyObject *args, PyObject *kwds) { + BEGIN_OBJC_ENTRY PyObject* canvas; if (!PyArg_ParseTuple(args, "O", &canvas)) { return -1; @@ -649,15 +734,18 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) Window* window = self->window; [window setDelegate: view]; [window makeFirstResponder: view]; + [window setReleasedWhenClosed:NO]; [[window contentView] addSubview: view]; [view updateDevicePixelRatio: [window backingScaleFactor]]; + END_OBJC_ENTRY return 0; } static PyObject* FigureManager__set_window_mode(FigureManager* self, PyObject* args) { + BEGIN_OBJC_ENTRY const char* window_mode; if (!PyArg_ParseTuple(args, "s", &window_mode) || !self->window) { return NULL; @@ -671,7 +759,8 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) } else { // system settings [self->window setTabbingMode: NSWindowTabbingModeAutomatic]; } - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* @@ -684,28 +773,39 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) static void FigureManager_dealloc(FigureManager* self) { + BEGIN_OBJC_ENTRY [self->window close]; + [self->window setDelegate:nil]; + [self->window release]; + END_OBJC_ENTRY Py_TYPE(self)->tp_free((PyObject*)self); } static PyObject* FigureManager__show(FigureManager* self) { + BEGIN_OBJC_ENTRY [self->window makeKeyAndOrderFront: nil]; - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureManager__raise(FigureManager* self) { + BEGIN_OBJC_ENTRY [self->window orderFrontRegardless]; - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureManager_destroy(FigureManager* self) { + BEGIN_OBJC_ENTRY [self->window close]; + [self->window setDelegate:nil]; + [self->window release]; self->window = NULL; // call super(self, FigureManager).destroy() - it seems we need the @@ -726,11 +826,13 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) } Py_DECREF(result); - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureManager_set_icon(PyObject* null, PyObject* args) { + BEGIN_OBJC_ENTRY PyObject* icon_path; if (!PyArg_ParseTuple(args, "O&", &PyUnicode_FSDecoder, &icon_path)) { return NULL; @@ -740,38 +842,35 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) Py_DECREF(icon_path); return NULL; } - @autoreleasepool { - NSString* ns_icon_path = [NSString stringWithUTF8String: icon_path_ptr]; - Py_DECREF(icon_path); - if (!ns_icon_path) { - PyErr_SetString(PyExc_RuntimeError, "Could not convert to NSString*"); - return NULL; - } - NSImage* image = [[[NSImage alloc] initByReferencingFile: ns_icon_path] autorelease]; - if (!image) { - PyErr_SetString(PyExc_RuntimeError, "Could not create NSImage*"); - return NULL; - } - if (!image.valid) { - PyErr_SetString(PyExc_RuntimeError, "Image is not valid"); - return NULL; - } - @try { - NSApplication* app = [NSApplication sharedApplication]; - app.applicationIconImage = image; - } - @catch (NSException* exception) { - PyErr_SetString(PyExc_RuntimeError, exception.reason.UTF8String); - return NULL; - } + + NSString* ns_icon_path = [NSString stringWithUTF8String: icon_path_ptr]; + Py_DECREF(icon_path); + if (!ns_icon_path) { + PyErr_SetString(PyExc_RuntimeError, "Could not convert to NSString*"); + return NULL; + } + NSImage* image = [[[NSImage alloc] initByReferencingFile: ns_icon_path] autorelease]; + if (!image) { + PyErr_SetString(PyExc_RuntimeError, "Could not create NSImage*"); + return NULL; + } + if (!image.valid) { + PyErr_SetString(PyExc_RuntimeError, "Image is not valid"); + return NULL; } - Py_RETURN_NONE; + + NSApplication* app = [NSApplication sharedApplication]; + app.applicationIconImage = image; + + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureManager_set_window_title(FigureManager* self, PyObject *args, PyObject *kwds) { + BEGIN_OBJC_ENTRY const char* title; if (!PyArg_ParseTuple(args, "s", &title)) { return NULL; @@ -780,23 +879,26 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) // not return nil here; the nullable annotation is a false positive. // NOLINTNEXTLINE(clang-analyzer-nullability.NullablePassedToNonnull) [self->window setTitle: [NSString stringWithUTF8String: title]]; - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureManager_get_window_title(FigureManager* self) { + BEGIN_OBJC_ENTRY NSString* title = [self->window title]; if (title) { return PyUnicode_FromString([title UTF8String]); - } else { - Py_RETURN_NONE; } + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureManager_resize(FigureManager* self, PyObject *args, PyObject *kwds) { + BEGIN_OBJC_ENTRY int width, height; if (!PyArg_ParseTuple(args, "ii", &width, &height)) { return NULL; @@ -809,14 +911,17 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) // 36 comes from hard-coded size of toolbar later in code [window setContentSize: NSMakeSize(width, height + 36.)]; } - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyObject* FigureManager_full_screen_toggle(FigureManager* self) { + BEGIN_OBJC_ENTRY [self->window toggleFullScreen: nil]; - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyTypeObject FigureManagerType = { @@ -938,6 +1043,7 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } static PyObject* NavigationToolbar2_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + BEGIN_OBJC_ENTRY lazy_init(); NavigationToolbar2Handler* handler = [NavigationToolbar2Handler alloc]; if (!handler) { return NULL; } @@ -948,11 +1054,14 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } } self->handler = handler; return (PyObject*)self; + END_OBJC_ENTRY + return NULL; } static int NavigationToolbar2_init(NavigationToolbar2 *self, PyObject *args, PyObject *kwds) { + BEGIN_OBJC_ENTRY FigureCanvas* canvas; const char* images[7]; const char* tooltips[7]; @@ -1062,14 +1171,17 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } [[window contentView] display]; self->messagebox = messagebox; + END_OBJC_ENTRY return 0; } static void NavigationToolbar2_dealloc(NavigationToolbar2 *self) { + BEGIN_OBJC_ENTRY [self->handler release]; [self->messagebox release]; + END_OBJC_ENTRY Py_TYPE(self)->tp_free((PyObject*)self); } @@ -1082,6 +1194,7 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } static PyObject* NavigationToolbar2_set_message(NavigationToolbar2 *self, PyObject* args) { + BEGIN_OBJC_ENTRY const char* message; if (!PyArg_ParseTuple(args, "s", &message)) { return NULL; } @@ -1113,7 +1226,8 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } [[messagebox.superview window] disableCursorRects]; } - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static PyTypeObject NavigationToolbar2Type = { @@ -1139,6 +1253,8 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } static PyObject* choose_save_file(PyObject* unused, PyObject* args) { + BEGIN_OBJC_ENTRY + int result; const char* title; const char* directory; @@ -1162,7 +1278,9 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } } return PyUnicode_FromString([filename UTF8String]); } - Py_RETURN_NONE; + + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } @implementation Window @@ -1775,17 +1893,26 @@ - (void)flagsChanged:(NSEvent *)event static PyObject* show(PyObject* self) { - [NSApp activateIgnoringOtherApps: YES]; - NSArray *windowsArray = [NSApp windows]; - NSEnumerator *enumerator = [windowsArray objectEnumerator]; - NSWindow *window; - while ((window = [enumerator nextObject])) { - [window orderFront:nil]; + BEGIN_OBJC_ENTRY + + // Iterating over -[NSApp windows] will add the windows to the topmost + // autorelease pool, wrap in @autoreleasepool as -[NSApp run] is long-running. + @autoreleasepool { + [NSApp activateIgnoringOtherApps: YES]; + NSArray *windowsArray = [NSApp windows]; + NSEnumerator *enumerator = [windowsArray objectEnumerator]; + NSWindow *window; + while ((window = [enumerator nextObject])) { + [window orderFront:nil]; + } } + Py_BEGIN_ALLOW_THREADS [NSApp run]; Py_END_ALLOW_THREADS - Py_RETURN_NONE; + + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } typedef struct { @@ -1797,6 +1924,7 @@ - (void)flagsChanged:(NSEvent *)event static PyObject* Timer_new(PyTypeObject* type, PyObject *args, PyObject *kwds) { + BEGIN_OBJC_ENTRY lazy_init(); Timer* self = (Timer*)type->tp_alloc(type, 0); if (!self) { @@ -1804,6 +1932,8 @@ - (void)flagsChanged:(NSEvent *)event } self->timer = NULL; return (PyObject*) self; + END_OBJC_ENTRY + return NULL; } static PyObject* @@ -1816,6 +1946,7 @@ - (void)flagsChanged:(NSEvent *)event static PyObject* Timer__timer_start(Timer* self, PyObject* args) { + BEGIN_OBJC_ENTRY NSTimeInterval interval; PyObject* py_interval = NULL, * py_single = NULL, * py_on_timer = NULL; int single; @@ -1850,11 +1981,8 @@ - (void)flagsChanged:(NSEvent *)event Py_XDECREF(py_interval); Py_XDECREF(py_single); Py_XDECREF(py_on_timer); - if (PyErr_Occurred()) { - return NULL; - } else { - Py_RETURN_NONE; - } + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static void @@ -1869,14 +1997,18 @@ - (void)flagsChanged:(NSEvent *)event static PyObject* Timer__timer_stop(Timer* self) { + BEGIN_OBJC_ENTRY Timer__timer_stop_impl(self); - Py_RETURN_NONE; + END_OBJC_ENTRY + RETURN_NULL_OR_NONE } static void Timer_dealloc(Timer* self) { + BEGIN_OBJC_ENTRY Timer__timer_stop_impl(self); + END_OBJC_ENTRY Py_TYPE(self)->tp_free((PyObject*)self); }