Repository: numpy/numpy

Issue:
BUG: Applying np.fix on scalar returns 0-D array
Appears that when NumPy's [`fix`]( https://docs.scipy.org/doc/numpy/reference/generated/numpy.fix.html ) function is given a scalar, it returns a 0-D array as the result. However, the documentation clearly shows it returning a scalar when given a scalar. Either this is a bug or the documentation needs to be updated. IMHO having a scalar returned when giving a scalar seems desirable. Example code and [`conda`]( http://conda.pydata.org/ ) [environment]( https://conda.io/docs/using/envs.html#use-environment-from-file ) details below.

Code sample:
```python
In [1]: import numpy as np

In [2]: np.fix(3.14)
Out[2]: array(3.0)

In [3]: np.fix(3)
Out[3]: array(3.0)

In [4]: np.fix([2.1, 2.9, -2.1, -2.9])
Out[4]: array([ 2.,  2., -2., -2.])
```

<br>
Environment:
<br>
<br>

<details>

```yaml
# environment.yml
name: test
channels: !!python/tuple
- !!python/unicode
  'conda-forge'
- !!python/unicode
  'defaults'
dependencies:
- conda-forge::appnope=0.1.0=py27_0
- conda-forge::backports.shutil_get_terminal_size=1.0.0=py27_1
- conda-forge::blas=1.1=openblas
- conda-forge::ca-certificates=2017.1.23=1
- conda-forge::certifi=2017.4.17=py27_0
- conda-forge::decorator=4.0.11=py27_0
- conda-forge::enum34=1.1.6=py27_1
- conda-forge::ipython=5.3.0=py27_0
- conda-forge::ipython_genutils=0.2.0=py27_0
- conda-forge::libgfortran=3.0.0=0
- conda-forge::ncurses=5.9=10
- conda-forge::numpy=1.12.1=py27_blas_openblas_200
- conda-forge::openblas=0.2.19=2
- conda-forge::openssl=1.0.2k=0
- conda-forge::pathlib2=2.2.1=py27_0
- conda-forge::pexpect=4.2.1=py27_0
- conda-forge::pickleshare=0.7.3=py27_0
- conda-forge::prompt_toolkit=1.0.14=py27_0
- conda-forge::ptyprocess=0.5.1=py27_0
- conda-forge::pygments=2.2.0=py27_0
- conda-forge::python=2.7.13=0
- conda-forge::readline=6.2=0
- conda-forge::scandir=1.5=py27_1
- conda-forge::setuptools=33.1.1=py27_0
- conda-forge::simplegeneric=0.8.1=py27_0
- conda-forge::six=1.10.0=py27_1
- conda-forge::sqlite=3.13.0=1
- conda-forge::tk=8.5.19=1
- conda-forge::traitlets=4.3.2=py27_0
- conda-forge::wcwidth=0.1.7=py27_0
- conda-forge::zlib=1.2.11=0
```

</details>

<br>
Note: Sorry if this is a duplicate issue. Searching the issue tracker for "fix" was not very effective.
BUG: Applying np.fix on scalar returns 0-D array
Appears that when NumPy's [`fix`]( https://docs.scipy.org/doc/numpy/reference/generated/numpy.fix.html ) function is given a scalar, it returns a 0-D array as the result. However, the documentation clearly shows it returning a scalar when given a scalar. Either this is a bug or the documentation needs to be updated. IMHO having a scalar returned when giving a scalar seems desirable. Example code and [`conda`]( http://conda.pydata.org/ ) [environment]( https://conda.io/docs/using/envs.html#use-environment-from-file ) details below.

Code sample:
```python
In [1]: import numpy as np

In [2]: np.fix(3.14)
Out[2]: array(3.0)

In [3]: np.fix(3)
Out[3]: array(3.0)

In [4]: np.fix([2.1, 2.9, -2.1, -2.9])
Out[4]: array([ 2.,  2., -2., -2.])
```

<br>
Environment:
<br>
<br>

<details>

