diff options
| author | Sebastian Berg <sebastian@sipsolutions.net> | 2021-04-01 18:09:28 -0500 |
|---|---|---|
| committer | Sebastian Berg <sebastian@sipsolutions.net> | 2021-04-08 12:25:10 -0500 |
| commit | 5e310e94de468359332ea5366d6555df5bc85231 (patch) | |
| tree | 36a18b62c521896a0253a47864fcd47cb46106f9 | |
| parent | 0fe69ae3fc513aefd32a58ae8ccac294b12300d2 (diff) | |
| download | numpy-5e310e94de468359332ea5366d6555df5bc85231.tar.gz | |
API,DEP: Move ufunc signature parsing to the start
This may have slight affects on users, see release notes.
Mainly, we have to parse the type tuple up-front (because we need
to replace the current type resolution). Since we _must_ de-facto
replace the current type resolution, I do not see why we should
duplicate code for the odd possibility of someone actually calling
`ufunc.type_resolver()` with a `typetup` that is not a actually a
type tuple.
This commit also deprecates `signature="l"` as meaning (normally)
the same as `dtype="l"`.
| -rw-r--r-- | doc/release/upcoming_changes/18718.c_api.rst | 13 | ||||
| -rw-r--r-- | doc/release/upcoming_changes/18718.compatibility.rst | 53 | ||||
| -rw-r--r-- | numpy/core/src/umath/ufunc_object.c | 328 | ||||
| -rw-r--r-- | numpy/core/src/umath/ufunc_type_resolution.c | 224 | ||||
| -rw-r--r-- | numpy/core/tests/test_deprecations.py | 34 | ||||
| -rw-r--r-- | numpy/core/tests/test_ufunc.py | 51 | ||||
| -rw-r--r-- | numpy/typing/tests/data/pass/ufuncs.py | 2 |
7 files changed, 528 insertions, 177 deletions
diff --git a/doc/release/upcoming_changes/18718.c_api.rst b/doc/release/upcoming_changes/18718.c_api.rst new file mode 100644 index 000000000..eb9121ab6 --- /dev/null +++ b/doc/release/upcoming_changes/18718.c_api.rst @@ -0,0 +1,13 @@ +Use of ``ufunc->type_resolver`` and "type tuple" +------------------------------------------------ +NumPy now normalizes the "type tuple" argument to the +type resolver functions before calling it. Note that in +the use of this type resolver is legacy behaviour and NumPy +will not do so when possible. +Calling ``ufunc->type_resolver`` or ``PyUFunc_DefaultTypeResolver`` +is strongly discouraged and will now enforce a normalized +type tuple if done. +Note that this does not affect providing a type resolver, which +is expected to keep working in most circumstances. +If you have an unexpected use-case for calling the type resolver, +please inform the NumPy developers so that a solution can be found. diff --git a/doc/release/upcoming_changes/18718.compatibility.rst b/doc/release/upcoming_changes/18718.compatibility.rst new file mode 100644 index 000000000..19563a25a --- /dev/null +++ b/doc/release/upcoming_changes/18718.compatibility.rst @@ -0,0 +1,53 @@ +Changes to comparisons with ``dtype=...`` +----------------------------------------- +When the ``dtype=`` (or ``signature``) arguments to comparison +ufuncs (``equal``, ``less``, etc.) is used, this will denote +the desired output dtype in the future. +This means that: + + np.equal(2, 3, dtype=object) + +will give a ``FutureWarning`` that it will return an ``object`` +array in the future, which currently happens for: + + np.equal(None, None, dtype=object) + +due to the fact that ``np.array(None)`` is already an object +array. (This also happens for some other dtypes.) + +Since comparisons normally only return boolean arrays, providing +any other dtype will always raise an error in the future and +give a ``DeprecationWarning`` now. + + +Changes to ``dtype`` and ``signature`` arguments in ufuncs +---------------------------------------------------------- +The universal function arguments ``dtype`` and ``signature`` +which are also valid for reduction such as ``np.add.reduce`` +(which is the implementation for ``np.sum``) will now issue +a warning when the ``dtype`` provided is not a "basic" dtype. + +NumPy almost always ignored metadata, byteorder or time units +on these inputs. NumPy will now always ignore it and issue +a warning if byteorder or time unit changed. +The following are the most important examples of changes which +will issue the warning and in some cases previously returned +different results:: + + # The following will now warn on most systems (unchanged result): + np.add(3, 5, dtype=">i32") + + # The biggest impact is for timedelta or datetimes: + arr = np.arange(10, dtype="m8[s]") + # The examples always ignored the time unit "ns" (using the + # unit of `arr`. They now issue a warning: + np.add(arr, arr, dtype="m8[ns]") + np.maximum.reduce(arr, dtype="m8[ns]") + + # The following issue a warning but previously did return + # a "ns" result. + np.add(3, 5, dtype="m8[ns]") # Now return generic time units + np.maximum(arr, arr, dtype="m8[ns]") # Now returns "s" (from `arr`) + +The same applies for functions like ``np.sum`` which use these internally. +This change is necessary to achieve consistent handling within NumPy. diff --git a/numpy/core/src/umath/ufunc_object.c b/numpy/core/src/umath/ufunc_object.c index 128706277..d975b0883 100644 --- a/numpy/core/src/umath/ufunc_object.c +++ b/numpy/core/src/umath/ufunc_object.c @@ -47,6 +47,7 @@ #include "npy_import.h" #include "extobj.h" #include "common.h" +#include "dtypemeta.h" #include "numpyos.h" /********** PRINTF DEBUG TRACING **************/ @@ -2682,7 +2683,6 @@ PyUFunc_GenericFunctionInternal(PyUFuncObject *ufunc, PyArrayObject **op, int trivial_loop_ok = 0; - nin = ufunc->nin; nout = ufunc->nout; nop = nin + nout; @@ -4033,6 +4033,10 @@ _not_NoValue(PyObject *obj, PyObject **out) return 1; } + +/* forward declaration */ +static PyArray_DTypeMeta * _get_dtype(PyObject *dtype_obj); + /* * This code handles reduce, reduceat, and accumulate * (accumulate and reduce are special cases of the more general reduceat @@ -4192,8 +4196,14 @@ PyUFunc_GenericReduction(PyUFuncObject *ufunc, goto fail; } } - if (otype_obj && !PyArray_DescrConverter2(otype_obj, &otype)) { - goto fail; + if (otype_obj && otype_obj != Py_None) { + /* Use `_get_dtype` because `dtype` is a DType and not the instance */ + PyArray_DTypeMeta *dtype = _get_dtype(otype_obj); + if (dtype == NULL) { + goto fail; + } + Py_INCREF(dtype->singleton); + otype = dtype->singleton; } if (out_obj && !PyArray_OutputConverter(out_obj, &out)) { goto fail; @@ -4396,35 +4406,42 @@ fail: /* - * Sets typetup to a new reference to the passed in dtype information - * tuple or NULL. Returns -1 on failure. + * Perform a basic check on `dtype`, `sig`, and `signature` since only one + * may be set. If `sig` is used, writes it into `out_signature` (which should + * be set to `signature_obj` so that following code only requires to handle + * `signature_obj`). + * + * Does NOT incref the output! This only copies the borrowed references + * gotten during the argument parsing. + * + * This function does not do any normalization of the input dtype tuples, + * this happens after the array-ufunc override check currently. */ static int -_get_typetup(PyObject *sig_obj, PyObject *signature_obj, PyObject *dtype, - PyObject **out_typetup) +_check_and_copy_sig_to_signature( + PyObject *sig_obj, PyObject *signature_obj, PyObject *dtype, + PyObject **out_signature) { - *out_typetup = NULL; + *out_signature = NULL; if (signature_obj != NULL) { - Py_INCREF(signature_obj); - *out_typetup = signature_obj; + *out_signature = signature_obj; } if (sig_obj != NULL) { - if (*out_typetup != NULL) { + if (*out_signature != NULL) { PyErr_SetString(PyExc_TypeError, "cannot specify both 'sig' and 'signature'"); - Py_SETREF(*out_typetup, NULL); + *out_signature = NULL; return -1; } Py_INCREF(sig_obj); - *out_typetup = sig_obj; + *out_signature = sig_obj; } if (dtype != NULL) { - if (*out_typetup != NULL) { + if (*out_signature != NULL) { PyErr_SetString(PyExc_TypeError, "cannot specify both 'signature' and 'dtype'"); - Py_SETREF(*out_typetup, NULL); return -1; } /* dtype needs to be converted, delay after the override check */ @@ -4432,32 +4449,264 @@ _get_typetup(PyObject *sig_obj, PyObject *signature_obj, PyObject *dtype, return 0; } + +/* + * Note: This function currently lets DType classes pass, but in general + * the class (not the descriptor instance) is the preferred input, so the + * parsing should eventually be adapted to prefer classes and possible + * deprecated instances. (Users should not notice that much, since `np.float64` + * or "float64" usually denotes the DType class rather than the instance.) + */ +static PyArray_DTypeMeta * +_get_dtype(PyObject *dtype_obj) { + if (PyObject_TypeCheck(dtype_obj, &PyArrayDTypeMeta_Type)) { + Py_INCREF(dtype_obj); + return (PyArray_DTypeMeta *)dtype_obj; + } + else { + PyArray_Descr *descr = NULL; + if (!PyArray_DescrConverter(dtype_obj, &descr)) { + return NULL; + } + PyArray_DTypeMeta *out = NPY_DTYPE(descr); + if (NPY_UNLIKELY(!out->legacy)) { + /* TODO: this path was unreachable when added. */ + PyErr_SetString(PyExc_TypeError, + "Cannot pass a new user DType instance to the `dtype` or " + "`signature` arguments of ufuncs. Pass the DType class " + "instead."); + Py_DECREF(descr); + return NULL; + } + else if (NPY_UNLIKELY(out->singleton != descr)) { + /* This does not warn about `metadata`, but units is important. */ + if (!PyArray_EquivTypes(out->singleton, descr)) { + if (PyErr_WarnFormat(PyExc_UserWarning, 1, + "The `dtype` and `signature` arguments to " + "ufuncs only select the general DType and not details " + "such as the byte order or time unit. " + "In very rare cases NumPy <1.21 may have preserved the " + "time unit for `dtype=`. The cases are mainly " + "`np.minimum(arr1, arr2, dtype='m8[ms]')` and when the " + "output is timedelta, but the input is integer. " + "(See NumPy 1.21.0 release notes for details.)\n" + "If you wish to set an exact output dtype, you must " + "currently pass `out=` instead.") < 0) { + Py_DECREF(descr); + return NULL; + } + } + } + Py_INCREF(out); + Py_DECREF(descr); + return out; + } +} + + +static int +_make_new_typetup( + int nop, PyArray_DTypeMeta *signature[], PyObject **out_typetup) { + *out_typetup = PyTuple_New(nop); + if (*out_typetup == NULL) { + return -1; + } + for (int i = 0; i < nop; i++) { + PyObject *item; + if (signature[i] == NULL) { + item = Py_None; + } + else { + if (!signature[i]->legacy || signature[i]->abstract) { + /* + * The legacy type resolution can't deal with these. + * This path will return `None` or so in the future to + * set an error later if the legacy type resolution is used. + */ + PyErr_SetString(PyExc_RuntimeError, + "Internal NumPy error: new DType in signature not yet " + "supported. (This should be unreachable code!)"); + Py_SETREF(*out_typetup, NULL); + return -1; + } + item = (PyObject *)signature[i]->singleton; + } + Py_INCREF(item); + PyTuple_SET_ITEM(*out_typetup, i, item); + } + return 0; +} + + /* - * Finish conversion parsing of the type tuple. This is currenlty only - * conversion of the `dtype` argument, but should do more in the future. + * Finish conversion parsing of the type tuple. NumPy always only honored + * the type number for passed in descriptors/dtypes. + * The `dtype` argument is interpreted as the first output DType (not + * descriptor). + * Unlike the dtype of an `out` array, it influences loop selection! * - * TODO: The parsing of the typetup should be moved here (followup cleanup). + * NOTE: This function replaces the type tuple if passed in (it steals + * the original reference and returns a new object and reference)! + * The caller must XDECREF the type tuple both on error or success. + * + * The function returns a new, normalized type-tuple. */ static int -_convert_typetup(PyObject *dtype_obj, PyObject **out_typetup) +_get_normalized_typetup(PyUFuncObject *ufunc, + PyObject *dtype_obj, PyObject *signature_obj, PyObject **out_typetup) { + if (dtype_obj == NULL && signature_obj == NULL) { + return 0; + } + + int res = -1; + int nin = ufunc->nin, nout = ufunc->nout, nop = nin + nout; + /* + * TODO: `signature` will be the main result in the future and + * not the typetup. (Type tuple construction can be deffered to when + * the legacy fallback is used). + */ + PyArray_DTypeMeta *signature[NPY_MAXARGS]; + memset(signature, '\0', sizeof(*signature) * nop); + if (dtype_obj != NULL) { - PyArray_Descr *dtype = NULL; - if (!PyArray_DescrConverter2(dtype_obj, &dtype)) { - return -1; - } - if (dtype == NULL) { - /* dtype=None, no need to set typetup. */ + if (dtype_obj == Py_None) { + /* If `dtype=None` is passed, no need to do anything */ + assert(*out_typetup == NULL); return 0; } - *out_typetup = PyTuple_Pack(1, (PyObject *)dtype); - Py_DECREF(dtype); - if (*out_typetup == NULL) { + if (nout == 0) { + /* This may be allowed (NumPy does not do this)? */ + PyErr_SetString(PyExc_TypeError, + "Cannot provide `dtype` when a ufunc has no outputs"); return -1; } + signature[nin] = _get_dtype(dtype_obj); + if (signature[nin] == NULL) { + return -1; + } + res = _make_new_typetup(nop, signature, out_typetup); + goto finish; } - /* sig and signature are not converted here right now. */ - return 0; + + assert(signature_obj != NULL); + /* Fill in specified_types from the tuple or string (signature_obj) */ + if (PyTuple_Check(signature_obj)) { + int nonecount = 0; + Py_ssize_t n = PyTuple_GET_SIZE(signature_obj); + if (n == 1 && nop != 1) { + /* + * Special handling, because we deprecate this path. The path + * probably mainly existed since the `dtype=obj` was passed through + * as `(obj,)` and parsed later. + */ + if (PyTuple_GET_ITEM(signature_obj, 0) == Py_None) { + PyErr_SetString(PyExc_TypeError, + "a single item type tuple cannot contain None."); + goto finish; + } + if (DEPRECATE("The use of a length 1 tuple for the ufunc " + "`signature` is deprecated. Use `dtype` or fill the" + "tuple with `None`s.") < 0) { + goto finish; + } + /* Use the same logic as for `dtype=` */ + res = _get_normalized_typetup(ufunc, + PyTuple_GET_ITEM(signature_obj, 0), NULL, out_typetup); + goto finish; + } + if (n != nop) { + PyErr_Format(PyExc_ValueError, + "a type-tuple must be specified of length %d for ufunc '%s'", + nop, ufunc_get_name_cstr(ufunc)); + goto finish; + } + for (int i = 0; i < nop; ++i) { + PyObject *item = PyTuple_GET_ITEM(signature_obj, i); + if (item == Py_None) { + ++nonecount; + } + else { + signature[i] = _get_dtype(item); + if (signature[i] == NULL) { + goto finish; + } + } + } + if (nonecount == n) { + PyErr_SetString(PyExc_ValueError, + "the type-tuple provided to the ufunc " + "must specify at least one none-None dtype"); + goto finish; + } + } + else if (PyBytes_Check(signature_obj) || PyUnicode_Check(signature_obj)) { + PyObject *str_object = NULL; + + if (PyBytes_Check(signature_obj)) { + str_object = PyUnicode_FromEncodedObject(signature_obj, NULL, NULL); + if (str_object == NULL) { + goto finish; + } + } + else { + Py_INCREF(signature_obj); + str_object = signature_obj; + } + + Py_ssize_t length; + const char *str = PyUnicode_AsUTF8AndSize(str_object, &length); + if (str == NULL) { + Py_DECREF(str_object); + goto finish; + } + + if (length != 1 && (length != nin+nout + 2 || + str[nin] != '-' || str[nin+1] != '>')) { + PyErr_Format(PyExc_ValueError, + "a type-string for %s, %d typecode(s) before and %d after " + "the -> sign", ufunc_get_name_cstr(ufunc), nin, nout); + Py_DECREF(str_object); + goto finish; + } + if (length == 1 && nin+nout != 1) { + Py_DECREF(str_object); + if (DEPRECATE("The use of a length 1 string for the ufunc " + "`signature` is deprecated. Use `dtype` attribute or " + "pass a tuple with `None`s.") < 0) { + goto finish; + } + /* `signature="l"` is the same as `dtype="l"` */ + res = _get_normalized_typetup(ufunc, str_object, NULL, out_typetup); + goto finish; + } + else { + for (int i = 0; i < nin+nout; ++i) { + npy_intp istr = i < nin ? i : i+2; + PyArray_Descr *descr = PyArray_DescrFromType(str[istr]); + if (descr == NULL) { + Py_DECREF(str_object); + goto finish; + } + signature[i] = NPY_DTYPE(descr); + Py_INCREF(signature[i]); + Py_DECREF(descr); + } + Py_DECREF(str_object); + } + } + else { + PyErr_SetString(PyExc_TypeError, + "The signature object to ufunc must be a string or a tuple."); + goto finish; + } + res = _make_new_typetup(nop, signature, out_typetup); + + finish: + for (int i =0; i < nop; i++) { + Py_XDECREF(signature[i]); + } + return res; } @@ -4613,9 +4862,13 @@ ufunc_generic_fastcall(PyUFuncObject *ufunc, goto fail; } } - - /* Only one of signature, sig, and dtype should be passed */ - if (_get_typetup(sig_obj, signature_obj, dtype_obj, &typetup) < 0) { + /* + * Only one of signature, sig, and dtype should be passed. If `sig` + * was passed, this puts it into `signature_obj` instead (these + * are borrowed references). + */ + if (_check_and_copy_sig_to_signature( + sig_obj, signature_obj, dtype_obj, &signature_obj) < 0) { goto fail; } } @@ -4635,7 +4888,6 @@ ufunc_generic_fastcall(PyUFuncObject *ufunc, goto fail; } else if (override) { - Py_XDECREF(typetup); Py_DECREF(full_args.in); Py_XDECREF(full_args.out); return override; @@ -4650,8 +4902,11 @@ ufunc_generic_fastcall(PyUFuncObject *ufunc, Py_SETREF(full_args.in, new_in); } - /* Finish argument parsing/converting for the dtype and all others */ - if (_convert_typetup(dtype_obj, &typetup) < 0) { + /* + * Parse the passed `dtype` or `signature` into an array containing + * PyArray_DTypeMeta and/or None. + */ + if (_get_normalized_typetup(ufunc, dtype_obj, signature_obj, &typetup) < 0) { goto fail; } @@ -4734,7 +4989,6 @@ ufunc_generic_fastcall(PyUFuncObject *ufunc, Py_XDECREF(typetup); Py_XDECREF(full_args.in); Py_XDECREF(full_args.out); - if (ufunc->nout == 1) { return retobj[0]; } diff --git a/numpy/core/src/umath/ufunc_type_resolution.c b/numpy/core/src/umath/ufunc_type_resolution.c index c46346118..4cf3b3076 100644 --- a/numpy/core/src/umath/ufunc_type_resolution.c +++ b/numpy/core/src/umath/ufunc_type_resolution.c @@ -355,30 +355,43 @@ PyUFunc_SimpleBinaryComparisonTypeResolver(PyUFuncObject *ufunc, Py_INCREF(out_dtypes[1]); } else { - PyObject *item; - PyArray_Descr *dtype = NULL; - + PyArray_Descr *descr; /* - * If the type tuple isn't a single-element tuple, let the - * default type resolution handle this one. + * If the type tuple was originally a single element (probably), + * issue a deprecation warning, but otherwise accept it. Since the + * result dtype is always boolean, this is not actually valid unless it + * is `object` (but if there is an object input we already deferred). */ - if (!PyTuple_Check(type_tup) || PyTuple_GET_SIZE(type_tup) != 1) { + if (PyTuple_Check(type_tup) && PyTuple_GET_SIZE(type_tup) == 3 && + PyTuple_GET_ITEM(type_tup, 0) == Py_None && + PyTuple_GET_ITEM(type_tup, 1) == Py_None && + PyArray_DescrCheck(PyTuple_GET_ITEM(type_tup, 2))) { + descr = (PyArray_Descr *)PyTuple_GET_ITEM(type_tup, 2); + if (descr->type_num == NPY_OBJECT) { + if (DEPRECATE_FUTUREWARNING( + "using `dtype=object` (or equivalent signature) will " + "return object arrays in the future also when the " + "inputs do not already have `object` dtype.") < 0) { + return -1; + } + } + else if (descr->type_num != NPY_BOOL) { + if (DEPRECATE( + "using `dtype=` in comparisons is only useful for " + "`dtype=object` (and will do nothing for bool). " + "This operation will fail in the future.") < 0) { + return -1; + } + } + } + else { + /* Usually a failure, but let the the default version handle it */ return PyUFunc_DefaultTypeResolver(ufunc, casting, operands, type_tup, out_dtypes); } - item = PyTuple_GET_ITEM(type_tup, 0); - - if (item == Py_None) { - PyErr_SetString(PyExc_ValueError, - "require data type in the type tuple"); - return -1; - } - else if (!PyArray_DescrConverter(item, &dtype)) { - return -1; - } - - out_dtypes[0] = ensure_dtype_nbo(dtype); + Py_INCREF(descr); + out_dtypes[0] = ensure_dtype_nbo(descr); if (out_dtypes[0] == NULL) { return -1; } @@ -536,34 +549,42 @@ PyUFunc_SimpleUniformOperationTypeResolver( } } else { - PyObject *item; - PyArray_Descr *dtype = NULL; - /* - * If the type tuple isn't a single-element tuple, let the - * default type resolution handle this one. + * This is a fast-path, since all descriptors will be identical, mainly + * when only a single descriptor was passed (which would set the out + * one in the tuple), there is no need to check all loops. */ - if (!PyTuple_Check(type_tup) || PyTuple_GET_SIZE(type_tup) != 1) { + PyArray_Descr *descr = NULL; + if (PyTuple_CheckExact(type_tup) && + PyTuple_GET_SIZE(type_tup) == nop) { + for (int i = 0; i < nop; i++) { + PyObject *item = PyTuple_GET_ITEM(type_tup, i); + if (item == Py_None) { + continue; + } + if (!PyArray_DescrCheck(item)) { + /* bad type tuple (maybe not normalized correctly?) */ + descr = NULL; + break; + } + if (descr != NULL && descr != (PyArray_Descr *)item) { + /* descriptor mismatch, probably a bad signature. */ + descr = NULL; + break; + } + descr = (PyArray_Descr *)item; + } + } + if (descr == NULL) { + /* in all bad/unlikely cases, use the default type resolver: */ return PyUFunc_DefaultTypeResolver(ufunc, casting, operands, type_tup, out_dtypes); } - - item = PyTuple_GET_ITEM(type_tup, 0); - - if (item == Py_None) { - PyErr_SetString(PyExc_ValueError, - "require data type in the type tuple"); - return -1; - } - else if (!PyArray_DescrConverter(item, &dtype)) { - return -1; - } - - out_dtypes[0] = ensure_dtype_nbo(dtype); - Py_DECREF(dtype); - if (out_dtypes[0] == NULL) { - return -1; + else if (descr->type_num == PyArray_DESCR(operands[0])->type_num) { + /* Prefer the input descriptor if it matches (preserve metadata) */ + descr = PyArray_DESCR(operands[0]); } + out_dtypes[0] = ensure_dtype_nbo(descr); } /* All types are the same - copy the first one to the rest */ @@ -2057,8 +2078,7 @@ type_tuple_type_resolver(PyUFuncObject *self, int any_object, PyArray_Descr **out_dtype) { - npy_intp i, j, n, nin = self->nin, nop = nin + self->nout; - int n_specified = 0; + int i, j, nin = self->nin, nop = nin + self->nout; int specified_types[NPY_MAXARGS], types[NPY_MAXARGS]; const char *ufunc_name; int no_castable_output = 0, use_min_scalar; @@ -2071,105 +2091,45 @@ type_tuple_type_resolver(PyUFuncObject *self, use_min_scalar = should_use_min_scalar(nin, op, 0, NULL); /* Fill in specified_types from the tuple or string */ - if (PyTuple_Check(type_tup)) { - int nonecount = 0; - n = PyTuple_GET_SIZE(type_tup); - if (n != 1 && n != nop) { - PyErr_Format(PyExc_ValueError, - "a type-tuple must be specified " - "of length 1 or %d for ufunc '%s'", (int)nop, - ufunc_get_name_cstr(self)); + const char *bad_type_tup_msg = ( + "Only NumPy must call `ufunc->type_resolver()` explicitly. " + "NumPy ensures that a type-tuple is normalized now to be a tuple " + "only containing None or descriptors. If anything else is passed " + "(you are seeing this message), the `type_resolver()` was called " + "directly by a third party. " + "This is unexpected, please inform the NumPy developers about it. " + "Also note that `type_resolver` will be phased out, since it must " + "be replaced."); + + if (PyTuple_CheckExact(type_tup)) { + Py_ssize_t n = PyTuple_GET_SIZE(type_tup); + if (n != nop) { + PyErr_SetString(PyExc_RuntimeError, bad_type_tup_msg); return -1; } - - for (i = 0; i < n; ++i) { + for (i = 0; i < nop; ++i) { PyObject *item = PyTuple_GET_ITEM(type_tup, i); if (item == Py_None) { specified_types[i] = NPY_NOTYPE; - ++nonecount; } else { - PyArray_Descr *dtype = NULL; - if (!PyArray_DescrConverter(item, &dtype)) { + if (!PyArray_DescrCheck(item)) { + PyErr_SetString(PyExc_RuntimeError, bad_type_tup_msg); return -1; } - specified_types[i] = dtype->type_num; - Py_DECREF(dtype); + specified_types[i] = ((PyArray_Descr *)item)->type_num; } } - - if (nonecount == n) { - PyErr_SetString(PyExc_ValueError, - "the type-tuple provided to the ufunc " - "must specify at least one none-None dtype"); - return -1; - } - - n_specified = n; } - else if (PyBytes_Check(type_tup) || PyUnicode_Check(type_tup)) { - Py_ssize_t length; - char *str; - PyObject *str_obj = NULL; - - if (PyUnicode_Check(type_tup)) { - str_obj = PyUnicode_AsASCIIString(type_tup); - if (str_obj == NULL) { - return -1; - } - type_tup = str_obj; - } - - if (PyBytes_AsStringAndSize(type_tup, &str, &length) < 0) { - Py_XDECREF(str_obj); - return -1; - } - if (length != 1 && (length != nop + 2 || - str[nin] != '-' || str[nin+1] != '>')) { - PyErr_Format(PyExc_ValueError, - "a type-string for %s, " \ - "requires 1 typecode, or " - "%d typecode(s) before " \ - "and %d after the -> sign", - ufunc_get_name_cstr(self), - self->nin, self->nout); - Py_XDECREF(str_obj); - return -1; - } - if (length == 1) { - PyArray_Descr *dtype; - n_specified = 1; - dtype = PyArray_DescrFromType(str[0]); - if (dtype == NULL) { - Py_XDECREF(str_obj); - return -1; - } - specified_types[0] = dtype->type_num; - Py_DECREF(dtype); - } - else { - PyArray_Descr *dtype; - n_specified = (int)nop; - - for (i = 0; i < nop; ++i) { - npy_intp istr = i < nin ? i : i+2; - - dtype = PyArray_DescrFromType(str[istr]); - if (dtype == NULL) { - Py_XDECREF(str_obj); - return -1; - } - specified_types[i] = dtype->type_num; - Py_DECREF(dtype); - } - } - Py_XDECREF(str_obj); + else { + PyErr_SetString(PyExc_RuntimeError, bad_type_tup_msg); + return -1; } /* If the ufunc has userloops, search for them. */ if (self->userloops) { switch (type_tuple_userloop_type_resolver(self, - n_specified, specified_types, + nop, specified_types, op, casting, any_object, use_min_scalar, out_dtype)) { @@ -2190,19 +2150,13 @@ type_tuple_type_resolver(PyUFuncObject *self, types[j] = orig_types[j]; } - if (n_specified == nop) { - for (j = 0; j < nop; ++j) { - if (types[j] != specified_types[j] && - specified_types[j] != NPY_NOTYPE) { - break; - } - } - if (j < nop) { - /* no match */ - continue; + for (j = 0; j < nop; ++j) { + if (types[j] != specified_types[j] && + specified_types[j] != NPY_NOTYPE) { + break; } } - else if (types[nin] != specified_types[0]) { + if (j < nop) { /* no match */ continue; } diff --git a/numpy/core/tests/test_deprecations.py b/numpy/core/tests/test_deprecations.py index 4d840ec1a..ec4112e69 100644 --- a/numpy/core/tests/test_deprecations.py +++ b/numpy/core/tests/test_deprecations.py @@ -1141,3 +1141,37 @@ class TestStringPromotion(_DeprecationTestCase): # np.equal uses a different type resolver: with pytest.raises(TypeError): self.assert_not_deprecated(lambda: np.equal(arr1, arr2)) + + +class TestSingleElementSignature(_DeprecationTestCase): + # Deprecated 2021-04-01, NumPy 1.21 + message = r"The use of a length 1" + + def test_deprecated(self): + self.assert_deprecated(lambda: np.add(1, 2, signature="d")) + self.assert_deprecated(lambda: np.add(1, 2, sig=(np.dtype("l"),))) + + +class TestComparisonBadDType(_DeprecationTestCase): + # Deprecated 2021-04-01, NumPy 1.21 + message = r"using `dtype=` in comparisons is only useful for" + + def test_deprecated(self): + self.assert_deprecated(lambda: np.equal(1, 1, dtype=np.int64)) + # Not an error only for the transition + self.assert_deprecated(lambda: np.equal(1, 1, sig=(None, None, "l"))) + + def test_not_deprecated(self): + np.equal(True, False, dtype=bool) + np.equal(3, 5, dtype=bool, casting="unsafe") + np.equal([None], [4], dtype=object) + +class TestComparisonBadObjectDType(_DeprecationTestCase): + # Deprecated 2021-04-01, NumPy 1.21 (different branch of the above one) + message = r"using `dtype=object` \(or equivalent signature\) will" + warning_cls = FutureWarning + + def test_deprecated(self): + self.assert_deprecated(lambda: np.equal(1, 1, dtype=object)) + self.assert_deprecated( + lambda: np.equal(1, 1, sig=(None, None, object))) diff --git a/numpy/core/tests/test_ufunc.py b/numpy/core/tests/test_ufunc.py index 96bfe7c33..7b71a4a65 100644 --- a/numpy/core/tests/test_ufunc.py +++ b/numpy/core/tests/test_ufunc.py @@ -410,9 +410,12 @@ class TestUfunc: def test_forced_sig(self): a = 0.5*np.arange(3, dtype='f8') assert_equal(np.add(a, 0.5), [0.5, 1, 1.5]) - assert_equal(np.add(a, 0.5, sig='i', casting='unsafe'), [0, 0, 1]) + with pytest.warns(DeprecationWarning): + assert_equal(np.add(a, 0.5, sig='i', casting='unsafe'), [0, 0, 1]) assert_equal(np.add(a, 0.5, sig='ii->i', casting='unsafe'), [0, 0, 1]) - assert_equal(np.add(a, 0.5, sig=('i4',), casting='unsafe'), [0, 0, 1]) + with pytest.warns(DeprecationWarning): + assert_equal(np.add(a, 0.5, sig=('i4',), casting='unsafe'), + [0, 0, 1]) assert_equal(np.add(a, 0.5, sig=('i4', 'i4', 'i4'), casting='unsafe'), [0, 0, 1]) @@ -420,18 +423,58 @@ class TestUfunc: np.add(a, 0.5, out=b) assert_equal(b, [0.5, 1, 1.5]) b[:] = 0 - np.add(a, 0.5, sig='i', out=b, casting='unsafe') + with pytest.warns(DeprecationWarning): + np.add(a, 0.5, sig='i', out=b, casting='unsafe') assert_equal(b, [0, 0, 1]) b[:] = 0 np.add(a, 0.5, sig='ii->i', out=b, casting='unsafe') assert_equal(b, [0, 0, 1]) b[:] = 0 - np.add(a, 0.5, sig=('i4',), out=b, casting='unsafe') + with pytest.warns(DeprecationWarning): + np.add(a, 0.5, sig=('i4',), out=b, casting='unsafe') assert_equal(b, [0, 0, 1]) b[:] = 0 np.add(a, 0.5, sig=('i4', 'i4', 'i4'), out=b, casting='unsafe') assert_equal(b, [0, 0, 1]) + def test_forced_dtype_times(self): + # Signatures only set the type numbers (not the actual loop dtypes) + # so using `M` in a signature/dtype should generally work: + a = np.array(['2010-01-02', '1999-03-14', '1833-03'], dtype='>M8[D]') + np.maximum(a, a, dtype="M") + np.maximum.reduce(a, dtype="M") + + arr = np.arange(10, dtype="m8[s]") + np.add(arr, arr, dtype="m") + np.maximum(arr, arr, dtype="m") + + def test_forced_dtype_warning(self): + # does not warn (test relies on bad pickling behaviour, simply remove + # it if the `assert int64 is not int64_2` should start failing. + int64 = np.dtype("int64") + int64_2 = pickle.loads(pickle.dumps(int64)) + assert int64 is not int64_2 + np.add(3, 4, dtype=int64_2) + + arr = np.arange(10, dtype="m8[s]") + with pytest.warns(UserWarning, + match="The `dtype` and `signature` arguments to") as rec: + np.add(3, 5, dtype=int64.newbyteorder()) + np.add(3, 5, dtype="m8[ns]") # previously used the "ns" + np.add(arr, arr, dtype="m8[ns]") # never preserved the "ns" + np.maximum(arr, arr, dtype="m8[ns]") # previously used the "ns" + np.maximum.reduce(arr, dtype="m8[ns]") # never preserved the "ns" + + assert len(rec) == 5 # each of the above call should cause one + + # Also check the error paths: + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + with pytest.raises(UserWarning): + np.add(3, 5, dtype="m8[ns]") + with pytest.raises(UserWarning): + np.maximum.reduce(arr, dtype="m8[ns]") + def test_true_divide(self): a = np.array(10) b = np.array(20) diff --git a/numpy/typing/tests/data/pass/ufuncs.py b/numpy/typing/tests/data/pass/ufuncs.py index ad4d483d4..3c93fb2cf 100644 --- a/numpy/typing/tests/data/pass/ufuncs.py +++ b/numpy/typing/tests/data/pass/ufuncs.py @@ -4,7 +4,7 @@ np.sin(1) np.sin([1, 2, 3]) np.sin(1, out=np.empty(1)) np.matmul(np.ones((2, 2, 2)), np.ones((2, 2, 2)), axes=[(0, 1), (0, 1), (0, 1)]) -np.sin(1, signature="D") +np.sin(1, signature="D->D") np.sin(1, extobj=[16, 1, lambda: None]) # NOTE: `np.generic` subclasses are not guaranteed to support addition; # re-enable this we can infer the exact return type of `np.sin(...)`. |
