Skip to content

Commit

Permalink
Fixed edge case where pyfunc has no attribute __name__
Browse files Browse the repository at this point in the history
  • Loading branch information
MatteoRaso committed Apr 2, 2023
1 parent 40bb77e commit dfaa72d
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 51 deletions.
105 changes: 66 additions & 39 deletions numpy/lib/function_base.py
Expand Up @@ -24,7 +24,7 @@
from numpy.core.function_base import add_newdoc
from numpy.lib.twodim_base import diag
from numpy.core.multiarray import (
_place, add_docstring, bincount, normalize_axis_index, _monotonicity,
_insert, add_docstring, bincount, normalize_axis_index, _monotonicity,
interp as compiled_interp, interp_complex as compiled_interp_complex
)
from numpy.core.umath import _add_newdoc_ufunc as add_newdoc_ufunc
Expand Down Expand Up @@ -1311,8 +1311,6 @@ def gradient(f, *varargs, axis=None, edge_order=1):

if len_axes == 1:
return outvals[0]
elif np._using_numpy2_behavior():
return tuple(outvals)
else:
return outvals

Expand Down Expand Up @@ -1951,7 +1949,11 @@ def place(arr, mask, vals):
[44, 55, 44]])
"""
return _place(arr, mask, vals)
if not isinstance(arr, np.ndarray):
raise TypeError("argument 1 must be numpy.ndarray, "
"not {name}".format(name=type(arr).__name__))

return _insert(arr, mask, vals)


def disp(mesg, device=None, linefeed=True):
Expand Down Expand Up @@ -2117,10 +2119,10 @@ def _create_arrays(broadcast_shape, dim_sizes, list_of_core_dims, dtypes,
@set_module('numpy')
class vectorize:
"""
vectorize(pyfunc, otypes=None, doc=None, excluded=None, cache=False,
signature=None)
vectorize(pyfunc=np._NoValue, otypes=None, doc=None, excluded=None,
cache=False, signature=None)
Generalized function class.
Returns an object that acts like pyfunc, but takes arrays as input.
Define a vectorized function which takes a nested sequence of objects or
numpy arrays as inputs and returns a single numpy array or a tuple of numpy
Expand All @@ -2134,8 +2136,9 @@ class vectorize:
Parameters
----------
pyfunc : callable
pyfunc : callable, optional
A python function or method.
Can be omitted to produce a decorator with keyword arguments.
otypes : str or list of dtypes, optional
The output data type. It must be specified as either a string of
typecode characters or a list of data type specifiers. There should
Expand Down Expand Up @@ -2167,8 +2170,9 @@ class vectorize:
Returns
-------
vectorized : callable
Vectorized function.
out : callable
A vectorized function if ``pyfunc`` was provided,
a decorator otherwise.
See Also
--------
Expand Down Expand Up @@ -2265,18 +2269,44 @@ class vectorize:
[0., 0., 1., 2., 1., 0.],
[0., 0., 0., 1., 2., 1.]])
Decorator syntax is supported. The decorator can be called as
a function to provide keyword arguments.
>>>@np.vectorize
...def identity(x):
... return x
...
>>>identity([0, 1, 2])
array([0, 1, 2])
>>>@np.vectorize(otypes=[float])
...def as_float(x):
... return x
...
>>>as_float([0, 1, 2])
array([0., 1., 2.])
"""
def __init__(self, pyfunc, otypes=None, doc=None, excluded=None,
cache=False, signature=None):
def __init__(self, pyfunc=np._NoValue, otypes=None, doc=None,
excluded=None, cache=False, signature=None):

if (pyfunc != np._NoValue) and (not callable(pyfunc)):
#Splitting the error message to keep
#the length below 79 characters.
part1 = "When used as a decorator, "
part2 = "only accepts keyword arguments."
raise TypeError(part1 + part2)

self.pyfunc = pyfunc
self.cache = cache
self.signature = signature
self._ufunc = {} # Caching to improve default performance
if pyfunc != np._NoValue and hasattr(pyfunc, '__name__'):
self.__name__ = pyfunc.__name__

self._ufunc = {} # Caching to improve default performance
self._doc = None
self.__doc__ = doc
if doc is None:
self.__doc__ = pyfunc.__doc__
else:
self.__doc__ = doc
self._doc = doc

if isinstance(otypes, str):
for char in otypes:
Expand All @@ -2298,7 +2328,15 @@ def __init__(self, pyfunc, otypes=None, doc=None, excluded=None,
else:
self._in_and_out_core_dims = None

def __call__(self, *args, **kwargs):
def _init_stage_2(self, pyfunc, *args, **kwargs):
self.__name__ = pyfunc.__name__
self.pyfunc = pyfunc
if self._doc is None:
self.__doc__ = pyfunc.__doc__
else:
self.__doc__ = self._doc

def _call_as_normal(self, *args, **kwargs):
"""
Return arrays with the results of `pyfunc` broadcast (vectorized) over
`args` and `kwargs` not in `excluded`.
Expand Down Expand Up @@ -2328,6 +2366,13 @@ def func(*vargs):

