diff --git a/doc/release/upcoming_changes/29129.enhancement.rst b/doc/release/upcoming_changes/29129.enhancement.rst new file mode 100644 index 000000000000..9a14f13c1f4a --- /dev/null +++ b/doc/release/upcoming_changes/29129.enhancement.rst @@ -0,0 +1,7 @@ +``'same_value'`` for casting by value +------------------------------------- +The ``casting`` kwarg now has a ``'same_value'`` option that checks the actual +values can be round-trip cast without changing value. Currently it is only +implemented in `ndarray.astype`. This will raise a ``ValueError`` if any of the +values in the array would change as a result of the cast, including rounding of +floats or overflowing of ints. diff --git a/doc/source/reference/c-api/array.rst b/doc/source/reference/c-api/array.rst index dc043b77f187..9996f977ad7e 100644 --- a/doc/source/reference/c-api/array.rst +++ b/doc/source/reference/c-api/array.rst @@ -4452,5 +4452,10 @@ Enumerated Types Allow any cast, no matter what kind of data loss may occur. + .. c:enumerator:: NPY_SAME_VALUE_CASTING + + Allow any cast, but error if any values change during the cast. Currently + supported only in ``ndarray.astype(... casting='same_value')`` + .. index:: pair: ndarray; C-API diff --git a/numpy/__init__.cython-30.pxd b/numpy/__init__.cython-30.pxd index 86c91cf617a5..c71898626070 100644 --- a/numpy/__init__.cython-30.pxd +++ b/numpy/__init__.cython-30.pxd @@ -156,6 +156,7 @@ cdef extern from "numpy/arrayobject.h": NPY_SAFE_CASTING NPY_SAME_KIND_CASTING NPY_UNSAFE_CASTING + NPY_SAME_VALUE_CASTING ctypedef enum NPY_CLIPMODE: NPY_CLIP diff --git a/numpy/__init__.pxd b/numpy/__init__.pxd index eb0764126116..40a24b6c7cc1 100644 --- a/numpy/__init__.pxd +++ b/numpy/__init__.pxd @@ -165,6 +165,7 @@ cdef extern from "numpy/arrayobject.h": NPY_SAFE_CASTING NPY_SAME_KIND_CASTING NPY_UNSAFE_CASTING + NPY_SAME_VALUE_CASTING ctypedef enum NPY_CLIPMODE: NPY_CLIP diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index d8c57c87cbbe..b493e44ed7cd 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -965,7 +965,7 @@ _DTypeBuiltinKind: TypeAlias = L[0, 1, 2] _ArrayAPIVersion: TypeAlias = L["2021.12", "2022.12", "2023.12", "2024.12"] -_CastingKind: TypeAlias = L["no", "equiv", "safe", "same_kind", "unsafe"] +_CastingKind: TypeAlias = L["no", "equiv", "safe", "same_kind", "same_value", "unsafe"] _OrderKACF: TypeAlias = L["K", "A", "C", "F"] | None _OrderACF: TypeAlias = L["A", "C", "F"] | None diff --git a/numpy/_core/_add_newdocs.py b/numpy/_core/_add_newdocs.py index 90d33d4b810a..6f2e94350d5e 100644 --- a/numpy/_core/_add_newdocs.py +++ b/numpy/_core/_add_newdocs.py @@ -3185,7 +3185,7 @@ 'C' order otherwise, and 'K' means as close to the order the array elements appear in memory as possible. Default is 'K'. - casting : {'no', 'equiv', 'safe', 'same_kind', 'unsafe'}, optional + casting : {'no', 'equiv', 'safe', 'same_kind', 'same_value', 'unsafe'}, optional Controls what kind of data casting may occur. Defaults to 'unsafe' for backwards compatibility. @@ -3195,6 +3195,12 @@ * 'same_kind' means only safe casts or casts within a kind, like float64 to float32, are allowed. * 'unsafe' means any data conversions may be done. + * 'same_value' means any data conversions may be done, but the values + must not change, including rounding of floats or overflow of ints + + .. versionadded:: 2.4 + Support for ``'same_value'`` was added. + subok : bool, optional If True, then sub-classes will be passed-through (default), otherwise the returned array will be forced to be a base-class array. @@ -3217,6 +3223,9 @@ ComplexWarning When casting from complex to float or int. To avoid this, one should use ``a.real.astype(t)``. + ValueError + When casting using ``'same_value'`` and the values change or would + overflow Examples -------- diff --git a/numpy/_core/code_generators/cversions.txt b/numpy/_core/code_generators/cversions.txt index 0d642d760b21..a04dd784c67f 100644 --- a/numpy/_core/code_generators/cversions.txt +++ b/numpy/_core/code_generators/cversions.txt @@ -79,5 +79,6 @@ # Version 19 (NumPy 2.2.0) No change 0x00000013 = 2b8f1f4da822491ff030b2b37dff07e3 # Version 20 (NumPy 2.3.0) -# Version 20 (NumPy 2.4.0) No change 0x00000014 = e56b74d32a934d085e7c3414cb9999b8, +# Version 21 (NumPy 2.4.0) Add 'same_value' casting, header additions +0x00000015 = e56b74d32a934d085e7c3414cb9999b8, diff --git a/numpy/_core/include/numpy/dtype_api.h b/numpy/_core/include/numpy/dtype_api.h index b37c9fbb6821..f902a9a74d19 100644 --- a/numpy/_core/include/numpy/dtype_api.h +++ b/numpy/_core/include/numpy/dtype_api.h @@ -107,7 +107,15 @@ typedef struct PyArrayMethod_Context_tag { /* Operand descriptors, filled in by resolve_descriptors */ PyArray_Descr *const *descriptors; + #if NPY_FEATURE_VERSION > NPY_2_3_API_VERSION + void * _reserved; + /* + * Optional flag to pass information into the inner loop + * If set, it will be NPY_CASTING + */ + uint64_t flags; /* Structure may grow (this is harmless for DType authors) */ + #endif } PyArrayMethod_Context; @@ -144,7 +152,6 @@ typedef struct { #define NPY_METH_contiguous_indexed_loop 9 #define _NPY_METH_static_data 10 - /* * The resolve descriptors function, must be able to handle NULL values for * all output (but not input) `given_descrs` and fill `loop_descrs`. diff --git a/numpy/_core/include/numpy/ndarraytypes.h b/numpy/_core/include/numpy/ndarraytypes.h index baa42406ac88..445a75057b26 100644 --- a/numpy/_core/include/numpy/ndarraytypes.h +++ b/numpy/_core/include/numpy/ndarraytypes.h @@ -227,6 +227,15 @@ typedef enum { NPY_SAME_KIND_CASTING=3, /* Allow any casts */ NPY_UNSAFE_CASTING=4, + /* + * Allow any casts, check that no values overflow/change. For users + * we only accept same-value casting, but array methods (cast impls) + * we need to know if same-value is supported on a same-kind cast. + * Safe, equiv, and no-casts are assumed to always be same-value safe. + */ + _NPY_SAME_VALUE_CASTING_FLAG = 8, // Use one bit to indicate same-value + NPY_SAME_VALUE_SAME_KIND_CASTING=(_NPY_SAME_VALUE_CASTING_FLAG | NPY_SAME_KIND_CASTING), + NPY_SAME_VALUE_CASTING=(_NPY_SAME_VALUE_CASTING_FLAG | NPY_UNSAFE_CASTING), } NPY_CASTING; typedef enum { diff --git a/numpy/_core/include/numpy/numpyconfig.h b/numpy/_core/include/numpy/numpyconfig.h index 52d7e2b5d7d7..c129a3aceb6d 100644 --- a/numpy/_core/include/numpy/numpyconfig.h +++ b/numpy/_core/include/numpy/numpyconfig.h @@ -84,6 +84,7 @@ #define NPY_2_1_API_VERSION 0x00000013 #define NPY_2_2_API_VERSION 0x00000013 #define NPY_2_3_API_VERSION 0x00000014 +#define NPY_2_4_API_VERSION 0x00000015 /* @@ -172,8 +173,10 @@ #define NPY_FEATURE_VERSION_STRING "2.0" #elif NPY_FEATURE_VERSION == NPY_2_1_API_VERSION #define NPY_FEATURE_VERSION_STRING "2.1" -#elif NPY_FEATURE_VERSION == NPY_2_3_API_VERSION /* also 2.4 */ +#elif NPY_FEATURE_VERSION == NPY_2_3_API_VERSION #define NPY_FEATURE_VERSION_STRING "2.3" +#elif NPY_FEATURE_VERSION == NPY_2_4_API_VERSION + #define NPY_FEATURE_VERSION_STRING "2.4" #else #error "Missing version string define for new NumPy version." #endif diff --git a/numpy/_core/meson.build b/numpy/_core/meson.build index 1b79aa39781c..4cf57212f3da 100644 --- a/numpy/_core/meson.build +++ b/numpy/_core/meson.build @@ -50,7 +50,8 @@ C_ABI_VERSION = '0x02000000' # 0x00000013 - 2.1.x # 0x00000013 - 2.2.x # 0x00000014 - 2.3.x -C_API_VERSION = '0x00000014' +# 0x00000015 - 2.4.x +C_API_VERSION = '0x00000015' # Check whether we have a mismatch between the set C API VERSION and the # actual C API VERSION. Will raise a MismatchCAPIError if so. diff --git a/numpy/_core/src/common/array_assign.h b/numpy/_core/src/common/array_assign.h index 8a28ed1d3a01..cc5f044ef080 100644 --- a/numpy/_core/src/common/array_assign.h +++ b/numpy/_core/src/common/array_assign.h @@ -46,7 +46,7 @@ PyArray_AssignRawScalar(PyArrayObject *dst, NPY_NO_EXPORT int raw_array_assign_scalar(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, - PyArray_Descr *src_dtype, char *src_data); + PyArray_Descr *src_dtype, char *src_data, NPY_CASTING casting); /* * Assigns the scalar value to every element of the destination raw array @@ -59,7 +59,7 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, PyArray_Descr *src_dtype, char *src_data, PyArray_Descr *wheremask_dtype, char *wheremask_data, - npy_intp const *wheremask_strides); + npy_intp const *wheremask_strides, NPY_CASTING casting); /******** LOW-LEVEL ARRAY MANIPULATION HELPERS ********/ diff --git a/numpy/_core/src/multiarray/_multiarray_tests.c.src b/numpy/_core/src/multiarray/_multiarray_tests.c.src index 068fabc7fee8..1bd49df5404e 100644 --- a/numpy/_core/src/multiarray/_multiarray_tests.c.src +++ b/numpy/_core/src/multiarray/_multiarray_tests.c.src @@ -2168,6 +2168,7 @@ run_casting_converter(PyObject* NPY_UNUSED(self), PyObject *args) case NPY_SAFE_CASTING: return PyUnicode_FromString("NPY_SAFE_CASTING"); case NPY_SAME_KIND_CASTING: return PyUnicode_FromString("NPY_SAME_KIND_CASTING"); case NPY_UNSAFE_CASTING: return PyUnicode_FromString("NPY_UNSAFE_CASTING"); + case NPY_SAME_VALUE_CASTING: return PyUnicode_FromString("NPY_SAME_VALUE_CASTING"); default: return PyLong_FromLong(casting); } } diff --git a/numpy/_core/src/multiarray/array_assign_array.c b/numpy/_core/src/multiarray/array_assign_array.c index 8886d1cacb40..4a524558b68f 100644 --- a/numpy/_core/src/multiarray/array_assign_array.c +++ b/numpy/_core/src/multiarray/array_assign_array.c @@ -79,7 +79,8 @@ copycast_isaligned(int ndim, npy_intp const *shape, NPY_NO_EXPORT int raw_array_assign_array(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, - PyArray_Descr *src_dtype, char *src_data, npy_intp const *src_strides) + PyArray_Descr *src_dtype, char *src_data, npy_intp const *src_strides, + int flags) { int idim; npy_intp shape_it[NPY_MAXDIMS]; @@ -87,14 +88,11 @@ raw_array_assign_array(int ndim, npy_intp const *shape, npy_intp src_strides_it[NPY_MAXDIMS]; npy_intp coord[NPY_MAXDIMS]; - int aligned; + int aligned = flags & 0x01; + int same_value_cast = (flags & 0x02) == 0x02; NPY_BEGIN_THREADS_DEF; - aligned = - copycast_isaligned(ndim, shape, dst_dtype, dst_data, dst_strides) && - copycast_isaligned(ndim, shape, src_dtype, src_data, src_strides); - /* Use raw iteration with no heap allocation */ if (PyArray_PrepareTwoRawArrayIter( ndim, shape, @@ -120,21 +118,25 @@ raw_array_assign_array(int ndim, npy_intp const *shape, /* Get the function to do the casting */ NPY_cast_info cast_info; - NPY_ARRAYMETHOD_FLAGS flags; + NPY_ARRAYMETHOD_FLAGS method_flags; if (PyArray_GetDTypeTransferFunction(aligned, src_strides_it[0], dst_strides_it[0], src_dtype, dst_dtype, 0, - &cast_info, &flags) != NPY_SUCCEED) { + &cast_info, &method_flags) != NPY_SUCCEED) { return -1; } - if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { + if (!(method_flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { npy_clear_floatstatus_barrier((char*)&src_data); } + if (same_value_cast) { + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + } + /* Ensure number of elements exceeds threshold for threading */ - if (!(flags & NPY_METH_REQUIRES_PYAPI)) { + if (!(method_flags & NPY_METH_REQUIRES_PYAPI)) { npy_intp nitems = 1, i; for (i = 0; i < ndim; i++) { nitems *= shape_it[i]; @@ -144,11 +146,14 @@ raw_array_assign_array(int ndim, npy_intp const *shape, npy_intp strides[2] = {src_strides_it[0], dst_strides_it[0]}; + int result = 0; NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { /* Process the innermost dimension */ char *args[2] = {src_data, dst_data}; - if (cast_info.func(&cast_info.context, - args, &shape_it[0], strides, cast_info.auxdata) < 0) { + result = cast_info.func(&cast_info.context, + args, &shape_it[0], strides, + cast_info.auxdata); + if (result < 0) { goto fail; } } NPY_RAW_ITER_TWO_NEXT(idim, ndim, coord, shape_it, @@ -158,7 +163,7 @@ raw_array_assign_array(int ndim, npy_intp const *shape, NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); - if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { + if (!(method_flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { int fpes = npy_get_floatstatus_barrier((char*)&src_data); if (fpes && PyUFunc_GiveFloatingpointErrors("cast", fpes) < 0) { return -1; @@ -183,7 +188,7 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, PyArray_Descr *src_dtype, char *src_data, npy_intp const *src_strides, PyArray_Descr *wheremask_dtype, char *wheremask_data, - npy_intp const *wheremask_strides) + npy_intp const *wheremask_strides, int flags) { int idim; npy_intp shape_it[NPY_MAXDIMS]; @@ -192,14 +197,11 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, npy_intp wheremask_strides_it[NPY_MAXDIMS]; npy_intp coord[NPY_MAXDIMS]; - int aligned; + int aligned = flags & 0x01; + int same_value_cast = (flags & 0x02) == 0x02; NPY_BEGIN_THREADS_DEF; - aligned = - copycast_isaligned(ndim, shape, dst_dtype, dst_data, dst_strides) && - copycast_isaligned(ndim, shape, src_dtype, src_data, src_strides); - /* Use raw iteration with no heap allocation */ if (PyArray_PrepareThreeRawArrayIter( ndim, shape, @@ -229,39 +231,45 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, /* Get the function to do the casting */ NPY_cast_info cast_info; - NPY_ARRAYMETHOD_FLAGS flags; + NPY_ARRAYMETHOD_FLAGS method_flags; if (PyArray_GetMaskedDTypeTransferFunction(aligned, src_strides_it[0], dst_strides_it[0], wheremask_strides_it[0], src_dtype, dst_dtype, wheremask_dtype, 0, - &cast_info, &flags) != NPY_SUCCEED) { + &cast_info, &method_flags) != NPY_SUCCEED) { return -1; } + if (same_value_cast) { + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + } - if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { + if (!(method_flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { npy_clear_floatstatus_barrier(src_data); } - if (!(flags & NPY_METH_REQUIRES_PYAPI)) { + if (!(method_flags & NPY_METH_REQUIRES_PYAPI)) { npy_intp nitems = 1, i; for (i = 0; i < ndim; i++) { nitems *= shape_it[i]; } NPY_BEGIN_THREADS_THRESHOLDED(nitems); } + npy_intp strides[2] = {src_strides_it[0], dst_strides_it[0]}; + int result = 0; NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { PyArray_MaskedStridedUnaryOp *stransfer; stransfer = (PyArray_MaskedStridedUnaryOp *)cast_info.func; /* Process the innermost dimension */ char *args[2] = {src_data, dst_data}; - if (stransfer(&cast_info.context, - args, &shape_it[0], strides, - (npy_bool *)wheremask_data, wheremask_strides_it[0], - cast_info.auxdata) < 0) { + result = stransfer(&cast_info.context, + args, &shape_it[0], strides, + (npy_bool *)wheremask_data, wheremask_strides_it[0], + cast_info.auxdata); + if (result < 0) { goto fail; } } NPY_RAW_ITER_THREE_NEXT(idim, ndim, coord, shape_it, @@ -272,15 +280,13 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); - if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { + if (!(method_flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { int fpes = npy_get_floatstatus_barrier(src_data); if (fpes && PyUFunc_GiveFloatingpointErrors("cast", fpes) < 0) { return -1; } } - return 0; - fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); @@ -307,7 +313,6 @@ PyArray_AssignArray(PyArrayObject *dst, PyArrayObject *src, NPY_CASTING casting) { int copied_src = 0; - npy_intp src_strides[NPY_MAXDIMS]; /* Use array_assign_scalar if 'src' NDIM is 0 */ @@ -438,12 +443,17 @@ PyArray_AssignArray(PyArrayObject *dst, PyArrayObject *src, } } + int aligned = + copycast_isaligned(PyArray_NDIM(dst), PyArray_DIMS(dst), PyArray_DESCR(dst), PyArray_DATA(dst), PyArray_STRIDES(dst)) && + copycast_isaligned(PyArray_NDIM(dst), PyArray_DIMS(dst), PyArray_DESCR(src), PyArray_DATA(src), src_strides); + int flags = ((NPY_SAME_VALUE_CASTING == casting) << 1) | aligned; + if (wheremask == NULL) { /* A straightforward value assignment */ /* Do the assignment with raw array iteration */ if (raw_array_assign_array(PyArray_NDIM(dst), PyArray_DIMS(dst), PyArray_DESCR(dst), PyArray_DATA(dst), PyArray_STRIDES(dst), - PyArray_DESCR(src), PyArray_DATA(src), src_strides) < 0) { + PyArray_DESCR(src), PyArray_DATA(src), src_strides, flags) < 0){ goto fail; } } @@ -465,7 +475,7 @@ PyArray_AssignArray(PyArrayObject *dst, PyArrayObject *src, PyArray_DESCR(dst), PyArray_DATA(dst), PyArray_STRIDES(dst), PyArray_DESCR(src), PyArray_DATA(src), src_strides, PyArray_DESCR(wheremask), PyArray_DATA(wheremask), - wheremask_strides) < 0) { + wheremask_strides, flags) < 0) { goto fail; } } diff --git a/numpy/_core/src/multiarray/array_assign_scalar.c b/numpy/_core/src/multiarray/array_assign_scalar.c index 0199ba969eb9..54d0f13c86af 100644 --- a/numpy/_core/src/multiarray/array_assign_scalar.c +++ b/numpy/_core/src/multiarray/array_assign_scalar.c @@ -37,7 +37,7 @@ NPY_NO_EXPORT int raw_array_assign_scalar(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, - PyArray_Descr *src_dtype, char *src_data) + PyArray_Descr *src_dtype, char *src_data, NPY_CASTING casting) { int idim; npy_intp shape_it[NPY_MAXDIMS], dst_strides_it[NPY_MAXDIMS]; @@ -86,13 +86,19 @@ raw_array_assign_scalar(int ndim, npy_intp const *shape, NPY_BEGIN_THREADS_THRESHOLDED(nitems); } + if (casting == NPY_SAME_VALUE_CASTING) { + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + } + npy_intp strides[2] = {0, dst_strides_it[0]}; + int result = 0; NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { /* Process the innermost dimension */ char *args[2] = {src_data, dst_data}; - if (cast_info.func(&cast_info.context, - args, &shape_it[0], strides, cast_info.auxdata) < 0) { + result = cast_info.func(&cast_info.context, + args, &shape_it[0], strides, cast_info.auxdata); + if (result < 0) { goto fail; } } NPY_RAW_ITER_ONE_NEXT(idim, ndim, coord, @@ -126,7 +132,7 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, PyArray_Descr *src_dtype, char *src_data, PyArray_Descr *wheremask_dtype, char *wheremask_data, - npy_intp const *wheremask_strides) + npy_intp const *wheremask_strides, NPY_CASTING casting) { int idim; npy_intp shape_it[NPY_MAXDIMS], dst_strides_it[NPY_MAXDIMS]; @@ -177,8 +183,12 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, } NPY_BEGIN_THREADS_THRESHOLDED(nitems); } + if (casting == NPY_SAME_VALUE_CASTING) { + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + } npy_intp strides[2] = {0, dst_strides_it[0]}; + int result = 0; NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { /* Process the innermost dimension */ @@ -186,10 +196,11 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, stransfer = (PyArray_MaskedStridedUnaryOp *)cast_info.func; char *args[2] = {src_data, dst_data}; - if (stransfer(&cast_info.context, + result = stransfer(&cast_info.context, args, &shape_it[0], strides, (npy_bool *)wheremask_data, wheremask_strides_it[0], - cast_info.auxdata) < 0) { + cast_info.auxdata); + if (result < 0) { goto fail; } } NPY_RAW_ITER_TWO_NEXT(idim, ndim, coord, shape_it, @@ -298,7 +309,7 @@ PyArray_AssignRawScalar(PyArrayObject *dst, /* Do the assignment with raw array iteration */ if (raw_array_assign_scalar(PyArray_NDIM(dst), PyArray_DIMS(dst), PyArray_DESCR(dst), PyArray_DATA(dst), PyArray_STRIDES(dst), - src_dtype, src_data) < 0) { + src_dtype, src_data, casting) < 0) { goto fail; } } @@ -319,7 +330,7 @@ PyArray_AssignRawScalar(PyArrayObject *dst, PyArray_DESCR(dst), PyArray_DATA(dst), PyArray_STRIDES(dst), src_dtype, src_data, PyArray_DESCR(wheremask), PyArray_DATA(wheremask), - wheremask_strides) < 0) { + wheremask_strides, casting) < 0) { goto fail; } } diff --git a/numpy/_core/src/multiarray/array_coercion.c b/numpy/_core/src/multiarray/array_coercion.c index 8271fb6812d1..854d53ab40d4 100644 --- a/numpy/_core/src/multiarray/array_coercion.c +++ b/numpy/_core/src/multiarray/array_coercion.c @@ -418,8 +418,10 @@ npy_cast_raw_scalar_item( char *args[2] = {from_item, to_item}; const npy_intp strides[2] = {0, 0}; const npy_intp length = 1; - if (cast_info.func(&cast_info.context, - args, &length, strides, cast_info.auxdata) < 0) { + int result = 0; + result = cast_info.func(&cast_info.context, + args, &length, strides, cast_info.auxdata); + if (result < 0) { NPY_cast_info_xfree(&cast_info); return -1; } diff --git a/numpy/_core/src/multiarray/array_method.c b/numpy/_core/src/multiarray/array_method.c index 5554cad5e2dd..67d3a56aefbc 100644 --- a/numpy/_core/src/multiarray/array_method.c +++ b/numpy/_core/src/multiarray/array_method.c @@ -39,6 +39,7 @@ #include "convert_datatype.h" #include "common.h" #include "numpy/ufuncobject.h" +#include "dtype_transfer.h" /* @@ -190,6 +191,8 @@ validate_spec(PyArrayMethod_Spec *spec) case NPY_SAFE_CASTING: case NPY_SAME_KIND_CASTING: case NPY_UNSAFE_CASTING: + case NPY_SAME_VALUE_CASTING: + case NPY_SAME_VALUE_SAME_KIND_CASTING: break; default: if (spec->casting != -1) { @@ -792,11 +795,10 @@ boundarraymethod__simple_strided_call( return NULL; } - PyArrayMethod_Context context = { - .caller = NULL, - .method = self->method, - .descriptors = descrs, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, descrs); + context.method = self->method; + PyArrayMethod_StridedLoop *strided_loop = NULL; NpyAuxData *loop_data = NULL; NPY_ARRAYMETHOD_FLAGS flags = 0; @@ -984,3 +986,4 @@ NPY_NO_EXPORT PyTypeObject PyBoundArrayMethod_Type = { .tp_methods = boundarraymethod_methods, .tp_getset = boundarraymethods_getters, }; + diff --git a/numpy/_core/src/multiarray/common.c b/numpy/_core/src/multiarray/common.c index 8236ec5c65ae..4d1d9c238418 100644 --- a/numpy/_core/src/multiarray/common.c +++ b/numpy/_core/src/multiarray/common.c @@ -25,15 +25,6 @@ * variable is misnamed, but it's part of the public API so I'm not sure we * can just change it. Maybe someone should try and see if anyone notices. */ -/* - * In numpy 1.6 and earlier, this was NPY_UNSAFE_CASTING. In a future - * release, it will become NPY_SAME_KIND_CASTING. Right now, during the - * transitional period, we continue to follow the NPY_UNSAFE_CASTING rules (to - * avoid breaking people's code), but we also check for whether the cast would - * be allowed under the NPY_SAME_KIND_CASTING rules, and if not we issue a - * warning (that people's code will be broken in a future release.) - */ - NPY_NO_EXPORT NPY_CASTING NPY_DEFAULT_ASSIGN_CASTING = NPY_SAME_KIND_CASTING; diff --git a/numpy/_core/src/multiarray/conversion_utils.c b/numpy/_core/src/multiarray/conversion_utils.c index d487aa16727d..164aa2e4c8b4 100644 --- a/numpy/_core/src/multiarray/conversion_utils.c +++ b/numpy/_core/src/multiarray/conversion_utils.c @@ -911,7 +911,7 @@ PyArray_CorrelatemodeConverter(PyObject *object, NPY_CORRELATEMODE *val) } } -static int casting_parser(char const *str, Py_ssize_t length, void *data) +static int casting_parser_full(char const *str, Py_ssize_t length, void *data, int can_use_same_value) { NPY_CASTING *casting = (NPY_CASTING *)data; if (length < 2) { @@ -941,6 +941,10 @@ static int casting_parser(char const *str, Py_ssize_t length, void *data) *casting = NPY_SAME_KIND_CASTING; return 0; } + if (can_use_same_value && length == 10 && strcmp(str, "same_value") == 0) { + *casting = NPY_SAME_VALUE_CASTING; + return 0; + } break; case 's': if (length == 6 && strcmp(str, "unsafe") == 0) { @@ -952,6 +956,11 @@ static int casting_parser(char const *str, Py_ssize_t length, void *data) return -1; } +static int casting_parser(char const *str, Py_ssize_t length, void *data) +{ + return casting_parser_full(str, length, data, 0); +} + /*NUMPY_API * Convert any Python object, *obj*, to an NPY_CASTING enum. */ @@ -961,10 +970,26 @@ PyArray_CastingConverter(PyObject *obj, NPY_CASTING *casting) return string_converter_helper( obj, (void *)casting, casting_parser, "casting", "must be one of 'no', 'equiv', 'safe', " - "'same_kind', or 'unsafe'"); + "'same_kind', 'unsafe'"); + return 0; +} + +static int casting_parser_same_value(char const *str, Py_ssize_t length, void *data) +{ + return casting_parser_full(str, length, data, 1); +} + +NPY_NO_EXPORT int +PyArray_CastingConverterSameValue(PyObject *obj, NPY_CASTING *casting) +{ + return string_converter_helper( + obj, (void *)casting, casting_parser_same_value, "casting", + "must be one of 'no', 'equiv', 'safe', " + "'same_kind', 'unsafe', 'same_value'"); return 0; } + /***************************** * Other conversion functions *****************************/ diff --git a/numpy/_core/src/multiarray/convert.c b/numpy/_core/src/multiarray/convert.c index 8e0177616955..983d9bc19ce6 100644 --- a/numpy/_core/src/multiarray/convert.c +++ b/numpy/_core/src/multiarray/convert.c @@ -429,7 +429,7 @@ PyArray_FillWithScalar(PyArrayObject *arr, PyObject *obj) int retcode = raw_array_assign_scalar( PyArray_NDIM(arr), PyArray_DIMS(arr), descr, PyArray_BYTES(arr), PyArray_STRIDES(arr), - descr, (void *)value); + descr, (void *)value, NPY_UNSAFE_CASTING); if (PyDataType_REFCHK(descr)) { PyArray_ClearBuffer(descr, (void *)value, 0, 1, 1); diff --git a/numpy/_core/src/multiarray/convert_datatype.c b/numpy/_core/src/multiarray/convert_datatype.c index d34d852a706b..ca5905299a55 100644 --- a/numpy/_core/src/multiarray/convert_datatype.c +++ b/numpy/_core/src/multiarray/convert_datatype.c @@ -271,11 +271,29 @@ PyArray_MinCastSafety(NPY_CASTING casting1, NPY_CASTING casting2) if (casting1 < 0 || casting2 < 0) { return -1; } - /* larger casting values are less safe */ - if (casting1 > casting2) { + if ((casting1 & _NPY_SAME_VALUE_CASTING_FLAG) == (casting2 & _NPY_SAME_VALUE_CASTING_FLAG)) { + /* larger casting values are less safe, unless same-value mismatches */ + if (casting1 > casting2) { + return casting1; + } + return casting2; + } + else if (casting1 & _NPY_SAME_VALUE_CASTING_FLAG && casting2 <= NPY_SAFE_CASTING) { return casting1; } - return casting2; + else if (casting2 & _NPY_SAME_VALUE_CASTING_FLAG && casting1 <= NPY_SAFE_CASTING) { + return casting2; + } + else { + /* The min cast-safety isn't same-value compatible, so unset the flag. */ + casting1 &= ~_NPY_SAME_VALUE_CASTING_FLAG; + casting2 &= ~_NPY_SAME_VALUE_CASTING_FLAG; + /* with same-value casting out of the picture, use comparison */ + if (casting1 > casting2) { + return casting1; + } + return casting2; + } } @@ -746,13 +764,13 @@ can_cast_pyscalar_scalar_to( } else if (PyDataType_ISFLOAT(to)) { if (flags & NPY_ARRAY_WAS_PYTHON_COMPLEX) { - return casting == NPY_UNSAFE_CASTING; + return ((casting == NPY_UNSAFE_CASTING) || (casting == NPY_SAME_VALUE_CASTING)); } return 1; } else if (PyDataType_ISINTEGER(to)) { if (!(flags & NPY_ARRAY_WAS_PYTHON_INT)) { - return casting == NPY_UNSAFE_CASTING; + return ((casting == NPY_UNSAFE_CASTING) || (casting == NPY_SAME_VALUE_CASTING)); } return 1; } @@ -839,6 +857,8 @@ npy_casting_to_string(NPY_CASTING casting) return "'same_kind'"; case NPY_UNSAFE_CASTING: return "'unsafe'"; + case NPY_SAME_VALUE_CASTING: + return "'same_value'"; default: return ""; } @@ -2320,12 +2340,17 @@ add_numeric_cast(PyArray_DTypeMeta *from, PyArray_DTypeMeta *to) else if (_npy_can_cast_safely_table[from->type_num][to->type_num]) { spec.casting = NPY_SAFE_CASTING; } - else if (dtype_kind_to_ordering(dtypes[0]->singleton->kind) <= - dtype_kind_to_ordering(dtypes[1]->singleton->kind)) { - spec.casting = NPY_SAME_KIND_CASTING; - } else { - spec.casting = NPY_UNSAFE_CASTING; + if (dtype_kind_to_ordering(dtypes[0]->singleton->kind) <= + dtype_kind_to_ordering(dtypes[1]->singleton->kind)) { + spec.casting = NPY_SAME_KIND_CASTING; + } + else { + spec.casting = NPY_UNSAFE_CASTING; + } + if (from != &PyArray_BoolDType && to != &PyArray_BoolDType) { + spec.casting |= _NPY_SAME_VALUE_CASTING_FLAG; + } } /* Create a bound method, unbind and store it */ @@ -2463,10 +2488,10 @@ cast_to_string_resolve_descriptors( return -1; } - if (self->casting == NPY_UNSAFE_CASTING) { + if ((self->casting == NPY_UNSAFE_CASTING) || (self->casting == NPY_SAME_VALUE_CASTING)){ assert(dtypes[0]->type_num == NPY_UNICODE && dtypes[1]->type_num == NPY_STRING); - return NPY_UNSAFE_CASTING; + return self->casting; } if (loop_descrs[1]->elsize >= size) { diff --git a/numpy/_core/src/multiarray/ctors.c b/numpy/_core/src/multiarray/ctors.c index 4f466677c57c..c720d1f8e2d2 100644 --- a/numpy/_core/src/multiarray/ctors.c +++ b/numpy/_core/src/multiarray/ctors.c @@ -2805,12 +2805,13 @@ PyArray_CopyAsFlat(PyArrayObject *dst, PyArrayObject *src, NPY_ORDER order) npy_intp strides[2] = {src_stride, dst_stride}; int res = 0; + int result = 0; for(;;) { /* Transfer the biggest amount that fits both */ count = (src_count < dst_count) ? src_count : dst_count; - if (cast_info.func(&cast_info.context, - args, &count, strides, cast_info.auxdata) < 0) { - res = -1; + result = cast_info.func(&cast_info.context, + args, &count, strides, cast_info.auxdata); + if (result < 0) { break; } @@ -2852,6 +2853,9 @@ PyArray_CopyAsFlat(PyArrayObject *dst, PyArrayObject *src, NPY_ORDER order) if (!NpyIter_Deallocate(src_iter)) { res = -1; } + if (result < 0) { + res = result; + } if (res == 0 && !(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { int fpes = npy_get_floatstatus_barrier((char *)&src_iter); diff --git a/numpy/_core/src/multiarray/datetime.c b/numpy/_core/src/multiarray/datetime.c index d820474532ca..0adfa70b6aa5 100644 --- a/numpy/_core/src/multiarray/datetime.c +++ b/numpy/_core/src/multiarray/datetime.c @@ -1261,6 +1261,8 @@ can_cast_datetime64_units(NPY_DATETIMEUNIT src_unit, return (src_unit <= dst_unit); } + case NPY_SAME_VALUE_CASTING: + return 0; /* Enforce equality with 'no' or 'equiv' casting */ default: return src_unit == dst_unit; @@ -1302,6 +1304,7 @@ can_cast_timedelta64_units(NPY_DATETIMEUNIT src_unit, * 'safe' casting. */ case NPY_SAFE_CASTING: + case NPY_SAME_VALUE_CASTING: if (src_unit == NPY_FR_GENERIC || dst_unit == NPY_FR_GENERIC) { return src_unit == NPY_FR_GENERIC; } @@ -1334,6 +1337,7 @@ can_cast_datetime64_metadata(PyArray_DatetimeMetaData *src_meta, casting); case NPY_SAFE_CASTING: + case NPY_SAME_VALUE_CASTING: return can_cast_datetime64_units(src_meta->base, dst_meta->base, casting) && datetime_metadata_divides(src_meta, dst_meta, 0); @@ -1361,6 +1365,7 @@ can_cast_timedelta64_metadata(PyArray_DatetimeMetaData *src_meta, casting); case NPY_SAFE_CASTING: + case NPY_SAME_VALUE_CASTING: return can_cast_timedelta64_units(src_meta->base, dst_meta->base, casting) && datetime_metadata_divides(src_meta, dst_meta, 1); diff --git a/numpy/_core/src/multiarray/datetime_strings.c b/numpy/_core/src/multiarray/datetime_strings.c index f92eec3f5a59..d3011d082325 100644 --- a/numpy/_core/src/multiarray/datetime_strings.c +++ b/numpy/_core/src/multiarray/datetime_strings.c @@ -984,7 +984,7 @@ NpyDatetime_MakeISO8601Datetime( * the string representation, so ensure that the data * is being cast according to the casting rule. */ - if (casting != NPY_UNSAFE_CASTING) { + if ((casting != NPY_UNSAFE_CASTING) && (casting != NPY_SAME_VALUE_CASTING)) { /* Producing a date as a local time is always 'unsafe' */ if (base <= NPY_FR_D && local) { PyErr_SetString(PyExc_TypeError, "Cannot create a local " diff --git a/numpy/_core/src/multiarray/dtype_transfer.c b/numpy/_core/src/multiarray/dtype_transfer.c index 64d5bfa89e8e..dbad10842aff 100644 --- a/numpy/_core/src/multiarray/dtype_transfer.c +++ b/numpy/_core/src/multiarray/dtype_transfer.c @@ -2910,8 +2910,6 @@ _clear_cast_info_after_get_loop_failure(NPY_cast_info *cast_info) * TODO: Expand the view functionality for general offsets, not just 0: * Partial casts could be skipped also for `view_offset != 0`. * - * The `out_needs_api` flag must be initialized. - * * NOTE: In theory casting errors here could be slightly misleading in case * of a multi-step casting scenario. It should be possible to improve * this in the future. @@ -3428,11 +3426,13 @@ PyArray_CastRawArrays(npy_intp count, /* Cast */ char *args[2] = {src, dst}; npy_intp strides[2] = {src_stride, dst_stride}; - cast_info.func(&cast_info.context, args, &count, strides, cast_info.auxdata); + int result = cast_info.func(&cast_info.context, args, &count, strides, cast_info.auxdata); /* Cleanup */ NPY_cast_info_xfree(&cast_info); - + if (result < 0) { + return NPY_FAIL; + } if (flags & NPY_METH_REQUIRES_PYAPI && PyErr_Occurred()) { return NPY_FAIL; } @@ -3831,4 +3831,4 @@ PyArray_PrepareThreeRawArrayIter(int ndim, npy_intp const *shape, *out_dataC = dataC; *out_ndim = ndim; return 0; -} \ No newline at end of file +} diff --git a/numpy/_core/src/multiarray/dtype_transfer.h b/numpy/_core/src/multiarray/dtype_transfer.h index 04df5cb64c22..a354820e5d45 100644 --- a/numpy/_core/src/multiarray/dtype_transfer.h +++ b/numpy/_core/src/multiarray/dtype_transfer.h @@ -25,6 +25,15 @@ typedef struct { } NPY_cast_info; +static inline void +NPY_context_init(PyArrayMethod_Context *context, PyArray_Descr *descr[2]) +{ + context->descriptors = descr; + context->caller = NULL; + context->_reserved = NULL; + context->flags = 0; +} + /* * Create a new cast-info struct with cast_info->context.descriptors linked. * Compilers should inline this to ensure the whole struct is not actually @@ -40,13 +49,9 @@ NPY_cast_info_init(NPY_cast_info *cast_info) * a scratch space to `NPY_cast_info` and link to that instead. */ cast_info->auxdata = NULL; - cast_info->context.descriptors = cast_info->descriptors; - - // TODO: Delete this again probably maybe create a new minimal init macro - cast_info->context.caller = NULL; + NPY_context_init(&(cast_info->context), cast_info->descriptors); } - /* * Free's all references and data held inside the struct (not the struct). * First checks whether `cast_info.func == NULL`, and assume it is @@ -100,6 +105,7 @@ NPY_cast_info_copy(NPY_cast_info *cast_info, NPY_cast_info *original) Py_XINCREF(cast_info->descriptors[1]); cast_info->context.caller = original->context.caller; Py_XINCREF(cast_info->context.caller); + cast_info->context.flags = original->context.flags; cast_info->context.method = original->context.method; Py_XINCREF(cast_info->context.method); if (original->auxdata == NULL) { diff --git a/numpy/_core/src/multiarray/legacy_dtype_implementation.c b/numpy/_core/src/multiarray/legacy_dtype_implementation.c index abfc1bd0e3cd..401223dd4e12 100644 --- a/numpy/_core/src/multiarray/legacy_dtype_implementation.c +++ b/numpy/_core/src/multiarray/legacy_dtype_implementation.c @@ -367,7 +367,7 @@ PyArray_LegacyCanCastTypeTo(PyArray_Descr *from, PyArray_Descr *to, * field; recurse just in case the single field is itself structured. */ if (!PyDataType_HASFIELDS(to) && !PyDataType_ISOBJECT(to)) { - if (casting == NPY_UNSAFE_CASTING && + if ((casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING)) && PyDict_Size(lfrom->fields) == 1) { Py_ssize_t ppos = 0; PyObject *tuple; @@ -399,7 +399,7 @@ PyArray_LegacyCanCastTypeTo(PyArray_Descr *from, PyArray_Descr *to, * casting; this is not correct, but needed since the treatment in can_cast * below got out of sync with astype; see gh-13667. */ - if (casting == NPY_UNSAFE_CASTING) { + if (casting == NPY_UNSAFE_CASTING || casting == NPY_SAME_VALUE_CASTING) { return 1; } } @@ -408,14 +408,14 @@ PyArray_LegacyCanCastTypeTo(PyArray_Descr *from, PyArray_Descr *to, * If "from" is a simple data type and "to" has fields, then only * unsafe casting works (and that works always, even to multiple fields). */ - return casting == NPY_UNSAFE_CASTING; + return (casting == NPY_UNSAFE_CASTING || casting == NPY_SAME_VALUE_CASTING); } /* * Everything else we consider castable for unsafe for now. * FIXME: ensure what we do here is consistent with "astype", * i.e., deal more correctly with subarrays and user-defined dtype. */ - else if (casting == NPY_UNSAFE_CASTING) { + else if (casting == NPY_UNSAFE_CASTING || casting == NPY_SAME_VALUE_CASTING) { return 1; } /* diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index 0c4eb3dd9a8d..7f0373202346 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -17,6 +17,7 @@ #include #include #include +#include #include "lowlevel_strided_loops.h" #include "array_assign.h" @@ -24,6 +25,7 @@ #include "usertypes.h" #include "umathmodule.h" +#include "gil_utils.h" /* * x86 platform works with unaligned access but the compiler is allowed to @@ -742,6 +744,7 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * * #is_float1 = 0*12, 1, 0, 0, 1, 0, 0# * #is_double1 = 0*13, 1, 0, 0, 1, 0# * #is_complex1 = 0*15, 1*3# + * #is_unsigned1 = 1*6, 0*12# */ /**begin repeat1 @@ -766,6 +769,16 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * * npy_byte, npy_short, npy_int, npy_long, npy_longlong, * _npy_half, npy_float, npy_double, npy_longdouble, * npy_float, npy_double, npy_longdouble# + * #type2max = 0, + * UCHAR_MAX, USHRT_MAX, UINT_MAX, ULONG_MAX, ULLONG_MAX, + * SCHAR_MAX, SHRT_MAX, INT_MAX, LONG_MAX, LLONG_MAX, + * 65500, FLT_MAX, DBL_MAX, LDBL_MAX, + * FLT_MAX, DBL_MAX, LDBL_MAX# + * #type2min = 0, + * 0, 0, 0, 0, 0, + * SCHAR_MIN, SHRT_MIN, INT_MIN, LONG_MIN, LLONG_MIN, + * -65500, -FLT_MAX, -DBL_MAX, -LDBL_MAX, + * -FLT_MAX, -DBL_MAX, -LDBL_MAX# * #is_bool2 = 1, 0*17# * #is_emu_half2 = 0*11, EMULATED_FP16, 0*6# * #is_native_half2 = 0*11, NATIVE_FP16, 0*6# @@ -810,40 +823,52 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * /* Determine an appropriate casting conversion function */ #if @is_emu_half1@ - +# define _TO_RTYPE1(x) npy_half_to_float(x) # if @is_float2@ # define _CONVERT_FN(x) npy_halfbits_to_floatbits(x) +# define _ROUND_TRIP(x) npy_floatbits_to_halfbits(_CONVERT_FN(x)) # elif @is_double2@ # define _CONVERT_FN(x) npy_halfbits_to_doublebits(x) +# define _ROUND_TRIP(x) npy_doublebits_to_halfbits(_CONVERT_FN(x)) # elif @is_emu_half2@ # define _CONVERT_FN(x) (x) +# define _ROUND_TRIP(x) (x) # elif @is_bool2@ # define _CONVERT_FN(x) ((npy_bool)!npy_half_iszero(x)) +# define _ROUND_TRIP(x) npy_float_to_half((float)(!npy_half_iszero(x))) # else # define _CONVERT_FN(x) ((_TYPE2)npy_half_to_float(x)) +# define _ROUND_TRIP(x) npy_float_to_half((float)_CONVERT_FN(x)) # endif #elif @is_emu_half2@ +# define _TO_RTYPE1(x) (@rtype1@)(x) # if @is_float1@ # define _CONVERT_FN(x) npy_floatbits_to_halfbits(x) +# define _ROUND_TRIP(x) (@rtype1@)npy_halfbits_to_floatbits(_CONVERT_FN(x)) # elif @is_double1@ # define _CONVERT_FN(x) npy_doublebits_to_halfbits(x) +# define _ROUND_TRIP(x) (@rtype1@)npy_halfbits_to_doublebits(_CONVERT_FN(x)) # elif @is_emu_half1@ # define _CONVERT_FN(x) (x) +# define _ROUND_TRIP(x) (x) # elif @is_bool1@ # define _CONVERT_FN(x) npy_float_to_half((float)(x!=0)) +# define _ROUND_TRIP(x) (x) # else # define _CONVERT_FN(x) npy_float_to_half((float)x) +# define _ROUND_TRIP(x) ((@rtype1@)npy_half_to_float(_CONVERT_FN(x))) # endif #else - # if @is_bool2@ || @is_bool1@ # define _CONVERT_FN(x) ((npy_bool)(x != 0)) # else # define _CONVERT_FN(x) ((_TYPE2)x) # endif +# define _TO_RTYPE1(x) (@rtype1@)(x) +# define _ROUND_TRIP(x) _TO_RTYPE1(_CONVERT_FN(x)) #endif @@ -869,6 +894,60 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * #define GCC_CAST_OPT_LEVEL NPY_GCC_OPT_3 #endif +#define _RETURN_SAME_VALUE_FAILURE \ + npy_gil_error(PyExc_ValueError, "could not cast 'same_value' @name1@ to @name2@"); \ + return -1 + +#if !@is_bool2@ +/* + * Check various modes of failure to accurately cast src_value to dst + */ +static GCC_CAST_OPT_LEVEL int +@prefix@_check_same_value_@name1@_to_@name2@(@rtype1@ src_value) { + + /* 1. NaN/Infs always work for float to float and otherwise never */ +#if (@is_float1@ || @is_emu_half1@ || @is_double1@ || @is_native_half1@) + if (!npy_isfinite(_TO_RTYPE1(src_value))) { +# if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) + return 0; /* float to float can preserve NaN/Inf */ +# else + _RETURN_SAME_VALUE_FAILURE; /* cannot preserve NaN/Inf */ +# endif + } +#endif + /* + * 2. Check that the src does not overflow the dst. + * This is complicated by a warning that, for instance, int8 cannot + * overflow int64max + */ +# ifdef __GNUC__ +# pragma GCC diagnostic push +# ifdef __clang__ +# pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare" +# endif +# pragma GCC diagnostic ignored "-Wtautological-compare" +# endif +# if !@is_bool1@ + if (_TO_RTYPE1(src_value) > @type2max@) { + _RETURN_SAME_VALUE_FAILURE; + } +# if !@is_unsigned1@ + if (_TO_RTYPE1(src_value) < @type2min@) { + _RETURN_SAME_VALUE_FAILURE; + } +# endif +# endif /* !is_bool1 */ + /* 3. Check that the value can round trip exactly */ + if (src_value != _ROUND_TRIP(src_value)) { + _RETURN_SAME_VALUE_FAILURE; + } +# ifdef __GNUC__ +# pragma GCC diagnostic pop +# endif /* __GNUC__ */ + return 0; +} +#endif + static GCC_CAST_OPT_LEVEL int @prefix@_cast_@name1@_to_@name2@( PyArrayMethod_Context *context, char *const *args, @@ -898,7 +977,10 @@ static GCC_CAST_OPT_LEVEL int assert(N == 0 || npy_is_aligned(dst, NPY_ALIGNOF(_TYPE2))); #endif - /*printf("@prefix@_cast_@name1@_to_@name2@\n");*/ + /* printf("@prefix@_cast_@name1@_to_@name2@\n"); */ +#if !@is_bool2@ + int same_value_casting = ((context->flags & NPY_SAME_VALUE_CASTING) == NPY_SAME_VALUE_CASTING); +#endif while (N--) { #if @aligned@ @@ -915,31 +997,81 @@ static GCC_CAST_OPT_LEVEL int # if @is_complex2@ dst_value[0] = _CONVERT_FN(src_value[0]); dst_value[1] = _CONVERT_FN(src_value[1]); + if (same_value_casting) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) < 0) { + return -1; + } + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[1]) < 0) { + return -1; + } + } # elif !@aligned@ # if @is_bool2@ dst_value = _CONVERT_FN(src_value[0]) || _CONVERT_FN(src_value[1]); # else dst_value = _CONVERT_FN(src_value[0]); + if (same_value_casting) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) < 0) { + return -1; + } + if (src_value[1] != 0) { + npy_gil_error(PyExc_ValueError, "could not cast 'same_value' @name1@ to @name2@: imag is not 0"); + return -1; + } + } # endif # else # if @is_bool2@ *(_TYPE2 *)dst = _CONVERT_FN(src_value[0]) || _CONVERT_FN(src_value[1]); # else *(_TYPE2 *)dst = _CONVERT_FN(src_value[0]); + if (same_value_casting) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) < 0) { + return -1; + } + if (src_value[1] != 0) { + npy_gil_error(PyExc_ValueError, "could not cast 'same_value' @name1@ to @name2@: imag is not 0"); + return -1; + } + } # endif # endif -#else +#else // @is_complex1@ # if @is_complex2@ # if !@aligned@ dst_value[0] = _CONVERT_FN(src_value); + if (same_value_casting) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value) < 0) { + return -1; + } + } # else dst_value[0] = _CONVERT_FN(*(_TYPE1 *)src); + if (same_value_casting) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)src) < 0) { + return -1; + } + } # endif dst_value[1] = 0; # elif !@aligned@ dst_value = _CONVERT_FN(src_value); +# if !@is_bool2@ + if (same_value_casting) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value) < 0) { + return -1; + } + } +# endif // @is_bool2@ # else *(_TYPE2 *)dst = _CONVERT_FN(*(_TYPE1 *)src); +# if !@is_bool2@ + if (same_value_casting) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*((@rtype1@ *)src)) < 0) { + return -1; + } + } +# endif // @is_bool2@ # endif #endif @@ -977,6 +1109,9 @@ static GCC_CAST_OPT_LEVEL int #undef _CONVERT_FN #undef _TYPE2 #undef _TYPE1 +#undef _TO_RTYPE1 +#undef _ROUND_TRIP +#undef _RETURN_SAME_VALUE_FAILURE #endif diff --git a/numpy/_core/src/multiarray/mapping.c b/numpy/_core/src/multiarray/mapping.c index 12f25534f1d0..0229015f92aa 100644 --- a/numpy/_core/src/multiarray/mapping.c +++ b/numpy/_core/src/multiarray/mapping.c @@ -2132,7 +2132,8 @@ array_assign_subscript(PyArrayObject *self, PyObject *ind, PyObject *op) * Could add a casting check, but apparently most assignments do * not care about safe casting. */ - if (mapiter_set(mit, &cast_info, meth_flags, is_aligned) < 0) { + int result = mapiter_set(mit, &cast_info, meth_flags, is_aligned); + if (result < 0) { goto fail; } diff --git a/numpy/_core/src/multiarray/methods.c b/numpy/_core/src/multiarray/methods.c index 50f7f5f3c73b..880d719bb98a 100644 --- a/numpy/_core/src/multiarray/methods.c +++ b/numpy/_core/src/multiarray/methods.c @@ -752,6 +752,10 @@ array_toscalar(PyArrayObject *self, PyObject *args) return PyArray_MultiIndexGetItem(self, multi_index); } + +NPY_NO_EXPORT int +PyArray_CastingConverterSameValue(PyObject *obj, NPY_CASTING *casting); + static PyObject * array_astype(PyArrayObject *self, PyObject *const *args, Py_ssize_t len_args, PyObject *kwnames) @@ -770,7 +774,7 @@ array_astype(PyArrayObject *self, if (npy_parse_arguments("astype", args, len_args, kwnames, "dtype", &PyArray_DTypeOrDescrConverterRequired, &dt_info, "|order", &PyArray_OrderConverter, &order, - "|casting", &PyArray_CastingConverter, &casting, + "|casting", &PyArray_CastingConverterSameValue, &casting, "|subok", &PyArray_PythonPyIntFromInt, &subok, "|copy", &PyArray_AsTypeCopyConverter, &forcecopy, NULL, NULL, NULL) < 0) { @@ -840,7 +844,12 @@ array_astype(PyArrayObject *self, ((PyArrayObject_fields *)ret)->nd = PyArray_NDIM(self); ((PyArrayObject_fields *)ret)->descr = dtype; } - int success = PyArray_CopyInto(ret, self); + int success; + if (casting == NPY_SAME_VALUE_CASTING) { + success = PyArray_AssignArray(ret, self, NULL, casting); + } else { + success = PyArray_AssignArray(ret, self, NULL, NPY_UNSAFE_CASTING); + } Py_DECREF(dtype); ((PyArrayObject_fields *)ret)->nd = out_ndim; diff --git a/numpy/_core/src/umath/legacy_array_method.c b/numpy/_core/src/umath/legacy_array_method.c index 705262fedd38..7a85937fcc8f 100644 --- a/numpy/_core/src/umath/legacy_array_method.c +++ b/numpy/_core/src/umath/legacy_array_method.c @@ -439,11 +439,10 @@ PyArray_NewLegacyWrappingArrayMethod(PyUFuncObject *ufunc, descrs[i] = bound_res->dtypes[i]->singleton; } - PyArrayMethod_Context context = { - (PyObject *)ufunc, - bound_res->method, - descrs, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, descrs); + context.caller = (PyObject *)ufunc; + context.method = bound_res->method; int ret = get_initial_from_ufunc(&context, 0, context.method->legacy_initial); diff --git a/numpy/_core/src/umath/reduction.c b/numpy/_core/src/umath/reduction.c index b376b94936bc..384ac052b226 100644 --- a/numpy/_core/src/umath/reduction.c +++ b/numpy/_core/src/umath/reduction.c @@ -372,7 +372,7 @@ PyUFunc_ReduceWrapper(PyArrayMethod_Context *context, PyArray_NDIM(result), PyArray_DIMS(result), PyArray_DESCR(result), PyArray_BYTES(result), PyArray_STRIDES(result), - op_dtypes[0], initial_buf); + op_dtypes[0], initial_buf, NPY_UNSAFE_CASTING); if (ret < 0) { goto fail; } diff --git a/numpy/_core/src/umath/ufunc_object.c b/numpy/_core/src/umath/ufunc_object.c index 1d2c3edbd3b9..6b4ee70fec1e 100644 --- a/numpy/_core/src/umath/ufunc_object.c +++ b/numpy/_core/src/umath/ufunc_object.c @@ -2084,11 +2084,10 @@ PyUFunc_GeneralizedFunctionInternal(PyUFuncObject *ufunc, NPY_SIZEOF_INTP * nop); /* Final preparation of the arraymethod call */ - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = operation_descrs, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, operation_descrs); + context.caller = (PyObject *)ufunc; + context.method = ufuncimpl; PyArrayMethod_StridedLoop *strided_loop; NPY_ARRAYMETHOD_FLAGS flags = 0; @@ -2203,11 +2202,10 @@ PyUFunc_GenericFunctionInternal(PyUFuncObject *ufunc, } /* Final preparation of the arraymethod call */ - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = operation_descrs, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, operation_descrs); + context.caller = (PyObject *)ufunc; + context.method = ufuncimpl; /* Do the ufunc loop */ if (wheremask != NULL) { @@ -2553,11 +2551,10 @@ PyUFunc_Reduce(PyUFuncObject *ufunc, return NULL; } - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = descrs, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, descrs); + context.caller = (PyObject *)ufunc; + context.method = ufuncimpl; PyArrayObject *result = PyUFunc_ReduceWrapper(&context, arr, out, wheremask, axis_flags, keepdims, @@ -2629,12 +2626,10 @@ PyUFunc_Accumulate(PyUFuncObject *ufunc, PyArrayObject *arr, PyArrayObject *out, assert(PyArray_EquivTypes(descrs[0], descrs[1]) && PyArray_EquivTypes(descrs[0], descrs[2])); - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = descrs, - }; - + PyArrayMethod_Context context; + NPY_context_init(&context, descrs); + context.caller = (PyObject *)ufunc, + context.method = ufuncimpl, ndim = PyArray_NDIM(arr); #if NPY_UF_DBG_TRACING @@ -3061,12 +3056,10 @@ PyUFunc_Reduceat(PyUFuncObject *ufunc, PyArrayObject *arr, PyArrayObject *ind, goto fail; } - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = descrs, - }; - + PyArrayMethod_Context context; + NPY_context_init(&context, descrs); + context.caller = (PyObject *)ufunc, + context.method = ufuncimpl, ndim = PyArray_NDIM(arr); #if NPY_UF_DBG_TRACING @@ -5902,11 +5895,10 @@ ufunc_at(PyUFuncObject *ufunc, PyObject *args) } } - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = operation_descrs, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, operation_descrs); + context.caller = (PyObject *)ufunc; + context.method = ufuncimpl; /* Use contiguous strides; if there is such a loop it may be faster */ npy_intp strides[3] = { diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 91ecc0dc75b0..e3222c89bcb6 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -10,13 +10,14 @@ import enum import random import textwrap +import warnings import pytest import numpy as np from numpy._core._multiarray_umath import _get_castingimpl as get_castingimpl from numpy.lib.stride_tricks import as_strided -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_equal, assert_equal # Simple skips object, parametric and long double (unsupported by struct) simple_dtypes = "?bhilqBHILQefdFD" @@ -76,8 +77,12 @@ class Casting(enum.IntEnum): safe = 2 same_kind = 3 unsafe = 4 + same_value_same_kind = 8 | 3 + same_value = 8 | 4 +same_value_dtypes = tuple(type(np.dtype(c)) for c in "bhilqBHILQefdgFDG") + def _get_cancast_table(): table = textwrap.dedent(""" X ? b h i l q B H I L Q e f d g F D G S U V O M m @@ -117,6 +122,12 @@ def _get_cancast_table(): cancast[from_dt] = {} for to_dt, c in zip(dtypes, row[2::2]): cancast[from_dt][to_dt] = convert_cast[c] + # Of the types checked, numeric cast support same-value + if from_dt in same_value_dtypes and to_dt in same_value_dtypes: + if cancast[from_dt][to_dt] == Casting.unsafe: + cancast[from_dt][to_dt] = Casting.same_value + if cancast[from_dt][to_dt] == Casting.same_kind: + cancast[from_dt][to_dt] = Casting.same_value_same_kind return cancast @@ -304,6 +315,7 @@ def test_simple_direct_casts(self, from_dt): to_dt = to_dt.values[0] cast = get_castingimpl(type(from_dt), type(to_dt)) + print("from_dt", from_dt, "to_dt", to_dt) casting, (from_res, to_res), view_off = cast._resolve_descriptors( (from_dt, to_dt)) @@ -317,7 +329,9 @@ def test_simple_direct_casts(self, from_dt): arr1, arr2, values = self.get_data(from_dt, to_dt) + print("2", arr1, arr2, cast) cast._simple_strided_call((arr1, arr2)) + print("3") # Check via python list assert arr2.tolist() == values @@ -815,3 +829,129 @@ def test_nonstandard_bool_to_other(self, dtype): res = nonstandard_bools.astype(dtype) expected = [0, 1, 1] assert_array_equal(res, expected) + + @pytest.mark.parametrize("to_dtype", + np.typecodes["AllInteger"] + np.typecodes["AllFloat"]) + @pytest.mark.parametrize("from_dtype", + np.typecodes["AllInteger"] + np.typecodes["AllFloat"]) + @pytest.mark.filterwarnings("ignore::numpy.exceptions.ComplexWarning") + def test_same_value_overflow(self, from_dtype, to_dtype): + if from_dtype == to_dtype: + return + top1 = 0 + top2 = 0 + try: + top1 = np.iinfo(from_dtype).max + except ValueError: + top1 = np.finfo(from_dtype).max + try: + top2 = np.iinfo(to_dtype).max + except ValueError: + top2 = np.finfo(to_dtype).max + # No need to test if top2 > top1, since the test will also do the + # reverse dtype matching. Catch then warning if the comparison warns, + # i.e. np.int16(65535) < np.float16(6.55e4) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always", RuntimeWarning) + if top2 >= top1: + # will be tested when the dtypes are reversed + return + # Happy path + arr1 = np.array([0] * 10, dtype=from_dtype) + arr2 = np.array([0] * 10, dtype=to_dtype) + arr1_astype = arr1.astype(to_dtype, casting='same_value') + assert_equal(arr1_astype, arr2, strict=True) + # Make it overflow, both aligned and unaligned + arr1[0] = top1 + aligned = np.empty(arr1.itemsize * arr1.size + 1, 'uint8') + unaligned = aligned[1:].view(arr1.dtype) + unaligned[:] = arr1 + with pytest.raises(ValueError): + # Casting float to float with overflow should raise + # RuntimeWarning (fperror) + # Casting float to int with overflow sometimes raises + # RuntimeWarning (fperror) + # Casting with overflow and 'same_value', should raise ValueError + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", RuntimeWarning) + arr1.astype(to_dtype, casting='same_value') + assert len(w) < 2 + with pytest.raises(ValueError): + # again, unaligned + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", RuntimeWarning) + unaligned.astype(to_dtype, casting='same_value') + assert len(w) < 2 + + @pytest.mark.parametrize("to_dtype", + np.typecodes["AllInteger"]) + @pytest.mark.parametrize("from_dtype", + np.typecodes["AllFloat"]) + @pytest.mark.filterwarnings("ignore::RuntimeWarning") + def test_same_value_float_to_int(self, from_dtype, to_dtype): + # Should not raise, since the values can round trip + arr1 = np.arange(10, dtype=from_dtype) + aligned = np.empty(arr1.itemsize * arr1.size + 1, 'uint8') + unaligned = aligned[1:].view(arr1.dtype) + unaligned[:] = arr1 + arr2 = np.arange(10, dtype=to_dtype) + assert_array_equal(arr1.astype(to_dtype, casting='same_value'), arr2) + assert_array_equal(unaligned.astype(to_dtype, casting='same_value'), arr2) + + # Should raise, since values cannot round trip. Might warn too about + # FPE errors + arr1_66 = arr1 + 0.666 + unaligned_66 = unaligned + 0.66 + with pytest.raises(ValueError): + arr1_66.astype(to_dtype, casting='same_value') + with pytest.raises(ValueError): + unaligned_66.astype(to_dtype, casting='same_value') + + @pytest.mark.parametrize("to_dtype", + np.typecodes["AllInteger"]) + @pytest.mark.parametrize("from_dtype", + np.typecodes["AllFloat"]) + @pytest.mark.filterwarnings("ignore::RuntimeWarning") + def test_same_value_float_to_int_scalar(self, from_dtype, to_dtype): + # Should not raise, since the values can round trip + s1 = np.array(10, dtype=from_dtype) + assert s1.astype(to_dtype, casting='same_value') == 10 + + # Should raise, since values cannot round trip + s1_66 = s1 + 0.666 + with pytest.raises(ValueError): + s1_66.astype(to_dtype, casting='same_value') + + @pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf]) + @pytest.mark.filterwarnings("ignore::numpy.exceptions.ComplexWarning") + @pytest.mark.filterwarnings("ignore::RuntimeWarning") + def test_same_value_naninf(self, value): + # These work, but may trigger FPE warnings on macOS + np.array([value], dtype=np.half).astype(np.cdouble, casting='same_value') + np.array([value], dtype=np.half).astype(np.double, casting='same_value') + np.array([value], dtype=np.float32).astype(np.cdouble, casting='same_value') + np.array([value], dtype=np.float32).astype(np.double, casting='same_value') + np.array([value], dtype=np.float32).astype(np.half, casting='same_value') + np.array([value], dtype=np.complex64).astype(np.half, casting='same_value') + # These fail + with pytest.raises(ValueError): + np.array([value], dtype=np.half).astype(np.int64, casting='same_value') + with pytest.raises(ValueError): + np.array([value], dtype=np.complex64).astype(np.int64, casting='same_value') + with pytest.raises(ValueError): + np.array([value], dtype=np.float32).astype(np.int64, casting='same_value') + + @pytest.mark.filterwarnings("ignore::numpy.exceptions.ComplexWarning") + def test_same_value_complex(self): + arr = np.array([complex(1, 1)], dtype=np.cdouble) + # This works + arr.astype(np.complex64, casting='same_value') + # Casting with a non-zero imag part fails + with pytest.raises(ValueError): + arr.astype(np.float32, casting='same_value') + + def test_same_value_scalar(self): + i = np.array(123, dtype=np.int64) + f = np.array(123, dtype=np.float64) + assert i.astype(np.float64, casting='same_value') == f + assert f.astype(np.int64, casting='same_value') == f diff --git a/numpy/_core/tests/test_conversion_utils.py b/numpy/_core/tests/test_conversion_utils.py index d63ca9e58df5..0ce3cdc2bc00 100644 --- a/numpy/_core/tests/test_conversion_utils.py +++ b/numpy/_core/tests/test_conversion_utils.py @@ -172,9 +172,11 @@ def test_valid(self): self._check("no", "NPY_NO_CASTING") self._check("equiv", "NPY_EQUIV_CASTING") self._check("safe", "NPY_SAFE_CASTING") - self._check("same_kind", "NPY_SAME_KIND_CASTING") self._check("unsafe", "NPY_UNSAFE_CASTING") + def test_invalid(self): + # Currently, 'same_value' is supported only in ndarray.astype + self._check_value_error("same_value") class TestIntpConverter: """ Tests of PyArray_IntpConverter """ diff --git a/numpy/_core/tests/test_datetime.py b/numpy/_core/tests/test_datetime.py index a10ca15bc373..54c569a31cae 100644 --- a/numpy/_core/tests/test_datetime.py +++ b/numpy/_core/tests/test_datetime.py @@ -1844,6 +1844,10 @@ def test_datetime_as_string(self): '2032-07-18') assert_equal(np.datetime_as_string(a, unit='D', casting='unsafe'), '2032-07-18') + + with pytest.raises(ValueError): + np.datetime_as_string(a, unit='Y', casting='same_value') + assert_equal(np.datetime_as_string(a, unit='h'), '2032-07-18T12') assert_equal(np.datetime_as_string(a, unit='m'), '2032-07-18T12:23') diff --git a/numpy/_core/tests/test_einsum.py b/numpy/_core/tests/test_einsum.py index 84d4af1707b6..13d147651cd7 100644 --- a/numpy/_core/tests/test_einsum.py +++ b/numpy/_core/tests/test_einsum.py @@ -79,6 +79,11 @@ def test_einsum_errors(self, do_opt, einsum_fn): b = np.ones((3, 4, 5)) einsum_fn('aabcb,abc', a, b) + with pytest.raises(ValueError): + a = np.arange(3) + # einsum_path does not yet accept kwarg 'casting' + np.einsum('ij->j', [a, a], casting='same_value') + def test_einsum_sorting_behavior(self): # Case 1: 26 dimensions (all lowercase indices) n1 = 26 diff --git a/numpy/_core/tests/test_shape_base.py b/numpy/_core/tests/test_shape_base.py index 1b9728e5c006..87e2d864fc36 100644 --- a/numpy/_core/tests/test_shape_base.py +++ b/numpy/_core/tests/test_shape_base.py @@ -383,6 +383,11 @@ def test_concatenate(self): assert_(out is rout) assert_equal(res, rout) + def test_concatenate_same_value(self): + r4 = list(range(4)) + with pytest.raises(ValueError): + concatenate([r4, r4], casting="same_value") + @pytest.mark.skipif( IS_PYPY, reason="PYPY handles sq_concat, nb_add differently than cpython" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy