Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ci/mypy-stubtest-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ matplotlib\.ticker\.LogitLocator\.nonsingular
# Stdlib/Enum considered inconsistent (no fault of ours, I don't think)
matplotlib\.backend_bases\._Mode\.__new__

# pybind11 internals
matplotlib\..*\.__pybind11_native_enum__
# nanobind internals
matplotlib\..*\.__nb_enum__

# 3.6 Pending deprecations
matplotlib\.figure\.Figure\.set_constrained_layout
Expand Down
8 changes: 4 additions & 4 deletions lib/matplotlib/_tri.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ class Triangulation:
x: npt.NDArray[np.float64],
y: npt.NDArray[np.float64],
triangles: npt.NDArray[np.int_],
mask: npt.NDArray[np.bool_] | tuple[()],
edges: npt.NDArray[np.int_] | tuple[()],
neighbors: npt.NDArray[np.int_] | tuple[()],
mask: npt.NDArray[np.bool_] | None,
edges: npt.NDArray[np.int_] | None,
neighbors: npt.NDArray[np.int_] | None,
correct_triangle_orientation: bool,
): ...
def calculate_plane_coefficients(self, z: npt.ArrayLike) -> npt.NDArray[np.float64]: ...
def get_edges(self) -> npt.NDArray[np.int_]: ...
def get_neighbors(self) -> npt.NDArray[np.int_]: ...
def set_mask(self, mask: npt.NDArray[np.bool_] | tuple[()]) -> None: ...
def set_mask(self, mask: npt.NDArray[np.bool_] | None) -> None: ...
7 changes: 7 additions & 0 deletions lib/matplotlib/font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from __future__ import annotations

import atexit
from base64 import b64encode
import dataclasses
from functools import cache, lru_cache
Expand Down Expand Up @@ -1574,9 +1575,15 @@ def _find_fonts_by_props(self, prop, fontext='ttf', directory=None,

return fpaths

@lru_cache()
def _register_findfont_cached_cleanup(self):
# Needed to clear the cache before nanobind's leak check runs
atexit.register(self._findfont_cached.cache_clear)

@lru_cache(1024)
def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
rebuild_if_missing, rc_params):
self._register_findfont_cached_cleanup()

prop = FontProperties._from_any(prop)

