From 3d6fe285ba4ec8d17c4d5e6998122507bf051c7c Mon Sep 17 00:00:00 2001 From: thc1006 <84045975+thc1006@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:05:17 +0800 Subject: [PATCH 1/3] Fix Line3D.draw requiring .shape on array-like coordinates set_data_3d documents its parameters as array-like and checks only that each is iterable, storing them as given. draw then read self._verts3d[0].shape, so a list raised AttributeError. The access sits inside the invalid-scale masking branch, so a finite list draws and only a non-finite coordinate reaches it. That is why this survived until 3.11 added the branch. Use np.shape, which leaves what get_data_3d returns unchanged. Closes #32127 --- lib/mpl_toolkits/mplot3d/art3d.py | 5 ++++- lib/mpl_toolkits/mplot3d/tests/test_art3d.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index b0d7312bff3d..20212c5d8e89 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -358,9 +358,12 @@ def draw(self, renderer): if self._axlim_clip: scale_mask |= _viewlim_mask(*self._verts3d, self.axes) if np.any(scale_mask): + # np.shape rather than .shape: set_data_3d documents its + # parameters as array-like and stores whatever it is given, so + # _verts3d can hold lists. mask = np.broadcast_to( scale_mask, - (len(self._verts3d), *self._verts3d[0].shape) + (len(self._verts3d), *np.shape(self._verts3d[0])) ) xs3d, ys3d, zs3d = np.ma.array(self._verts3d, dtype=float, mask=mask).filled(np.nan) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py index aca943f9e0c0..87e21b71809a 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py @@ -13,6 +13,20 @@ ) +def test_line3d_draws_array_like_coordinates(): + # set_data_3d documents its parameters as array-like and stores whatever it + # is given, so _verts3d can hold lists. Drawing must not require an ndarray. + fig = plt.figure() + ax = fig.add_subplot(projection="3d") + line, = ax.plot([0.], [0.], [0.]) + + line.set_data_3d([np.nan], [np.nan], [np.nan]) + + # Only a coordinate that is invalid for the scale reaches the masking + # branch, which is why this needs the non-finite value above. + fig.canvas.draw() + + @pytest.mark.parametrize("zdir, expected", [ ("x", (1, 0, 0)), ("y", (0, 1, 0)), From aec8cd41b6c531860dd6c110612ff66ba0e67c45 Mon Sep 17 00:00:00 2001 From: thc1006 <84045975+thc1006@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:24:14 +0800 Subject: [PATCH 2/3] Widen the Line3D array-like test, and correct how the branch is reached The test covered one way in: a list of all-NaN coordinates on a linear scale. The masking branch is reached three ways, and the earlier commit message said only a non-finite coordinate could get there, which is wrong. It is entered when a coordinate is non-finite, when it is outside the active scale's domain, or when it is outside the view limits and axlim_clip is enabled. The last two need no non-finite value at all: a plain 0.0 on a log axis reaches it, and so does a finite in-domain point that is simply off screen. Parametrized over list and tuple, and over five ways in: NaN, inf, a finite value outside a log domain, a finite value masked by axlim_clip, and a multi-vertex line where only one vertex is masked. Against a released 3.11.1 with only art3d.py replaced, all ten fail before this change and pass after, and the rest of test_art3d.py stays green. --- lib/mpl_toolkits/mplot3d/art3d.py | 2 +- lib/mpl_toolkits/mplot3d/tests/test_art3d.py | 35 ++++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 20212c5d8e89..315ace83f306 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -360,7 +360,7 @@ def draw(self, renderer): if np.any(scale_mask): # np.shape rather than .shape: set_data_3d documents its # parameters as array-like and stores whatever it is given, so - # _verts3d can hold lists. + # _verts3d can hold lists or tuples. mask = np.broadcast_to( scale_mask, (len(self._verts3d), *np.shape(self._verts3d[0])) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py index 87e21b71809a..8150e4089821 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py @@ -13,17 +13,38 @@ ) -def test_line3d_draws_array_like_coordinates(): +@pytest.mark.parametrize("container", [list, tuple]) +@pytest.mark.parametrize("case", ["nan", "inf", "log_domain", "axlim_clip", "one_of_many"]) +def test_line3d_draws_array_like_coordinates(container, case): # set_data_3d documents its parameters as array-like and stores whatever it - # is given, so _verts3d can hold lists. Drawing must not require an ndarray. + # is given, so _verts3d can hold lists or tuples. Drawing must not require + # an ndarray. + # + # The masking branch that reads a shape is entered when a coordinate is + # non-finite, outside the active scale's domain, or outside the view limits + # with axlim_clip enabled, so each of those is a way in. fig = plt.figure() ax = fig.add_subplot(projection="3d") - line, = ax.plot([0.], [0.], [0.]) + line, = ax.plot([1.], [1.], [1.], + axlim_clip=(case == "axlim_clip")) + + if case == "nan": + xs, ys, zs = [np.nan], [1.], [1.] + elif case == "inf": + xs, ys, zs = [np.inf], [1.], [1.] + elif case == "log_domain": + # Finite, and invalid only because of the scale. + ax.set_xscale("log") + xs, ys, zs = [0.], [1.], [1.] + elif case == "axlim_clip": + # Finite and valid for the scale; masked for being out of view. + xs, ys, zs = [1e6], [1e6], [1e6] + else: + # A line where only one vertex is masked. + xs, ys, zs = [1., np.nan], [1., 2.], [1., 2.] + + line.set_data_3d(container(xs), container(ys), container(zs)) - line.set_data_3d([np.nan], [np.nan], [np.nan]) - - # Only a coordinate that is invalid for the scale reaches the masking - # branch, which is why this needs the non-finite value above. fig.canvas.draw() From c2477bce838c0f8bc8af55ca95b0155034e7b4e5 Mon Sep 17 00:00:00 2001 From: thc1006 <84045975+thc1006@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:25:57 +0800 Subject: [PATCH 3/3] Wrap the parametrize line to keep it under the line length --- lib/mpl_toolkits/mplot3d/tests/test_art3d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py index 8150e4089821..f8f47dad806d 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py @@ -14,7 +14,8 @@ @pytest.mark.parametrize("container", [list, tuple]) -@pytest.mark.parametrize("case", ["nan", "inf", "log_domain", "axlim_clip", "one_of_many"]) +@pytest.mark.parametrize( + "case", ["nan", "inf", "log_domain", "axlim_clip", "one_of_many"]) def test_line3d_draws_array_like_coordinates(container, case): # set_data_3d documents its parameters as array-like and stores whatever it # is given, so _verts3d can hold lists or tuples. Drawing must not require