Repository: cupy/cupy
Pull request: Support NumPy 1.13 again
This PR includes #281, #336 and #344,
This PR fixes #342.

Problem statement:
numpy<1.13 do not have numpy.AxisError 
When using numpy 1.12.1:
```python
>>> import cupy
>>> cupy.cumprod
<function cumprod at 0x7f9a460b4c80>
>>> cupy.cumprod(cupy.ndarray(()), axis=-10000)                                                                                                                                                             
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/path/to/cupy/math/sumprod.py", line 206, in cumprod
    raise numpy.AxisError('axis(={}) out of bounds'.format(axis))
AttributeError: module 'numpy' has no attribute 'AxisError'
```

Hints from issue discussion:
`numpy.AxisError` was introduced at https://github.com/cupy/cupy/pull/281.

Commit message: Add underscore

Patch to review:
diff --git a/cupy/core/__init__.py b/cupy/core/__init__.py
index 7fc262f748b..dfddabc943b 100644
--- a/cupy/core/__init__.py
+++ b/cupy/core/__init__.py
@@ -3,6 +3,7 @@
 
 
 # import class and function
+from cupy.core.core import _AxisError  # NOQA
 from cupy.core.core import absolute  # NOQA
 from cupy.core.core import add  # NOQA
 from cupy.core.core import array  # NOQA
diff --git a/cupy/core/core.pyx b/cupy/core/core.pyx
index 7b094e57d39..7ce9246d823 100644
--- a/cupy/core/core.pyx
+++ b/cupy/core/core.pyx
@@ -36,6 +36,17 @@ cdef inline _should_use_rop(x, y):
     return xp < yp and not isinstance(y, ndarray)
 
 