return self._vectorize_call(func=func, args=vargs)

def __call__(self, *args, **kwargs):
if self.pyfunc is np._NoValue:
self._init_stage_2(*args, **kwargs)
return self

return self._call_as_normal(*args, **kwargs)

def _get_ufunc_and_otypes(self, func, args):
"""Return (ufunc, otypes)."""
# frompyfunc will fail if args is empty
Expand Down Expand Up @@ -2693,7 +2738,7 @@ def cov(m, y=None, rowvar=True, bias=False, ddof=None, fweights=None,

if fact <= 0:
warnings.warn("Degrees of freedom <= 0 for slice",
RuntimeWarning, stacklevel=2)
RuntimeWarning, stacklevel=3)
fact = 0.0

X -= avg[:, None]
Expand Down Expand Up @@ -2842,7 +2887,7 @@ def corrcoef(x, y=None, rowvar=True, bias=np._NoValue, ddof=np._NoValue, *,
if bias is not np._NoValue or ddof is not np._NoValue:
# 2015-03-15, 1.10
warnings.warn('bias and ddof have no effect and are deprecated',
DeprecationWarning, stacklevel=2)
DeprecationWarning, stacklevel=3)
c = cov(x, y, rowvar, dtype=dtype)
try:
d = diag(c)
Expand Down Expand Up @@ -3682,7 +3727,7 @@ def msort(a):
warnings.warn(
"msort is deprecated, use np.sort(a, axis=0) instead",
DeprecationWarning,
stacklevel=2,
stacklevel=3,
)
b = array(a, subok=True, copy=True)
b.sort(0)
Expand Down Expand Up @@ -4910,24 +4955,6 @@ def trapz(y, x=None, dx=1.0, axis=-1):
return ret


# __array_function__ has no __code__ or other attributes normal Python funcs we
# wrap everything into a C callable. SciPy however, tries to "clone" `trapz`
# into a new Python function which requires `__code__` and a few other
# attributes. So we create a dummy clone and copy over its attributes allowing
# SciPy <= 1.10 to work: https://github.com/scipy/scipy/issues/17811
assert not hasattr(trapz, "__code__")

def _fake_trapz(y, x=None, dx=1.0, axis=-1):
return trapz(y, x=x, dx=dx, axis=axis)


trapz.__code__ = _fake_trapz.__code__
trapz.__globals__ = _fake_trapz.__globals__
trapz.__defaults__ = _fake_trapz.__defaults__
trapz.__closure__ = _fake_trapz.__closure__
trapz.__kwdefaults__ = _fake_trapz.__kwdefaults__


def _meshgrid_dispatcher(*xi, copy=None, sparse=None, indexing=None):
return xi

