Skip to content

Commit 053703f

Browse files
authored
Merge pull request #16527 from timhoffm/validate-add_subplot
Validate positional parameters of add_subplot()
2 parents 9f67e4b + 419fafe commit 053703f

File tree

4 files changed

+74
-32
lines changed

4 files changed

+74
-32
lines changed

doc/api/next_api_changes/deprecations.rst

+6
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ Stricter rcParam validation
247247
(case-insensitive) to the option "line". This is deprecated; in a future
248248
version only the exact string "line" (case-sensitive) will be supported.
249249

250+
``add_subplot()`` validates its inputs
251+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
252+
In particular, for ``add_subplot(rows, cols, index)``, all parameters must
253+
be integral. Previously strings and floats were accepted and converted to
254+
int. This will now emit a deprecation warning.
255+
250256
Toggling axes navigation from the keyboard using "a" and digit keys
251257
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
252258
Axes navigation can still be toggled programmatically using

lib/matplotlib/figure.py

+42-26
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from matplotlib.axes import Axes, SubplotBase, subplot_class_factory
2929
from matplotlib.blocking_input import BlockingMouseInput, BlockingKeyMouseInput
30-
from matplotlib.gridspec import GridSpec
30+
from matplotlib.gridspec import GridSpec, SubplotSpec
3131
import matplotlib.legend as mlegend
3232
from matplotlib.patches import Rectangle
3333
from matplotlib.text import Text
@@ -1255,19 +1255,19 @@ def add_subplot(self, *args, **kwargs):
12551255
12561256
Parameters
12571257
----------
1258-
*args, default: (1, 1, 1)
1259-
Either a 3-digit integer or three separate integers
1260-
describing the position of the subplot. If the three
1261-
integers are *nrows*, *ncols*, and *index* in order, the
1262-
subplot will take the *index* position on a grid with *nrows*
1263-
rows and *ncols* columns. *index* starts at 1 in the upper left
1264-
corner and increases to the right.
1265-
1266-
*pos* is a three digit integer, where the first digit is the
1267-
number of rows, the second the number of columns, and the third
1268-
the index of the subplot. i.e. fig.add_subplot(235) is the same as
1269-
fig.add_subplot(2, 3, 5). Note that all integers must be less than
1270-
10 for this form to work.
1258+
*args, int or (int, int, int) or `SubplotSpec`, default: (1, 1, 1)
1259+
The position of the subplot described by one of
1260+
1261+
- Three integers (*nrows*, *ncols*, *index*). The subplot will
1262+
take the *index* position on a grid with *nrows* rows and
1263+
*ncols* columns. *index* starts at 1 in the upper left corner
1264+
and increases to the right.
1265+
- A 3-digit integer. The digits are interpreted as if given
1266+
separately as three single-digit integers, i.e.
1267+
``fig.add_subplot(235)`` is the same as
1268+
``fig.add_subplot(2, 3, 5)``. Note that this can only be used
1269+
if there are no more than 9 subplots.
1270+
- A `.SubplotSpec`.
12711271
12721272
In rare circumstances, `.add_subplot` may be called with a single
12731273
argument, a subplot axes instance already created in the
@@ -1347,27 +1347,43 @@ def add_subplot(self, *args, **kwargs):
13471347
ax1.remove() # delete ax1 from the figure
13481348
fig.add_subplot(ax1) # add ax1 back to the figure
13491349
"""
1350-
if not len(args):
1351-
args = (1, 1, 1)
1352-
1353-
if len(args) == 1 and isinstance(args[0], Integral):
1354-
if not 100 <= args[0] <= 999:
1355-
raise ValueError("Integer subplot specification must be a "
1356-
"three-digit number, not {}".format(args[0]))
1357-
args = tuple(map(int, str(args[0])))
1358-
13591350
if 'figure' in kwargs:
13601351
# Axes itself allows for a 'figure' kwarg, but since we want to
13611352
# bind the created Axes to self, it is not allowed here.
13621353
raise TypeError(
13631354
"add_subplot() got an unexpected keyword argument 'figure'")
13641355

1365-
if isinstance(args[0], SubplotBase):
1356+
nargs = len(args)
1357+
if nargs == 0:
1358+
args = (1, 1, 1)
1359+
elif nargs == 1:
1360+
if isinstance(args[0], Integral):
1361+
if not 100 <= args[0] <= 999:
1362+
raise ValueError(f"Integer subplot specification must be "
1363+
f"a three-digit number, not {args[0]}")
1364+
args = tuple(map(int, str(args[0])))
1365+
elif isinstance(args[0], (SubplotBase, SubplotSpec)):
1366+
pass # no further validation or normalization needed
1367+
else:
1368+
raise TypeError('Positional arguments are not a valid '
1369+
'position specification.')
1370+
elif nargs == 3:
1371+
for arg in args:
1372+
if not isinstance(arg, Integral):
1373+
cbook.warn_deprecated(
1374+
"3.3",
1375+
message="Passing non-integers as three-element "
1376+
"position specification is deprecated.")
1377+
args = tuple(map(int, args))
1378+
else:
1379+
raise TypeError(f'add_subplot() takes 1 or 3 positional arguments '
1380+
f'but {nargs} were given')
13661381

1382+
if isinstance(args[0], SubplotBase):
13671383
ax = args[0]
13681384
if ax.get_figure() is not self:
1369-
raise ValueError(
1370-
"The Subplot must have been created in the present figure")
1385+
raise ValueError("The Subplot must have been created in "
1386+
"the present figure")
13711387
# make a key for the subplot (which includes the axes object id
13721388
# in the hash)
13731389
key = self._make_key(*args, **kwargs)

lib/matplotlib/tests/test_axes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4055,7 +4055,7 @@ def test_mixed_collection():
40554055

40564056

40574057
def test_subplot_key_hash():
4058-
ax = plt.subplot(np.float64(5.5), np.int64(1), np.float64(1.2))
4058+
ax = plt.subplot(np.int32(5), np.int64(1), 1)
40594059
ax.twinx()
40604060
assert ax.get_subplotspec().get_geometry() == (5, 1, 0, 0)
40614061

lib/matplotlib/tests/test_figure.py

+25-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from contextlib import ExitStack as nullcontext # Py3.6
99

1010
import matplotlib as mpl
11-
from matplotlib import rcParams
11+
from matplotlib import cbook, rcParams
1212
from matplotlib.testing.decorators import image_comparison, check_figures_equal
1313
from matplotlib.axes import Axes
1414
from matplotlib.ticker import AutoMinorLocator, FixedFormatter, ScalarFormatter
@@ -176,15 +176,35 @@ def test_gca():
176176

177177
def test_add_subplot_invalid():
178178
fig = plt.figure()
179-
with pytest.raises(ValueError):
179+
with pytest.raises(ValueError, match='Number of columns must be > 0'):
180180
fig.add_subplot(2, 0, 1)
181-
with pytest.raises(ValueError):
181+
with pytest.raises(ValueError, match='Number of rows must be > 0'):
182182
fig.add_subplot(0, 2, 1)
183-
with pytest.raises(ValueError):
183+
with pytest.raises(ValueError, match='num must be 1 <= num <= 4'):
184184
fig.add_subplot(2, 2, 0)
185-
with pytest.raises(ValueError):
185+
with pytest.raises(ValueError, match='num must be 1 <= num <= 4'):
186186
fig.add_subplot(2, 2, 5)
187187

188+
with pytest.raises(ValueError, match='must be a three-digit number'):
189+
fig.add_subplot(42)
190+
with pytest.raises(ValueError, match='must be a three-digit number'):
191+
fig.add_subplot(1000)
192+
193+
with pytest.raises(TypeError, match='takes 1 or 3 positional arguments '
194+
'but 2 were given'):
195+
fig.add_subplot(2, 2)
196+
with pytest.raises(TypeError, match='takes 1 or 3 positional arguments '
197+
'but 4 were given'):
198+
fig.add_subplot(1, 2, 3, 4)
199+
with pytest.warns(cbook.MatplotlibDeprecationWarning,
200+
match='Passing non-integers as three-element position '
201+
'specification is deprecated'):
202+
fig.add_subplot('2', 2, 1)
203+
with pytest.warns(cbook.MatplotlibDeprecationWarning,
204+
match='Passing non-integers as three-element position '
205+
'specification is deprecated'):
206+
fig.add_subplot(2.0, 2, 1)
207+
188208

189209
@image_comparison(['figure_suptitle'])
190210
def test_suptitle():

0 commit comments

Comments
 (0)