Skip to content

Move integerness checks to SubplotSpec._from_subplot_args. #17350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 20, 2020
Merged
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
51 changes: 16 additions & 35 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1264,13 +1264,16 @@ def add_subplot(self, *args, **kwargs):

Parameters
----------
*args, int or (int, int, int) or `SubplotSpec`, default: (1, 1, 1)
*args : int, (int, int, *index*), or `.SubplotSpec`, default: (1, 1, 1)
The position of the subplot described by one of

- Three integers (*nrows*, *ncols*, *index*). The subplot will
take the *index* position on a grid with *nrows* rows and
*ncols* columns. *index* starts at 1 in the upper left corner
and increases to the right.
and increases to the right. *index* can also be a two-tuple
specifying the (*first*, *last*) indices (1-based, and including
*last*) of the subplot, e.g., ``fig.add_subplot(3, 1, (1, 2))``
makes a subplot that spans the upper 2/3 of the figure.
- A 3-digit integer. The digits are interpreted as if given
separately as three single-digit integers, i.e.
``fig.add_subplot(235)`` is the same as
Expand Down Expand Up @@ -1362,48 +1365,27 @@ def add_subplot(self, *args, **kwargs):
raise TypeError(
"add_subplot() got an unexpected keyword argument 'figure'")

nargs = len(args)
if nargs == 0:
args = (1, 1, 1)
elif nargs == 1:
if isinstance(args[0], Integral):
if not 100 <= args[0] <= 999:
raise ValueError(f"Integer subplot specification must be "
f"a three-digit number, not {args[0]}")
args = tuple(map(int, str(args[0])))
elif isinstance(args[0], (SubplotBase, SubplotSpec)):
pass # no further validation or normalization needed
else:
raise TypeError('Positional arguments are not a valid '
'position specification.')
elif nargs == 3:
for arg in args:
if not isinstance(arg, Integral):
cbook.warn_deprecated(
"3.3",
message="Passing non-integers as three-element "
"position specification is deprecated since "
"%(since)s and will be removed %(removal)s.")
args = tuple(map(int, args))
else:
raise TypeError(f'add_subplot() takes 1 or 3 positional arguments '
f'but {nargs} were given')

if isinstance(args[0], SubplotBase):
if len(args) == 1 and isinstance(args[0], SubplotBase):
ax = args[0]
if ax.get_figure() is not self:
raise ValueError("The Subplot must have been created in "
"the present figure")
# make a key for the subplot (which includes the axes object id
# in the hash)
key = self._make_key(*args, **kwargs)

else:
if not args:
args = (1, 1, 1)
# Normalize correct ijk values to (i, j, k) here so that
# add_subplot(111) == add_subplot(1, 1, 1). Invalid values will
# trigger errors later (via SubplotSpec._from_subplot_args).
if (len(args) == 1 and isinstance(args[0], Integral)
and 100 <= args[0] <= 999):
args = tuple(map(int, str(args[0])))
projection_class, kwargs, key = \
self._process_projection_requirements(*args, **kwargs)

# try to find the axes with this key in the stack
ax = self._axstack.get(key)

ax = self._axstack.get(key) # search axes with this key in stack
if ax is not None:
if isinstance(ax, projection_class):
# the axes already existed, so set it as active & return
Expand All @@ -1416,7 +1398,6 @@ def add_subplot(self, *args, **kwargs):
# Without this, add_subplot would be simpler and
# more similar to add_axes.
self._axstack.remove(ax)

ax = subplot_class_factory(projection_class)(self, *args, **kwargs)

return self._add_axes_internal(key, ax)
Expand Down
49 changes: 33 additions & 16 deletions lib/matplotlib/gridspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def __init__(self, nrows, ncols, height_ratios=None, width_ratios=None):
relative height of ``height_ratios[i] / sum(height_ratios)``.
If not given, all rows will have the same height.
"""
if not isinstance(nrows, Integral) or nrows <= 0:
raise ValueError(f"Number of rows must be > 0, not {nrows}")
if not isinstance(ncols, Integral) or ncols <= 0:
raise ValueError(f"Number of columns must be > 0, not {ncols}")
self._nrows, self._ncols = nrows, ncols
self.set_height_ratios(height_ratios)
self.set_width_ratios(width_ratios)
Expand Down Expand Up @@ -643,36 +647,46 @@ def _from_subplot_args(figure, args):
- a `.SubplotSpec` -- returned as is;
- one or three numbers -- a MATLAB-style subplot specifier.
"""
message = ("Passing non-integers as three-element position "
"specification is deprecated since %(since)s and will be "
"removed %(removal)s.")
if len(args) == 1:
arg, = args
if isinstance(arg, SubplotSpec):
return arg
else:
if not isinstance(arg, Integral):
cbook.warn_deprecated("3.3", message=message)
arg = str(arg)
try:
s = str(int(arg))
rows, cols, num = map(int, s)
except ValueError as err:
raise ValueError("Single argument to subplot must be a "
"3-digit integer") from err
rows, cols, num = map(int, str(arg))
except ValueError:
raise ValueError(
f"Single argument to subplot must be a three-digit "
f"integer, not {arg}") from None
# num - 1 for converting from MATLAB to python indexing
return GridSpec(rows, cols, figure=figure)[num - 1]
elif len(args) == 3:
rows, cols, num = args
rows = int(rows)
cols = int(cols)
if rows <= 0:
raise ValueError(f"Number of rows must be > 0, not {rows}")
if cols <= 0:
raise ValueError(f"Number of columns must be > 0, not {cols}")
if not (isinstance(rows, Integral) and isinstance(cols, Integral)):
cbook.warn_deprecated("3.3", message=message)
rows, cols = map(int, [rows, cols])
gs = GridSpec(rows, cols, figure=figure)
if isinstance(num, tuple) and len(num) == 2:
i, j = map(int, num)
return GridSpec(rows, cols, figure=figure)[i-1:j]
if not all(isinstance(n, Integral) for n in num):
cbook.warn_deprecated("3.3", message=message)
i, j = map(int, num)
else:
i, j = num
return gs[i-1:j]
else:
if not isinstance(num, Integral):
cbook.warn_deprecated("3.3", message=message)
num = int(num)
if num < 1 or num > rows*cols:
raise ValueError(
f"num must be 1 <= num <= {rows*cols}, not {num}")
# num - 1 for converting from MATLAB to python indexing
return GridSpec(rows, cols, figure=figure)[int(num) - 1]
return gs[num - 1] # -1 due to MATLAB indexing.
else:
raise TypeError(f"subplot() takes 1 or 3 positional arguments but "
f"{len(args)} were given")
Expand Down Expand Up @@ -727,7 +741,10 @@ def rowspan(self):
def colspan(self):
"""The columns spanned by this subplot, as a `range` object."""
ncols = self.get_gridspec().ncols
return range(self.num1 % ncols, self.num2 % ncols + 1)
# We explicitly support num2 refering to a column on num1's *left*, so
# we must sort the column indices here so that the range makes sense.
c1, c2 = sorted([self.num1 % ncols, self.num2 % ncols])
return range(c1, c2 + 1)