```yaml
# environment.yml
name: test
channels: !!python/tuple
- !!python/unicode
  'conda-forge'
- !!python/unicode
  'defaults'
dependencies:
- conda-forge::appnope=0.1.0=py27_0
- conda-forge::backports.shutil_get_terminal_size=1.0.0=py27_1
- conda-forge::blas=1.1=openblas
- conda-forge::ca-certificates=2017.1.23=1
- conda-forge::certifi=2017.4.17=py27_0
- conda-forge::decorator=4.0.11=py27_0
- conda-forge::enum34=1.1.6=py27_1
- conda-forge::ipython=5.3.0=py27_0
- conda-forge::ipython_genutils=0.2.0=py27_0
- conda-forge::libgfortran=3.0.0=0
- conda-forge::ncurses=5.9=10
- conda-forge::numpy=1.12.1=py27_blas_openblas_200
- conda-forge::openblas=0.2.19=2
- conda-forge::openssl=1.0.2k=0
- conda-forge::pathlib2=2.2.1=py27_0
- conda-forge::pexpect=4.2.1=py27_0
- conda-forge::pickleshare=0.7.3=py27_0
- conda-forge::prompt_toolkit=1.0.14=py27_0
- conda-forge::ptyprocess=0.5.1=py27_0
- conda-forge::pygments=2.2.0=py27_0
- conda-forge::python=2.7.13=0
- conda-forge::readline=6.2=0
- conda-forge::scandir=1.5=py27_1
- conda-forge::setuptools=33.1.1=py27_0
- conda-forge::simplegeneric=0.8.1=py27_0
- conda-forge::six=1.10.0=py27_1
- conda-forge::sqlite=3.13.0=1
- conda-forge::tk=8.5.19=1
- conda-forge::traitlets=4.3.2=py27_0
- conda-forge::wcwidth=0.1.7=py27_0
- conda-forge::zlib=1.2.11=0
```

</details>

<br>
Note: Sorry if this is a duplicate issue. Searching the issue tracker for "fix" was not very effective.


