summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian Berg <sebastian@sipsolutions.net>2021-04-01 18:09:28 -0500
committerSebastian Berg <sebastian@sipsolutions.net>2021-04-08 12:25:10 -0500
commit5e310e94de468359332ea5366d6555df5bc85231 (patch)
tree36a18b62c521896a0253a47864fcd47cb46106f9
parent0fe69ae3fc513aefd32a58ae8ccac294b12300d2 (diff)
downloadnumpy-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.rst13
-rw-r--r--doc/release/upcoming_changes/18718.compatibility.rst53
-rw-r--r--numpy/core/src/umath/ufunc_object.c328
-rw-r--r--numpy/core/src/umath/ufunc_type_resolution.c224
-rw-r--r--numpy/core/tests/test_deprecations.py34
-rw-r--r--numpy/core/tests/test_ufunc.py51
-rw-r--r--numpy/typing/tests/data/pass/ufuncs.py2
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(...)`.