Skip to content

Commit f435332

Browse files
BvB93eric-wieserseberg
authored
ENH: Add the axis and ndim attributes to np.AxisError (numpy#19459)
This PR adds the new axis and ndim attributes to the np.AxisError class, an addition inspired by similar changes introduced to AttributeError in Python 3.10. It also provided an excuse to update the classes' documentation and tests, both of which were previously rather lacking. * ENH: Add the `axis` and `ndim` attributes to `np.AxisError` * MAINT: Let the new `AxisError` attributes survive pickling * DOC: Update the `AxisError` documentation * TST: Update the `AxisError` tests * DOC: Add a release note * MAINT: Improve the description of `AxisError`s overloaded constructor * TST: Fix a few typing test failures * DOC: Clarify a fix a few parts of the `AxisError` docstrings Co-Authored-By: Eric Wieser <[email protected]> * DOC: Reguide fix * DOC: Replace `versionadded` with `versionchanged` Co-authored-by: Eric Wieser <[email protected]> * DOC: Mention that `AxisError` is a `ValueError` and `IndexError` subclass Co-Authored-By: Sebastian Berg <[email protected]> * ENH: Delay assembling of the error message until `__str__` is called Co-Authored-By: Eric Wieser <[email protected]> * DOC: Update the `AxisError` string representation in its docstring * DOC: Update `versionadded`-related information Co-Authored-By: Eric Wieser <[email protected]> * DOC: Fix sphinx warnings Co-authored-by: Eric Wieser <[email protected]> Co-authored-by: Eric Wieser <[email protected]> Co-authored-by: Sebastian Berg <[email protected]>
1 parent 3cec3ae commit f435332

File tree

8 files changed

+139
-23
lines changed

8 files changed

+139
-23
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The ``ndim`` and ``axis`` attributes have been added to `numpy.AxisError`
2+
-------------------------------------------------------------------------
3+
The ``ndim`` and ``axis`` parameters are now also stored as attributes
4+
within each `numpy.AxisError` instance.

doc/source/reference/routines.other.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,11 @@ Matlab-like Functions
5555
:toctree: generated/
5656

5757
who
58-
disp
58+
disp
59+
60+
Exceptions
61+
----------
62+
.. autosummary::
63+
:toctree: generated/
64+
65+
AxisError

numpy/__init__.pyi

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3681,9 +3681,12 @@ class RankWarning(UserWarning): ...
36813681
class TooHardError(RuntimeError): ...
36823682

36833683
class AxisError(ValueError, IndexError):
3684-
def __init__(
3685-
self, axis: int, ndim: Optional[int] = ..., msg_prefix: Optional[str] = ...
3686-
) -> None: ...
3684+
axis: None | int
3685+
ndim: None | int
3686+
@overload
3687+
def __init__(self, axis: str, ndim: None = ..., msg_prefix: None = ...) -> None: ...
3688+
@overload
3689+
def __init__(self, axis: int, ndim: int, msg_prefix: None | str = ...) -> None: ...
36873690

36883691
_CallType = TypeVar("_CallType", bound=Union[_ErrFunc, _SupportsWrite])
36893692

numpy/core/_exceptions.py

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,94 @@ class TooHardError(RuntimeError):
122122

123123
@set_module('numpy')
124124
class AxisError(ValueError, IndexError):
125-
""" Axis supplied was invalid. """
126-
def __init__(self, axis, ndim=None, msg_prefix=None):
127-
# single-argument form just delegates to base class
128-
if ndim is None and msg_prefix is None:
129-
msg = axis
125+
"""Axis supplied was invalid.
126+
127+
This is raised whenever an ``axis`` parameter is specified that is larger
128+
than the number of array dimensions.
129+
For compatibility with code written against older numpy versions, which
130+
raised a mixture of `ValueError` and `IndexError` for this situation, this
131+
exception subclasses both to ensure that ``except ValueError`` and
132+
``except IndexError`` statements continue to catch `AxisError`.
133+
134+
.. versionadded:: 1.13
135+
136+
Parameters
137+
----------
138+
axis : int or str
139+
The out of bounds axis or a custom exception message.
140+
If an axis is provided, then `ndim` should be specified as well.
141+
ndim : int, optional
142+
The number of array dimensions.
143+
msg_prefix : str, optional
144+
A prefix for the exception message.
145+
146+
Attributes
147+
----------
148+
axis : int, optional
149+
The out of bounds axis or ``None`` if a custom exception
150+
message was provided. This should be the axis as passed by
151+
the user, before any normalization to resolve negative indices.
152+
153+
.. versionadded:: 1.22
154+
ndim : int, optional
155+
The number of array dimensions or ``None`` if a custom exception
156+
message was provided.
157+
158+
.. versionadded:: 1.22
159+
160+
161+
Examples
162+
--------
163+
>>> array_1d = np.arange(10)
164+
>>> np.cumsum(array_1d, axis=1)
165+
Traceback (most recent call last):
166+
...
167+
numpy.AxisError: axis 1 is out of bounds for array of dimension 1
168+
169+
Negative axes are preserved:
170+
171+
>>> np.cumsum(array_1d, axis=-2)
172+
Traceback (most recent call last):
173+
...
174+
numpy.AxisError: axis -2 is out of bounds for array of dimension 1
130175
131-
# do the string formatting here, to save work in the C code
176+
The class constructor generally takes the axis and arrays'
177+
dimensionality as arguments:
178+
179+
>>> print(np.AxisError(2, 1, msg_prefix='error'))
180+
error: axis 2 is out of bounds for array of dimension 1
181+
182+
Alternatively, a custom exception message can be passed:
183+
184+
>>> print(np.AxisError('Custom error message'))
185+
Custom error message
186+
187+
"""
188+
189+
__slots__ = ("axis", "ndim", "_msg")
190+
191+
def __init__(self, axis, ndim=None, msg_prefix=None):
192+
if ndim is msg_prefix is None:
193+
# single-argument form: directly set the error message
194+
self._msg = axis
195+
self.axis = None
196+
self.ndim = None
132197
else:
133-
msg = ("axis {} is out of bounds for array of dimension {}"
134-
.format(axis, ndim))
135-
if msg_prefix is not None:
136-
msg = "{}: {}".format(msg_prefix, msg)
198+
self._msg = msg_prefix
199+
self.axis = axis
200+
self.ndim = ndim
137201

138-
super().__init__(msg)
202+
def __str__(self):
203+
axis = self.axis
204+
ndim = self.ndim
205+
206+
if axis is ndim is None:
207+
return self._msg
208+
else:
209+
msg = f"axis {axis} is out of bounds for array of dimension {ndim}"
210+
if self._msg is not None:
211+
msg = f"{self._msg}: {msg}"
212+
return msg
139213

140214

141215
@_display_as_base

numpy/core/tests/test__exceptions.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pickle
66

7+
import pytest
78
import numpy as np
89

910
_ArrayMemoryError = np.core._exceptions._ArrayMemoryError
@@ -56,3 +57,32 @@ class TestUFuncNoLoopError:
5657
def test_pickling(self):
5758
""" Test that _UFuncNoLoopError can be pickled """
5859
assert isinstance(pickle.dumps(_UFuncNoLoopError), bytes)
60+
61+
62+
@pytest.mark.parametrize("args", [
63+
(2, 1, None),
64+
(2, 1, "test_prefix"),
65+
("test message",),
66+
])
67+
class TestAxisError:
68+
def test_attr(self, args):
69+
"""Validate attribute types."""
70+
exc = np.AxisError(*args)
71+
if len(args) == 1:
72+
assert exc.axis is None
73+
assert exc.ndim is None
74+
else:
75+
axis, ndim, *_ = args
76+
assert exc.axis == axis
77+
assert exc.ndim == ndim
78+
79+
def test_pickling(self, args):
80+
"""Test that `AxisError` can be pickled."""
81+
exc = np.AxisError(*args)
82+
exc2 = pickle.loads(pickle.dumps(exc))
83+
84+
assert type(exc) is type(exc2)
85+
for name in ("axis", "ndim", "args"):
86+
attr1 = getattr(exc, name)
87+
attr2 = getattr(exc2, name)
88+
assert attr1 == attr2, name
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import numpy as np
22

3-
np.AxisError(1.0) # E: Argument 1 to "AxisError" has incompatible type
4-
np.AxisError(1, ndim=2.0) # E: Argument "ndim" to "AxisError" has incompatible type
5-
np.AxisError(
6-
2, msg_prefix=404 # E: Argument "msg_prefix" to "AxisError" has incompatible type
7-
)
3+
np.AxisError(1.0) # E: No overload variant
4+
np.AxisError(1, ndim=2.0) # E: No overload variant
5+
np.AxisError(2, msg_prefix=404) # E: No overload variant
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import numpy as np
22

3-
np.AxisError(1)
3+
np.AxisError("test")
44
np.AxisError(1, ndim=2)
5-
np.AxisError(1, ndim=None)
65
np.AxisError(1, ndim=2, msg_prefix="error")
76
np.AxisError(1, ndim=2, msg_prefix=None)

numpy/typing/tests/data/reveal/warnings_and_errors.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
reveal_type(np.ComplexWarning()) # E: numpy.ComplexWarning
88
reveal_type(np.RankWarning()) # E: numpy.RankWarning
99
reveal_type(np.TooHardError()) # E: numpy.TooHardError
10-
reveal_type(np.AxisError(1)) # E: numpy.AxisError
10+
reveal_type(np.AxisError("test")) # E: numpy.AxisError
11+
reveal_type(np.AxisError(5, 1)) # E: numpy.AxisError

0 commit comments

Comments
 (0)