+class IndexOrValueError(IndexError, ValueError):
+
+    def __init__(self, *args, **kwargs):
+        super(IndexOrValueError, self).__init__(*args, **kwargs)
+
+try:
+    _AxisError = numpy.AxisError
+except AttributeError:
+    _AxisError = IndexOrValueError
+
+
 cdef class ndarray:
 
     """Multi-dimensional array on a CUDA device.
@@ -605,8 +616,9 @@ cdef class ndarray:
                 if _axis < 0:
                     _axis += ndim
                 if _axis < 0 or _axis >= ndim:
-                    msg = "'axis' entry %d is out of bounds [-%d, %d)"
-                    raise ValueError(msg % (axis_orig, ndim, ndim))
+                    raise _AxisError(
+                        "'axis' entry %d is out of bounds [-%d, %d)" %
+                        (axis_orig, ndim, ndim))
                 if axis_flags[_axis] == 1:
                     raise ValueError("duplicate value in 'axis'")
                 axis_flags[_axis] = 1
@@ -621,8 +633,9 @@ cdef class ndarray:
                 pass
             else:
                 if _axis < 0 or _axis >= ndim:
-                    msg = "'axis' entry %d is out of bounds [-%d, %d)"
-                    raise ValueError(msg % (axis_orig, ndim, ndim))
+                    raise _AxisError(
+                        "'axis' entry %d is out of bounds [-%d, %d)" %
+                        (axis_orig, ndim, ndim))
                 axis_flags[_axis] = 1
 
         # Verify that the axes requested are all of size one
@@ -2273,7 +2286,7 @@ cpdef ndarray concatenate_method(tup, int axis):
             if axis < 0:
                 axis += ndim
             if axis < 0 or axis >= ndim:
-                raise IndexError(
+                raise _AxisError(
                     'axis {} out of bounds [0, {})'.format(axis, ndim))
             dtype = a.dtype
             continue
@@ -2649,7 +2662,7 @@ cpdef ndarray _take(ndarray a, indices, li=None, ri=None, ndarray out=None):
         index_range = a.size
     else:
         if not (-a.ndim <= li < a.ndim and -a.ndim <= ri < a.ndim):
-            raise ValueError('Axis overrun')
+            raise _AxisError('Axis overrun')
         if a.ndim != 0:
             li %= a.ndim
             ri %= a.ndim
diff --git a/cupy/manipulation/join.py b/cupy/manipulation/join.py
index 339febb1f7a..ac1b0784253 100644
--- a/cupy/manipulation/join.py
+++ b/cupy/manipulation/join.py
@@ -128,5 +128,6 @@ def _get_positive_axis(ndim, axis):
     if a < 0:
         a += ndim
     if a < 0 or a >= ndim:
-        raise IndexError('axis {} out of bounds [0, {})'.format(axis, ndim))
+        raise core._AxisError(
+            'axis {} out of bounds [0, {})'.format(axis, ndim))
     return a
diff --git a/cupy/manipulation/rearrange.py b/cupy/manipulation/rearrange.py
index 17197b0aaac..9d11a162a5f 100644
--- a/cupy/manipulation/rearrange.py
+++ b/cupy/manipulation/rearrange.py
@@ -1,4 +1,5 @@
 import cupy
+from cupy import core
 
 
 def flip(a, axis):
@@ -23,7 +24,8 @@ def flip(a, axis):
 
     axis = int(axis)
     if not -a_ndim <= axis < a_ndim:
-        raise ValueError('axis must be >= %d and < %d' % (-a_ndim, a_ndim))
+        raise ValueError(
+            'axis must be >= %d and < %d' % (-a_ndim, a_ndim))
 
     return _flip(a, axis)
 
@@ -99,7 +101,8 @@ def roll(a, shift, axis=None):
         if axis < 0:
             axis += a.ndim
         if not 0 <= axis < a.ndim:
-            raise ValueError('axis must be >= %d and < %d' % (-a.ndim, a.ndim))
+            raise core._AxisError(
+                'axis must be >= %d and < %d' % (-a.ndim, a.ndim))
         size = a.shape[axis]
         if size == 0:
             return a
diff --git a/cupy/math/sumprod.py b/cupy/math/sumprod.py
index 083d486e3f1..7e0824fef67 100644
--- a/cupy/math/sumprod.py
+++ b/cupy/math/sumprod.py
@@ -125,7 +125,7 @@ def cumsum(a, axis=None, dtype=None, out=None):
     if axis is None:
         out = out.ravel()
     elif not (-a.ndim <= axis < a.ndim):
-        raise ValueError('axis(={}) out of bounds'.format(axis))
+        raise core._AxisError('axis(={}) out of bounds'.format(axis))
     else:
         return _proc_as_batch(_cumsum_batch, out, axis=axis)
 
@@ -203,7 +203,7 @@ def cumprod(a, axis=None, dtype=None, out=None):
     if axis is None:
         out = out.ravel()
     elif not (-a.ndim <= axis < a.ndim):
-        raise ValueError('axis(={}) out of bounds'.format(axis))
+        raise core._AxisError('axis(={}) out of bounds'.format(axis))
     else:
         return _proc_as_batch(_cumprod_batch, out, axis=axis)
 
diff --git a/cupy/statistics/histogram.py b/cupy/statistics/histogram.py
index 0753e10c471..22e67878e29 100644
--- a/cupy/statistics/histogram.py
+++ b/cupy/statistics/histogram.py
@@ -40,7 +40,7 @@ def bincount(x, weights=None, minlength=None):
         raise ValueError('The weights and list don\'t have the same length.')
     if minlength is not None:
         minlength = int(minlength)
-        if minlength <= 0:
+        if minlength < 0:
             raise ValueError('minlength must be positive')
 
     size = int(cupy.max(x)) + 1
diff --git a/docs/source/install.rst b/docs/source/install.rst
index ac041b8a9cc..6124084fa3e 100644
--- a/docs/source/install.rst
+++ b/docs/source/install.rst
@@ -44,7 +44,7 @@ Before installing CuPy, we recommend to upgrade ``setuptools`` if you are using
 The following Python packages are required to install CuPy.
 The latest version of each package will automatically be installed if missing.
 
-* `NumPy <http://www.numpy.org/>`_ 1.9, 1.10, 1.11, 1.12
+* `NumPy <http://www.numpy.org/>`_ 1.9, 1.10, 1.11, 1.12, 1.13
 * `Six <https://pythonhosted.org/six/>`_ 1.9+
 
 CUDA support
diff --git a/tests/cupy_tests/core_tests/test_fusion.py b/tests/cupy_tests/core_tests/test_fusion.py
index 938af177e4f..3c0838b73af 100644
--- a/tests/cupy_tests/core_tests/test_fusion.py
+++ b/tests/cupy_tests/core_tests/test_fusion.py
@@ -949,7 +949,7 @@ def g(x, y, z):
 
         return g(a, b, c)
 
-    @testing.for_all_dtypes()
+    @testing.for_all_dtypes(no_bool=True)
     @testing.numpy_cupy_array_equal()
     def test_fuse3(self, xp, dtype):
         a = xp.array([2, 2, 2, 2, 3, 3, 3, 3], dtype=dtype)
diff --git a/tests/cupy_tests/core_tests/test_ndarray.py b/tests/cupy_tests/core_tests/test_ndarray.py
index d30f93b2950..dd694f7639e 100644
--- a/tests/cupy_tests/core_tests/test_ndarray.py
+++ b/tests/cupy_tests/core_tests/test_ndarray.py
@@ -3,6 +3,7 @@
 
 import numpy
 
+import cupy
 from cupy import core
 from cupy import cuda
 from cupy import get_array_module
@@ -217,11 +218,18 @@ def test_take(self, xp, dtype):
 class TestNdarrayTakeErrorAxisOverRun(unittest.TestCase):
 
     @testing.for_all_dtypes()
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
-    def test_axis_overrun(self, xp, dtype):
+    def test_axis_overrun1(self, xp, dtype):
         a = testing.shaped_arange(self.shape, xp, dtype)
         wrap_take(a, self.indices, axis=self.axis)
 
+    @testing.for_all_dtypes()
+    def test_axis_overrun2(self, dtype):
+        a = testing.shaped_arange(self.shape, cupy, dtype)
+        with self.assertRaises(core._AxisError):
+            wrap_take(a, self.indices, axis=self.axis)
+
 
 @testing.parameterize(
     {"shape": (3, 4, 5), "indices": (2, 3), "out_shape": (2, 4)},
diff --git a/tests/cupy_tests/core_tests/test_ndarray_unary_op.py b/tests/cupy_tests/core_tests/test_ndarray_unary_op.py
index a95da06c563..039229672dd 100644
--- a/tests/cupy_tests/core_tests/test_ndarray_unary_op.py
+++ b/tests/cupy_tests/core_tests/test_ndarray_unary_op.py
@@ -51,8 +51,11 @@ def check_array_op(self, op, xp, dtype):
         a = testing.shaped_arange((2, 3), xp, dtype)
         return op(a)
 
-    def test_neg_array(self):
-        self.check_array_op(operator.neg)
+    @testing.for_all_dtypes(no_bool=True)
+    @testing.numpy_cupy_allclose()
+    def test_neg_array(self, xp, dtype):
+        a = testing.shaped_arange((2, 3), xp, dtype)
+        return operator.neg(a)
 
     def test_pos_array(self):
         self.check_array_op(operator.pos)
@@ -66,8 +69,11 @@ def check_zerodim_op(self, op, xp, dtype):
         a = xp.array(-2, dtype)
         return op(a)
 
-    def test_neg_zerodim(self):
-        self.check_zerodim_op(operator.neg)
+    @testing.for_all_dtypes(no_bool=True)
+    @testing.numpy_cupy_allclose()
+    def test_neg_zerodim(self, xp, dtype):
+        a = xp.array(-2, dtype)
+        return operator.neg(a)
 
     def test_pos_zerodim(self):
         self.check_zerodim_op(operator.pos)
diff --git a/tests/cupy_tests/manipulation_tests/test_dims.py b/tests/cupy_tests/manipulation_tests/test_dims.py
index 4f41303602a..12e43029fed 100644
--- a/tests/cupy_tests/manipulation_tests/test_dims.py
+++ b/tests/cupy_tests/manipulation_tests/test_dims.py
@@ -161,11 +161,17 @@ def test_squeze_int_axis2(self, xp):
         a = testing.shaped_arange((1, 2, 1, 3, 1, 1, 4, 1), xp)
         return a.squeeze(axis=-3)
 
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
-    def test_squeze_int_axis_failure(self, xp):
+    def test_squeze_int_axis_failure1(self, xp):
         a = testing.shaped_arange((1, 2, 1, 3, 1, 1, 4, 1), xp)
         a.squeeze(axis=-9)
 
+    def test_squeze_int_axis_failure2(self):
+        a = testing.shaped_arange((1, 2, 1, 3, 1, 1, 4, 1), cupy)
+        with self.assertRaises(cupy.core._AxisError):
+            a.squeeze(axis=-9)
+
     @testing.numpy_cupy_array_equal()
     def test_squeze_tuple_axis1(self, xp):
         a = testing.shaped_arange((1, 2, 1, 3, 1, 1, 4, 1), xp)
@@ -186,6 +192,7 @@ def test_squeze_tuple_axis4(self, xp):
         a = testing.shaped_arange((1, 2, 1, 3, 1, 1, 4, 1), xp)
         return a.squeeze(axis=())
 
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
     def test_squeze_tuple_axis_failure1(self, xp):
         a = testing.shaped_arange((1, 2, 1, 3, 1, 1, 4, 1), xp)
@@ -196,6 +203,11 @@ def test_squeze_tuple_axis_failure2(self, xp):
         a = testing.shaped_arange((1, 2, 1, 3, 1, 1, 4, 1), xp)
         a.squeeze(axis=(2, 2))
 
+    def test_squeze_tuple_axis_failure3(self):
+        a = testing.shaped_arange((1, 2, 1, 3, 1, 1, 4, 1), cupy)
+        with self.assertRaises(cupy.core._AxisError):
+            a.squeeze(axis=(-9,))
+
     @testing.numpy_cupy_array_equal()
     def test_squeeze_scalar1(self, xp):
         a = testing.shaped_arange((), xp)
@@ -206,16 +218,28 @@ def test_squeeze_scalar2(self, xp):
         a = testing.shaped_arange((), xp)
         return a.squeeze(axis=-1)
 
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
     def test_squeeze_scalar_failure1(self, xp):
         a = testing.shaped_arange((), xp)
         a.squeeze(axis=-2)
 
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
     def test_squeeze_scalar_failure2(self, xp):
         a = testing.shaped_arange((), xp)
         a.squeeze(axis=1)
 
+    def test_squeeze_scalar_failure3(self):
+        a = testing.shaped_arange((), cupy)
+        with self.assertRaises(cupy.core._AxisError):
+            a.squeeze(axis=-2)
+
+    def test_squeeze_scalar_failure4(self):
+        a = testing.shaped_arange((), cupy)
+        with self.assertRaises(cupy.core._AxisError):
+            a.squeeze(axis=1)
+
     @testing.numpy_cupy_raises()
     def test_squeeze_failure(self, xp):
         a = testing.shaped_arange((2, 1, 3, 4), xp)
diff --git a/tests/cupy_tests/manipulation_tests/test_join.py b/tests/cupy_tests/manipulation_tests/test_join.py
index 78371335259..a33835fde5a 100644
--- a/tests/cupy_tests/manipulation_tests/test_join.py
+++ b/tests/cupy_tests/manipulation_tests/test_join.py
@@ -206,8 +206,13 @@ def test_stack_different_shape(self, xp):
         b = testing.shaped_arange((2, 4), xp)
         return xp.stack([a, b])
 
-    @testing.with_requires('numpy>=1.10')
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
-    def test_stack_out_of_bounds(self, xp):
+    def test_stack_out_of_bounds1(self, xp):
         a = testing.shaped_arange((2, 3), xp)
         return xp.stack([a, a], axis=3)
+
+    def test_stack_out_of_bounds2(self):
+        a = testing.shaped_arange((2, 3), cupy)
+        with self.assertRaises(cupy.core._AxisError):
+            return cupy.stack([a, a], axis=3)
diff --git a/tests/cupy_tests/manipulation_tests/test_rearrange.py b/tests/cupy_tests/manipulation_tests/test_rearrange.py
index 2b1724e83eb..11044f0e2ee 100644
--- a/tests/cupy_tests/manipulation_tests/test_rearrange.py
+++ b/tests/cupy_tests/manipulation_tests/test_rearrange.py
@@ -1,5 +1,6 @@
 import unittest
 
+import cupy
 from cupy import testing
 
 
@@ -57,17 +58,31 @@ def test_roll_zero_array(self, xp, dtype):
         return xp.roll(x, 5)
 
     @testing.for_all_dtypes()
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
-    def test_roll_invalid_axis(self, xp, dtype):
+    def test_roll_invalid_axis1(self, xp, dtype):
         x = testing.shaped_arange((5, 2), xp, dtype)
         return xp.roll(x, 1, axis=2)
 
     @testing.for_all_dtypes()
+    def test_roll_invalid_axis2(self, dtype):
+        x = testing.shaped_arange((5, 2), cupy, dtype)
+        with self.assertRaises(cupy.core._AxisError):
+            return cupy.roll(x, 1, axis=2)
+
+    @testing.for_all_dtypes()
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
-    def test_roll_invalid_negative_axis(self, xp, dtype):
+    def test_roll_invalid_negative_axis1(self, xp, dtype):
         x = testing.shaped_arange((5, 2), xp, dtype)
         return xp.roll(x, 1, axis=-3)
 
+    @testing.for_all_dtypes()
+    def test_roll_invalid_negative_axis2(self, dtype):
+        x = testing.shaped_arange((5, 2), cupy, dtype)
+        with self.assertRaises(cupy.core._AxisError):
+            return cupy.roll(x, 1, axis=-3)
+
 
 @testing.gpu
 class TestFliplr(unittest.TestCase):
diff --git a/tests/cupy_tests/math_tests/test_sumprod.py b/tests/cupy_tests/math_tests/test_sumprod.py
index e229a7e0f69..83602e2055c 100644
--- a/tests/cupy_tests/math_tests/test_sumprod.py
+++ b/tests/cupy_tests/math_tests/test_sumprod.py
@@ -188,17 +188,31 @@ def test_cumsum_axis(self, xp, dtype):
         return xp.cumsum(a, axis=self.axis)
 
     @testing.for_all_dtypes()
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
-    def test_invalid_axis_lower(self, xp, dtype):
+    def test_invalid_axis_lower1(self, xp, dtype):
         a = testing.shaped_arange((4, 5), xp, dtype)
         return xp.cumsum(a, axis=-a.ndim - 1)
 
     @testing.for_all_dtypes()
+    def test_invalid_axis_lower2(self, dtype):
+        a = testing.shaped_arange((4, 5), cupy, dtype)
+        with self.assertRaises(cupy.core._AxisError):
+            return cupy.cumsum(a, axis=-a.ndim - 1)
+
+    @testing.for_all_dtypes()
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
-    def test_invalid_axis_upper(self, xp, dtype):
+    def test_invalid_axis_upper1(self, xp, dtype):
         a = testing.shaped_arange((4, 5), xp, dtype)
         return xp.cumsum(a, axis=a.ndim + 1)
 
+    @testing.for_all_dtypes()
+    def test_invalid_axis_upper2(self, dtype):
+        a = testing.shaped_arange((4, 5), cupy, dtype)
+        with self.assertRaises(cupy.core._AxisError):
+            return cupy.cumsum(a, axis=a.ndim + 1)
+
 
 @testing.gpu
 class TestCumprod(unittest.TestCase):
@@ -230,13 +244,27 @@ def test_cumprod_huge_array(self, xp):
         return xp.cumprod(a)
 
     @testing.for_all_dtypes()
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
-    def test_invalid_axis_lower(self, xp, dtype):
+    def test_invalid_axis_lower1(self, xp, dtype):
         a = testing.shaped_arange((4, 5), xp, dtype)
         return xp.cumprod(a, axis=-a.ndim - 1)
 
     @testing.for_all_dtypes()
+    def test_invalid_axis_lower2(self, dtype):
+        a = testing.shaped_arange((4, 5), cupy, dtype)
+        with self.assertRaises(cupy.core._AxisError):
+            return cupy.cumprod(a, axis=-a.ndim - 1)
+
+    @testing.for_all_dtypes()
+    @testing.with_requires('numpy>=1.13')
     @testing.numpy_cupy_raises()
-    def test_invalid_axis_upper(self, xp, dtype):
+    def test_invalid_axis_upper1(self, xp, dtype):
         a = testing.shaped_arange((4, 5), xp, dtype)
         return xp.cumprod(a, axis=a.ndim)
+
+    @testing.for_all_dtypes()
+    def test_invalid_axis_upper2(self, dtype):
+        a = testing.shaped_arange((4, 5), cupy, dtype)
+        with self.assertRaises(cupy.core._AxisError):
+            return cupy.cumprod(a, axis=a.ndim)
diff --git a/tests/cupy_tests/statics_tests/test_histogram.py b/tests/cupy_tests/statics_tests/test_histogram.py
index f60a40224f5..5ce81c36fa2 100644
--- a/tests/cupy_tests/statics_tests/test_histogram.py
+++ b/tests/cupy_tests/statics_tests/test_histogram.py
@@ -91,7 +91,14 @@ def test_bincount_too_small(self, xp, dtype):
         return xp.bincount(x)
 
     @for_all_dtypes_bincount()
-    @testing.numpy_cupy_raises()
-    def test_bincount_zero_minlength(self, xp, dtype):
+    @testing.with_requires('numpy>=1.13')
+    @testing.numpy_cupy_allclose(accept_error=TypeError)
+    def test_bincount_zero(self, xp, dtype):
         x = testing.shaped_arange((3,), xp, dtype)
         return xp.bincount(x, minlength=0)
+
+    @for_all_dtypes_bincount()
+    @testing.numpy_cupy_raises()
+    def test_bincount_too_small_minlength(self, xp, dtype):
+        x = testing.shaped_arange((3,), xp, dtype)
+        return xp.bincount(x, minlength=-1)


Code review comments:
diff_hunk: @@ -36,6 +36,17 @@ cdef inline _should_use_rop(x, y):
     return xp < yp and not isinstance(y, ndarray)
 
 
+class IndexOrValueError(IndexError, ValueError):
path: cupy/core/core.pyx
text: @user1:
If so, do we need to move it in `except` clause?

@author:
OK.