Repository: numpy/numpy

Issue:
np.max() doesn't behave properly for ndarray subclasses that define __array_ufunc__
Take the following example:

```
import numpy as np

class MyArr(np.ndarray):
    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
        func = getattr(ufunc, method)
        inp = [i.view(np.ndarray) for i in inputs]
        return func(*inp, **kwargs)

data = np.array([[1, 2, 3], [1, 2, 3]])

arr = data.view(MyArr)

print(data.max())
print(arr.max())
```

On numpy 1.13rc1 this prints:

```
3
[1 2 3]
```

For the call to `arr.max()` numpy calls `__array_ufunc__`, but there doesn't seem to be a way to tell from inside `__array_ufunc__` that we should be reducing across the whole array and not just the first dimension. I think this would work properly if numpy were calling `__array_ufunc__` with `axis=None`.

Ping @mhvk 


Discussion:
Can you `print(inp, kwargs)` in there too, and give the output?
Actually, nevermind, I see the issue.

@mhvk, your normalization in `normalize_reduce_args` discards all arguments which are `None`. This is bad for arguments whose default is not `None`.

Why do we do this check at all?
Similarly for `np.any`:

```
print(np.any(data))
print(np.any(arr))
```

prints:

```
True
[ True  True  True]
```

Here the issue is that numpy is calling np.logical_or.reduce via `umr_any`.
Thanks, that is most clearly a bug!  I'll try to get a fix in asap!
p.s. The check on `None` was meant for `out` -- it is definitely not intended to make this general (it may have something to do with trying to combine all normalizations and then undoing it...)
Actually, no, now that I look at it again, I did this on purpose, mistakenly thinking that `None` was always the default (and wanting to remove any keyword arguments that were at their default). But in fact for `axis`, `0` is the default. Anyway, I'll correct this to only remove `out` (also for `accumulate`, etc.); I should have realized the code sees the arguments early enough that anything there is explicitly passed in.

Resolving patch:
diff --git a/numpy/add_newdocs.py b/numpy/add_newdocs.py
--- a/numpy/add_newdocs.py
+++ b/numpy/add_newdocs.py
@@ -5444,9 +5444,11 @@ def luf(lamdaexpr, *args, **kwargs):
     ----------
     *x : array_like
         Input arrays.
-    out : ndarray or tuple of ndarray, optional
+    out : ndarray, None, or tuple of ndarray and None, optional
         Alternate array object(s) in which to put the result; if provided, it
-        must have a shape that the inputs broadcast to.
+        must have a shape that the inputs broadcast to. A tuple of arrays
+        (possible only as a keyword argument) must have length equal to the
+        number of outputs; use `None` for outputs to be allocated by the ufunc.
     where : array_like, optional
         Values of True indicate to calculate the ufunc at that position, values
         of False indicate to leave the value in the output alone.
@@ -5667,9 +5669,14 @@ def luf(lamdaexpr, *args, **kwargs):
         The type used to represent the intermediate results. Defaults
         to the data-type of the output array if this is provided, or
         the data-type of the input array if no output array is provided.
-    out : ndarray, optional
-        A location into which the result is stored. If not provided, a
-        freshly-allocated array is returned.
+    out : ndarray, None, or tuple of ndarray and None, optional
+        A location into which the result is stored. If not provided or `None`,
+        a freshly-allocated array is returned. For consistency with
+        :ref:`ufunc.__call__`, if given as a keyword, this may be wrapped in a
+        1-element tuple.
+
+        .. versionchanged:: 1.13.0
+           Tuples are allowed for keyword argument.
     keepdims : bool, optional
         If this is set to True, the axes which are reduced are left
         in the result as dimensions with size one. With this option,
@@ -5741,9 +5748,14 @@ def luf(lamdaexpr, *args, **kwargs):
         The data-type used to represent the intermediate results. Defaults
         to the data-type of the output array if such is provided, or the
         the data-type of the input array if no output array is provided.