Expand Down
7 changes: 7 additions & 0 deletions lib/matplotlib/mathtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
metrics for those fonts.
"""

import atexit
import functools
import logging

Expand Down Expand Up @@ -85,8 +86,14 @@ def parse(self, s, dpi=72, prop=None, *, antialiased=None):
}[self._output_type]
return self._parse_cached(s, dpi, prop, antialiased, load_glyph_flags)

@functools.lru_cache()
def _register_parse_cached_cleanup(self):
# Needed to clear the cache before nanobind's leak check runs
atexit.register(self._parse_cached.cache_clear)

@functools.lru_cache(50)
def _parse_cached(self, s, dpi, prop, antialiased, load_glyph_flags):
self._register_parse_cached_cleanup()
if prop is None:
prop = FontProperties()
fontset_class = _api.getitem_checked(
Expand Down
33 changes: 4 additions & 29 deletions lib/matplotlib/tests/test_ft2font.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def test_ft2font_invalid_args(tmp_path):
file = fm.findfont('DejaVu Sans')

# hinting_factor argument.
with pytest.raises(TypeError, match='incompatible constructor arguments'):
with pytest.raises(TypeError, match='incompatible function arguments'):
ft2font.FT2Font(file, 1.3)
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match='text.hinting_factor rcParam was deprecated .+ 3.11'):
Expand All @@ -221,14 +221,14 @@ def test_ft2font_invalid_args(tmp_path):
match='The hinting_factor parameter was deprecated'):
ft2font.FT2Font(file, 0)

with pytest.raises(TypeError, match='incompatible constructor arguments'):
with pytest.raises(TypeError, match='incompatible function arguments'):
# failing to be a list will fail before the 0
ft2font.FT2Font(file, _fallback_list=(0,))
with pytest.raises(TypeError, match='incompatible constructor arguments'):
with pytest.raises(TypeError, match='incompatible function arguments'):
ft2font.FT2Font(file, _fallback_list=[0])

# kerning_factor argument.
with pytest.raises(TypeError, match='incompatible constructor arguments'):
with pytest.raises(TypeError, match='incompatible function arguments'):
ft2font.FT2Font(file, _kerning_factor=1.3)
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match='text.kerning_factor rcParam was deprecated .+ 3.11'):
Expand Down Expand Up @@ -1077,31 +1077,6 @@ def test__layout():
assert Path(item.ft_object.fname).name == 'cmr10.ttf'


def test_render_glyph_cache():
# Reusing a cached outline must not change what is rendered.
ft = fm.get_font(fm.findfont('DejaVu Sans'))
ft.set_size(12, 100)
index = ft.get_char_index(ord('e'))
identity = [[0x10000, 0], [0, 0x10000]]

def render(delta=(0, 0)):
ft._set_transform(identity, list(delta))
return ft._render_glyph(index, ft2font.LoadFlags.DEFAULT,
ft2font.RenderMode.NORMAL)

first = render()
reference = first.buffer.copy()
# A whole-pixel shift reuses the outline and only moves the glyph.
shifted = render(delta=(0x40 * 3, 0x40 * 5))
assert np.array_equal(shifted.buffer, reference)
assert (shifted.left, shifted.top) == (first.left + 3, first.top + 5)
# A fractional shift must reach the rasterizer rather than be rounded away.
assert not np.array_equal(render(delta=(0x20, 0x20)).buffer, reference)
# The size is part of the key.
ft.set_size(24, 100)
assert render().buffer.shape != reference.shape


def test_layout_cache():
# A cached layout must match a fresh one, and the size is part of the key.
ft = fm.get_font(fm.findfont('DejaVu Sans'))
Expand Down
72 changes: 37 additions & 35 deletions lib/matplotlib/tests/test_triangulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1187,66 +1187,68 @@ def test_internal_cpp_api() -> None:
# Following github issue 8197.
from matplotlib import _tri # noqa: F401, ensure lazy-loaded module *is* loaded.

# C++ Triangulation.
with pytest.raises(
TypeError,
match=r'__init__\(\): incompatible constructor arguments.'):
mpl._tri.Triangulation() # type: ignore[call-arg]

with pytest.raises(
ValueError, match=r'x and y must be 1D arrays of the same length'):
mpl._tri.Triangulation(np.array([]), np.array([1]), np.array([[]]), (), (), (),
False)
def test_incompatible_init(*args):
with pytest.raises(
TypeError, match=r'__init__\(\): incompatible function arguments.'
):
mpl._tri.Triangulation(*args)

x = np.array([0, 1, 1], dtype=np.float64)
y = np.array([0, 0, 1], dtype=np.float64)
with pytest.raises(
ValueError,
match=r'triangles must be a 2D array of shape \(\?,3\)'):
mpl._tri.Triangulation(x, y, np.array([[0, 1]]), (), (), (), False)

tris = np.array([[0, 1, 2]], dtype=np.int_)
with pytest.raises(
ValueError,
match=r'mask must be a 1D array with the same length as the '
r'triangles array'):
mpl._tri.Triangulation(x, y, tris, np.array([0, 1]), (), (), False)

test_incompatible_init()

# mask should be a 1D array
bad_mask = np.array([[1]])
test_incompatible_init(x, y, tris, None, bad_mask, None, False)

# triangles must be a 2D array of shape (?,3)
bad_triangles = np.array([[0, 1]])
test_incompatible_init(x, y, bad_triangles, None, None, None, False),

# neighbors must also be a 2D array of shape (?,3)
bad_neighbors = np.array([[-1]])
test_incompatible_init(x, y, tris, None, None, bad_neighbors, False)

with pytest.raises(
ValueError, match=r'edges must be a 2D array with shape \(\?,2\)'):
mpl._tri.Triangulation(x, y, tris, (), np.array([[1]]), (), False)
ValueError, match=r'x and y must be 1D arrays of the same length'):
mpl._tri.Triangulation(
np.array([], dtype=np.float64),
np.array([1.0], dtype=np.float64),
np.empty((0, 3), dtype=np.int32),
None, None, None, False)

with pytest.raises(
ValueError,
match=r'neighbors must be a 2D array with the same shape as the '
match=r'mask must be a 1D array with the same length as the '
r'triangles array'):
mpl._tri.Triangulation(x, y, tris, (), (), np.array([[-1]]), False)
mpl._tri.Triangulation(x, y, tris, np.array([0, 1]), None, None, False)

triang = mpl._tri.Triangulation(x, y, tris, (), (), (), False)
triang = mpl._tri.Triangulation(x, y, tris, None, None, None, False)

with pytest.raises(
ValueError,
match=r'z must be a 1D array with the same length as the '
r'triangulation x and y arrays'):
triang.calculate_plane_coefficients([])
triang.calculate_plane_coefficients(np.array([]))

for mask in ([0, 1], None):
with pytest.raises(
ValueError,
match=r'mask must be a 1D array with the same length as the '
r'triangles array'):
triang.set_mask(mask) # type: ignore[arg-type]
with pytest.raises(
ValueError,
match=r'mask must be a 1D array with the same length as the '
r'triangles array'):
triang.set_mask(np.array([0, 1])) # type: ignore[arg-type]

triang.set_mask(np.array([True]))
assert_array_equal(triang.get_edges(), np.empty((0, 2)))

triang.set_mask(()) # Equivalent to Python Triangulation mask=None
triang.set_mask(None)
assert_array_equal(triang.get_edges(), [[1, 0], [2, 0], [2, 1]])

# C++ TriContourGenerator.
with pytest.raises(
TypeError,
match=r'__init__\(\): incompatible constructor arguments.'):
match=r'__init__\(\): incompatible function arguments.'):
mpl._tri.TriContourGenerator() # type: ignore[call-arg]

with pytest.raises(
Expand All @@ -1264,7 +1266,7 @@ def test_internal_cpp_api() -> None:
# C++ TrapezoidMapTriFinder.
with pytest.raises(
TypeError,
match=r'__init__\(\): incompatible constructor arguments.'):
match=r'__init__\(\): incompatible function arguments.'):
mpl._tri.TrapezoidMapTriFinder() # type: ignore[call-arg]

trifinder = mpl._tri.TrapezoidMapTriFinder(triang)
Expand Down
15 changes: 8 additions & 7 deletions lib/matplotlib/tri/_triangulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def __init__(self, x, y, triangles=None, mask=None):
if triangles is None:
# No triangulation specified, so use matplotlib._qhull to obtain
# Delaunay triangulation.
self.triangles, self._neighbors = _qhull.delaunay(x, y, sys.flags.verbose)
self.triangles, self._neighbors = _qhull.delaunay(self.x, self.y,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good catch. It is functionally identical but will be slightly faster in some circumstances so we should do it even if this PR is never merged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pulled this out into #32417

sys.flags.verbose)
self.is_delaunay = True
else:
# Triangulation specified. Copy, since we may correct triangle
Expand Down Expand Up @@ -98,6 +99,7 @@ def calculate_plane_coefficients(self, z):
position in triangle tri to be calculated using
``z = array[tri, 0] * x + array[tri, 1] * y + array[tri, 2]``.
"""
z = np.asarray(z, dtype=np.float64)
return self.get_cpp_triangulation().calculate_plane_coefficients(z)

@property
Expand All @@ -122,11 +124,11 @@ def get_cpp_triangulation(self):
from matplotlib import _tri
if self._cpp_triangulation is None:
self._cpp_triangulation = _tri.Triangulation(
# For unset arrays use empty tuple which has size of zero.
# For unset arrays use None
self.x, self.y, self.triangles,
self.mask if self.mask is not None else (),
self._edges if self._edges is not None else (),
self._neighbors if self._neighbors is not None else (),
self.mask,
self._edges,
self._neighbors,
not self.is_delaunay)
return self._cpp_triangulation

Expand Down Expand Up @@ -235,8 +237,7 @@ def set_mask(self, mask):

# Set mask in C++ Triangulation.
if self._cpp_triangulation is not None:
self._cpp_triangulation.set_mask(
self.mask if self.mask is not None else ())
self._cpp_triangulation.set_mask(self.mask)

# Clear derived fields so they are recalculated when needed.
self._edges = None
Expand Down
4 changes: 4 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ py3 = py_mod.find_installation(pure: false)
py3_dep = py3.dependency()

pybind11_dep = dependency('pybind11', version: '>=3')
nanobind_dep = declare_dependency(
dependencies: dependency('nanobind'),
compile_args: ['-DNOMINMAX'],
)

subdir('extern')
subdir('src')
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ requires = [
# you really need it and aren't using an sdist.
"meson-python>=0.13.2,!=0.17.*",
"pybind11>=3",
"nanobind",
# setuptools_scm 10 breaks versioning in editable installs. You can remove this pin
# if you're a downstream distributor just building wheels or your equivalent.
"setuptools_scm>=7,<10",
Expand All @@ -81,6 +82,7 @@ build = [
# Should be the same as `[build-system] requires` above.
"meson-python>=0.13.1,!=0.17.*",
"pybind11>=3",
"nanobind",
"setuptools_scm>=7,<10",
# Not required by us but setuptools_scm without a version, so _if_
# installed, then setuptools_scm 8 requires at least this version.
Expand Down
Loading
Loading