summaryrefslogtreecommitdiff
path: root/numpy/core/src/multiarray
diff options
context:
space:
mode:
authorCharles Harris <charlesr.harris@gmail.com>2022-06-26 12:52:52 -0600
committerGitHub <noreply@github.com>2022-06-26 12:52:52 -0600
commitb65f0b7b8ba7e80b65773e06aae22a8369678868 (patch)
tree596a6389b54c72b2ca5a9a66d974ef0ab5b2a4af /numpy/core/src/multiarray
parent11e465e30823f044887d3f5e2f648007ce0077c2 (diff)
parent7dcfaafa0b78618a6ec2a5279090729f7ec583f0 (diff)
downloadnumpy-b65f0b7b8ba7e80b65773e06aae22a8369678868.tar.gz
Merge pull request #21626 from seberg/weak-scalars
API: Introduce optional (and partial) NEP 50 weak scalar logic
Diffstat (limited to 'numpy/core/src/multiarray')
-rw-r--r--numpy/core/src/multiarray/abstractdtypes.c71
-rw-r--r--numpy/core/src/multiarray/abstractdtypes.h53
-rw-r--r--numpy/core/src/multiarray/arrayobject.h16
-rw-r--r--numpy/core/src/multiarray/convert_datatype.c383
-rw-r--r--numpy/core/src/multiarray/convert_datatype.h27
-rw-r--r--numpy/core/src/multiarray/multiarraymodule.c17
6 files changed, 445 insertions, 122 deletions
diff --git a/numpy/core/src/multiarray/abstractdtypes.c b/numpy/core/src/multiarray/abstractdtypes.c
index b0345c46b..3e89d045e 100644
--- a/numpy/core/src/multiarray/abstractdtypes.c
+++ b/numpy/core/src/multiarray/abstractdtypes.c
@@ -164,7 +164,39 @@ int_common_dtype(PyArray_DTypeMeta *NPY_UNUSED(cls), PyArray_DTypeMeta *other)
}
else if (NPY_DT_is_legacy(other)) {
/* This is a back-compat fallback to usually do the right thing... */
- return PyArray_DTypeFromTypeNum(NPY_UINT8);
+ PyArray_DTypeMeta *uint8_dt = PyArray_DTypeFromTypeNum(NPY_UINT8);
+ PyArray_DTypeMeta *res = NPY_DT_CALL_common_dtype(other, uint8_dt);
+ Py_DECREF(uint8_dt);
+ if (res == NULL) {
+ PyErr_Clear();
+ }
+ else if (res == (PyArray_DTypeMeta *)Py_NotImplemented) {
+ Py_DECREF(res);
+ }
+ else {
+ return res;
+ }
+ /* Try again with `int8`, an error may have been set, though */
+ PyArray_DTypeMeta *int8_dt = PyArray_DTypeFromTypeNum(NPY_INT8);
+ res = NPY_DT_CALL_common_dtype(other, int8_dt);
+ Py_DECREF(int8_dt);
+ if (res == NULL) {
+ PyErr_Clear();
+ }
+ else if (res == (PyArray_DTypeMeta *)Py_NotImplemented) {
+ Py_DECREF(res);
+ }
+ else {
+ return res;
+ }
+ /* And finally, we will try the default integer, just for sports... */
+ PyArray_DTypeMeta *default_int = PyArray_DTypeFromTypeNum(NPY_LONG);
+ res = NPY_DT_CALL_common_dtype(other, default_int);
+ Py_DECREF(default_int);
+ if (res == NULL) {
+ PyErr_Clear();
+ }
+ return res;
}
Py_INCREF(Py_NotImplemented);
return (PyArray_DTypeMeta *)Py_NotImplemented;
@@ -191,7 +223,23 @@ float_common_dtype(PyArray_DTypeMeta *cls, PyArray_DTypeMeta *other)
}
else if (NPY_DT_is_legacy(other)) {
/* This is a back-compat fallback to usually do the right thing... */
- return PyArray_DTypeFromTypeNum(NPY_HALF);
+ PyArray_DTypeMeta *half_dt = PyArray_DTypeFromTypeNum(NPY_HALF);
+ PyArray_DTypeMeta *res = NPY_DT_CALL_common_dtype(other, half_dt);
+ Py_DECREF(half_dt);
+ if (res == NULL) {
+ PyErr_Clear();
+ }
+ else if (res == (PyArray_DTypeMeta *)Py_NotImplemented) {
+ Py_DECREF(res);
+ }
+ else {
+ return res;
+ }
+ /* Retry with double (the default float) */
+ PyArray_DTypeMeta *double_dt = PyArray_DTypeFromTypeNum(NPY_DOUBLE);
+ res = NPY_DT_CALL_common_dtype(other, double_dt);
+ Py_DECREF(double_dt);
+ return res;
}
Py_INCREF(Py_NotImplemented);
return (PyArray_DTypeMeta *)Py_NotImplemented;
@@ -229,7 +277,24 @@ complex_common_dtype(PyArray_DTypeMeta *cls, PyArray_DTypeMeta *other)
}
else if (NPY_DT_is_legacy(other)) {
/* This is a back-compat fallback to usually do the right thing... */
- return PyArray_DTypeFromTypeNum(NPY_CFLOAT);
+ PyArray_DTypeMeta *cfloat_dt = PyArray_DTypeFromTypeNum(NPY_CFLOAT);
+ PyArray_DTypeMeta *res = NPY_DT_CALL_common_dtype(other, cfloat_dt);
+ Py_DECREF(cfloat_dt);
+ if (res == NULL) {
+ PyErr_Clear();
+ }
+ else if (res == (PyArray_DTypeMeta *)Py_NotImplemented) {
+ Py_DECREF(res);
+ }
+ else {
+ return res;
+ }
+ /* Retry with cdouble (the default complex) */
+ PyArray_DTypeMeta *cdouble_dt = PyArray_DTypeFromTypeNum(NPY_CDOUBLE);
+ res = NPY_DT_CALL_common_dtype(other, cdouble_dt);
+ Py_DECREF(cdouble_dt);
+ return res;
+
}
else if (other == &PyArray_PyIntAbstractDType ||
other == &PyArray_PyFloatAbstractDType) {
diff --git a/numpy/core/src/multiarray/abstractdtypes.h b/numpy/core/src/multiarray/abstractdtypes.h
index 42c192cac..6901ec213 100644
--- a/numpy/core/src/multiarray/abstractdtypes.h
+++ b/numpy/core/src/multiarray/abstractdtypes.h
@@ -1,6 +1,7 @@
#ifndef NUMPY_CORE_SRC_MULTIARRAY_ABSTRACTDTYPES_H_
#define NUMPY_CORE_SRC_MULTIARRAY_ABSTRACTDTYPES_H_
+#include "arrayobject.h"
#include "dtypemeta.h"
@@ -16,4 +17,56 @@ NPY_NO_EXPORT extern PyArray_DTypeMeta PyArray_PyComplexAbstractDType;
NPY_NO_EXPORT int
initialize_and_map_pytypes_to_dtypes(void);
+
+/*
+ * When we get a Python int, float, or complex, we may have to use weak
+ * promotion logic.
+ * To implement this, we sometimes have to tag the converted (temporary)
+ * array when the original object was a Python scalar.
+ *
+ * @param obj The original Python object.
+ * @param arr The array into which the Python object was converted.
+ * @param[in,out] **dtype A pointer to the array's DType, if not NULL it will be
+ * replaced with the abstract DType.
+ * @return 0 if the `obj` was not a python scalar, and 1 if it was.
+ */
+static NPY_INLINE int
+npy_mark_tmp_array_if_pyscalar(
+ PyObject *obj, PyArrayObject *arr, PyArray_DTypeMeta **dtype)
+{
+ /*
+ * We check the array dtype for two reasons: First, booleans are
+ * integer subclasses. Second, an int, float, or complex could have
+ * a custom DType registered, and then we should use that.
+ * Further, `np.float64` is a double subclass, so must reject it.
+ */
+ if (PyLong_Check(obj)
+ && (PyArray_ISINTEGER(arr) || PyArray_ISOBJECT(arr))) {
+ ((PyArrayObject_fields *)arr)->flags |= NPY_ARRAY_WAS_PYTHON_INT;
+ if (dtype != NULL) {
+ Py_INCREF(&PyArray_PyIntAbstractDType);
+ Py_SETREF(*dtype, &PyArray_PyIntAbstractDType);
+ }
+ return 1;
+ }
+ else if (PyFloat_Check(obj) && !PyArray_IsScalar(obj, Double)
+ && PyArray_TYPE(arr) == NPY_DOUBLE) {
+ ((PyArrayObject_fields *)arr)->flags |= NPY_ARRAY_WAS_PYTHON_FLOAT;
+ if (dtype != NULL) {
+ Py_INCREF(&PyArray_PyFloatAbstractDType);
+ Py_SETREF(*dtype, &PyArray_PyFloatAbstractDType);
+ }
+ return 1;
+ }
+ else if (PyComplex_Check(obj) && PyArray_TYPE(arr) == NPY_CDOUBLE) {
+ ((PyArrayObject_fields *)arr)->flags |= NPY_ARRAY_WAS_PYTHON_COMPLEX;
+ if (dtype != NULL) {
+ Py_INCREF(&PyArray_PyComplexAbstractDType);
+ Py_SETREF(*dtype, &PyArray_PyComplexAbstractDType);
+ }
+ return 1;
+ }
+ return 0;
+}
+
#endif /* NUMPY_CORE_SRC_MULTIARRAY_ABSTRACTDTYPES_H_ */
diff --git a/numpy/core/src/multiarray/arrayobject.h b/numpy/core/src/multiarray/arrayobject.h
index fb9b0bd81..f7d0734db 100644
--- a/numpy/core/src/multiarray/arrayobject.h
+++ b/numpy/core/src/multiarray/arrayobject.h
@@ -26,4 +26,20 @@ array_might_be_written(PyArrayObject *obj);
*/
static const int NPY_ARRAY_WARN_ON_WRITE = (1 << 31);
+
+/*
+ * These flags are used internally to indicate an array that was previously
+ * a Python scalar (int, float, complex). The dtype of such an array should
+ * be considered as any integer, floating, or complex rather than the explicit
+ * dtype attached to the array.
+ *
+ * These flags must only be used in local context when the array in question
+ * is not returned. Use three flags, to avoid having to double check the
+ * actual dtype when the flags are used.
+ */
+static const int NPY_ARRAY_WAS_PYTHON_INT = (1 << 30);
+static const int NPY_ARRAY_WAS_PYTHON_FLOAT = (1 << 29);
+static const int NPY_ARRAY_WAS_PYTHON_COMPLEX = (1 << 28);
+static const int NPY_ARRAY_WAS_PYTHON_LITERAL = (1 << 30 | 1 << 29 | 1 << 28);
+
#endif /* NUMPY_CORE_SRC_MULTIARRAY_ARRAYOBJECT_H_ */
diff --git a/numpy/core/src/multiarray/convert_datatype.c b/numpy/core/src/multiarray/convert_datatype.c
index bc8a3bf88..c578a1b44 100644
--- a/numpy/core/src/multiarray/convert_datatype.c
+++ b/numpy/core/src/multiarray/convert_datatype.c
@@ -31,6 +31,7 @@
#include "array_method.h"
#include "usertypes.h"
#include "dtype_transfer.h"
+#include "arrayobject.h"
/*
@@ -44,6 +45,11 @@
*/
NPY_NO_EXPORT npy_intp REQUIRED_STR_LEN[] = {0, 3, 5, 10, 10, 20, 20, 20, 20};
+/*
+ * Whether or not legacy value-based promotion/casting is used.
+ */
+NPY_NO_EXPORT int npy_promotion_state = NPY_USE_LEGACY_PROMOTION;
+NPY_NO_EXPORT PyObject *NO_NEP50_WARNING_CTX = NULL;
static PyObject *
PyArray_GetGenericToVoidCastingImpl(void);
@@ -58,6 +64,77 @@ static PyObject *
PyArray_GetObjectToGenericCastingImpl(void);
+/*
+ * Return 1 if promotion warnings should be given and 0 if they are currently
+ * suppressed in the local context.
+ */
+NPY_NO_EXPORT int
+npy_give_promotion_warnings(void)
+{
+ PyObject *val;
+
+ npy_cache_import(
+ "numpy.core._ufunc_config", "NO_NEP50_WARNING",
+ &NO_NEP50_WARNING_CTX);
+ if (NO_NEP50_WARNING_CTX == NULL) {
+ PyErr_WriteUnraisable(NULL);
+ return 1;
+ }
+
+ if (PyContextVar_Get(NO_NEP50_WARNING_CTX, Py_False, &val) < 0) {
+ /* Errors should not really happen, but if it does assume we warn. */
+ PyErr_WriteUnraisable(NULL);
+ return 1;
+ }
+ Py_DECREF(val);
+ /* only when the no-warnings context is false, we give warnings */
+ return val == Py_False;
+}
+
+
+NPY_NO_EXPORT PyObject *
+npy__get_promotion_state(PyObject *NPY_UNUSED(mod), PyObject *NPY_UNUSED(arg)) {
+ if (npy_promotion_state == NPY_USE_WEAK_PROMOTION) {
+ return PyUnicode_FromString("weak");
+ }
+ else if (npy_promotion_state == NPY_USE_WEAK_PROMOTION_AND_WARN) {
+ return PyUnicode_FromString("weak_and_warn");
+ }
+ else if (npy_promotion_state == NPY_USE_LEGACY_PROMOTION) {
+ return PyUnicode_FromString("legacy");
+ }
+ PyErr_SetString(PyExc_SystemError, "invalid promotion state!");
+ return NULL;
+}
+
+
+NPY_NO_EXPORT PyObject *
+npy__set_promotion_state(PyObject *NPY_UNUSED(mod), PyObject *arg)
+{
+ if (!PyUnicode_Check(arg)) {
+ PyErr_SetString(PyExc_TypeError,
+ "_set_promotion_state() argument or NPY_PROMOTION_STATE "
+ "must be a string.");
+ return NULL;
+ }
+ if (PyUnicode_CompareWithASCIIString(arg, "weak") == 0) {
+ npy_promotion_state = NPY_USE_WEAK_PROMOTION;
+ }
+ else if (PyUnicode_CompareWithASCIIString(arg, "weak_and_warn") == 0) {
+ npy_promotion_state = NPY_USE_WEAK_PROMOTION_AND_WARN;
+ }
+ else if (PyUnicode_CompareWithASCIIString(arg, "legacy") == 0) {
+ npy_promotion_state = NPY_USE_LEGACY_PROMOTION;
+ }
+ else {
+ PyErr_Format(PyExc_TypeError,
+ "_set_promotion_state() argument or NPY_PROMOTION_STATE must be "
+ "'weak', 'legacy', or 'weak_and_warn' but got '%.100S'", arg);
+ return NULL;
+ }
+ Py_RETURN_NONE;
+}
+
/**
* Fetch the casting implementation from one DType to another.
*
@@ -776,6 +853,55 @@ can_cast_scalar_to(PyArray_Descr *scal_type, char *scal_data,
return ret;
}
+
+NPY_NO_EXPORT npy_bool
+can_cast_pyscalar_scalar_to(
+ int flags, PyArray_Descr *to, NPY_CASTING casting)
+{
+ /*
+ * This function only works reliably for legacy (NumPy dtypes).
+ * If we end up here for a non-legacy DType, it is a bug.
+ */
+ assert(NPY_DT_is_legacy(NPY_DTYPE(to)));
+
+ /*
+ * Quickly check for the typical numeric cases, where the casting rules
+ * can be hardcoded fairly easily.
+ */
+ if (PyDataType_ISCOMPLEX(to)) {
+ return 1;
+ }
+ else if (PyDataType_ISFLOAT(to)) {
+ if (flags & NPY_ARRAY_WAS_PYTHON_COMPLEX) {
+ return casting == NPY_UNSAFE_CASTING;
+ }
+ return 1;
+ }
+ else if (PyDataType_ISINTEGER(to)) {
+ if (!(flags & NPY_ARRAY_WAS_PYTHON_INT)) {
+ return casting == NPY_UNSAFE_CASTING;
+ }
+ return 1;
+ }
+
+ /*
+ * For all other cases we use the default dtype.
+ */
+ PyArray_Descr *from;
+ if (flags & NPY_ARRAY_WAS_PYTHON_INT) {
+ from = PyArray_DescrFromType(NPY_LONG);
+ }
+ else if (flags & NPY_ARRAY_WAS_PYTHON_FLOAT) {
+ from = PyArray_DescrFromType(NPY_DOUBLE);
+ }
+ else {
+ from = PyArray_DescrFromType(NPY_CDOUBLE);
+ }
+ int res = PyArray_CanCastTypeTo(from, to, casting);
+ Py_DECREF(from);
+ return res;
+}
+
/*NUMPY_API
* Returns 1 if the array object may be cast to the given data type using
* the casting rule, 0 otherwise. This differs from PyArray_CanCastTo in
@@ -794,12 +920,25 @@ PyArray_CanCastArrayTo(PyArrayObject *arr, PyArray_Descr *to,
to = NULL;
}
- /*
- * If it's a scalar, check the value. (This only currently matters for
- * numeric types and for `to == NULL` it can't be numeric.)
- */
- if (PyArray_NDIM(arr) == 0 && !PyArray_HASFIELDS(arr) && to != NULL) {
- return can_cast_scalar_to(from, PyArray_DATA(arr), to, casting);
+ if (npy_promotion_state == NPY_USE_LEGACY_PROMOTION) {
+ /*
+ * If it's a scalar, check the value. (This only currently matters for
+ * numeric types and for `to == NULL` it can't be numeric.)
+ */
+ if (PyArray_NDIM(arr) == 0 && !PyArray_HASFIELDS(arr) && to != NULL) {
+ return can_cast_scalar_to(from, PyArray_DATA(arr), to, casting);
+ }
+ }
+ else {
+ /*
+ * If it's a scalar, check the value. (This only currently matters for
+ * numeric types and for `to == NULL` it can't be numeric.)
+ */
+ if (PyArray_FLAGS(arr) & NPY_ARRAY_WAS_PYTHON_LITERAL && to != NULL) {
+ return can_cast_pyscalar_scalar_to(
+ PyArray_FLAGS(arr) & NPY_ARRAY_WAS_PYTHON_LITERAL, to,
+ casting);
+ }
}
/* Otherwise, use the standard rules (same as `PyArray_CanCastTypeTo`) */
@@ -1561,40 +1700,44 @@ should_use_min_scalar(npy_intp narrs, PyArrayObject **arr,
}
-/*
- * Utility function used only in PyArray_ResultType for value-based logic.
- * See that function for the meaning and contents of the parameters.
- */
-static PyArray_Descr *
-get_descr_from_cast_or_value(
- npy_intp i,
- PyArrayObject *arrs[],
- npy_intp ndtypes,
- PyArray_Descr *descriptor,
- PyArray_DTypeMeta *common_dtype)
-{
- PyArray_Descr *curr;
- if (NPY_LIKELY(i < ndtypes ||
- !(PyArray_FLAGS(arrs[i-ndtypes]) & _NPY_ARRAY_WAS_PYSCALAR))) {
- curr = PyArray_CastDescrToDType(descriptor, common_dtype);
- }
- else {
- /*
- * Unlike `PyArray_CastToDTypeAndPromoteDescriptors`, deal with
- * plain Python values "graciously". This recovers the original
- * value the long route, but it should almost never happen...
- */
- PyObject *tmp = PyArray_GETITEM(arrs[i-ndtypes],
- PyArray_BYTES(arrs[i-ndtypes]));
- if (tmp == NULL) {
- return NULL;
+NPY_NO_EXPORT int
+should_use_min_scalar_weak_literals(int narrs, PyArrayObject **arr) {
+ int all_scalars = 1;
+ int max_scalar_kind = -1;
+ int max_array_kind = -1;
+
+ for (int i = 0; i < narrs; i++) {
+ if (PyArray_FLAGS(arr[i]) & NPY_ARRAY_WAS_PYTHON_INT) {
+ /* A Python integer could be `u` so is effectively that: */
+ int new = dtype_kind_to_simplified_ordering('u');
+ if (new > max_scalar_kind) {
+ max_scalar_kind = new;
+ }
}
- curr = NPY_DT_CALL_discover_descr_from_pyobject(common_dtype, tmp);
- Py_DECREF(tmp);
+ /* For the new logic, only complex or not matters: */
+ else if (PyArray_FLAGS(arr[i]) & NPY_ARRAY_WAS_PYTHON_FLOAT) {
+ max_scalar_kind = dtype_kind_to_simplified_ordering('f');
+ }
+ else if (PyArray_FLAGS(arr[i]) & NPY_ARRAY_WAS_PYTHON_COMPLEX) {
+ max_scalar_kind = dtype_kind_to_simplified_ordering('f');
+ }
+ else {
+ all_scalars = 0;
+ int kind = dtype_kind_to_simplified_ordering(
+ PyArray_DESCR(arr[i])->kind);
+ if (kind > max_array_kind) {
+ max_array_kind = kind;
+ }
+ }
+ }
+ if (!all_scalars && max_array_kind >= max_scalar_kind) {
+ return 1;
}
- return curr;
+
+ return 0;
}
+
/*NUMPY_API
*
* Produces the result type of a bunch of inputs, using the same rules
@@ -1667,30 +1810,13 @@ PyArray_ResultType(
at_least_one_scalar = 1;
}
- if (!(PyArray_FLAGS(arrs[i]) & _NPY_ARRAY_WAS_PYSCALAR)) {
- /* This was not a scalar with an abstract DType */
- all_descriptors[i_all] = PyArray_DTYPE(arrs[i]);
- all_DTypes[i_all] = NPY_DTYPE(all_descriptors[i_all]);
- Py_INCREF(all_DTypes[i_all]);
- all_pyscalar = 0;
- continue;
- }
-
/*
- * The original was a Python scalar with an abstract DType.
- * In a future world, this type of code may need to work on the
- * DType level first and discover those from the original value.
- * But, right now we limit the logic to int, float, and complex
- * and do it here to allow for a transition without losing all of
- * our remaining sanity.
+ * If the original was a Python scalar/literal, we use only the
+ * corresponding abstract DType (and no descriptor) below.
+ * Otherwise, we propagate the descriptor as well.
*/
- if (PyArray_ISFLOAT(arrs[i])) {
- all_DTypes[i_all] = &PyArray_PyFloatAbstractDType;
- }
- else if (PyArray_ISCOMPLEX(arrs[i])) {
- all_DTypes[i_all] = &PyArray_PyComplexAbstractDType;
- }
- else {
+ all_descriptors[i_all] = NULL; /* no descriptor for py-scalars */
+ if (PyArray_FLAGS(arrs[i]) & NPY_ARRAY_WAS_PYTHON_INT) {
/* This could even be an object dtype here for large ints */
all_DTypes[i_all] = &PyArray_PyIntAbstractDType;
if (PyArray_TYPE(arrs[i]) != NPY_LONG) {
@@ -1698,12 +1824,18 @@ PyArray_ResultType(
all_pyscalar = 0;
}
}
+ else if (PyArray_FLAGS(arrs[i]) & NPY_ARRAY_WAS_PYTHON_FLOAT) {
+ all_DTypes[i_all] = &PyArray_PyFloatAbstractDType;
+ }
+ else if (PyArray_FLAGS(arrs[i]) & NPY_ARRAY_WAS_PYTHON_COMPLEX) {
+ all_DTypes[i_all] = &PyArray_PyComplexAbstractDType;
+ }
+ else {
+ all_descriptors[i_all] = PyArray_DTYPE(arrs[i]);
+ all_DTypes[i_all] = NPY_DTYPE(all_descriptors[i_all]);
+ all_pyscalar = 0;
+ }
Py_INCREF(all_DTypes[i_all]);
- /*
- * Leave the descriptor empty, if we need it, we will have to go
- * to more extreme lengths unfortunately.
- */
- all_descriptors[i_all] = NULL;
}
PyArray_DTypeMeta *common_dtype = PyArray_PromoteDTypeSequence(
@@ -1730,23 +1862,20 @@ PyArray_ResultType(
* NOTE: Code duplicates `PyArray_CastToDTypeAndPromoteDescriptors`, but
* supports special handling of the abstract values.
*/
- if (!NPY_DT_is_parametric(common_dtype)) {
- /* Note that this "fast" path loses all metadata */
- result = NPY_DT_CALL_default_descr(common_dtype);
- }
- else {
- result = get_descr_from_cast_or_value(
- 0, arrs, ndtypes, all_descriptors[0], common_dtype);
- if (result == NULL) {
- goto error;
- }
-
- for (npy_intp i = 1; i < ndtypes+narrs; i++) {
- PyArray_Descr *curr = get_descr_from_cast_or_value(
- i, arrs, ndtypes, all_descriptors[i], common_dtype);
+ if (NPY_DT_is_parametric(common_dtype)) {
+ for (npy_intp i = 0; i < ndtypes+narrs; i++) {
+ if (all_descriptors[i] == NULL) {
+ continue; /* originally a python scalar/literal */
+ }
+ PyArray_Descr *curr = PyArray_CastDescrToDType(
+ all_descriptors[i], common_dtype);
if (curr == NULL) {
goto error;
}
+ if (result == NULL) {
+ result = curr;
+ continue;
+ }
Py_SETREF(result, NPY_DT_SLOTS(common_dtype)->common_instance(result, curr));
Py_DECREF(curr);
if (result == NULL) {
@@ -1754,27 +1883,33 @@ PyArray_ResultType(
}
}
}
+ if (result == NULL) {
+ /*
+ * If the DType is not parametric, or all were weak scalars,
+ * a result may not yet be set.
+ */
+ result = NPY_DT_CALL_default_descr(common_dtype);
+ if (result == NULL) {
+ goto error;
+ }
+ }
/*
- * Unfortunately, when 0-D "scalar" arrays are involved and mixed, we
- * have to use the value-based logic. The intention is to move away from
- * the complex logic arising from it. We thus fall back to the legacy
- * version here.
- * It may be possible to micro-optimize this to skip some of the above
- * logic when this path is necessary.
+ * Unfortunately, when 0-D "scalar" arrays are involved and mixed, we *may*
+ * have to use the value-based logic.
+ * `PyArray_CheckLegacyResultType` may behave differently based on the
+ * current value of `npy_legacy_promotion`:
+ * 1. It does nothing (we use the "new" behavior)
+ * 2. It does nothing, but warns if there the result would differ.
+ * 3. It replaces the result based on the legacy value-based logic.
*/
if (at_least_one_scalar && !all_pyscalar && result->type_num < NPY_NTYPES) {
- PyArray_Descr *legacy_result = PyArray_LegacyResultType(
- narrs, arrs, ndtypes, descrs);
- if (legacy_result == NULL) {
- /*
- * Going from error to success should not really happen, but is
- * probably OK if it does.
- */
- goto error;
+ if (PyArray_CheckLegacyResultType(
+ &result, narrs, arrs, ndtypes, descrs) < 0) {
+ Py_DECREF(common_dtype);
+ Py_DECREF(result);
+ return NULL;
}
- /* Return the old "legacy" result (could warn here if different) */
- Py_SETREF(result, legacy_result);
}
Py_DECREF(common_dtype);
@@ -1802,38 +1937,39 @@ PyArray_ResultType(
* of all the inputs. Data types passed directly are treated as array
* types.
*/
-NPY_NO_EXPORT PyArray_Descr *
-PyArray_LegacyResultType(
+NPY_NO_EXPORT int
+PyArray_CheckLegacyResultType(
+ PyArray_Descr **new_result,
npy_intp narrs, PyArrayObject **arr,
npy_intp ndtypes, PyArray_Descr **dtypes)
{
+ PyArray_Descr *ret = NULL;
+ if (npy_promotion_state == NPY_USE_WEAK_PROMOTION) {
+ return 0;
+ }
+ if (npy_promotion_state == NPY_USE_WEAK_PROMOTION_AND_WARN
+ && !npy_give_promotion_warnings()) {
+ return 0;
+ }
+
npy_intp i;
- /* If there's just one type, pass it through */
+ /* If there's just one type, results must match */
if (narrs + ndtypes == 1) {
- PyArray_Descr *ret = NULL;
- if (narrs == 1) {
- ret = PyArray_DESCR(arr[0]);
- }
- else {
- ret = dtypes[0];
- }
- Py_INCREF(ret);
- return ret;
+ return 0;
}
int use_min_scalar = should_use_min_scalar(narrs, arr, ndtypes, dtypes);
/* Loop through all the types, promoting them */
if (!use_min_scalar) {
- PyArray_Descr *ret;
/* Build a single array of all the dtypes */
PyArray_Descr **all_dtypes = PyArray_malloc(
sizeof(*all_dtypes) * (narrs + ndtypes));
if (all_dtypes == NULL) {
PyErr_NoMemory();
- return NULL;
+ return -1;
}
for (i = 0; i < narrs; ++i) {
all_dtypes[i] = PyArray_DESCR(arr[i]);
@@ -1843,11 +1979,9 @@ PyArray_LegacyResultType(
}
ret = PyArray_PromoteTypeSequence(all_dtypes, narrs + ndtypes);
PyArray_free(all_dtypes);
- return ret;
}
else {
int ret_is_small_unsigned = 0;
- PyArray_Descr *ret = NULL;
for (i = 0; i < narrs; ++i) {
int tmp_is_small_unsigned;
@@ -1855,7 +1989,7 @@ PyArray_LegacyResultType(
arr[i], &tmp_is_small_unsigned);
if (tmp == NULL) {
Py_XDECREF(ret);
- return NULL;
+ return -1;
}
/* Combine it with the existing type */
if (ret == NULL) {
@@ -1869,7 +2003,7 @@ PyArray_LegacyResultType(
Py_DECREF(ret);
ret = tmpret;
if (ret == NULL) {
- return NULL;
+ return -1;
}
ret_is_small_unsigned = tmp_is_small_unsigned &&
@@ -1890,7 +2024,7 @@ PyArray_LegacyResultType(
Py_DECREF(ret);
ret = tmpret;
if (ret == NULL) {
- return NULL;
+ return -1;
}
}
}
@@ -1899,9 +2033,32 @@ PyArray_LegacyResultType(
PyErr_SetString(PyExc_TypeError,
"no arrays or types available to calculate result type");
}
+ }
+
+ if (ret == NULL) {
+ return -1;
+ }
- return ret;
+ int unchanged_result = PyArray_EquivTypes(*new_result, ret);
+ if (unchanged_result) {
+ Py_DECREF(ret);
+ return 0;
}
+ if (npy_promotion_state == NPY_USE_LEGACY_PROMOTION) {
+ Py_SETREF(*new_result, ret);
+ return 0;
+ }
+
+ assert(npy_promotion_state == NPY_USE_WEAK_PROMOTION_AND_WARN);
+ if (PyErr_WarnFormat(PyExc_UserWarning, 1,
+ "result dtype changed due to the removal of value-based "
+ "promotion from NumPy. Changed from %S to %S.",
+ ret, *new_result) < 0) {
+ Py_DECREF(ret);
+ return -1;
+ }
+ Py_DECREF(ret);
+ return 0;
}
/**
diff --git a/numpy/core/src/multiarray/convert_datatype.h b/numpy/core/src/multiarray/convert_datatype.h
index af6d790cf..b6bc7d8a7 100644
--- a/numpy/core/src/multiarray/convert_datatype.h
+++ b/numpy/core/src/multiarray/convert_datatype.h
@@ -9,6 +9,21 @@ extern "C" {
extern NPY_NO_EXPORT npy_intp REQUIRED_STR_LEN[];
+#define NPY_USE_LEGACY_PROMOTION 0
+#define NPY_USE_WEAK_PROMOTION 1
+#define NPY_USE_WEAK_PROMOTION_AND_WARN 2
+extern NPY_NO_EXPORT int npy_promotion_state;
+extern NPY_NO_EXPORT PyObject *NO_NEP50_WARNING_CTX;
+
+NPY_NO_EXPORT int
+npy_give_promotion_warnings(void);
+
+NPY_NO_EXPORT PyObject *
+npy__get_promotion_state(PyObject *NPY_UNUSED(mod), PyObject *NPY_UNUSED(arg));
+
+NPY_NO_EXPORT PyObject *
+npy__set_promotion_state(PyObject *NPY_UNUSED(mod), PyObject *arg);
+
NPY_NO_EXPORT PyObject *
PyArray_GetCastingImpl(PyArray_DTypeMeta *from, PyArray_DTypeMeta *to);
@@ -24,8 +39,9 @@ PyArray_ObjectType(PyObject *op, int minimum_type);
NPY_NO_EXPORT PyArrayObject **
PyArray_ConvertToCommonType(PyObject *op, int *retn);
-NPY_NO_EXPORT PyArray_Descr *
-PyArray_LegacyResultType(
+NPY_NO_EXPORT int
+PyArray_CheckLegacyResultType(
+ PyArray_Descr **new_result,
npy_intp narrs, PyArrayObject **arr,
npy_intp ndtypes, PyArray_Descr **dtypes);
@@ -40,10 +56,17 @@ NPY_NO_EXPORT npy_bool
can_cast_scalar_to(PyArray_Descr *scal_type, char *scal_data,
PyArray_Descr *to, NPY_CASTING casting);
+NPY_NO_EXPORT npy_bool
+can_cast_pyscalar_scalar_to(
+ int flags, PyArray_Descr *to, NPY_CASTING casting);
+
NPY_NO_EXPORT int
should_use_min_scalar(npy_intp narrs, PyArrayObject **arr,
npy_intp ndtypes, PyArray_Descr **dtypes);
+NPY_NO_EXPORT int
+should_use_min_scalar_weak_literals(int narrs, PyArrayObject **arr);
+
NPY_NO_EXPORT const char *
npy_casting_to_string(NPY_CASTING casting);
diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c
index 96d0c893d..16069d619 100644
--- a/numpy/core/src/multiarray/multiarraymodule.c
+++ b/numpy/core/src/multiarray/multiarraymodule.c
@@ -3537,10 +3537,11 @@ array_result_type(PyObject *NPY_UNUSED(dummy), PyObject *args)
if (arr[narr] == NULL) {
goto finish;
}
- if (PyLong_CheckExact(obj) || PyFloat_CheckExact(obj) ||
- PyComplex_CheckExact(obj)) {
- ((PyArrayObject_fields *)arr[narr])->flags |= _NPY_ARRAY_WAS_PYSCALAR;
- }
+ /*
+ * Mark array if it was a python scalar (we do not need the actual
+ * DType here yet, this is figured out inside ResultType.
+ */
+ npy_mark_tmp_array_if_pyscalar(obj, arr[narr], NULL);
++narr;
}
else {
@@ -4494,6 +4495,14 @@ static struct PyMethodDef array_module_methods[] = {
{"get_handler_version",
(PyCFunction) get_handler_version,
METH_VARARGS, NULL},
+ {"_get_promotion_state",
+ (PyCFunction)npy__get_promotion_state,
+ METH_NOARGS, "Get the current NEP 50 promotion state."},
+ {"_set_promotion_state",
+ (PyCFunction)npy__set_promotion_state,
+ METH_O, "Set the NEP 50 promotion state. This is not thread-safe.\n"
+ "The optional warnings can be safely silenced using the \n"
+ "`np._no_nep50_warning()` context manager."},
{"_add_newdoc_ufunc", (PyCFunction)add_newdoc_ufunc,
METH_VARARGS, NULL},
{"_get_sfloat_dtype",