diff options
| author | Charles Harris <charlesr.harris@gmail.com> | 2022-06-26 12:52:52 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-26 12:52:52 -0600 |
| commit | b65f0b7b8ba7e80b65773e06aae22a8369678868 (patch) | |
| tree | 596a6389b54c72b2ca5a9a66d974ef0ab5b2a4af /numpy/core/src/multiarray | |
| parent | 11e465e30823f044887d3f5e2f648007ce0077c2 (diff) | |
| parent | 7dcfaafa0b78618a6ec2a5279090729f7ec583f0 (diff) | |
| download | numpy-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.c | 71 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/abstractdtypes.h | 53 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/arrayobject.h | 16 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/convert_datatype.c | 383 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/convert_datatype.h | 27 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/multiarraymodule.c | 17 |
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", |