def get_position(self, figure, return_all=False):
"""
Expand Down
28 changes: 15 additions & 13 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -935,19 +935,21 @@ def subplot(*args, **kwargs):

Parameters
----------
*args, default: (1, 1, 1)
Either a 3-digit integer or three separate integers
describing the position of the subplot. If the three
integers are *nrows*, *ncols*, and *index* in order, the
subplot will take the *index* position on a grid with *nrows*
rows and *ncols* columns. *index* starts at 1 in the upper left
corner and increases to the right.

*pos* is a three digit integer, where the first digit is the
number of rows, the second the number of columns, and the third
the index of the subplot. i.e. fig.add_subplot(235) is the same as
fig.add_subplot(2, 3, 5). Note that all integers must be less than
10 for this form to work.
*args : int, (int, int, *index*), or `.SubplotSpec`, default: (1, 1, 1)
The position of the subplot described by one of

- Three integers (*nrows*, *ncols*, *index*). The subplot will take the
*index* position on a grid with *nrows* rows and *ncols* columns.
*index* starts at 1 in the upper left corner and increases to the
right. *index* can also be a two-tuple specifying the (*first*,
*last*) indices (1-based, and including *last*) of the subplot, e.g.,
``fig.add_subplot(3, 1, (1, 2))`` makes a subplot that spans the
upper 2/3 of the figure.
- A 3-digit integer. The digits are interpreted as if given separately
as three single-digit integers, i.e. ``fig.add_subplot(235)`` is the
same as ``fig.add_subplot(2, 3, 5)``. Note that this can only be used
if there are no more than 9 subplots.
- A `.SubplotSpec`.

projection : {None, 'aitoff', 'hammer', 'lambert', 'mollweide', \
'polar', 'rectilinear', str}, optional
Expand Down
22 changes: 20 additions & 2 deletions lib/matplotlib/tests/test_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,9 @@ def test_add_subplot_invalid():
with pytest.raises(ValueError, match='num must be 1 <= num <= 4'):
fig.add_subplot(2, 2, 5)

with pytest.raises(ValueError, match='must be a three-digit number'):
with pytest.raises(ValueError, match='must be a three-digit integer'):
fig.add_subplot(42)
with pytest.raises(ValueError, match='must be a three-digit number'):
with pytest.raises(ValueError, match='must be a three-digit integer'):
fig.add_subplot(1000)

with pytest.raises(TypeError, match='takes 1 or 3 positional arguments '
Expand Down Expand Up @@ -548,3 +548,21 @@ def test_picking_does_not_stale():
inaxes=ax, guiEvent=None)
fig.pick(mouse_event)
assert not fig.stale


def test_add_subplot_twotuple():
fig = plt.figure()
ax1 = fig.add_subplot(3, 2, (3, 5))
assert ax1.get_subplotspec().rowspan == range(1, 3)
assert ax1.get_subplotspec().colspan == range(0, 1)
ax2 = fig.add_subplot(3, 2, (4, 6))
assert ax2.get_subplotspec().rowspan == range(1, 3)
assert ax2.get_subplotspec().colspan == range(1, 2)
ax3 = fig.add_subplot(3, 2, (3, 6))
assert ax3.get_subplotspec().rowspan == range(1, 3)
assert ax3.get_subplotspec().colspan == range(0, 2)
ax4 = fig.add_subplot(3, 2, (4, 5))
assert ax4.get_subplotspec().rowspan == range(1, 3)
assert ax4.get_subplotspec().colspan == range(0, 2)
with pytest.raises(IndexError):
fig.add_subplot(3, 2, (6, 3))