Skip to content

Commit 419fafe

Browse files
committed
Validate positional parameters of add_subplot()
1 parent 8135aad commit 419fafe

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
@@ -235,6 +235,12 @@ Stricter rcParam validation
235235
(case-insensitive) to the option "line". This is deprecated; in a future
236236
version only the exact string "line" (case-sensitive) will be supported.
237237

238+
``add_subplot()`` validates its inputs
239+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
240+
In particular, for ``add_subplot(rows, cols, index)``, all parameters must
241+
be integral. Previously strings and floats were accepted and converted to
242+
int. This will now emit a deprecation warning.
243+
238244
Toggling axes navigation from the keyboard using "a" and digit keys
239245
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
240246
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
@@ -1254,19 +1254,19 @@ def add_subplot(self, *args, **kwargs):
12541254
12551255
Parameters
12561256
----------
1257-
*args, default: (1, 1, 1)
1258-
Either a 3-digit integer or three separate integers
1259-
describing the position of the subplot. If the three
1260-
integers are *nrows*, *ncols*, and *index* in order, the
1261-
subplot will take the *index* position on a grid with *nrows*
1262-
rows and *ncols* columns. *index* starts at 1 in the upper left
1263-
corner and increases to the right.
1264-
1265-
*pos* is a three digit integer, where the first digit is the
1266-
number of rows, the second the number of columns, and the third
1267-
the index of the subplot. i.e. fig.add_subplot(235) is the same as
1268-
fig.add_subplot(2, 3, 5). Note that all integers must be less than
1269-
10 for this form to work.
1257+
*args, int or (int, int, int) or `SubplotSpec`, default: (1, 1, 1)
1258+
The position of the subplot described by one of
1259+
1260+
- Three integers (*nrows*, *ncols*, *index*). The subplot will
1261+
take the *index* position on a grid with *nrows* rows and
1262+
*ncols* columns. *index* starts at 1 in the upper left corner
1263+
and increases to the right.
1264+
- A 3-digit integer. The digits are interpreted as if given
1265+
separately as three single-digit integers, i.e.
1266+
``fig.add_subplot(235)`` is the same as
1267+
``fig.add_subplot(2, 3, 5)``. Note that this can only be used
1268+
if there are no more than 9 subplots.
1269+
- A `.SubplotSpec`.
12701270
12711271
In rare circumstances, `.add_subplot` may be called with a single
12721272
argument, a subplot axes instance already created in the
@@ -1346,27 +1346,43 @@ def add_subplot(self, *args, **kwargs):
13461346
ax1.remove() # delete ax1 from the figure
13471347
fig.add_subplot(ax1) # add ax1 back to the figure
13481348
"""
1349-
if not len(args):
1350-
args = (1, 1, 1)
1351-
1352-
if len(args) == 1 and isinstance(args[0], Integral):
1353-
if not 100 <= args[0] <= 999:
1354-
raise ValueError("Integer subplot specification must be a "
1355-
"three-digit number, not {}".format(args[0]))
1356-
args = tuple(map(int, str(args[0])))
1357-
13581349
if 'figure' in kwargs:
13591350
# Axes itself allows for a 'figure' kwarg, but since we want to
13601351
# bind the created Axes to self, it is not allowed here.
13611352
raise TypeError(
13621353
"add_subplot() got an unexpected keyword argument 'figure'")
13631354

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

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

lib/matplotlib/tests/test_axes.py

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

40514051

40524052
def test_subplot_key_hash():
4053-
ax = plt.subplot(np.float64(5.5), np.int64(1), np.float64(1.2))
4053+
ax = plt.subplot(np.int32(5), np.int64(1), 1)
40544054
ax.twinx()
40554055
assert ax.get_subplotspec().get_geometry() == (5, 1, 0, 0)
40564056

lib/matplotlib/tests/test_figure.py

+25-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import warnings
55

66
import matplotlib as mpl
7-
from matplotlib import rcParams
7+
from matplotlib import cbook, rcParams
88
from matplotlib.testing.decorators import image_comparison, check_figures_equal
99
from matplotlib.axes import Axes
1010
from matplotlib.ticker import AutoMinorLocator, FixedFormatter, ScalarFormatter
@@ -172,15 +172,35 @@ def test_gca():
172172

173173
def test_add_subplot_invalid():
174174
fig = plt.figure()
175-
with pytest.raises(ValueError):
175+
with pytest.raises(ValueError, match='Number of columns must be > 0'):
176176
fig.add_subplot(2, 0, 1)
177-
with pytest.raises(ValueError):
177+
with pytest.raises(ValueError, match='Number of rows must be > 0'):
178178
fig.add_subplot(0, 2, 1)
179-
with pytest.raises(ValueError):
179+
with pytest.raises(ValueError, match='num must be 1 <= num <= 4'):
180180
fig.add_subplot(2, 2, 0)
181-
with pytest.raises(ValueError):
181+
with pytest.raises(ValueError, match='num must be 1 <= num <= 4'):
182182
fig.add_subplot(2, 2, 5)
183183

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

185205
@image_comparison(['figure_suptitle'])
186206
def test_suptitle():

0 commit comments

Comments
 (0)