diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index bde902013412..befc83162075 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -181,7 +181,7 @@ jobs: export PATH="/usr/local/bin:$PATH" python -m pip install --no-build-isolation 'contourpy>=1.0.1' python -m pip install --upgrade cycler fonttools \ - packaging pyparsing python-dateutil setuptools-scm \ + packaging pyparsing python-dateutil 'setuptools-scm<10' \ -r requirements_test.txt sphinx ipython python -m pip install --upgrade pycairo 'cairocffi>=0.8' PyGObject && python -c 'import gi; gi.require_version("Gtk", "3.0"); from gi.repository import Gtk' && diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea402d954137..906c6c4408d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -243,7 +243,7 @@ jobs: # Preinstall build requirements to enable no-build-isolation builds. python -m pip install --upgrade $PRE \ 'contourpy>=1.0.1' cycler fonttools kiwisolver importlib_resources \ - packaging pillow 'pyparsing!=3.1.0' python-dateutil setuptools-scm \ + packaging pillow 'pyparsing!=3.1.0' python-dateutil 'setuptools-scm<10' \ 'meson-python>=0.13.1' 'pybind11>=2.13.2' \ -r requirements/testing/all.txt \ ${{ matrix.extra-requirements }} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cfd4f65dc775..5dce09e6d36b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -50,7 +50,7 @@ stages: strategy: matrix: Linux_py310: - vmImage: 'ubuntu-20.04' # keep one job pinned to the oldest image + vmImage: 'ubuntu-22.04' # keep one job pinned to the oldest image python.version: '3.10' Linux_py311: vmImage: 'ubuntu-latest' diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 46ec06e0a9f1..85d9be15673e 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -28,7 +28,6 @@ matplotlib\.ticker\.LogitLocator\.nonsingular # Stdlib/Enum considered inconsistent (no fault of ours, I don't think) matplotlib\.backend_bases\._Mode\.__new__ -matplotlib\.units\.Number\.__hash__ # 3.6 Pending deprecations matplotlib\.figure\.Figure\.set_constrained_layout diff --git a/doc/_static/switcher.json b/doc/_static/switcher.json index 36e743db21b8..cc275467efc4 100644 --- a/doc/_static/switcher.json +++ b/doc/_static/switcher.json @@ -1,7 +1,7 @@ [ { "name": "3.10 (stable)", - "version": "3.10.8", + "version": "3.10.9", "url": "https://matplotlib.org/stable/", "preferred": true }, diff --git a/doc/_static/zenodo_cache/17595503.svg b/doc/_static/zenodo_cache/17595503.svg new file mode 100644 index 000000000000..891bd118d125 --- /dev/null +++ b/doc/_static/zenodo_cache/17595503.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.17595503 + + + 10.5281/zenodo.17595503 + + + \ No newline at end of file diff --git a/doc/api/prev_api_changes/api_changes_3.10.9.rst b/doc/api/prev_api_changes/api_changes_3.10.9.rst new file mode 100644 index 000000000000..592faadc347b --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.10.9.rst @@ -0,0 +1,20 @@ +API Changes for 3.10.9 +====================== + + +Deprecations +------------ + + +Arbitrary code in ``axes.prop_cycle`` rcParam strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``axes.prop_cycle`` rcParam accepts Python expressions that are evaluated +in a limited context. The evaluation context has been further limited and some +expressions that previously worked (list comprehensions, for example) no longer +will. This change is made without a deprecation period to improve security. +The previously documented cycler operations at +https://matplotlib.org/cycler/ are still supported. + +This change was originally slated for v3.11.0 of Matplotlib, but was additionally +backported due to the security implications. diff --git a/doc/conf.py b/doc/conf.py index 292aff3e1983..45fe32f79f88 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -891,3 +891,4 @@ def setup(app): if sphinx.version_info[:2] < (7, 1): app.connect('html-page-context', add_html_cache_busting, priority=1000) generate_ScalarMappable_docs() + app.config.autodoc_use_legacy_class_based = True diff --git a/doc/project/citing.rst b/doc/project/citing.rst index c5e56e6f12d4..1f8c6509e9e6 100644 --- a/doc/project/citing.rst +++ b/doc/project/citing.rst @@ -32,6 +32,9 @@ By version .. START OF AUTOGENERATED +v3.10.8 + .. image:: ../_static/zenodo_cache/17595503.svg + :target: https://doi.org/10.5281/zenodo.17595503 v3.10.7 .. image:: ../_static/zenodo_cache/17298696.svg :target: https://doi.org/10.5281/zenodo.17298696 diff --git a/doc/users/github_stats.rst b/doc/users/github_stats.rst index 8d93759ac756..182f17b5e73c 100644 --- a/doc/users/github_stats.rst +++ b/doc/users/github_stats.rst @@ -1,19 +1,22 @@ .. _github-stats: -GitHub statistics for 3.10.8 (Nov 12, 2025) +GitHub statistics for 3.10.9 (Apr 23, 2026) =========================================== -GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/11/12 +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/04/23 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 4 issues and merged 16 pull requests. -The full list can be seen `on GitHub `__ +We closed 10 issues and merged 34 pull requests. +The full list can be seen `on GitHub `__ -The following 35 authors contributed 445 commits. +The following 37 authors contributed 519 commits. * Aasma Gupta +* Aman Srivastava * Antony Lee +* beelauuu +* Ben Root * Christine P. Chai * David Stansby * dependabot[bot] @@ -21,7 +24,6 @@ The following 35 authors contributed 445 commits. * G.D. McBain * Greg Lucas * hannah -* heinrich5991 * hu-xiaonan * Ian Thomas * Inês Cachola @@ -29,52 +31,76 @@ The following 35 authors contributed 445 commits. * Jouni K. Seppänen * Khushi_29 * Kyle Sunden -* Lucas Gruwez * Lumberbot (aka Jack) +* m-sahare * N R Navaneet * Nathan G. Wiseman -* Nathan Goldbaum -* Nick Coish * Oscar Gustafsson * Praful Gulani * Qian Zhang -* Rafael Katri * Raphael Erik Hviding +* Raphael Quast * Roman * Ruth Comer * saikarna913 * Scott Shambaugh +* Steve Berardi * Thomas A Caswell * Tim Hoffmann * Trygve Magnus Ræder +* Vikash Kumar GitHub issues and pull requests: -Pull Requests (16): +Pull Requests (34): -* :ghpull:`30717`: Backport PR #30714 on branch v3.10.x (FIX: Gracefully handle numpy arrays as input to check_in_list()) -* :ghpull:`30714`: FIX: Gracefully handle numpy arrays as input to check_in_list() -* :ghpull:`30560`: Consistent zoom boxes -* :ghpull:`30711`: Backport PR #30697 on branch v3.10.x (BUG: raise when creating a MacOS FigureManager outside the main thread) -* :ghpull:`30697`: BUG: raise when creating a MacOS FigureManager outside the main thread -* :ghpull:`30656`: Backport PR #29810 on branch v3.10.x (Declare free-threaded support in MacOS backend extension) -* :ghpull:`30702`: Backport PR #30624 on branch v3.10.x (TST: Increase tolerances for Ghostscript 10.06) -* :ghpull:`30700`: Backport PR #30698 on branch v3.10.x (BLD: update trove metadata to support py3.14) -* :ghpull:`30624`: TST: Increase tolerances for Ghostscript 10.06 -* :ghpull:`30698`: BLD: update trove metadata to support py3.14 -* :ghpull:`30688`: Backport PR #30687 on branch v3.10.x (DOC: Fix pip link) -* :ghpull:`30675`: Backport PR #30657 on branch v3.10.x (Fix AttributeError: module 'gi' has no attribute 'require_version') -* :ghpull:`30674`: Backport PR #30672 on branch v3.10.x (Use pathlib.Path instead of matplotlib.path.Path in text.pyi) -* :ghpull:`30672`: Use pathlib.Path instead of matplotlib.path.Path in text.pyi -* :ghpull:`30657`: Fix ``AttributeError: module 'gi' has no attribute 'require_version'`` -* :ghpull:`29810`: Declare free-threaded support in MacOS backend extension +* :ghpull:`31556`: FIX: Inverted PyErr_Occurred check in enum type caster (_enums.h) +* :ghpull:`31078`: Backport PR #31075 on branch v3.10.x (Fix remove method for figure title and xy-labels) +* :ghpull:`31280`: Backport PR #31278 on branch v3.10.x (Fix ``clabel`` manual argument not accepting unit-typed coordinates) +* :ghpull:`31520`: Backport PR #31020 on branch v3.10.x (DOC: Fix doc builds with Sphinx 9) +* :ghpull:`31511`: Backport PR #31504 on branch v3.10.x (Re-order variants to prioritize narrower types) +* :ghpull:`31504`: Re-order variants to prioritize narrower types +* :ghpull:`31445`: Backport PR #31437: mathtext: Fix type inconsistency with fontmaps +* :ghpull:`31437`: mathtext: Fix type inconsistency with fontmaps +* :ghpull:`31411`: Backport PR #31323 on branch v3.10.x (FIX: Prevent crash when removing a subfigure containing subplots) +* :ghpull:`31421`: Backport PR #31420 on branch v3.10.x (Fix outdated Savannah URL for freetype download) +* :ghpull:`31420`: Fix outdated Savannah URL for freetype download +* :ghpull:`31418`: Backport PR #31401: BLD: Temporarily pin setuptools-scm<10 +* :ghpull:`31323`: FIX: Prevent crash when removing a subfigure containing subplots +* :ghpull:`31401`: BLD: Temporarily pin setuptools-scm<10 +* :ghpull:`31278`: Fix ``clabel`` manual argument not accepting unit-typed coordinates +* :ghpull:`31154`: Backport PR #31153 on branch v3.10.x (TST: Use correct method of clearing mock objects) +* :ghpull:`31153`: TST: Use correct method of clearing mock objects +* :ghpull:`31075`: Fix remove method for figure title and xy-labels +* :ghpull:`31036`: Backport PR #31035 on branch v3.10.x (DOCS: Fix typo in time array step size comment) +* :ghpull:`30986`: Backport PR #30985 on branch v3.10.x (MNT: do not assign a numpy array shape) +* :ghpull:`30985`: MNT: do not assign a numpy array shape +* :ghpull:`30971`: Backport PR #30969 on branch v3.10.x (DOC: Simplify barh() example) +* :ghpull:`30965`: Backport PR #30952 on branch v3.10.x (DOC: Tutorial on API shortcuts) +* :ghpull:`30964`: Backport PR #30960 on branch v3.10.x (SVG backend - handle font weight as integer) +* :ghpull:`30960`: SVG backend - handle font weight as integer +* :ghpull:`30924`: Backport PR #30910 on branch v3.10.x (DOC: Improve writer parameter docs of Animation.save()) +* :ghpull:`30870`: Backport PR #30869 on branch v3.10.x (FIX: Accept array for zdir) +* :ghpull:`30869`: FIX: Accept array for zdir +* :ghpull:`30860`: Backport PR #30858 on branch v3.10.x (DOC: reinstate "codex" search term) +* :ghpull:`30818`: Backport PR #30817 on branch v3.10.x (Update sphinx-gallery header patch) +* :ghpull:`30801`: Backport PR #30763 on branch v3.10.x (DOC: Add example how to align tick labels) +* :ghpull:`30791`: Backport PR #30788 on branch v3.10.8-doc (Fix typo in key-mapping for "f11") +* :ghpull:`30790`: Backport PR #30788 on branch v3.10.x (Fix typo in key-mapping for "f11") +* :ghpull:`30788`: Fix typo in key-mapping for "f11" -Issues (4): +Issues (10): -* :ghissue:`30706`: [Bug]: Axes.grouped_bar() with non-string orientation (e.g., NumPy array) raises ambiguous truth-value error instead of clean ValueError -* :ghissue:`30666`: [Bug]: calling pyplot.gca() outside the main thread crashes the interpreter with the MacOS backend -* :ghissue:`30669`: [Bug]: Type hint for fontproperties keyword in text.pyi is wrong -* :ghissue:`30654`: [Bug]: error plotting: AttributeError: module 'gi' has no attribute 'require_version' +* :ghissue:`31495`: Unavoidable warnings with pybind11 main branch +* :ghissue:`31433`: [MNT]: Mypy error +* :ghissue:`31340`: [Bug]: outdated savannah URL in subprojects/freetype-2.6.1.wrap +* :ghissue:`31319`: [Bug]: Crash when removing a subfigure with a subplot in a figure +* :ghissue:`27525`: [Bug]: clabel manual argument does not accept units +* :ghissue:`31112`: [TST] Upcoming dependency test failures +* :ghissue:`31073`: [Bug]: Crash when Removing Suptitle in a Figure with Constrained Layout +* :ghissue:`30981`: [TST] Upcoming dependency test failures +* :ghissue:`30868`: [Bug]: Axe3D text() method does not allow zdir=numpy.array(...) +* :ghissue:`21566`: [ENH]: set_horizontalalignment("right") on Y axis labels when yaxis.ticks_right() is used. Previous GitHub statistics diff --git a/doc/users/prev_whats_new/github_stats_3.10.8.rst b/doc/users/prev_whats_new/github_stats_3.10.8.rst new file mode 100644 index 000000000000..dd3d19e036eb --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.10.8.rst @@ -0,0 +1,77 @@ +.. _github-stats_3-10-8: + +GitHub statistics for 3.10.8 (Nov 12, 2025) +=========================================== + +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/11/12 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 4 issues and merged 16 pull requests. +The full list can be seen `on GitHub `__ + +The following 35 authors contributed 445 commits. + +* Aasma Gupta +* Antony Lee +* Christine P. Chai +* David Stansby +* dependabot[bot] +* Elliott Sales de Andrade +* G.D. McBain +* Greg Lucas +* hannah +* heinrich5991 +* hu-xiaonan +* Ian Thomas +* Inês Cachola +* Jody Klymak +* Jouni K. Seppänen +* Khushi_29 +* Kyle Sunden +* Lucas Gruwez +* Lumberbot (aka Jack) +* N R Navaneet +* Nathan G. Wiseman +* Nathan Goldbaum +* Nick Coish +* Oscar Gustafsson +* Praful Gulani +* Qian Zhang +* Rafael Katri +* Raphael Erik Hviding +* Roman +* Ruth Comer +* saikarna913 +* Scott Shambaugh +* Thomas A Caswell +* Tim Hoffmann +* Trygve Magnus Ræder + +GitHub issues and pull requests: + +Pull Requests (16): + +* :ghpull:`30717`: Backport PR #30714 on branch v3.10.x (FIX: Gracefully handle numpy arrays as input to check_in_list()) +* :ghpull:`30714`: FIX: Gracefully handle numpy arrays as input to check_in_list() +* :ghpull:`30560`: Consistent zoom boxes +* :ghpull:`30711`: Backport PR #30697 on branch v3.10.x (BUG: raise when creating a MacOS FigureManager outside the main thread) +* :ghpull:`30697`: BUG: raise when creating a MacOS FigureManager outside the main thread +* :ghpull:`30656`: Backport PR #29810 on branch v3.10.x (Declare free-threaded support in MacOS backend extension) +* :ghpull:`30702`: Backport PR #30624 on branch v3.10.x (TST: Increase tolerances for Ghostscript 10.06) +* :ghpull:`30700`: Backport PR #30698 on branch v3.10.x (BLD: update trove metadata to support py3.14) +* :ghpull:`30624`: TST: Increase tolerances for Ghostscript 10.06 +* :ghpull:`30698`: BLD: update trove metadata to support py3.14 +* :ghpull:`30688`: Backport PR #30687 on branch v3.10.x (DOC: Fix pip link) +* :ghpull:`30675`: Backport PR #30657 on branch v3.10.x (Fix AttributeError: module 'gi' has no attribute 'require_version') +* :ghpull:`30674`: Backport PR #30672 on branch v3.10.x (Use pathlib.Path instead of matplotlib.path.Path in text.pyi) +* :ghpull:`30672`: Use pathlib.Path instead of matplotlib.path.Path in text.pyi +* :ghpull:`30657`: Fix ``AttributeError: module 'gi' has no attribute 'require_version'`` +* :ghpull:`29810`: Declare free-threaded support in MacOS backend extension + +Issues (4): + +* :ghissue:`30706`: [Bug]: Axes.grouped_bar() with non-string orientation (e.g., NumPy array) raises ambiguous truth-value error instead of clean ValueError +* :ghissue:`30666`: [Bug]: calling pyplot.gca() outside the main thread crashes the interpreter with the MacOS backend +* :ghissue:`30669`: [Bug]: Type hint for fontproperties keyword in text.pyi is wrong +* :ghissue:`30654`: [Bug]: error plotting: AttributeError: module 'gi' has no attribute 'require_version' diff --git a/doc/users/release_notes.rst b/doc/users/release_notes.rst index dbdcd61437ac..d952093ca254 100644 --- a/doc/users/release_notes.rst +++ b/doc/users/release_notes.rst @@ -18,10 +18,12 @@ Version 3.10 :maxdepth: 1 prev_whats_new/whats_new_3.10.0.rst + ../api/prev_api_changes/api_changes_3.10.9.rst ../api/prev_api_changes/api_changes_3.10.7.rst ../api/prev_api_changes/api_changes_3.10.1.rst ../api/prev_api_changes/api_changes_3.10.0.rst github_stats.rst + prev_whats_new/github_stats_3.10.8.rst prev_whats_new/github_stats_3.10.7.rst prev_whats_new/github_stats_3.10.6.rst prev_whats_new/github_stats_3.10.5.rst diff --git a/environment.yml b/environment.yml index 2a4f3eff69ea..f39ba3f93f2f 100644 --- a/environment.yml +++ b/environment.yml @@ -26,7 +26,7 @@ dependencies: - pyqt - python>=3.10 - python-dateutil>=2.1 - - setuptools_scm + - setuptools_scm<10 - wxpython # building documentation - colorspacious diff --git a/galleries/examples/animation/double_pendulum.py b/galleries/examples/animation/double_pendulum.py index 7a42a6d989ba..76076341d5c2 100644 --- a/galleries/examples/animation/double_pendulum.py +++ b/galleries/examples/animation/double_pendulum.py @@ -51,7 +51,7 @@ def derivs(t, state): return dydx -# create a time array from 0..t_stop sampled at 0.02 second steps +# create a time array from 0..t_stop sampled at 0.01 second steps dt = 0.01 t = np.arange(0, t_stop, dt) diff --git a/galleries/examples/lines_bars_and_markers/barh.py b/galleries/examples/lines_bars_and_markers/barh.py index 5493c7456c75..c8834e252cb6 100644 --- a/galleries/examples/lines_bars_and_markers/barh.py +++ b/galleries/examples/lines_bars_and_markers/barh.py @@ -15,12 +15,10 @@ # Example data people = ('Tom', 'Dick', 'Harry', 'Slim', 'Jim') -y_pos = np.arange(len(people)) performance = 3 + 10 * np.random.rand(len(people)) error = np.random.rand(len(people)) -ax.barh(y_pos, performance, xerr=error, align='center') -ax.set_yticks(y_pos, labels=people) +ax.barh(people, performance, xerr=error, align='center') ax.invert_yaxis() # labels read top-to-bottom ax.set_xlabel('Performance') ax.set_title('How fast do you want to go today?') diff --git a/galleries/tutorials/coding_shortcuts.py b/galleries/tutorials/coding_shortcuts.py new file mode 100644 index 000000000000..46868482598f --- /dev/null +++ b/galleries/tutorials/coding_shortcuts.py @@ -0,0 +1,172 @@ +""" +================ +Coding shortcuts +================ + +Matplotlib's primary and universal API is the :ref:`Axes interface `. +While it is clearly structured and powerful, it can sometimes feel overly verbose and +thus cumbersome to write. This page collects patterns for condensing the code +of the Axes-based API and achieving the same results with less typing for many simpler +plots. + +.. note:: + + The :ref:`pyplot interface ` is an alternative more compact + interface, and was historically modeled to be similar to MATLAB. It remains a + valid approach for those who want to use it. However, it has the disadvantage that + it achieves its brevity through implicit assumptions that you have to understand. + + Since it follows a different paradigm, switching between the Axes interface and + the pyplot interface requires a shift of the mental model, and some code rewrite, + if the code develops to a point at which pyplot no longer provides enough + flexibility. + +This tutorial goes the other way round, starting from the standard verbose Axes +interface and using its capabilities for shortcuts when you don't need all the +generality. + +Let's assume we want to make a plot of the number of daylight hours per day over the +year in London. + +The standard approach with the Axes interface looks like this. +""" + +import matplotlib.pyplot as plt +import numpy as np + +day = np.arange(365) +hours = 4.276 * np.sin(2 * np.pi * (day - 80)/365) + 12.203 + +fig, ax = plt.subplots() +ax.plot(day, hours, color="orange") +ax.set_xlabel("day") +ax.set_ylabel("daylight hours") +ax.set_title("London") +plt.show() + +# %% +# Note that we've included ``plt.show()`` here. This is needed to show the plot window +# when running from a command line or in a Python script. If you run a Jupyter notebook, +# this command is automatically executed at the end of each cell. +# +# For the rest of the tutorial, we'll assume that we are in a notebook and leave this +# out for brevity. Depending on your context you may still need it. +# +# If you instead want to save to a file, use ``fig.savefig("daylight.png")``. +# +# +# Collect Axes properties into a single ``set()`` call +# ==================================================== +# +# The properties of Matplotlib Artists can be modified through their respective +# ``set_*()`` methods. Artists additionally have a generic ``set()`` method, that takes +# keyword arguments and is equivalent to calling all the respective ``set_*()`` methods. +# :: +# +# ax.set_xlabel("day") +# ax.set_ylabel("daylight hours") +# +# can also be written as :: +# +# ax.set(xlabel="day", ylabel="daylight hours") +# +# This is the most simple and effective reduction you can do. With that we can shorten +# the above plot to + +fig, ax = plt.subplots() +ax.plot(day, hours, color="orange") +ax.set(xlabel="day", ylabel="daylight hours", title="London") + +# %% +# +# This works as long as you only need to pass one parameter to the ``set_*()`` function. +# The individual functions are still necessary if you want more control, e.g. +# ``set_title("London", fontsize=16)``. +# +# +# Not storing a reference to the figure +# ===================================== +# Another nuisance of ``fig, ax = plt.subplots()`` is that you always create a ``fig`` +# variable, even if you don't use it. A slightly shorter version would be using the +# standard variable for unused value in Python (``_``): ``_, ax = plt.subplots()``. +# However, that's only marginally better. +# +# You can work around this by separating figure and Axes creation and chaining them :: +# +# ax = plt.figure().add_subplot() +# +# This is a bit cleaner logically and has the slight advantage that you could set +# figure properties inline as well; e.g. ``plt.figure(facecolor="lightgoldenrod")``. +# But it has the disadvantage that it's longer than ``fig, ax = plt.subplots()``. +# +# You can still obtain the figure from the Axes if needed, e.g. :: +# +# ax.figure.savefig("daylight_hours.png") +# +# The example code now looks like this: + +ax = plt.figure().add_subplot() +ax.plot(day, hours, color="orange") +ax.set(xlabel="day", ylabel="daylight hours", title="London") + +# %% +# Define Axes properties during axes creation +# =========================================== +# The ``set_*`` methods as well as ``set`` modify existing objects. You can +# alternatively define them right at creation. Since you typically don't instantiate +# classes yourself in Matplotlib, but rather call some factory function that creates +# the object and wires it up correctly with the plot, this may seem less obvious. But +# in fact you just pass the desired properties to the factory functions. You are likely +# doing this already in some places without realizing. Consider the function to create +# a line :: +# +# ax.plot(x, y, color="orange") +# +# This is equivalent to :: +# +# line, = ax.plot(x, y) +# line.set_color("orange") +# +# The same can be done with functions that create Axes. + +ax = plt.figure().add_subplot(xlabel="day", ylabel="daylight hours", title="London") +ax.plot(day, hours, color="orange") + +# %% +# .. important:: +# The Axes properties are only accepted as keyword arguments by +# `.Figure.add_subplot`, which creates a single Axes. +# +# For `.Figure.subplots` and `.pyplot.subplots`, you'd have to pass the properties +# as a dict via the keyword argument ``subplot_kw``. The limitation here is that +# such parameters are given to all Axes. For example, if you need two subplots +# (``fig, (ax1, ax2) = plt.subplots(1, 2)``) with different labels, you have to +# set them individually. +# +# Defining Axes properties during creation is best used for single subplots or when +# all subplots share the same properties. +# +# +# Using implicit figure creation +# ============================== +# You can go even further by tapping into the pyplot logic and use `.pyplot.axes` to +# create the axes: + +ax = plt.axes(xlabel="day", ylabel="daylight hours", title="London") +ax.plot(day, hours, color="orange") + +# %% +# .. warning:: +# When using this, you have to be aware of the implicit figure semantics of pyplot. +# ``plt.axes()`` will only create a new figure if no figure exists. Otherwise, it +# will add the Axes to the current existing figure, which is likely not what you +# want. +# +# Not storing a reference to the Axes +# =================================== +# If you only need to visualize one dataset, you can append the plot command +# directly to the Axes creation. This may be useful e.g. in notebooks, +# where you want to create a plot with some configuration, but as little distracting +# code as possible: + +plt.axes(xlabel="day", ylabel="daylight hours").plot(day, hours, color="orange") diff --git a/galleries/tutorials/index.rst b/galleries/tutorials/index.rst index 48187a862a2e..76c0037dca11 100644 --- a/galleries/tutorials/index.rst +++ b/galleries/tutorials/index.rst @@ -32,6 +32,23 @@ a :ref:`FAQ ` in our :ref:`user guide `. +.. raw:: html + +
+ +.. only:: html + + .. image:: /tutorials/images/thumb/sphx_glr_coding_shortcuts_thumb.png + :alt: Coding shortcuts + + :ref:`sphx_glr_tutorials_coding_shortcuts.py` + +.. raw:: html + +
Coding shortcuts
+
+ + .. raw:: html
@@ -92,6 +109,7 @@ a :ref:`FAQ ` in our :ref:`user guide `. :hidden: /tutorials/pyplot + /tutorials/coding_shortcuts /tutorials/images /tutorials/lifecycle /tutorials/artists diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index cf35dc1de7db..50077c6738fa 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -331,20 +331,15 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag # Per-instance cache. self._get_info = functools.cache(self._get_info) # type: ignore[method-assign] self._fonts = {} - self.fontmap: dict[str | int, str] = {} + self.fontmap: dict[str, str] = {} filename = findfont(self.default_font_prop) default_font = get_font(filename) self._fonts['default'] = default_font self._fonts['regular'] = default_font - def _get_font(self, font: str | int) -> FT2Font: - if font in self.fontmap: - basename = self.fontmap[font] - else: - # NOTE: An int is only passed by subclasses which have placed int keys into - # `self.fontmap`, so we must cast this to confirm it to typing. - basename = T.cast(str, font) + def _get_font(self, font: str) -> FT2Font: + basename = self.fontmap.get(font, font) cached_font = self._fonts.get(basename) if cached_font is None and os.path.exists(basename): cached_font = get_font(basename) @@ -574,12 +569,13 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag # include STIX sized alternatives for glyphs if fallback is STIX if isinstance(self._fallback_font, StixFonts): stixsizedaltfonts = { - 0: 'STIXGeneral', - 1: 'STIXSizeOneSym', - 2: 'STIXSizeTwoSym', - 3: 'STIXSizeThreeSym', - 4: 'STIXSizeFourSym', - 5: 'STIXSizeFiveSym'} + '0': 'STIXGeneral', + '1': 'STIXSizeOneSym', + '2': 'STIXSizeTwoSym', + '3': 'STIXSizeThreeSym', + '4': 'STIXSizeFourSym', + '5': 'STIXSizeFiveSym', + } for size, name in stixsizedaltfonts.items(): fullpath = findfont(name) @@ -637,7 +633,7 @@ def _get_glyph(self, fontname: str, font_class: str, g = self._fallback_font._get_glyph(fontname, font_class, sym) family = g[0].family_name - if family in list(BakomaFonts._fontmap.values()): + if family in BakomaFonts._fontmap.values(): family = "Computer Modern" _log.info("Substituting symbol %s from %s", sym, family) return g @@ -658,13 +654,12 @@ def _get_glyph(self, fontname: str, font_class: str, def get_sized_alternatives_for_symbol(self, fontname: str, sym: str) -> list[tuple[str, str]]: if self._fallback_font: - return self._fallback_font.get_sized_alternatives_for_symbol( - fontname, sym) + return self._fallback_font.get_sized_alternatives_for_symbol(fontname, sym) return [(fontname, sym)] class DejaVuFonts(UnicodeFonts, metaclass=abc.ABCMeta): - _fontmap: dict[str | int, str] = {} + _fontmap: dict[str, str] = {} def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): # This must come first so the backend's owner is set correctly @@ -676,11 +671,11 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags) # Include Stix sized alternatives for glyphs self._fontmap.update({ - 1: 'STIXSizeOneSym', - 2: 'STIXSizeTwoSym', - 3: 'STIXSizeThreeSym', - 4: 'STIXSizeFourSym', - 5: 'STIXSizeFiveSym', + '1': 'STIXSizeOneSym', + '2': 'STIXSizeTwoSym', + '3': 'STIXSizeThreeSym', + '4': 'STIXSizeFourSym', + '5': 'STIXSizeFiveSym', }) for key, name in self._fontmap.items(): fullpath = findfont(name) @@ -718,7 +713,7 @@ class DejaVuSerifFonts(DejaVuFonts): 'sf': 'DejaVu Sans', 'tt': 'DejaVu Sans Mono', 'ex': 'DejaVu Serif Display', - 0: 'DejaVu Serif', + '0': 'DejaVu Serif', } @@ -736,7 +731,7 @@ class DejaVuSansFonts(DejaVuFonts): 'sf': 'DejaVu Sans', 'tt': 'DejaVu Sans Mono', 'ex': 'DejaVu Sans Display', - 0: 'DejaVu Sans', + '0': 'DejaVu Sans', } @@ -752,7 +747,7 @@ class StixFonts(UnicodeFonts): - handles sized alternative characters for the STIXSizeX fonts. """ - _fontmap: dict[str | int, str] = { + _fontmap = { 'rm': 'STIXGeneral', 'it': 'STIXGeneral:italic', 'bf': 'STIXGeneral:weight=bold', @@ -760,12 +755,12 @@ class StixFonts(UnicodeFonts): 'nonunirm': 'STIXNonUnicode', 'nonuniit': 'STIXNonUnicode:italic', 'nonunibf': 'STIXNonUnicode:weight=bold', - 0: 'STIXGeneral', - 1: 'STIXSizeOneSym', - 2: 'STIXSizeTwoSym', - 3: 'STIXSizeThreeSym', - 4: 'STIXSizeFourSym', - 5: 'STIXSizeFiveSym', + '0': 'STIXGeneral', + '1': 'STIXSizeOneSym', + '2': 'STIXSizeTwoSym', + '3': 'STIXSizeThreeSym', + '4': 'STIXSizeFourSym', + '5': 'STIXSizeFiveSym', } _fallback_font = None _sans = False @@ -832,10 +827,8 @@ def _map_virtual_font(self, fontname: str, font_class: str, return fontname, uniindex @functools.cache - def get_sized_alternatives_for_symbol( # type: ignore[override] - self, - fontname: str, - sym: str) -> list[tuple[str, str]] | list[tuple[int, str]]: + def get_sized_alternatives_for_symbol(self, fontname: str, + sym: str) -> list[tuple[str, str]]: fixes = { '\\{': '{', '\\}': '}', '\\[': '[', '\\]': ']', '<': '\N{MATHEMATICAL LEFT ANGLE BRACKET}', @@ -846,8 +839,8 @@ def get_sized_alternatives_for_symbol( # type: ignore[override] uniindex = get_unicode_index(sym) except ValueError: return [(fontname, sym)] - alternatives = [(i, chr(uniindex)) for i in range(6) - if self._get_font(i).get_char_index(uniindex) != 0] + alternatives = [(str(i), chr(uniindex)) for i in range(6) + if self._get_font(str(i)).get_char_index(uniindex) != 0] # The largest size of the radical symbol in STIX has incorrect # metrics that cause it to be disconnected from the stem. if sym == r'\__sqrt__': @@ -1542,7 +1535,7 @@ def __init__(self, c: str, height: float, depth: float, state: ParserState, break shift = 0.0 - if state.font != 0 or len(alternatives) == 1: + if state.font != '0' or len(alternatives) == 1: if factor is None: factor = target_total / (char.height + char.depth) state.fontsize *= factor @@ -2530,7 +2523,7 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: # Handle regular sub/superscripts constants = _get_font_constant_set(state) lc_height = last_char.height - lc_baseline = 0 + lc_baseline = 0.0 if self.is_dropsub(last_char): lc_baseline = last_char.depth diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index c87c789048c4..25515da77ed7 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -240,10 +240,12 @@ def remove(self): # clear stale callback self.stale_callback = None _ax_flag = False - if hasattr(self, 'axes') and self.axes: + ax = getattr(self, 'axes', None) + mouseover_set = getattr(ax, '_mouseover_set', None) + if mouseover_set is not None: # remove from the mouse hit list - self.axes._mouseover_set.discard(self) - self.axes.stale = True + mouseover_set.discard(self) + ax.stale = True self.axes = None # decouple the artist from the Axes _ax_flag = True diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 87ed794022a0..784f97c0a1e4 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -937,6 +937,7 @@ def trigger(self, *args, **kwargs): self.toolmanager.message_event(message, self) +#: The default tools to add to a tool manager. default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward, 'zoom': ToolZoom, 'pan': ToolPan, 'subplots': ConfigureSubplotsBase, @@ -956,12 +957,13 @@ def trigger(self, *args, **kwargs): 'copy': ToolCopyToClipboardBase, } +#: The default tools to add to a container. default_toolbar_tools = [['navigation', ['home', 'back', 'forward']], ['zoompan', ['pan', 'zoom', 'subplots']], ['io', ['save', 'help']]] -def add_tools_to_manager(toolmanager, tools=default_tools): +def add_tools_to_manager(toolmanager, tools=None): """ Add multiple tools to a `.ToolManager`. @@ -971,14 +973,16 @@ def add_tools_to_manager(toolmanager, tools=default_tools): Manager to which the tools are added. tools : {str: class_like}, optional The tools to add in a {name: tool} dict, see - `.backend_managers.ToolManager.add_tool` for more info. + `.backend_managers.ToolManager.add_tool` for more info. If not specified, then + defaults to `.default_tools`. """ - + if tools is None: + tools = default_tools for name, tool in tools.items(): toolmanager.add_tool(name, tool) -def add_tools_to_container(container, tools=default_toolbar_tools): +def add_tools_to_container(container, tools=None): """ Add multiple tools to the container. @@ -990,9 +994,11 @@ def add_tools_to_container(container, tools=default_toolbar_tools): tools : list, optional List in the form ``[[group1, [tool1, tool2 ...]], [group2, [...]]]`` where the tools ``[tool1, tool2, ...]`` will display in group1. - See `.backend_bases.ToolContainerBase.add_tool` for details. + See `.backend_bases.ToolContainerBase.add_tool` for details. If not specified, + then defaults to `.default_toolbar_tools`. """ - + if tools is None: + tools = default_toolbar_tools for group, grouptools in tools: for position, tool in enumerate(grouptools): container.add_tool(tool, group, position) diff --git a/lib/matplotlib/backend_tools.pyi b/lib/matplotlib/backend_tools.pyi index 32fe8c2f5a79..fa89e3d66871 100644 --- a/lib/matplotlib/backend_tools.pyi +++ b/lib/matplotlib/backend_tools.pyi @@ -112,10 +112,10 @@ class ToolHelpBase(ToolBase): class ToolCopyToClipboardBase(ToolBase): ... -default_tools: dict[str, ToolBase] +default_tools: dict[str, type[ToolBase]] default_toolbar_tools: list[list[str | list[str]]] def add_tools_to_manager( - toolmanager: ToolManager, tools: dict[str, type[ToolBase]] = ... + toolmanager: ToolManager, tools: dict[str, type[ToolBase]] | None = ... ) -> None: ... -def add_tools_to_container(container: ToolContainerBase, tools: list[Any] = ...) -> None: ... +def add_tools_to_container(container: ToolContainerBase, tools: list[Any] | None = ...) -> None: ... diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 48b6e8ac152c..2d2e24c3286c 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -281,7 +281,7 @@ def _setup_latex_process(self, *, expect_reply=True): # it. try: self.latex = subprocess.Popen( - [mpl.rcParams["pgf.texsystem"], "-halt-on-error"], + [mpl.rcParams["pgf.texsystem"], "-halt-on-error", "-no-shell-escape"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, encoding="utf-8", cwd=self.tmpdir) except FileNotFoundError as err: @@ -848,7 +848,7 @@ def print_pdf(self, fname_or_fh, *, metadata=None, **kwargs): texcommand = mpl.rcParams["pgf.texsystem"] cbook._check_and_log_subprocess( [texcommand, "-interaction=nonstopmode", "-halt-on-error", - "figure.tex"], _log, cwd=tmpdir) + "-no-shell-escape", "figure.tex"], _log, cwd=tmpdir) with ((tmppath / "figure.pdf").open("rb") as orig, cbook.open_file_cm(fname_or_fh, "wb") as dest): shutil.copyfileobj(orig, dest) # copy file contents to target @@ -965,7 +965,7 @@ def _run_latex(self): tex_source.write_bytes(self._file.getvalue()) cbook._check_and_log_subprocess( [texcommand, "-interaction=nonstopmode", "-halt-on-error", - tex_source], + "-no-shell-escape", tex_source], _log, cwd=tmpdir) shutil.move(tex_source.with_suffix(".pdf"), self._output_name) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index f1f914ae5420..4dfdb2a6a095 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1257,8 +1257,9 @@ def _convert_psfrags(tmppath, psfrags, paper_width, paper_height, orientation): with TemporaryDirectory() as tmpdir: psfile = os.path.join(tmpdir, "tmp.ps") + # -R1 is a security flag used to prevent shell command execution cbook._check_and_log_subprocess( - ['dvips', '-q', '-R0', '-o', psfile, dvifile], _log) + ['dvips', '-q', '-R1', '-o', psfile, dvifile], _log) shutil.move(psfile, tmppath) # check if the dvips created a ps in landscape paper. Somehow, @@ -1302,7 +1303,7 @@ def gs_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False): cbook._check_and_log_subprocess( [mpl._get_executable_info("gs").executable, - "-dBATCH", "-dNOPAUSE", "-r%d" % dpi, "-sDEVICE=ps2write", + "-dBATCH", "-dNOPAUSE", "-dSAFER", "-r%d" % dpi, "-sDEVICE=ps2write", *paper_option, f"-sOutputFile={psfile}", tmpfile], _log) @@ -1346,6 +1347,7 @@ def xpdf_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False): # happy (https://ghostscript.com/doc/9.56.1/Use.htm#MS_Windows). cbook._check_and_log_subprocess( ["ps2pdf", + "-dSAFER", "-dAutoFilterColorImages#false", "-dAutoFilterGrayImages#false", "-sAutoRotatePages#None", diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 2193dc6b6cdc..7789ec2cd882 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1132,7 +1132,8 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): font_style['font-style'] = prop.get_style() if prop.get_variant() != 'normal': font_style['font-variant'] = prop.get_variant() - weight = fm.weight_dict[prop.get_weight()] + weight = prop.get_weight() + weight = fm.weight_dict.get(weight, weight) # convert to int if weight != 400: font_style['font-weight'] = f'{weight}' diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index f7318d578121..4f3e594e9202 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -446,6 +446,8 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5, if transform is None: transform = self.axes.transData if transform: + x = self.axes.convert_xunits(x) + y = self.axes.convert_yunits(y) x, y = transform.transform((x, y)) idx_level_min, idx_vtx_min, proj = self._find_nearest_contour( diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 089141727189..e5794954abb8 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -376,10 +376,17 @@ def _suplabels(self, t, info, **kwargs): else: suplab = self.text(x, y, t, **kwargs) setattr(self, info['name'], suplab) + suplab._remove_method = functools.partial(self._remove_suplabel, + name=info['name']) + suplab._autopos = autopos self.stale = True return suplab + def _remove_suplabel(self, label, name): + self.texts.remove(label) + setattr(self, name, None) + @_docstring.Substitution(x0=0.5, y0=0.98, name='super title', ha='center', va='top', rc='title') @_docstring.copy(_suplabels) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 9aa8dccde444..a44d5bd76b9a 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -744,7 +744,7 @@ def get_variant(self): def get_weight(self): """ - Set the font weight. Options are: A numeric value in the + Get the font weight. Options are: A numeric value in the range 0-1000 or one of 'light', 'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black' diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 37281afeaafa..3c8b52a73b6b 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -194,7 +194,7 @@ class FT2Font(Buffer): _kerning_factor: int = ... ) -> None: ... if sys.version_info[:2] >= (3, 12): - def __buffer__(self, flags: int) -> memoryview: ... + def __buffer__(self, /, flags: int) -> memoryview: ... def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... def clear(self) -> None: ... def draw_glyph_to_bitmap( @@ -286,7 +286,7 @@ class FT2Image(Buffer): def __init__(self, width: int, height: int) -> None: ... def draw_rect_filled(self, x0: int, y0: int, x1: int, y1: int) -> None: ... if sys.version_info[:2] >= (3, 12): - def __buffer__(self, flags: int) -> memoryview: ... + def __buffer__(self, /, flags: int) -> memoryview: ... @final class Glyph: diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index c23d9f818454..3a4da5f575fb 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -30,7 +30,7 @@ from matplotlib._enums import JoinStyle, CapStyle # Don't let the original cycler collide with our validating cycler -from cycler import Cycler, cycler as ccycler +from cycler import Cycler, concat as cconcat, cycler as ccycler @_api.caching_module_getattr @@ -759,11 +759,62 @@ def cycler(*args, **kwargs): return reduce(operator.add, (ccycler(k, v) for k, v in validated)) -class _DunderChecker(ast.NodeVisitor): - def visit_Attribute(self, node): - if node.attr.startswith("__") and node.attr.endswith("__"): - raise ValueError("cycler strings with dunders are forbidden") - self.generic_visit(node) +def _parse_cycler_string(s): + """ + Parse a string representation of a cycler into a Cycler object safely, + without using eval(). + + Accepts expressions like:: + + cycler('color', ['r', 'g', 'b']) + cycler('color', 'rgb') + cycler('linewidth', [1, 2, 3]) + cycler(c='rgb', lw=[1, 2, 3]) + cycler('c', 'rgb') * cycler('linestyle', ['-', '--']) + """ + try: + tree = ast.parse(s, mode='eval') + except SyntaxError as e: + raise ValueError(f"Could not parse {s!r}: {e}") from e + return _eval_cycler_expr(tree.body) + + +def _eval_cycler_expr(node): + """Recursively evaluate an AST node to build a Cycler object.""" + if isinstance(node, ast.BinOp): + left = _eval_cycler_expr(node.left) + right = _eval_cycler_expr(node.right) + if isinstance(node.op, ast.Add): + return left + right + if isinstance(node.op, ast.Mult): + return left * right + raise ValueError(f"Unsupported operator: {type(node.op).__name__}") + if isinstance(node, ast.Call): + if not (isinstance(node.func, ast.Name) + and node.func.id in ('cycler', 'concat')): + raise ValueError( + "only the 'cycler()' and 'concat()' functions are allowed") + func = cycler if node.func.id == 'cycler' else cconcat + args = [_eval_cycler_expr(a) for a in node.args] + kwargs = {kw.arg: _eval_cycler_expr(kw.value) for kw in node.keywords} + return func(*args, **kwargs) + if isinstance(node, ast.Subscript): + sl = node.slice + if not isinstance(sl, ast.Slice): + raise ValueError("only slicing is supported, not indexing") + s = slice( + ast.literal_eval(sl.lower) if sl.lower else None, + ast.literal_eval(sl.upper) if sl.upper else None, + ast.literal_eval(sl.step) if sl.step else None, + ) + value = _eval_cycler_expr(node.value) + return value[s] + # Allow literal values (int, strings, lists, tuples) as arguments + # to cycler() and concat(). + try: + return ast.literal_eval(node) + except (ValueError, TypeError): + raise ValueError( + f"Unsupported expression in cycler string: {ast.dump(node)}") # A validator dedicated to the named legend loc @@ -814,25 +865,11 @@ def _validate_legend_loc(loc): def validate_cycler(s): """Return a Cycler object from a string repr or the object itself.""" if isinstance(s, str): - # TODO: We might want to rethink this... - # While I think I have it quite locked down, it is execution of - # arbitrary code without sanitation. - # Combine this with the possibility that rcparams might come from the - # internet (future plans), this could be downright dangerous. - # I locked it down by only having the 'cycler()' function available. - # UPDATE: Partly plugging a security hole. - # I really should have read this: - # https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html - # We should replace this eval with a combo of PyParsing and - # ast.literal_eval() try: - _DunderChecker().visit(ast.parse(s)) - s = eval(s, {'cycler': cycler, '__builtins__': {}}) - except BaseException as e: + s = _parse_cycler_string(s) + except Exception as e: raise ValueError(f"{s!r} is not a valid cycler construction: {e}" ) from e - # Should make sure what comes from the above eval() - # is a Cycler object. if isinstance(s, Cycler): cycler_inst = s else: @@ -1101,7 +1138,7 @@ def _convert_validator_spec(key, conv): "axes.formatter.offset_threshold": validate_int, "axes.unicode_minus": validate_bool, # This entry can be either a cycler object or a string repr of a - # cycler-object, which gets eval()'ed to create the object. + # cycler-object, which is parsed safely via AST. "axes.prop_cycle": validate_cycler, # If "data", axes limits are set close to the data. # If "round_numbers" axes limits are set to the nearest round numbers. diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 19113d399626..73b1645468a6 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -164,7 +164,8 @@ def _check_for_pgf(texsystem): """, encoding="utf-8") try: subprocess.check_call( - [texsystem, "-halt-on-error", str(tex_path)], cwd=tmpdir, + [texsystem, "-halt-on-error", "-no-shell-escape", + str(tex_path)], cwd=tmpdir, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except (OSError, subprocess.CalledProcessError): return False diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 159fc70282b8..b9eb145b1410 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2182,9 +2182,8 @@ def test_pcolor_regression(pd): time_axis, y_axis = np.meshgrid(times, y_vals) shape = (len(y_vals) - 1, len(times) - 1) - z_data = np.arange(shape[0] * shape[1]) + z_data = np.arange(shape[0] * shape[1]).reshape(shape) - z_data.shape = shape try: register_matplotlib_converters() diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 509136fe323e..0f9f5f19afb5 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -73,7 +73,8 @@ def test_bold_font_output(): ax.plot(np.arange(10), np.arange(10)) ax.set_xlabel('nonbold-xlabel') ax.set_ylabel('bold-ylabel', fontweight='bold') - ax.set_title('bold-title', fontweight='bold') + # set weight as integer to assert it's handled properly + ax.set_title('bold-title', fontweight=600) @image_comparison(['bold_font_output_with_none_fonttype.svg']) @@ -83,7 +84,8 @@ def test_bold_font_output_with_none_fonttype(): ax.plot(np.arange(10), np.arange(10)) ax.set_xlabel('nonbold-xlabel') ax.set_ylabel('bold-ylabel', fontweight='bold') - ax.set_title('bold-title', fontweight='bold') + # set weight as integer to assert it's handled properly + ax.set_title('bold-title', fontweight=600) @check_figures_equal(tol=20) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 4e3c1bbc2bb5..594681391e61 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -648,7 +648,7 @@ def _impl_test_interactive_timers(): assert mock.call_count > 1 # Now turn it into a single shot timer and verify only one gets triggered - mock.call_count = 0 + mock.reset_mock() timer.single_shot = True timer.start() plt.pause(pause_time) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 7da4bab69c15..7ca9c0f77858 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1590,7 +1590,7 @@ def test_norm_callback(): assert increment.call_count == 2 # We only want autoscale() calls to send out one update signal - increment.call_count = 0 + increment.reset_mock() norm.autoscale([0, 1, 2]) assert increment.call_count == 1 diff --git a/lib/matplotlib/tests/test_datetime.py b/lib/matplotlib/tests/test_datetime.py index 276056d044ae..9fc133549017 100644 --- a/lib/matplotlib/tests/test_datetime.py +++ b/lib/matplotlib/tests/test_datetime.py @@ -259,11 +259,22 @@ def test_bxp(self): ax.xaxis.set_major_formatter(mpl.dates.DateFormatter("%Y-%m-%d")) ax.set_title('Box plot with datetime data') - @pytest.mark.xfail(reason="Test for clabel not written yet") @mpl.style.context("default") def test_clabel(self): + dates = [datetime.datetime(2023, 10, 1) + datetime.timedelta(days=i) + for i in range(10)] + x = np.arange(-10.0, 5.0, 0.5) + X, Y = np.meshgrid(x, dates) + Z = np.arange(X.size).reshape(X.shape) + fig, ax = plt.subplots() - ax.clabel(...) + CS = ax.contour(X, Y, Z) + labels = ax.clabel(CS, manual=[(x[0], dates[0])]) + assert len(labels) == 1 + assert labels[0].get_text() == '0' + x_pos, y_pos = labels[0].get_position() + assert x_pos == pytest.approx(-10.0, abs=1e-3) + assert y_pos == pytest.approx(mpl.dates.date2num(dates[0]), abs=1e-3) @mpl.style.context("default") def test_contour(self): diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 3a4ced254091..487b6b9688ec 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -364,6 +364,28 @@ def test_get_suptitle_supxlabel_supylabel(): assert fig.get_supylabel() == 'supylabel' +def test_remove_suptitle_supxlabel_supylabel(): + fig = plt.figure() + + title = fig.suptitle('suptitle') + xlabel = fig.supxlabel('supxlabel') + ylabel = fig.supylabel('supylabel') + + assert len(fig.texts) == 3 + assert fig._suptitle is not None + assert fig._supxlabel is not None + assert fig._supylabel is not None + + title.remove() + assert fig._suptitle is None + xlabel.remove() + assert fig._supxlabel is None + ylabel.remove() + assert fig._supylabel is None + + assert not fig.texts + + @image_comparison(['alpha_background'], # only test png and svg. The PDF output appears correct, # but Ghostscript does not preserve the background color. @@ -1551,6 +1573,7 @@ def test_subfigures_wspace_hspace(): def test_subfigure_remove(): fig = plt.figure() sfs = fig.subfigures(2, 2) + sfs[1, 1].subplots() sfs[1, 1].remove() assert len(fig.subfigs) == 3 diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index bea5e90ea4e5..d800731ac53a 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -272,16 +272,23 @@ def generate_validator_testcases(valid): cycler('linestyle', ['-', '--'])), (cycler(mew=[2, 5]), cycler('markeredgewidth', [2, 5])), + ("2 * cycler('color', 'rgb')", 2 * cycler('color', 'rgb')), + ("2 * cycler('color', 'r' + 'gb')", 2 * cycler('color', 'rgb')), + ("cycler(c='r' + 'gb', lw=[1, 2, 3])", + cycler('color', 'rgb') + cycler('linewidth', [1, 2, 3])), + ("cycler('color', 'rgb') * 2", cycler('color', 'rgb') * 2), + ("concat(cycler('color', 'rgb'), cycler('color', 'cmk'))", + cycler('color', list('rgbcmk'))), + ("cycler('color', 'rgbcmk')[:3]", cycler('color', list('rgb'))), + ("cycler('color', 'rgb')[::-1]", cycler('color', list('bgr'))), ), - # This is *so* incredibly important: validate_cycler() eval's - # an arbitrary string! I think I have it locked down enough, - # and that is what this is testing. - # TODO: Note that these tests are actually insufficient, as it may - # be that they raised errors, but still did an action prior to - # raising the exception. We should devise some additional tests - # for that... + # validate_cycler() parses an arbitrary string using a safe + # AST-based parser (no eval). These tests verify that only valid + # cycler expressions are accepted. 'fail': ((4, ValueError), # Gotta be a string or Cycler object ('cycler("bleh, [])', ValueError), # syntax error + ("cycler('color', 'rgb') * * cycler('color', 'rgb')", # syntax error + ValueError), ('Cycler("linewidth", [1, 2, 3])', ValueError), # only 'cycler()' function is allowed # do not allow dunder in string literals @@ -295,6 +302,9 @@ def generate_validator_testcases(valid): ValueError), ("cycler('c', [j.__class__(j).lower() for j in ['r', 'b']])", ValueError), + # list comprehensions are arbitrary code, even if "safe" + ("cycler('color', [x for x in ['r', 'g', 'b']])", + ValueError), ('1 + 2', ValueError), # doesn't produce a Cycler object ('os.system("echo Gotcha")', ValueError), # os not available ('import os', ValueError), # should not be able to import diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 0424aede16eb..6856329931ef 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -291,8 +291,8 @@ def make_dvi(cls, tex, fontsize): Path(tmpdir, "file.tex").write_text( cls._get_tex_source(tex, fontsize), encoding='utf-8') cls._run_checked_subprocess( - ["latex", "-interaction=nonstopmode", "--halt-on-error", - "file.tex"], tex, cwd=tmpdir) + ["latex", "-interaction=nonstopmode", "-halt-on-error", + "-no-shell-escape", "file.tex"], tex, cwd=tmpdir) Path(tmpdir, "file.dvi").replace(dvifile) # Also move the tex source to the main cache directory, but # only for backcompat. diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 6a3c0d684380..039d7133ded1 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -980,7 +980,7 @@ class CheckButtons(AxesWidget): For the check buttons to remain responsive you must keep a reference to this object. - Connect to the CheckButtons with the `.on_clicked` method. + Connect to the CheckButtons with the `~._Buttons.on_clicked` method. Attributes ---------- @@ -1545,7 +1545,7 @@ class RadioButtons(AxesWidget): For the buttons to remain responsive you must keep a reference to this object. - Connect to the RadioButtons with the `.on_clicked` method. + Connect to the RadioButtons with the `~._Buttons.on_clicked` method. Attributes ---------- diff --git a/pyproject.toml b/pyproject.toml index 23c441b52c9c..96b69829e674 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ requires-python = ">=3.10" dev = [ "meson-python>=0.13.1,<0.17.0", "pybind11>=2.13.2,!=2.13.3", - "setuptools_scm>=7", + "setuptools_scm>=7,<10", # Not required by us but setuptools_scm without a version, cso _if_ # installed, then setuptools_scm 8 requires at least this version. # Unfortunately, we can't do a sort of minimum-if-instaled dependency, so @@ -74,7 +74,7 @@ build-backend = "mesonpy" requires = [ "meson-python>=0.13.1,<0.17.0", "pybind11>=2.13.2,!=2.13.3", - "setuptools_scm>=7", + "setuptools_scm>=7,<10", ] [tool.meson-python.args] diff --git a/requirements/dev/build-requirements.txt b/requirements/dev/build-requirements.txt index 4d2a098c3c4f..372a7d669fb1 100644 --- a/requirements/dev/build-requirements.txt +++ b/requirements/dev/build-requirements.txt @@ -1,3 +1,3 @@ pybind11>=2.13.2,!=2.13.3 meson-python -setuptools-scm +setuptools-scm<10 diff --git a/requirements/testing/mypy.txt b/requirements/testing/mypy.txt index 0cef979a34bf..4421560494d7 100644 --- a/requirements/testing/mypy.txt +++ b/requirements/testing/mypy.txt @@ -22,5 +22,5 @@ packaging>=20.0 pillow>=8 pyparsing>=3 python-dateutil>=2.7 -setuptools_scm>=7 +setuptools_scm>=7,<10 setuptools>=64 diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 269e2aaa9ee5..80bb14e8cfec 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -58,8 +58,8 @@ PyRendererAgg_draw_path(RendererAgg *self, static void PyRendererAgg_draw_text_image(RendererAgg *self, py::array_t image_obj, - std::variant vx, - std::variant vy, + std::variant vx, + std::variant vy, double angle, GCAgg &gc) { diff --git a/src/_enums.h b/src/_enums.h index 18f3d9aac9fa..e607b93f50f2 100644 --- a/src/_enums.h +++ b/src/_enums.h @@ -80,7 +80,7 @@ namespace p11x { auto ival = PyLong_AsLong(tmp); \ value = decltype(value)(ival); \ Py_DECREF(tmp); \ - return !(ival == -1 && !PyErr_Occurred()); \ + return !(ival == -1 && PyErr_Occurred()); \ } else { \ return false; \ } \ diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 9b54721810d6..3008c0b08b2f 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -14,7 +14,7 @@ namespace py = pybind11; using namespace pybind11::literals; template -using double_or_ = std::variant; +using double_or_ = std::variant; template static T diff --git a/subprojects/freetype-2.6.1.wrap b/subprojects/freetype-2.6.1.wrap index 763362b84df0..270556f0d5d3 100644 --- a/subprojects/freetype-2.6.1.wrap +++ b/subprojects/freetype-2.6.1.wrap @@ -1,5 +1,5 @@ [wrap-file] -source_url = https://download.savannah.gnu.org/releases/freetype/freetype-old/freetype-2.6.1.tar.gz +source_url = https://download.savannah.nongnu.org/releases/freetype/freetype-old/freetype-2.6.1.tar.gz source_fallback_url = https://downloads.sourceforge.net/project/freetype/freetype2/2.6.1/freetype-2.6.1.tar.gz source_filename = freetype-2.6.1.tar.gz source_hash = 0a3c7dfbda6da1e8fce29232e8e96d987ababbbf71ebc8c75659e4132c367014 diff --git a/tools/cache_zenodo_svg.py b/tools/cache_zenodo_svg.py index 07b67a3e04ee..2ee72c6a89fa 100644 --- a/tools/cache_zenodo_svg.py +++ b/tools/cache_zenodo_svg.py @@ -63,6 +63,7 @@ def _get_xdg_cache_dir(): if __name__ == "__main__": data = { + "v3.10.8": "17595503", "v3.10.7": "17298696", "v3.10.6": "16999430", "v3.10.5": "16644850",