diff --git a/.github/labeler.yml b/.github/labeler.yml index 77b79146b47f..ee49868c8d7f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,6 +4,7 @@ - any-glob-to-any-file: - '.github/workflows/cibuildwheel.yml' - '.github/workflows/wasm.yml' + - 'pyproject.toml' "CI: Run cygwin": - changed-files: - any-glob-to-any-file: ['.github/workflows/cygwin.yml'] diff --git a/.github/workflows/autoclose_schedule.yml b/.github/workflows/autoclose_schedule.yml index a4eba770699c..006cb3dda986 100644 --- a/.github/workflows/autoclose_schedule.yml +++ b/.github/workflows/autoclose_schedule.yml @@ -22,10 +22,10 @@ jobs: name: autoclose labeled PRs runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.13' - name: Install PyGithub diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 2bb7f9544902..5618d0eb8764 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -42,12 +42,12 @@ jobs: SDIST_NAME: ${{ steps.sdist.outputs.SDIST_NAME }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 name: Install Python with: python-version: '3.11' @@ -97,31 +97,6 @@ jobs: permissions: contents: read runs-on: ${{ matrix.os }} - env: - CIBW_BEFORE_BUILD: >- - rm -rf {package}/build - CIBW_BEFORE_BUILD_WINDOWS: >- - pip install delvewheel && - rm -rf {package}/build - CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: >- - delvewheel repair -w {dest_dir} {wheel} - CIBW_AFTER_BUILD: >- - twine check {wheel} && - python {package}/ci/check_wheel_licenses.py {wheel} - # On Windows, we explicitly request MSVC compilers (as GitHub Action runners have - # MinGW on PATH that would be picked otherwise), switch to a static build for - # runtimes, but use dynamic linking for `VCRUNTIME140.dll`, `VCRUNTIME140_1.dll`, - # and the UCRT. This avoids requiring specific versions of `MSVCP140.dll`, while - # keeping shared state with the rest of the Python process/extensions. - CIBW_CONFIG_SETTINGS_WINDOWS: >- - setup-args="--vsenv" - setup-args="-Db_vscrt=mt" - setup-args="-Dcpp_link_args=['ucrt.lib','vcruntime.lib','/nodefaultlib:libucrt.lib','/nodefaultlib:libvcruntime.lib']" - CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 - CIBW_SKIP: "*-musllinux_aarch64" - CIBW_TEST_COMMAND: >- - python {package}/ci/check_version_number.py - MACOSX_DEPLOYMENT_TARGET: "10.12" strategy: matrix: include: @@ -150,26 +125,23 @@ jobs: run: Remove-Item -Recurse C:\Strawberry - name: Build wheels for CPython 3.14 - uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 + uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: CIBW_BUILD: "cp314-* cp314t-*" - CIBW_ENABLE: "cpython-freethreading cpython-prerelease" CIBW_ARCHS: ${{ matrix.cibw_archs }} - CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 - name: Build wheels for CPython 3.13 - uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 + uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: - CIBW_BUILD: "cp313-* cp313t-*" - CIBW_ENABLE: cpython-freethreading + CIBW_BUILD: "cp313-*" CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.12 - uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 + uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: diff --git a/.github/workflows/circleci.yml b/.github/workflows/circleci.yml index 49dc4ea3b3ec..017bba79148d 100644 --- a/.github/workflows/circleci.yml +++ b/.github/workflows/circleci.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest name: Post warnings/errors as review steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/clean_pr.yml b/.github/workflows/clean_pr.yml index 75f6a451c7ee..acc1994e7a9c 100644 --- a/.github/workflows/clean_pr.yml +++ b/.github/workflows/clean_pr.yml @@ -11,7 +11,7 @@ jobs: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: '0' persist-credentials: false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 71425e9cc3e9..d312d60100ad 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,12 +29,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} @@ -45,4 +45,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/conflictcheck.yml b/.github/workflows/conflictcheck.yml index 2058da8ca9fb..e353e47bfb79 100644 --- a/.github/workflows/conflictcheck.yml +++ b/.github/workflows/conflictcheck.yml @@ -19,7 +19,7 @@ jobs: pull-requests: write steps: - name: Check if PRs have merge conflicts - uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 + uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0 with: dirtyLabel: "status: needs rebase" repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index 4b790ea52a7d..02636954b8cf 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -80,12 +80,12 @@ jobs: - name: Fix line endings run: git config --global core.autocrlf input - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false - - uses: cygwin/cygwin-install-action@711d29f3da23c9f4a1798e369a6f01198c13b11a # v6 + - uses: cygwin/cygwin-install-action@3f0a3f9f988f7e96b8c18098ae05eaec175f5b52 # v6 with: packages: >- ccache gcc-g++ gdb git graphviz libcairo-devel libffi-devel @@ -141,21 +141,21 @@ jobs: # FreeType build fails with bash, succeeds with dash - name: Cache pip - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: C:\cygwin\home\runneradmin\.cache\pip key: Cygwin-py3.${{ matrix.python-minor-version }}-pip-${{ hashFiles('pyproject.toml') }} restore-keys: ${{ matrix.os }}-py3.${{ matrix.python-minor-version }}-pip- - name: Cache ccache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: C:\cygwin\home\runneradmin\.ccache key: Cygwin-py3.${{ matrix.python-minor-version }}-ccache-${{ hashFiles('src/*') }} restore-keys: Cygwin-py3.${{ matrix.python-minor-version }}-ccache- - name: Cache Matplotlib - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | C:\cygwin\home\runneradmin\.cache\matplotlib diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 7d3784e07e91..1f68ff3a9e86 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -11,14 +11,14 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.x" - - uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3 + - uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4 with: extra-args: --hook-stage manual --all-files @@ -29,12 +29,12 @@ jobs: contents: read checks: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.11' @@ -59,12 +59,12 @@ jobs: contents: read checks: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.11' @@ -90,12 +90,12 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.14' @@ -122,7 +122,7 @@ jobs: contents: read checks: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/mypy-stubtest.yml b/.github/workflows/mypy-stubtest.yml index 81fcd48462e8..2c78aaf08bc4 100644 --- a/.github/workflows/mypy-stubtest.yml +++ b/.github/workflows/mypy-stubtest.yml @@ -12,12 +12,12 @@ jobs: contents: read checks: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.11' diff --git a/.github/workflows/stale-tidy.yml b/.github/workflows/stale-tidy.yml index feb1fe701d70..e7d8272bdf24 100644 --- a/.github/workflows/stale-tidy.yml +++ b/.github/workflows/stale-tidy.yml @@ -13,7 +13,7 @@ jobs: permissions: issues: write steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 300 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 63f1a1ce3b05..4ebcdcee1f31 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,7 +14,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 20 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a3a9def4bd40..34861a727a75 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -97,13 +97,13 @@ jobs: pygobject-ver: '<3.52.0' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -186,7 +186,7 @@ jobs: esac - name: Cache pip - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 if: startsWith(runner.os, 'Linux') with: path: ~/.cache/pip @@ -196,7 +196,7 @@ jobs: restore-keys: | ${{ matrix.os }}-py${{ matrix.python-version }}-pip- - name: Cache pip - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 if: startsWith(runner.os, 'macOS') with: path: ~/Library/Caches/pip @@ -204,7 +204,7 @@ jobs: restore-keys: | ${{ matrix.os }}-py${{ matrix.python-version }}-pip- - name: Cache ccache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.ccache @@ -212,7 +212,7 @@ jobs: restore-keys: | ${{ matrix.os }}-py${{ matrix.python-version }}-ccache- - name: Cache Matplotlib - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cache/matplotlib @@ -396,7 +396,7 @@ jobs: fi - name: Upload code coverage if: ${{ !cancelled() && github.event_name != 'schedule' }} - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }}" token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index e1668d99af60..af6cfa5ee3a1 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -39,18 +39,18 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 name: Install Python with: python-version: '3.13' - name: Build wheels for wasm - uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 + uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0 env: CIBW_BUILD: "cp312-*" CIBW_PLATFORM: "pyodide" diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index f6e77b3d6000..c276e7a42548 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -22,9 +22,9 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 0e199889cb07..5bb4f0d36c5e 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -3,7 +3,6 @@ matplotlib\.backends\..* matplotlib\.tests(\..*)? matplotlib\.pylab(\..*)? matplotlib\._.* -matplotlib\.rcsetup\._listify_validator matplotlib\.rcsetup\._validate_linestyle matplotlib\.ft2font\.Glyph matplotlib\.ft2font\.LayoutItem diff --git a/doc/api/prev_api_changes/api_changes_0.99.x.rst b/doc/api/prev_api_changes/api_changes_0.99.x.rst index 4736d066d43e..012e64550da7 100644 --- a/doc/api/prev_api_changes/api_changes_0.99.x.rst +++ b/doc/api/prev_api_changes/api_changes_0.99.x.rst @@ -67,7 +67,7 @@ Changes beyond 0.99.x required by the experimental traited config and are somewhat out of date. If needed, install them independently. -.. _configobj: http://www.voidspace.org.uk/python/configobj.html +.. _configobj: https://configobj.readthedocs.io/en/latest/ .. _`enthought.traits`: http://code.enthought.com/pages/traits.html * The new rc parameter ``savefig.extension`` sets the filename extension diff --git a/galleries/examples/images_contours_and_fields/image_antialiasing.py b/galleries/examples/images_contours_and_fields/image_antialiasing.py index 10f563875767..5d3c2f81b843 100644 --- a/galleries/examples/images_contours_and_fields/image_antialiasing.py +++ b/galleries/examples/images_contours_and_fields/image_antialiasing.py @@ -63,7 +63,7 @@ # # ``interpolation_stage='data'``: Data -> Interpolate/Resample -> Normalize -> RGBA # -# For both keyword arguments, Matplotlib has a default "antialiased", that is +# For both keyword arguments, Matplotlib has a default "auto", that is # recommended for most situations, and is described below. Note that this # default behaves differently if the image is being down- or up-sampled, as # described below. @@ -166,12 +166,12 @@ # %% # A final example shows the desirability of performing the anti-aliasing at the # RGBA stage when using non-trivial interpolation kernels. In the following, -# the data in the upper 100 rows is exactly 0.0, and data in the inner circle +# the data in the outer circle is exactly 0.0, and data in the inner circle # is exactly 2.0. If we perform the *interpolation_stage* in 'data' space and # use an anti-aliasing filter (first panel), then floating point imprecision # makes some of the data values just a bit less than zero or a bit more than # 2.0, and they get assigned the under- or over- colors. This can be avoided if -# you do not use an anti-aliasing filter (*interpolation* set set to +# you do not use an anti-aliasing filter (*interpolation* set to # 'nearest'), however, that makes the part of the data susceptible to Moiré # patterns much worse (second panel). Therefore, we recommend the default # *interpolation* of 'hanning'/'auto', and *interpolation_stage* of diff --git a/lib/matplotlib/_type1font.py b/lib/matplotlib/_type1font.py index b4ee5edb233c..c7b73f9c0c7e 100644 --- a/lib/matplotlib/_type1font.py +++ b/lib/matplotlib/_type1font.py @@ -866,17 +866,16 @@ def subset(self, characters, name_prefix): encoding = {code: glyph for code, glyph in self.prop['Encoding'].items() if code in characters} - encoding[0] = '.notdef' # todo and done include strings (glyph names) - todo = set(encoding.values()) + todo = {'.notdef', *encoding.values()} done = set() seen_subrs = {0, 1, 2, 3} while todo: glyph = todo.pop() called_glyphs, called_subrs = _CharstringSimulator(self).run(glyph) + done.add(glyph) todo.update(called_glyphs - done) seen_subrs.update(called_subrs) - done.add(glyph) charstrings = self._subset_charstrings(done) subrs = self._subset_subrs(seen_subrs) @@ -949,7 +948,8 @@ def _charstring_tokens(data): def _postscript_encoding(self, encoding): """Return a PostScript encoding array for the encoding.""" return '\n'.join([ - '/Encoding 256 array\n0 1 255 { 1 index exch /.notdef put} for', + '/Encoding 256 array', + '0 1 255 { 1 index exch /.notdef put} for', *( f'dup {i} /{glyph} put' for i, glyph in sorted(encoding.items()) diff --git a/lib/matplotlib/artist.pyi b/lib/matplotlib/artist.pyi index 7b8b0c36be69..c70a9ac750fc 100644 --- a/lib/matplotlib/artist.pyi +++ b/lib/matplotlib/artist.pyi @@ -23,8 +23,8 @@ _T_Artist = TypeVar("_T_Artist", bound=Artist) def allow_rasterization(draw): ... class _XYPair(NamedTuple): - x: ArrayLike - y: ArrayLike + x: list[float] + y: list[float] class Artist: zorder: float diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index b43ad157f6b5..1ec52655676f 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -479,7 +479,7 @@ class Axes(_AxesBase): x: ArrayLike, y1: ArrayLike | float, y2: ArrayLike | float = ..., - where: Sequence[bool] | None = ..., + where: ArrayLike | None = ..., interpolate: bool = ..., step: Literal["pre", "post", "mid"] | None = ..., *, @@ -491,7 +491,7 @@ class Axes(_AxesBase): y: ArrayLike, x1: ArrayLike | float, x2: ArrayLike | float = ..., - where: Sequence[bool] | None = ..., + where: ArrayLike | None = ..., step: Literal["pre", "post", "mid"] | None = ..., interpolate: bool = ..., *, diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 0ddf18b12ec2..edd93f9f3d52 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3256,7 +3256,12 @@ def _update_title_position(self, renderer): if title.get_text(): for ax in axs: ax.yaxis.get_tightbbox(renderer) # update offsetText - if ax.yaxis.offsetText.get_text(): + # A hidden offset text (e.g. on the shared y axis of an + # inner subplot) is not drawn, so it must not move the + # title: its tight bbox is non-finite and would otherwise + # push the title to infinity. + if (ax.yaxis.offsetText.get_visible() + and ax.yaxis.offsetText.get_text()): bb = ax.yaxis.offsetText.get_tightbbox(renderer) if bb.intersection(title.get_tightbbox(renderer), bb): top = bb.ymax @@ -4737,7 +4742,10 @@ def get_tightbbox(self, renderer=None, *, call_axes_locator=True, for axis in self._axis_map.values(): if self.axison and axis.get_visible(): - ba = martist._get_tightbbox_for_layout_only(axis, renderer) + if for_layout_only: + ba = martist._get_tightbbox_for_layout_only(axis, renderer) + else: + ba = axis.get_tightbbox(renderer) if ba: bb.append(ba) self._update_title_position(renderer) diff --git a/lib/matplotlib/axes/_base.pyi b/lib/matplotlib/axes/_base.pyi index 4a70405346a5..f90ddc45f347 100644 --- a/lib/matplotlib/axes/_base.pyi +++ b/lib/matplotlib/axes/_base.pyi @@ -67,7 +67,7 @@ class _AxesBase(martist.Artist): def __init__( self, - fig: Figure, + fig: Figure | SubFigure, *args: tuple[float, float, float, float] | Bbox | int, facecolor: ColorType | None = ..., frameon: bool = ..., diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 27c3752858a7..633ce987269d 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -506,7 +506,7 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): mtext : `~matplotlib.text.Text` The original text object to be rendered. """ - self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") + self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX", mtext=mtext) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): """ diff --git a/lib/matplotlib/backends/registry.pyi b/lib/matplotlib/backends/registry.pyi index 565f044bf212..d1ba09cb523b 100644 --- a/lib/matplotlib/backends/registry.pyi +++ b/lib/matplotlib/backends/registry.pyi @@ -25,7 +25,7 @@ class BackendRegistry: def backend_for_gui_framework(self, framework: str) -> str | None: ... def is_valid_backend(self, backend: str) -> bool: ... def list_all(self) -> list[str]: ... - def list_builtin(self, filter_: BackendFilter | None) -> list[str]: ... + def list_builtin(self, filter_: BackendFilter | None = ...) -> list[str]: ... def list_gui_frameworks(self) -> list[str]: ... def load_backend_module(self, backend: str) -> ModuleType: ... def resolve_backend(self, backend: str | None) -> tuple[str, str | None]: ... diff --git a/lib/matplotlib/colorbar.pyi b/lib/matplotlib/colorbar.pyi index 07467ca74f3d..33a63ddd5335 100644 --- a/lib/matplotlib/colorbar.pyi +++ b/lib/matplotlib/colorbar.pyi @@ -13,11 +13,12 @@ from collections.abc import Sequence from typing import Any, Literal, overload from .typing import ColorType -class _ColorbarSpine(mspines.Spines): + +class _ColorbarSpine(mspines.Spine): def __init__(self, axes: Axes): ... - def get_window_extent(self, renderer: RendererBase | None = ...) -> Bbox:... + def get_window_extent(self, renderer: RendererBase | None = ...) -> Bbox: ... def set_xy(self, xy: ArrayLike) -> None: ... - def draw(self, renderer: RendererBase | None) -> None:... + def draw(self, renderer: RendererBase | None) -> None: ... class Colorbar: diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 53471e0f0a17..010f73131fbc 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -288,7 +288,15 @@ def same_color(c1, c2): """ Return whether the colors *c1* and *c2* are the same. - *c1*, *c2* can be single colors or lists/arrays of colors. + Parameters + ---------- + c1, c2 : :mpltype:`color` or list of :mpltype:`color` or RGB(A) array + If passing multiple colors, *c1* and *c2* must be of the same length. RGB(A) + arrays must be of shape (ncolors, 3) or (ncolors, 4). + + Returns + ------- + bool """ c1 = to_rgba_array(c1) c2 = to_rgba_array(c2) @@ -1225,7 +1233,7 @@ def from_list(name, colors, N=256, gamma=1.0, *, bad=None, under=None, over=None except Exception as e2: raise e2 from e vals = np.asarray(_vals) - if np.min(vals) < 0 or np.max(vals) > 1 or np.any(np.diff(vals) <= 0): + if np.min(vals) < 0 or np.max(vals) > 1 or np.any(np.diff(vals) < 0): raise ValueError( "the values passed in the (value, color) pairs " "must increase monotonically from 0 to 1." diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index 4bb1a8bae2a9..45cafeb23e3f 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -147,7 +147,7 @@ class FontManager: rebuild_if_missing: bool = ..., ) -> list[FontPath]: ... -def is_opentype_cff_font(filename: str) -> bool: ... +def is_opentype_cff_font(filename: str | os.PathLike) -> bool: ... def get_font( font_filepaths: Iterable[str | bytes | os.PathLike | FontPath] | str | bytes | os.PathLike | FontPath, ) -> ft2font.FT2Font: ... diff --git a/lib/matplotlib/image.pyi b/lib/matplotlib/image.pyi index 1fcc1a710bfd..4c2a8fbdb0e6 100644 --- a/lib/matplotlib/image.pyi +++ b/lib/matplotlib/image.pyi @@ -84,7 +84,7 @@ class _ImageBase(colorizer.ColorizingArtist): self, renderer: RendererBase, magnification: float = ..., unsampled: bool = ... ) -> tuple[np.ndarray, float, float, Affine2D]: ... def draw(self, renderer: RendererBase) -> None: ... - def write_png(self, fname: str | pathlib.Path | BinaryIO) -> None: ... + def write_png(self, fname: str | os.PathLike | BinaryIO) -> None: ... def set_data(self, A: ArrayLike | None) -> None: ... def set_array(self, A: ArrayLike | None) -> None: ... def get_shape(self) -> tuple[int, int, int]: ... diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 732ec874bf38..97fd1bc6209e 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -3497,7 +3497,7 @@ def fill_between( x: ArrayLike, y1: ArrayLike | float, y2: ArrayLike | float = 0, - where: Sequence[bool] | None = None, + where: ArrayLike | None = None, interpolate: bool = False, step: Literal["pre", "post", "mid"] | None = None, *, @@ -3522,7 +3522,7 @@ def fill_betweenx( y: ArrayLike, x1: ArrayLike | float, x2: ArrayLike | float = 0, - where: Sequence[bool] | None = None, + where: ArrayLike | None = None, step: Literal["pre", "post", "mid"] | None = None, interpolate: bool = False, *, diff --git a/lib/matplotlib/rcsetup.pyi b/lib/matplotlib/rcsetup.pyi index 120c0c93bec9..79f96430107a 100644 --- a/lib/matplotlib/rcsetup.pyi +++ b/lib/matplotlib/rcsetup.pyi @@ -7,7 +7,13 @@ from matplotlib.typing import ColorType, LineStyleType, MarkEveryType _T = TypeVar("_T") -def _listify_validator(s: Callable[[Any], _T]) -> Callable[[Any], list[_T]]: ... +def _listify_validator( + scalar_validator: Callable[[Any], _T], + allow_stringlist: bool = ..., + *, + n: int | None = ..., + doc: str | None = ..., +) -> Callable[[Any], list[_T]]: ... class ValidateInStrings: key: str diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index 2b94847a72b6..92547c3585f6 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -100,6 +100,9 @@ def _read_until(self, terminator): class _MagickConverter: def __call__(self, orig, dest): try: + # ImageMagick may not be permitted to follow a symlink, so resolve it + if orig.is_symlink(): + orig = orig.resolve() subprocess.run( [mpl._get_executable_info("magick").executable, orig, dest], check=True) diff --git a/lib/matplotlib/testing/compare.pyi b/lib/matplotlib/testing/compare.pyi index 8f11b3bebc1a..c673bdad6231 100644 --- a/lib/matplotlib/testing/compare.pyi +++ b/lib/matplotlib/testing/compare.pyi @@ -1,18 +1,19 @@ from collections.abc import Callable +from os import PathLike from typing import Literal, overload from numpy.typing import NDArray __all__ = ["calculate_rms", "comparable_formats", "compare_images"] -def make_test_filename(fname: str, purpose: str) -> str: ... +def make_test_filename(fname: str | PathLike, purpose: str) -> str: ... def get_cache_dir() -> str: ... def get_file_hash(path: str, block_size: int = ...) -> str: ... converter: dict[str, Callable[[str, str], None]] = {} def comparable_formats() -> list[str]: ... -def convert(filename: str, cache: bool) -> str: ... +def convert(filename: str | PathLike, cache: bool) -> str: ... def crop_to_same( actual_path: str, actual_image: NDArray, expected_path: str, expected_image: NDArray ) -> tuple[NDArray, NDArray]: ... diff --git a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.pdf b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.pdf index f2ebbeb528cc..bce9d583d4d4 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.pdf and b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png index e4a62a1c87ae..9f9d21b22528 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png and b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png differ diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index a00adcdf95f0..bfddddf6d665 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -181,7 +181,7 @@ def gen_writers(): # Smoke test for saving animations. In the future, we should probably # design more sophisticated tests which compare resulting frames a-la # matplotlib.testing.image_comparison -@pytest.mark.parametrize('writer, frame_format, output', gen_writers()) +@pytest.mark.parametrize('writer, frame_format, output', list(gen_writers())) @pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim']) def test_save_animation_smoketest(tmp_path, writer, frame_format, output, anim): if frame_format is not None: @@ -201,7 +201,7 @@ def test_save_animation_smoketest(tmp_path, writer, frame_format, output, anim): del anim -@pytest.mark.parametrize('writer, frame_format, output', gen_writers()) +@pytest.mark.parametrize('writer, frame_format, output', list(gen_writers())) def test_grabframe(tmp_path, writer, frame_format, output): WriterClass = animation.writers[writer] diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index b3797171cfae..9e27e9ab0a16 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -240,7 +240,7 @@ def test_matshow(fig_test, fig_ref): @image_comparison([f'formatter_ticker_{i:03d}.png' for i in range(1, 6)], style='mpl20', - tol=0.03 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.03) def test_formatter_ticker(): import matplotlib.testing.jpl_units as units units.register() @@ -5611,7 +5611,7 @@ def test_marker_styles(): @image_comparison(['rc_markerfill.png'], style='mpl20', - tol=0.033 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.033) def test_markers_fillstyle_rcparams(): fig, ax = plt.subplots() x = np.arange(7) @@ -5634,7 +5634,7 @@ def test_vertex_markers(): @image_comparison(['vline_hline_zorder.png', 'errorbar_zorder.png'], style='mpl20', - tol=0.02 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.02) def test_eb_line_zorder(): x = list(range(10)) @@ -6666,7 +6666,7 @@ def test_pie_linewidth_0(): @image_comparison(['pie_center_radius.png'], style='mpl20', - tol=0.01 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.01) def test_pie_center_radius(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -7965,6 +7965,22 @@ def test_title_above_offset(left, center): assert ycenter == yleft +def test_title_above_hidden_offset(): + # On an inner subplot with a shared y axis the offset text is hidden, but + # it still carries text. It must not be considered when positioning the + # title: its tight bbox is non-finite and used to push the title (and the + # subplot position computed by tight_layout) to NaN/inf. See #31881. + mpl.rcParams['axes.titley'] = None + fig, axs = plt.subplots(1, 2, sharey=True, tight_layout=True) + for i, ax in enumerate(axs): + ax.set_title(f'Subplot {i}') + ax.plot(range(10), [1e53] * 10) + fig.draw_without_rendering() # used to raise ValueError (NaN -> int) + for ax in axs: + assert np.isfinite(ax.title.get_window_extent().ymin) + assert np.all(np.isfinite(ax.get_position().bounds)) + + def test_title_no_move_off_page(): # If an Axes is off the figure (ie. if it is cropped during a save) # make sure that the automatic title repositioning does not get done. @@ -8314,6 +8330,21 @@ def test_tick_padding_tightbbox(): assert bb.y0 < bb2.y0 +def test_tightbbox_includes_long_label(): + fig, ax = plt.subplots() + + renderer = fig._get_renderer() + bbox_no_xlabel = ax.get_tightbbox(renderer, for_layout_only=False) + + ax.set_xlabel( + 'loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong') + bbox_long_xlabel = ax.get_tightbbox(renderer, for_layout_only=False) + + # When for_layout_only is False, the axes tightbbox should encompass its labels even + # if they are long enough to extent beyond its limits. + assert bbox_long_xlabel.width > bbox_no_xlabel.width + + def test_inset(): """ Ensure that inset_ax argument is indeed optional @@ -9841,7 +9872,7 @@ def test_zorder_and_explicit_rasterization(): @image_comparison(["preset_clip_paths.png"], remove_text=True, style="mpl20", - tol=0.01 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.01) def test_preset_clip_paths(): fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 2088ce764b5c..90db19110814 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -5,6 +5,7 @@ import string import numpy as np +from packaging.version import parse as parse_version import pytest import matplotlib as mpl @@ -112,7 +113,10 @@ def test_indexed_image(): with pikepdf.Pdf.open(buf) as pdf: page, = pdf.pages - image, = page.images.values() + if parse_version(pikepdf.__version__) < parse_version('10.9.0'): + image, = page.images.values() + else: + image, = page.get_images().values() pdf_image = pikepdf.PdfImage(image) assert pdf_image.indexed pil_image = pdf_image.as_pil_image() diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 5bc1f8aea973..2ef9b7c5d091 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1828,6 +1828,13 @@ def test_LinearSegmentedColormap_from_list_value_color_tuple(): ) +def test_LinearSegmentedColormap_from_list_repeat_values(): + # Smoke test that we can pass repeated values to make a solid band of color + # followed by a linear ramp. + value_color_tuples = [(0, "red"), (0.6, "red"), (0.6, "blue"), (1, "green")] + mcolors.LinearSegmentedColormap.from_list("lsc", value_color_tuples, N=11) + + @image_comparison(['test_norm_abc.png'], remove_text=True, style='_classic_test', tol=0 if platform.machine() == 'x86_64' else 0.05) def test_norm_abc(): diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index fbfb2515f42e..8391bda62e3c 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -1397,7 +1397,7 @@ def test_subfigure_dpi(): @image_comparison(['test_subfigure_ss.png'], style='mpl20', savefig_kwarg={'facecolor': 'teal'}, - tol=0.022 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.022) def test_subfigure_ss(): # test assigning the subfigure via subplotspec np.random.seed(19680801) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index ee7c7cea3aac..105feb37fa2f 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -203,6 +203,31 @@ def test_ft2font_invalid_args(tmp_path): ft2font.FT2Font(file, _kerning_factor=123) +def test_ft2font_oversized_read(): + # A file object whose read() returns *more* bytes than requested is + # misbehaving: FreeType only sizes its buffer for the requested count, so + # an over-long read must be rejected rather than overflow the buffer or be + # silently truncated. Verify that such an object causes construction to + # fail loudly instead. + data = Path(fm.findfont('DejaVu Sans')).read_bytes() + + class OversizedReader: + def __init__(self, data): + self._data = data + + def seek(self, offset): + pass + + def read(self, size): + # Ignore the requested size and hand back the whole file, which is + # far more than FreeType ever asks for. The initial read(0) probe + # in the constructor still gets an empty bytes object. + return self._data if size else b'' + + with pytest.raises(RuntimeError): + ft2font.FT2Font(OversizedReader(data)) + + @pytest.mark.parametrize('name, size, skippable', [('DejaVu Sans', 1, False), ('WenQuanYi Zen Hei', 3, True)]) def test_ft2font_face_index(name, size, skippable): diff --git a/lib/matplotlib/tests/test_inset.py b/lib/matplotlib/tests/test_inset.py index 81528580c723..bbf481c23253 100644 --- a/lib/matplotlib/tests/test_inset.py +++ b/lib/matplotlib/tests/test_inset.py @@ -93,8 +93,10 @@ def test_inset_indicator_zorder(): assert inset.get_zorder() == 42 -@image_comparison(['zoom_inset_connector_styles.png'], remove_text=True, style='mpl20', - tol=0.024 if platform.machine() in ['aarch64', 'arm64'] else 0) +@image_comparison(['zoom_inset_connector_styles.png'], + remove_text=True, style='mpl20', + tol=(0.024 if platform.machine() in ['aarch64', 'arm64', + 'loongarch64'] else 0)) def test_zoom_inset_connector_styles(): fig, axs = plt.subplots(2) for ax in axs: diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 67b433fe7447..84d3b7c4f09d 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -2,7 +2,6 @@ import io import itertools import platform -import sys import time from unittest import mock import warnings @@ -203,7 +202,7 @@ def test_alpha_rcparam(): @image_comparison(['fancy.png'], remove_text=True, style='mpl20', - tol=0.01 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.01) def test_fancy(): # using subplot triggers some offsetbox functionality untested elsewhere plt.subplot(121) @@ -665,7 +664,7 @@ def test_empty_bar_chart_with_legend(): @image_comparison(['shadow_argument_types.png'], remove_text=True, style='mpl20', - tol=0.028 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.028) def test_shadow_argument_types(): # Test that different arguments for shadow work as expected fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index a8f9236b5208..b200c3a9e048 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -221,7 +221,7 @@ def baseline_images(request, fontset, index, text): @pytest.mark.parametrize( - 'index, text', enumerate(math_tests), ids=range(len(math_tests))) + 'index, text', list(enumerate(math_tests)), ids=range(len(math_tests))) @pytest.mark.parametrize( 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif']) @pytest.mark.parametrize('baseline_images', ['mathtext'], indirect=True) @@ -237,7 +237,7 @@ def test_mathtext_rendering(baseline_images, fontset, index, text): horizontalalignment='center', verticalalignment='center') -@pytest.mark.parametrize('index, text', enumerate(svgastext_math_tests), +@pytest.mark.parametrize('index, text', list(enumerate(svgastext_math_tests)), ids=range(len(svgastext_math_tests))) @pytest.mark.parametrize('fontset', ['cm', 'dejavusans']) @pytest.mark.parametrize('baseline_images', ['mathtext0'], indirect=True) @@ -254,7 +254,7 @@ def test_mathtext_rendering_svgastext(baseline_images, fontset, index, text): horizontalalignment='center', verticalalignment='center') -@pytest.mark.parametrize('index, text', enumerate(lightweight_math_tests), +@pytest.mark.parametrize('index, text', list(enumerate(lightweight_math_tests)), ids=range(len(lightweight_math_tests))) @pytest.mark.parametrize('fontset', ['dejavusans']) @pytest.mark.parametrize('baseline_images', ['mathtext1'], indirect=True) @@ -266,7 +266,7 @@ def test_mathtext_rendering_lightweight(baseline_images, fontset, index, text): @pytest.mark.parametrize( - 'index, text', enumerate(font_tests), ids=range(len(font_tests))) + 'index, text', list(enumerate(font_tests)), ids=range(len(font_tests))) @pytest.mark.parametrize( 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif']) @pytest.mark.parametrize('baseline_images', ['mathfont'], indirect=True) diff --git a/lib/matplotlib/tests/test_mlab.py b/lib/matplotlib/tests/test_mlab.py index 82f877b4cc01..4f2ae49f4772 100644 --- a/lib/matplotlib/tests/test_mlab.py +++ b/lib/matplotlib/tests/test_mlab.py @@ -207,7 +207,8 @@ def test_detrend_linear_2d(self): scope='class') class TestSpectral: @pytest.fixture(scope='class', autouse=True) - def stim(self, request, fstims, iscomplex, sides, len_x, NFFT_density, + @classmethod + def stim(cls, request, fstims, iscomplex, sides, len_x, NFFT_density, nover_density, pad_to_density, pad_to_spectrum): Fs = 100. @@ -323,11 +324,6 @@ def stim(self, request, fstims, iscomplex, sides, len_x, NFFT_density, if iscomplex: y = y.astype('complex') - # Interestingly, the instance on which this fixture is called is not - # the same as the one on which a test is run. So we need to modify the - # class itself when using a class-scoped fixture. - cls = request.cls - cls.Fs = Fs cls.sides = sides cls.fstims = fstims diff --git a/lib/matplotlib/tests/test_path.py b/lib/matplotlib/tests/test_path.py index 3c7ad5e2f99b..c31f33647879 100644 --- a/lib/matplotlib/tests/test_path.py +++ b/lib/matplotlib/tests/test_path.py @@ -105,7 +105,7 @@ def test_contains_points_negative_radius(): (1., 2., 1., 2.)] -@pytest.mark.parametrize('path, extents', zip(_test_paths, _test_path_extents)) +@pytest.mark.parametrize('path, extents', list(zip(_test_paths, _test_path_extents))) def test_exact_extents(path, extents): # notice that if we just looked at the control points to get the bounding # box of each curve, we would get the wrong answers. For example, for diff --git a/lib/matplotlib/tests/test_patheffects.py b/lib/matplotlib/tests/test_patheffects.py index 7095f6b3855b..d8dfee3c1b94 100644 --- a/lib/matplotlib/tests/test_patheffects.py +++ b/lib/matplotlib/tests/test_patheffects.py @@ -1,4 +1,4 @@ -import sys +import platform import numpy as np @@ -30,7 +30,7 @@ def test_patheffect1(): @image_comparison(['patheffect2'], remove_text=True, style='mpl20', - tol=0.051 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.051) def test_patheffect2(): ax2 = plt.subplot() @@ -46,7 +46,7 @@ def test_patheffect2(): @image_comparison(['patheffect3'], style='mpl20', - tol=0.02 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.02) def test_patheffect3(): plt.figure(figsize=(8, 6)) p1, = plt.plot([1, 3, 5, 4, 3], 'o-b', lw=4) diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index 168047d0c07e..b7c55335aaa7 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -1,4 +1,4 @@ -import sys +import platform import numpy as np from numpy.testing import assert_allclose @@ -12,7 +12,7 @@ @image_comparison(['polar_axes.png'], style='default', - tol=0.009 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.009) def test_polar_annotations(): # You can specify the xypoint and the xytext in different positions and # coordinate systems, and optionally turn on a connecting line and mark the @@ -47,7 +47,7 @@ def test_polar_annotations(): @image_comparison(['polar_coords.png'], style='default', remove_text=True, - tol=0.013 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.013) def test_polar_coord_annotations(): # You can also use polar notation on a cartesian axes. Here the native # coordinate system ('data') is cartesian, so you need to specify the diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index dc36392ab676..e740d2454125 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -444,7 +444,7 @@ def generate_validator_testcases(valid): @pytest.mark.parametrize('validator, arg, target', - generate_validator_testcases(True)) + list(generate_validator_testcases(True))) def test_validator_valid(validator, arg, target): res = validator(arg) if isinstance(target, np.ndarray): @@ -457,7 +457,7 @@ def test_validator_valid(validator, arg, target): @pytest.mark.parametrize('validator, arg, exception_type', - generate_validator_testcases(False)) + list(generate_validator_testcases(False))) def test_validator_invalid(validator, arg, exception_type): with pytest.raises(exception_type): validator(arg) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 0d01677acc8c..7930a05ff497 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -461,7 +461,7 @@ class TestLogitLocator: @pytest.mark.parametrize( "lims, expected_low_ticks", - zip(ref_basic_limits, ref_basic_major_ticks), + list(zip(ref_basic_limits, ref_basic_major_ticks)), ) def test_basic_major(self, lims, expected_low_ticks): """ @@ -506,7 +506,7 @@ def test_nbins_major(self, lims): @pytest.mark.parametrize( "lims, expected_low_ticks", - zip(ref_basic_limits, ref_basic_major_ticks), + list(zip(ref_basic_limits, ref_basic_major_ticks)), ) def test_minor(self, lims, expected_low_ticks): """ diff --git a/lib/matplotlib/tests/test_usetex.py b/lib/matplotlib/tests/test_usetex.py index 87277f152789..55bd72ade5bf 100644 --- a/lib/matplotlib/tests/test_usetex.py +++ b/lib/matplotlib/tests/test_usetex.py @@ -26,10 +26,11 @@ def test_usetex(): kwargs = {"verticalalignment": "baseline", "size": 24, "bbox": dict(pad=0, edgecolor="k", facecolor="none")} ax.text(0.2, 0.7, - # the \LaTeX macro exercises character sizing and placement, + # The \LaTeX macro exercises character sizing and placement, # \left[ ... \right\} draw some variable-height characters, - # \sqrt and \frac draw horizontal rules, \mathrm changes the font - r'\LaTeX\ $\left[\int\limits_e^{2e}' + # \sqrt and \frac draw horizontal rules, \mathrm changes the font, + # the minus sign hits character code 0 in the Type 1 font that LaTeX uses. + r'\LaTeX\ $\left[\int\limits_e^{-2e}' r'\sqrt\frac{\log^3 x}{x}\,\mathrm{d}x \right\}$', **kwargs) ax.text(0.2, 0.3, "lg", **kwargs) @@ -43,6 +44,18 @@ def test_usetex(): ax.set_axis_off() +def test_usetex_fallback(monkeypatch): + # Smoke test that the base draw_tex implementation works. + from matplotlib.backend_bases import RendererBase + from matplotlib.backends.backend_agg import RendererAgg + monkeypatch.setattr(RendererAgg, 'draw_tex', RendererBase.draw_tex) + + plt.rcParams["text.usetex"] = True + fig, ax = plt.subplots() + ax.text(0, 0, "foo_bar") + fig.canvas.draw() + + @check_figures_equal(extensions=['png', 'pdf', 'svg']) def test_empty(fig_test, fig_ref): mpl.rcParams['text.usetex'] = True diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index a279de0dfd8b..2b46deae43dd 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -156,7 +156,7 @@ def invalidate(self): Invalidate this `TransformNode` and triggers an invalidation of its ancestors. Should be called any time the transform changes. """ - return self._invalidate_internal( + self._invalidate_internal( level=self._INVALID_AFFINE_ONLY if self.is_affine else self._INVALID_FULL, invalidating_node=self) diff --git a/lib/matplotlib/transforms.pyi b/lib/matplotlib/transforms.pyi index ebee3954a3a7..d3cd5f4ab692 100644 --- a/lib/matplotlib/transforms.pyi +++ b/lib/matplotlib/transforms.pyi @@ -9,9 +9,6 @@ from typing import Literal DEBUG: bool class TransformNode: - INVALID_NON_AFFINE: int - INVALID_AFFINE: int - INVALID: int # Implemented as a standard attr in base class, but functionally readonly and some subclasses implement as such @property def is_affine(self) -> bool: ... @@ -130,12 +127,39 @@ class Bbox(BboxBase): updatex: bool = ..., updatey: bool = ..., ) -> None: ... + # Access to the parent class property is broken in mypy right now, so ignore that + # check for the following read/write properties. + # https://github.com/python/mypy/issues/5936 + @BboxBase.x0.setter # type: ignore[attr-defined,misc] + def x0(self, val: float) -> None: ... + @BboxBase.y0.setter # type: ignore[attr-defined,misc] + def y0(self, val: float) -> None: ... + @BboxBase.x1.setter # type: ignore[attr-defined,misc] + def x1(self, val: float) -> None: ... + @BboxBase.y1.setter # type: ignore[attr-defined,misc] + def y1(self, val: float) -> None: ... + @BboxBase.p0.setter # type: ignore[attr-defined,misc] + def p0(self, val: tuple[float, float]) -> None: ... + @BboxBase.p1.setter # type: ignore[attr-defined,misc] + def p1(self, val: tuple[float, float]) -> None: ... + @BboxBase.intervalx.setter # type: ignore[attr-defined,misc] + def intervalx(self, interval: tuple[float, float]) -> None: ... + @BboxBase.intervaly.setter # type: ignore[attr-defined,misc] + def intervaly(self, interval: tuple[float, float]) -> None: ... + @BboxBase.bounds.setter # type: ignore[attr-defined,misc] + def bounds(self, bounds: tuple[float, float, float, float]) -> None: ... @property def minpos(self) -> float: ... + @minpos.setter + def minpos(self, val: float) -> None: ... @property def minposx(self) -> float: ... + @minposx.setter + def minposx(self, val: float) -> None: ... @property def minposy(self) -> float: ... + @minposy.setter + def minposy(self, val: float) -> None: ... def get_points(self) -> np.ndarray: ... def set_points(self, points: ArrayLike) -> None: ... def set(self, other: Bbox) -> None: ... diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index 76b5f2b642f3..68325fe284e2 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -12,7 +12,7 @@ """ from collections.abc import Hashable, Sequence import pathlib -from typing import Any, Literal, TypeAlias, TypeVar, Union +from typing import Any, Literal, TypeAlias, TypeVar from collections.abc import Callable, Mapping from . import path @@ -99,19 +99,14 @@ LogLevel: TypeAlias = Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] """Literal type for valid logging levels accepted by `matplotlib.set_loglevel()`.""" -CoordsBaseType = Union[ - str, - Artist, - Transform, - Callable[ - [RendererBase], - Union[Bbox, Transform] - ] -] -CoordsType = Union[ - CoordsBaseType, - tuple[CoordsBaseType, CoordsBaseType] -] +CoordsBaseType: TypeAlias = ( + str | + Artist | + Transform | + Callable[[RendererBase], Bbox | Transform] +) +CoordsType: TypeAlias = CoordsBaseType | tuple[CoordsBaseType, CoordsBaseType] +"""Annotation coordinate systems. See :doc:`/users/explain/text/annotations`.""" RcStyleType: TypeAlias = ( str | @@ -138,16 +133,22 @@ "axes_enter_event", "axes_leave_event", ] +"""Literal type for valid `.MouseEvent` names.""" KeyEventType: TypeAlias = Literal[ "key_press_event", "key_release_event" ] +"""Literal type for valid `.KeyEvent` names.""" DrawEventType: TypeAlias = Literal["draw_event"] +"""Literal type for valid `.DrawEvent` names.""" PickEventType: TypeAlias = Literal["pick_event"] +"""Literal type for valid `.PickEvent` names.""" ResizeEventType: TypeAlias = Literal["resize_event"] +"""Literal type for valid `.ResizeEvent` names.""" CloseEventType: TypeAlias = Literal["close_event"] +"""Literal type for valid `.CloseEvent` names.""" EventType: TypeAlias = Literal[ MouseEventType, @@ -157,6 +158,7 @@ ResizeEventType, CloseEventType, ] +"""Literal type for all valid events.""" LegendLocType: TypeAlias = ( Literal[ @@ -524,6 +526,7 @@ "ytick.minor.width", "ytick.right", ] +"""Valid specifiers for keys in `matplotlib.rcParams` and `matplotlib.rc_context`.""" RcGroupKeyType: TypeAlias = Literal[ "agg", @@ -590,3 +593,4 @@ "ytick.major", "ytick.minor", ] +"""Literal type for valid groups accepted by `matplotlib.rc`.""" diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 2ce76acf49d3..d2c3e66bfe37 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -4167,8 +4167,10 @@ def get_tightbbox(self, renderer=None, *, call_axes_locator=True, if self._axis3don: for axis in self._axis_map.values(): if axis.get_visible(): - axis_bb = martist._get_tightbbox_for_layout_only( - axis, renderer) + if for_layout_only: + axis_bb = martist._get_tightbbox_for_layout_only(axis, renderer) + else: + axis_bb = axis.get_tightbbox(renderer=renderer) if axis_bb: batch.append(axis_bb) return mtransforms.Bbox.union(batch) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 14bb1bbd9ba5..e724f04c7875 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -1724,7 +1724,7 @@ def test_errorbar3d_errorevery(): @mpl3d_image_comparison(['errorbar3d.png'], style='mpl20', - tol=0.015 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.015) def test_errorbar3d(): """Tests limits, color styling, and legend for 3D errorbars.""" fig = plt.figure() @@ -3204,3 +3204,24 @@ def test_plot_surface_log_scale_invalid_values(): zmin, zmax = ax.get_zlim() assert 1e-3 < zmin < zmax < 1e3, f"zlim corrupted: {(zmin, zmax)}" + + +def test_axes3d_tightbbox_includes_axis_labels(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + ax.scatter([1], [1], [1]) + + fig.draw_without_rendering() + renderer = fig._get_renderer() + bb_no_labels = ax.get_tightbbox(renderer) + + ax.set_xlabel('X label') + ax.set_ylabel('Y label') + ax.set_zlabel('Z label') + + fig.draw_without_rendering() + bb_full = ax.get_tightbbox(renderer) + + # The full bbox must be strictly larger in at least one dimension than the + # bbox not including labels. + assert bb_full.width > bb_no_labels.width or bb_full.height > bb_no_labels.height diff --git a/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py b/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py index a46c958222d8..86e0017699fb 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py @@ -1,4 +1,4 @@ -import sys +import platform import numpy as np @@ -28,7 +28,7 @@ def test_legend_bar(): @image_comparison(['fancy.png'], remove_text=True, style='mpl20', - tol=0.01 if sys.platform == 'darwin' else 0) + tol=0 if platform.machine() == 'x86_64' else 0.01) def test_fancy(): fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) ax.plot(np.arange(10), np.full(10, 5), np.full(10, 5), 'o--', label='line') diff --git a/pyproject.toml b/pyproject.toml index 02e2cf3b6734..20fff2d6deb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -401,6 +401,21 @@ addopts = [ "--import-mode=importlib", ] +[tool.cibuildwheel] +enable = ["cpython-freethreading"] +skip = "*-musllinux_aarch64" +manylinux-x86_64-image = "manylinux2014" + +before-build = "rm -rf {package}/build" +test-command = [ + # "python {package}/ci/check_wheel_licenses.py {wheel}", + "python {package}/ci/check_version_number.py", +] +test-environment = "PIP_PREFER_BINARY=true" + +[tool.cibuildwheel.macos.environment] +MACOSX_DEPLOYMENT_TARGET = "10.12" + [tool.cibuildwheel.pyodide] test-requires = "pytest" test-command = [ @@ -423,3 +438,26 @@ test-command = [ CFLAGS = "-fexceptions" CXXFLAGS = "-fexceptions" LDFLAGS = "-fexceptions" + +[tool.cibuildwheel.windows] +before-build = [ + "pip install delvewheel", + "rm -rf {package}/build", +] +repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}" + +[tool.cibuildwheel.windows.config-settings] +# On Windows, we explicitly request MSVC compilers (as GitHub Action runners have +# MinGW on PATH that would be picked otherwise), switch to a static build for +# runtimes, but use dynamic linking for `VCRUNTIME140.dll`, `VCRUNTIME140_1.dll`, +# and the UCRT. This avoids requiring specific versions of `MSVCP140.dll`, while +# keeping shared state with the rest of the Python process/extensions. +setup-args = [ + "--vsenv", + "-Db_vscrt=mt", + "-Dcpp_link_args=['ucrt.lib','vcruntime.lib','/nodefaultlib:libucrt.lib','/nodefaultlib:libvcruntime.lib']", +] + +[[tool.cibuildwheel.overrides]] +select = "cp314*" +manylinux-x86_64-image = "manylinux_2_28" diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 771f1db5a191..a8654ba1d8a3 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -453,6 +453,15 @@ read_from_file_callback(FT_Stream stream, unsigned long offset, unsigned char *b if (PyBytes_AsStringAndSize(read_result.ptr(), &tmpbuf, &n_read) == -1) { throw py::error_already_set(); } + if ((unsigned long)n_read > count) { + // A well-behaved read() never returns more than the requested + // number of bytes. FreeType only ever sized `buffer` for `count` + // bytes, so honoring an over-long read would overflow it. Rather + // than silently truncate (which would feed FreeType corrupt data), + // signal a failed read so that FT_Open_Face -- and in turn the + // FT2Font constructor -- raises. + n_read = 0; + } memcpy(buffer, tmpbuf, n_read); } catch (py::error_already_set &eas) { eas.discard_as_unraisable(__func__);