Expand All @@ -4936,7 +4963,7 @@ def _meshgrid_dispatcher(*xi, copy=None, sparse=None, indexing=None):
@array_function_dispatch(_meshgrid_dispatcher)
def meshgrid(*xi, copy=True, sparse=False, indexing='xy'):
"""
Return a list of coordinate matrices from coordinate vectors.
Return coordinate matrices from coordinate vectors.
Make N-D coordinate arrays for vectorized evaluations of
N-D scalar/vector fields over N-D grids, given
Expand Down Expand Up @@ -4977,7 +5004,7 @@ def meshgrid(*xi, copy=True, sparse=False, indexing='xy'):
Returns
-------
X1, X2,..., XN : list of ndarrays
X1, X2,..., XN : ndarray
For vectors `x1`, `x2`,..., `xn` with lengths ``Ni=len(xi)``,
returns ``(N1, N2, N3,..., Nn)`` shaped arrays if indexing='ij'
or ``(N2, N1, N3,..., Nn)`` shaped arrays if indexing='xy'
Expand Down Expand Up @@ -5414,7 +5441,7 @@ def insert(arr, obj, values, axis=None):
warnings.warn(
"in the future insert will treat boolean arrays and "
"array-likes as a boolean index instead of casting it to "
"integer", FutureWarning, stacklevel=2)
"integer", FutureWarning, stacklevel=3)
indices = indices.astype(intp)
# Code after warning period:
#if obj.ndim != 1:
Expand Down
80 changes: 68 additions & 12 deletions numpy/lib/tests/test_function_base.py
Expand Up @@ -8,7 +8,7 @@
import hypothesis
from hypothesis.extra.numpy import arrays
import hypothesis.strategies as st

from functools import partial

import numpy as np
from numpy import ma
Expand Down Expand Up @@ -229,8 +229,8 @@ def test_basic(self):
def test_nd(self):
y1 = [[0, 0, 0], [0, 1, 0], [1, 1, 0]]
assert_(np.any(y1))
assert_array_equal(np.any(y1, axis=0), [1, 1, 0])
assert_array_equal(np.any(y1, axis=1), [0, 1, 1])
assert_array_equal(np.sometrue(y1, axis=0), [1, 1, 0])
assert_array_equal(np.sometrue(y1, axis=1), [0, 1, 1])


class TestAll:
Expand All @@ -247,8 +247,8 @@ def test_basic(self):
def test_nd(self):
y1 = [[0, 0, 1], [0, 1, 1], [1, 1, 1]]
assert_(not np.all(y1))
assert_array_equal(np.all(y1, axis=0), [0, 0, 1])
assert_array_equal(np.all(y1, axis=1), [0, 0, 1])
assert_array_equal(np.alltrue(y1, axis=0), [0, 0, 1])
assert_array_equal(np.alltrue(y1, axis=1), [0, 0, 1])


class TestCopy:
Expand Down Expand Up @@ -1217,13 +1217,6 @@ def test_x_signed_int_big_jump(self, x_dtype):
dfdx = gradient(f, x)
assert_array_equal(dfdx, [0.5, 0.5])

def test_return_type(self):
res = np.gradient(([1, 2], [2, 3]))
if np._using_numpy2_behavior():
assert type(res) is tuple
else:
assert type(res) is list


class TestAngle:

Expand Down Expand Up @@ -1787,6 +1780,69 @@ class subclass(np.ndarray):
assert_equal(type(r), subclass)
assert_equal(r, m * v)

def test_name(self):
#See gh-23021
@np.vectorize
def f2(a, b):
return a + b

assert f2.__name__ == 'f2'

def test_decorator(self):
@vectorize
def addsubtract(a, b):
if a > b:
return a - b
else:
return a + b

r = addsubtract([0, 3, 6, 9], [1, 3, 5, 7])
assert_array_equal(r, [1, 6, 1, 2])

def test_docstring(self):
@vectorize
def f(x):
"""Docstring"""
return x

assert f.__doc__ == "Docstring"

def test_partial(self):
def foo(x, y):
return x + y

bar = partial(foo, 3)
vbar = np.vectorize(bar)
assert vbar(1) == 4

def test_signature_otypes_decorator(self):
@vectorize(signature='(n)->(n)', otypes=['float64'])
def f(x):
return x

r = f([1, 2, 3])
assert_equal(r.dtype, np.dtype('float64'))
assert_array_equal(r, [1, 2, 3])
assert f.__name__ == 'f'

def test_bad_input(self):
with assert_raises(TypeError):
A = np.vectorize(pyfunc = 3)

def test_no_keywords(self):
with assert_raises(TypeError):
@np.vectorize("string")
def foo():
return "bar"

def test_positional_regression_9477(self):
# This supplies the first keyword argument as a positional,
# to ensure that they are still properly forwarded after the
# enhancement for #9477
f = vectorize((lambda x: x), ['float64'])
r = f([2])
assert_equal(r.dtype, np.dtype('float64'))


class TestLeaks:
class A:
Expand Down

0 comments on commit dfaa72d

Please sign in to comment.