-    out : ndarray, optional
-        A location into which the result is stored. If not provided a
-        freshly-allocated array is returned.
+    out : ndarray, None, or tuple of ndarray and None, optional
+        A location into which the result is stored. If not provided or `None`,
+        a freshly-allocated array is returned. For consistency with
+        :ref:`ufunc.__call__`, if given as a keyword, this may be wrapped in a
+        1-element tuple.
+
+        .. versionchanged:: 1.13.0
+           Tuples are allowed for keyword argument.
     keepdims : bool
         Has no effect. Deprecated, and will be removed in future.
 
@@ -5820,9 +5832,14 @@ def luf(lamdaexpr, *args, **kwargs):
         The type used to represent the intermediate results. Defaults
         to the data type of the output array if this is provided, or
         the data type of the input array if no output array is provided.
-    out : ndarray, optional
-        A location into which the result is stored. If not provided a
-        freshly-allocated array is returned.
+    out : ndarray, None, or tuple of ndarray and None, optional
+        A location into which the result is stored. If not provided or `None`,
+        a freshly-allocated array is returned. For consistency with
+        :ref:`ufunc.__call__`, if given as a keyword, this may be wrapped in a
+        1-element tuple.
+
+        .. versionchanged:: 1.13.0
+           Tuples are allowed for keyword argument.
 
     Returns
     -------
diff --git a/numpy/core/code_generators/ufunc_docstrings.py b/numpy/core/code_generators/ufunc_docstrings.py
--- a/numpy/core/code_generators/ufunc_docstrings.py
+++ b/numpy/core/code_generators/ufunc_docstrings.py
@@ -19,9 +19,11 @@ def get(name):
 
 # common parameter text to all ufuncs
 _params_text = textwrap.dedent("""
-    out : ndarray or tuple of ndarray, optional
-        Alternate array object(s) in which to put the result; if provided, it
-        must have a shape that the inputs broadcast to.
+    out : ndarray, None, or tuple of ndarray and None, optional
+        A location into which the result is stored. If provided, it must have
+        a shape that the inputs broadcast to. If not provided or `None`,
+        a freshly-allocated array is returned. A tuple (possible only as a
+        keyword argument) must have length equal to the number of outputs.
     where : array_like, optional
         Values of True indicate to calculate the ufunc at that position, values
         of False indicate to leave the value in the output alone.
diff --git a/numpy/doc/subclassing.py b/numpy/doc/subclassing.py
--- a/numpy/doc/subclassing.py
+++ b/numpy/doc/subclassing.py
@@ -489,6 +489,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
                 return NotImplemented
 
             if method == 'at':
+                if isinstance(inputs[0], A):
+                    inputs[0].info = info
                 return
 
             if ufunc.nout == 1:


Test patch:
diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py
--- a/numpy/core/tests/test_multiarray.py
+++ b/numpy/core/tests/test_multiarray.py
@@ -3107,7 +3107,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kw):
             warnings.filterwarnings('always', '', DeprecationWarning)
             assert_equal(np.modf(dummy, out=a), (0,))
             assert_(w[0].category is DeprecationWarning)
-        assert_raises(TypeError, np.modf, dummy, out=(a,))
+        assert_raises(ValueError, np.modf, dummy, out=(a,))
 
         # 2 inputs, 1 output
         assert_equal(np.add(a, dummy), 0)
diff --git a/numpy/core/tests/test_umath.py b/numpy/core/tests/test_umath.py
--- a/numpy/core/tests/test_umath.py
+++ b/numpy/core/tests/test_umath.py
@@ -1755,15 +1755,18 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
                               'keepdims': 'keep0',
                               'axis': 'axis0'})
 
-        # reduce, output equal to None removed.
-        res = np.multiply.reduce(a, out=None)
-        assert_equal(res[4], {})
-        res = np.multiply.reduce(a, out=(None,))
-        assert_equal(res[4], {})
+        # reduce, output equal to None removed, but not other explicit ones,
+        # even if they are at their default value.
+        res = np.multiply.reduce(a, 0, None, None, False)
+        assert_equal(res[4], {'axis': 0, 'dtype': None, 'keepdims': False})
+        res = np.multiply.reduce(a, out=None, axis=0, keepdims=True)
+        assert_equal(res[4], {'axis': 0, 'keepdims': True})
+        res = np.multiply.reduce(a, None, out=(None,), dtype=None)
+        assert_equal(res[4], {'axis': None, 'dtype': None})
 
         # reduce, wrong args
-        assert_raises(TypeError, np.multiply.reduce, a, out=())
-        assert_raises(TypeError, np.multiply.reduce, a, out=('out0', 'out1'))
+        assert_raises(ValueError, np.multiply.reduce, a, out=())
+        assert_raises(ValueError, np.multiply.reduce, a, out=('out0', 'out1'))
         assert_raises(TypeError, np.multiply.reduce, a, 'axis0', axis='axis0')
 
         # accumulate, pos args
@@ -1788,14 +1791,16 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
                               'axis': 'axis0'})
 
         # accumulate, output equal to None removed.
-        res = np.multiply.accumulate(a, out=None)
-        assert_equal(res[4], {})
-        res = np.multiply.accumulate(a, out=(None,))
-        assert_equal(res[4], {})
+        res = np.multiply.accumulate(a, 0, None, None)
+        assert_equal(res[4], {'axis': 0, 'dtype': None})
+        res = np.multiply.accumulate(a, out=None, axis=0, dtype='dtype1')
+        assert_equal(res[4], {'axis': 0, 'dtype': 'dtype1'})
+        res = np.multiply.accumulate(a, None, out=(None,), dtype=None)
+        assert_equal(res[4], {'axis': None, 'dtype': None})
 
         # accumulate, wrong args
-        assert_raises(TypeError, np.multiply.accumulate, a, out=())
-        assert_raises(TypeError, np.multiply.accumulate, a,
+        assert_raises(ValueError, np.multiply.accumulate, a, out=())
+        assert_raises(ValueError, np.multiply.accumulate, a,
                       out=('out0', 'out1'))
         assert_raises(TypeError, np.multiply.accumulate, a,
                       'axis0', axis='axis0')
@@ -1822,14 +1827,16 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
                               'axis': 'axis0'})
 
         # reduceat, output equal to None removed.
-        res = np.multiply.reduceat(a, [4, 2], out=None)
-        assert_equal(res[4], {})
-        res = np.multiply.reduceat(a, [4, 2], out=(None,))
-        assert_equal(res[4], {})
+        res = np.multiply.reduceat(a, [4, 2], 0, None, None)
+        assert_equal(res[4], {'axis': 0, 'dtype': None})
+        res = np.multiply.reduceat(a, [4, 2], axis=None, out=None, dtype='dt')
+        assert_equal(res[4], {'axis': None, 'dtype': 'dt'})
+        res = np.multiply.reduceat(a, [4, 2], None, None, out=(None,))
+        assert_equal(res[4], {'axis': None, 'dtype': None})
 
         # reduceat, wrong args
-        assert_raises(TypeError, np.multiply.reduce, a, [4, 2], out=())
-        assert_raises(TypeError, np.multiply.reduce, a, [4, 2],
+        assert_raises(ValueError, np.multiply.reduce, a, [4, 2], out=())
+        assert_raises(ValueError, np.multiply.reduce, a, [4, 2],
                       out=('out0', 'out1'))
         assert_raises(TypeError, np.multiply.reduce, a, [4, 2],
                       'axis0', axis='axis0')
@@ -1907,12 +1914,12 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
         # wrong number of arguments in the tuple is an error too.
         assert_raises(TypeError, np.multiply, a, b, 'one', out='two')
         assert_raises(TypeError, np.multiply, a, b, 'one', 'two')
-        assert_raises(TypeError, np.multiply, a, b, out=('one', 'two'))
-        assert_raises(TypeError, np.multiply, a, out=())
+        assert_raises(ValueError, np.multiply, a, b, out=('one', 'two'))
+        assert_raises(ValueError, np.multiply, a, out=())
         assert_raises(TypeError, np.modf, a, 'one', out=('two', 'three'))
         assert_raises(TypeError, np.modf, a, 'one', 'two', 'three')
-        assert_raises(TypeError, np.modf, a, out=('one', 'two', 'three'))
-        assert_raises(TypeError, np.modf, a, out=('one',))
+        assert_raises(ValueError, np.modf, a, out=('one', 'two', 'three'))
+        assert_raises(ValueError, np.modf, a, out=('one',))
 
     def test_ufunc_override_exception(self):
 
@@ -1999,11 +2006,12 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
         assert_raises(TypeError, inner1d, a, out='two')
         assert_raises(TypeError, inner1d, a, a, 'one', out='two')
         assert_raises(TypeError, inner1d, a, a, 'one', 'two')
-        assert_raises(TypeError, inner1d, a, a, out=('one', 'two'))
-        assert_raises(TypeError, inner1d, a, a, out=())
+        assert_raises(ValueError, inner1d, a, a, out=('one', 'two'))
+        assert_raises(ValueError, inner1d, a, a, out=())
 
     def test_ufunc_override_with_super(self):
-
+        # NOTE: this class is given as an example in doc/subclassing.py;
+        # if you make any changes here, do update it there too.
         class A(np.ndarray):
             def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
                 args = []
@@ -2041,6 +2049,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
                     return NotImplemented
 
                 if method == 'at':
+                    if isinstance(inputs[0], A):
+                        inputs[0].info = info
                     return
 
                 if ufunc.nout == 1:
@@ -2107,6 +2117,70 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
         assert_(a.__array_ufunc__(np.add, '__call__', a, b) is NotImplemented)
         assert_(b.__array_ufunc__(np.add, '__call__', a, b) == "A!")
         assert_(np.add(a, b) == "A!")
+        # regression check for gh-9102 -- tests ufunc.reduce implicitly.
+        d = np.array([[1, 2, 3], [1, 2, 3]])
+        a = d.view(A)
+        c = a.any()
+        check = d.any()
+        assert_equal(c, check)
+        assert_(c.info, {'inputs': [0]})
+        c = a.max()
+        check = d.max()
+        assert_equal(c, check)
+        assert_(c.info, {'inputs': [0]})
+        b = np.array(0).view(A)
+        c = a.max(out=b)
+        assert_equal(c, check)
+        assert_(c is b)
+        assert_(c.info, {'inputs': [0], 'outputs': [0]})
+        check = a.max(axis=0)
+        b = np.zeros_like(check).view(A)
+        c = a.max(axis=0, out=b)
+        assert_equal(c, check)
+        assert_(c is b)
+        assert_(c.info, {'inputs': [0], 'outputs': [0]})
+        # simple explicit tests of reduce, accumulate, reduceat
+        check = np.add.reduce(d, axis=1)
+        c = np.add.reduce(a, axis=1)
+        assert_equal(c, check)
+        assert_(c.info, {'inputs': [0]})
+        b = np.zeros_like(c)
+        c = np.add.reduce(a, 1, None, b)
+        assert_equal(c, check)
+        assert_(c is b)
+        assert_(c.info, {'inputs': [0], 'outputs': [0]})
+        check = np.add.accumulate(d, axis=0)
+        c = np.add.accumulate(a, axis=0)
+        assert_equal(c, check)
+        assert_(c.info, {'inputs': [0]})
+        b = np.zeros_like(c)
+        c = np.add.accumulate(a, 0, None, b)
+        assert_equal(c, check)
+        assert_(c is b)
+        assert_(c.info, {'inputs': [0], 'outputs': [0]})
+        indices = [0, 2, 1]
+        check = np.add.reduceat(d, indices, axis=1)
+        c = np.add.reduceat(a, indices, axis=1)
+        assert_equal(c, check)
+        assert_(c.info, {'inputs': [0]})
+        b = np.zeros_like(c)
+        c = np.add.reduceat(a, indices, 1, None, b)
+        assert_equal(c, check)
+        assert_(c is b)
+        assert_(c.info, {'inputs': [0], 'outputs': [0]})
+        # and a few tests for at
+        d = np.array([[1, 2, 3], [1, 2, 3]])
+        check = d.copy()
+        a = d.copy().view(A)
+        np.add.at(check, ([0, 1], [0, 2]), 1.)
+        np.add.at(a, ([0, 1], [0, 2]), 1.)
+        assert_equal(a, check)
+        assert_(a.info, {'inputs': [0]})
+        b = np.array(1.).view(A)
+        a = d.copy().view(A)
+        np.add.at(a, ([0, 1], [0, 2]), b)
+        assert_equal(a, check)
+        assert_(a.info, {'inputs': [0, 2]})
 
 
 class TestChoose(TestCase):