Discussion:
Seems pretty dumb that that function:
* Has an `out` argument, when it doesn't have any optimized code path to use it (would be fixed by #8994)
* Calls that `out` argument `y`, unlike every other function (#8995)

Should be a trivial fix, changing the code to:
```python
x = nx.asanyarray(x)
y1 = nx.floor(x)
y2 = nx.ceil(x)
res = nx.where(x >= 0, y1, y2)
if y is not None:
    ### todo: deprecate here
    y[...] = res
    return y
return res
```
Note that whatever fix is applied here, it should be applied to every function within [that file](https://github.com/numpy/numpy/blob/master/numpy/lib/ufunclike.py) - they all suffer from the same problem
Seems pretty dumb that that function:
* Has an `out` argument, when it doesn't have any optimized code path to use it (would be fixed by #8994)
* Calls that `out` argument `y`, unlike every other function (#8995)

Should be a trivial fix, changing the code to:
```python
x = nx.asanyarray(x)
y1 = nx.floor(x)
y2 = nx.ceil(x)
res = nx.where(x >= 0, y1, y2)
if y is not None:
    ### todo: deprecate here
    y[...] = res
    return y
return res
```
Note that whatever fix is applied here, it should be applied to every function within [that file](https://github.com/numpy/numpy/blob/master/numpy/lib/ufunclike.py) - they all suffer from the same problem

Resolving patch:
diff --git a/numpy/lib/ufunclike.py b/numpy/lib/ufunclike.py
--- a/numpy/lib/ufunclike.py
+++ b/numpy/lib/ufunclike.py
@@ -8,8 +8,36 @@
 __all__ = ['fix', 'isneginf', 'isposinf']
 
 import numpy.core.numeric as nx
+import warnings
+import functools
 
-def fix(x, y=None):
+def _deprecate_out_named_y(f):
+    """
+    Allow the out argument to be passed as the name `y` (deprecated)
+
+    In future, this decorator should be removed.
+    """
+    @functools.wraps(f)
+    def func(x, out=None, **kwargs):
+        if 'y' in kwargs:
+            if 'out' in kwargs:
+                raise TypeError(
+                    "{} got multiple values for argument 'out'/'y'"
+                    .format(f.__name__)
+                )
+            out = kwargs.pop('y')
+            # NumPy 1.13.0, 2017-04-26
+            warnings.warn(
+                "The name of the out argument to {} has changed from `y` to "
+                "`out`, to match other ufuncs.".format(f.__name__),
+                DeprecationWarning, stacklevel=3)
+        return f(x, out=out, **kwargs)
+
+    return func
+
+
+@_deprecate_out_named_y
+def fix(x, out=None):
     """
     Round to nearest integer towards zero.
 
@@ -43,15 +71,18 @@ def fix(x, y=None):
     array([ 2.,  2., -2., -2.])
 
     """
-    x = nx.asanyarray(x)
-    y1 = nx.floor(x)
-    y2 = nx.ceil(x)
-    if y is None:
-        y = nx.asanyarray(y1)
-    y[...] = nx.where(x >= 0, y1, y2)
-    return y
-
-def isposinf(x, y=None):
+    # promote back to an array if flattened
+    res = nx.asanyarray(nx.ceil(x, out=out))
+    res = nx.floor(x, out=res, where=nx.greater_equal(x, 0))
+
+    # when no out argument is passed and no subclasses are involved, flatten
+    # scalars
+    if out is None and type(res) is nx.ndarray:
+        res = res[()]
+    return res
+
+@_deprecate_out_named_y
+def isposinf(x, out=None):
     """
     Test element-wise for positive infinity, return result as bool array.
 
@@ -64,7 +95,7 @@ def isposinf(x, y=None):
 
     Returns
     -------
-    y : ndarray
+    out : ndarray
         A boolean array with the same dimensions as the input.
         If second argument is not supplied then a boolean array is returned
         with values True where the corresponding element of the input is
@@ -74,7 +105,7 @@ def isposinf(x, y=None):
         If a second argument is supplied the result is stored there. If the
         type of that array is a numeric type the result is represented as zeros
         and ones, if the type is boolean then as False and True.
-        The return value `y` is then a reference to that array.
+        The return value `out` is then a reference to that array.
 
     See Also
     --------
@@ -107,13 +138,11 @@ def isposinf(x, y=None):
     array([0, 0, 1])
 
     """
-    if y is None:
-        x = nx.asarray(x)
-        y = nx.empty(x.shape, dtype=nx.bool_)
-    nx.logical_and(nx.isinf(x), ~nx.signbit(x), y)
-    return y
+    return nx.logical_and(nx.isinf(x), ~nx.signbit(x), out)
 
-def isneginf(x, y=None):
+
+@_deprecate_out_named_y
+def isneginf(x, out=None):
     """
     Test element-wise for negative infinity, return result as bool array.
 
@@ -121,13 +150,13 @@ def isneginf(x, y=None):
     ----------
     x : array_like
         The input array.
-    y : array_like, optional
+    out : array_like, optional
         A boolean array with the same shape and type as `x` to store the
         result.
 
     Returns
     -------
-    y : ndarray
+    out : ndarray
         A boolean array with the same dimensions as the input.
         If second argument is not supplied then a numpy boolean array is
         returned with values True where the corresponding element of the
@@ -137,7 +166,7 @@ def isneginf(x, y=None):
         If a second argument is supplied the result is stored there. If the
         type of that array is a numeric type the result is represented as
         zeros and ones, if the type is boolean then as False and True. The
-        return value `y` is then a reference to that array.
+        return value `out` is then a reference to that array.
 
     See Also
     --------
@@ -170,8 +199,4 @@ def isneginf(x, y=None):
     array([1, 0, 0])
 
     """
-    if y is None:
-        x = nx.asarray(x)
-        y = nx.empty(x.shape, dtype=nx.bool_)
-    nx.logical_and(nx.isinf(x), nx.signbit(x), y)
-    return y
+    return nx.logical_and(nx.isinf(x), nx.signbit(x), out)


Test patch:
diff --git a/numpy/lib/tests/test_ufunclike.py b/numpy/lib/tests/test_ufunclike.py
--- a/numpy/lib/tests/test_ufunclike.py
+++ b/numpy/lib/tests/test_ufunclike.py
@@ -1,9 +1,11 @@
 from __future__ import division, absolute_import, print_function
 
+import numpy as np
 import numpy.core as nx
 import numpy.lib.ufunclike as ufl
 from numpy.testing import (
-    run_module_suite, TestCase, assert_, assert_equal, assert_array_equal
+    run_module_suite, TestCase, assert_, assert_equal, assert_array_equal,
+    assert_warns
     )
 
 
@@ -61,5 +63,35 @@ def __array_wrap__(self, obj, context=None):
         assert_(isinstance(f, MyArray))
         assert_equal(f.metadata, 'foo')
 
+        # check 0d arrays don't decay to scalars
+        m0d = m[0,...]
+        m0d.metadata = 'bar'
+        f0d = ufl.fix(m0d)
+        assert_(isinstance(f0d, MyArray))
+        assert_equal(f0d.metadata, 'bar')
+
+    def test_deprecated(self):
+        # NumPy 1.13.0, 2017-04-26
+        assert_warns(DeprecationWarning, ufl.fix, [1, 2], y=nx.empty(2))
+        assert_warns(DeprecationWarning, ufl.isposinf, [1, 2], y=nx.empty(2))
+        assert_warns(DeprecationWarning, ufl.isneginf, [1, 2], y=nx.empty(2))
+
+    def test_scalar(self):
+        x = np.inf
+        actual = np.isposinf(x)
+        expected = np.True_
+        assert_equal(actual, expected)
+        assert_equal(type(actual), type(expected))
+
+        x = -3.4
+        actual = np.fix(x)
+        expected = np.float64(-3.0)
+        assert_equal(actual, expected)
+        assert_equal(type(actual), type(expected))
+
+        out = np.array(0.0)
+        actual = np.fix(x, out=out)
+        assert_(actual is out)
+
 if __name__ == "__main__":
     run_module_suite()