diff options
| author | Sebastian Berg <sebastian@sipsolutions.net> | 2021-12-20 13:27:40 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-12-20 13:27:40 -0600 |
| commit | 229c1757609efbbeea96b90746bcbe5ba41e0397 (patch) | |
| tree | 24af9054f4e543709195f9e298e51cd9c03dac03 /numpy | |
| parent | 1531820cec59939a34745c869d1365a85fc6869c (diff) | |
| download | numpy-229c1757609efbbeea96b90746bcbe5ba41e0397.tar.gz | |
MAINT: Use intp output param viewable casts/methods (#20176)
This removes the cast-is-view flag with a more generic intp output.
Such an output could (theoretically) be nonzero, indicating e.g.
the offset of a complex number into a real one.
(We do not use this yet, though!)
The only "tricky" part is that the MinCastSafety helper used to
deal with the view-offset as well, and now we have to deal with it
explicitly when e.g. multiple fields have to be checked.
Bumps the experimental-dtype-api number, since the signatures changed.
* MAINT,DOC: Cleanups and clarfications based on Marten's comments
Co-Authored-By: Marten van Kerkwijk <mhvk@astro.utoronto.ca>
* MAINT: Rename `PyArray_GetCastSafety` to `PyArray_GetCastInfo`
Better captures the fact that it also returns the view-offset
information now.
* MAINT: Fix structured cast-is-view logic
* MAINT: Address review comments by Marten
Co-authored-by: Marten van Kerkwijk <mhvk@astro.utoronto.ca>
Diffstat (limited to 'numpy')
| -rw-r--r-- | numpy/core/include/numpy/experimental_dtype_api.h | 22 | ||||
| -rw-r--r-- | numpy/core/include/numpy/ndarraytypes.h | 7 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/_multiarray_tests.c.src | 8 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/array_method.c | 60 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/array_method.h | 3 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/convert_datatype.c | 312 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/convert_datatype.h | 11 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/datetime.c | 33 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/dtype_transfer.c | 26 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/experimental_public_dtype_api.c | 2 | ||||
| -rw-r--r-- | numpy/core/src/multiarray/multiarraymodule.c | 3 | ||||
| -rw-r--r-- | numpy/core/src/umath/_scaled_float_dtype.c | 21 | ||||
| -rw-r--r-- | numpy/core/src/umath/legacy_array_method.c | 6 | ||||
| -rw-r--r-- | numpy/core/src/umath/legacy_array_method.h | 2 | ||||
| -rw-r--r-- | numpy/core/src/umath/ufunc_object.c | 12 | ||||
| -rw-r--r-- | numpy/core/tests/test_casting_unittests.py | 180 | ||||
| -rwxr-xr-x | numpy/testing/print_coercion_tables.py | 11 |
17 files changed, 509 insertions, 210 deletions
diff --git a/numpy/core/include/numpy/experimental_dtype_api.h b/numpy/core/include/numpy/experimental_dtype_api.h index effa66baf..83639f186 100644 --- a/numpy/core/include/numpy/experimental_dtype_api.h +++ b/numpy/core/include/numpy/experimental_dtype_api.h @@ -82,6 +82,15 @@ * The new DType API is designed in a way to make it potentially useful for * alternative "array-like" implementations. This will require careful * exposure of details and functions and is not part of this experimental API. + * + * Brief (incompatibility) changelog + * ================================= + * + * 2. None (only additions). + * 3. New `npy_intp *view_offset` argument for `resolve_descriptors`. + * This replaces the `NPY_CAST_IS_VIEW` flag. It can be set to 0 if the + * operation is a view, and is pre-initialized to `NPY_MIN_INTP` indicating + * that the operation is not a view. */ #ifndef NUMPY_CORE_INCLUDE_NUMPY_EXPERIMENTAL_DTYPE_API_H_ @@ -206,16 +215,6 @@ typedef int _ufunc_addpromoter_func( #define PyUFunc_AddPromoter \ (*(_ufunc_addpromoter_func *)(__experimental_dtype_api_table[1])) -/* - * In addition to the normal casting levels, NPY_CAST_IS_VIEW indicates - * that no cast operation is necessary at all (although a copy usually will be) - * - * NOTE: The most likely modification here is to add an additional - * `view_offset` output to resolve_descriptors. If set, it would - * indicate both that it is a view and what offset to use. This means that - * e.g. `arr.imag` could be implemented by an ArrayMethod. - */ -#define NPY_CAST_IS_VIEW _NPY_CAST_IS_VIEW /* * The resolve descriptors function, must be able to handle NULL values for @@ -236,7 +235,8 @@ typedef NPY_CASTING (resolve_descriptors_function)( /* Input descriptors (instances). Outputs may be NULL. */ PyArray_Descr **given_descrs, /* Exact loop descriptors to use, must not hold references on error */ - PyArray_Descr **loop_descrs); + PyArray_Descr **loop_descrs, + npy_intp *view_offset); /* NOT public yet: Signature needs adapting as external API. */ #define _NPY_METH_get_loop 2 diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index 0bd259983..47d063178 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -221,13 +221,6 @@ typedef enum { NPY_SAME_KIND_CASTING=3, /* Allow any casts */ NPY_UNSAFE_CASTING=4, - /* - * Flag to allow signalling that a cast is a view, this flag is not - * valid when requesting a cast of specific safety. - * _NPY_CAST_IS_VIEW|NPY_EQUIV_CASTING means the same as NPY_NO_CASTING. - */ - // TODO-DTYPES: Needs to be documented. - _NPY_CAST_IS_VIEW = 1 << 16, } NPY_CASTING; typedef enum { diff --git a/numpy/core/src/multiarray/_multiarray_tests.c.src b/numpy/core/src/multiarray/_multiarray_tests.c.src index 4c8f241f7..36937629d 100644 --- a/numpy/core/src/multiarray/_multiarray_tests.c.src +++ b/numpy/core/src/multiarray/_multiarray_tests.c.src @@ -1067,20 +1067,18 @@ get_all_cast_information(PyObject *NPY_UNUSED(mod), PyObject *NPY_UNUSED(args)) PyArrayMethodObject *cast = (PyArrayMethodObject *)cast_obj; /* Pass some information about this cast out! */ - PyObject *cast_info = Py_BuildValue("{sOsOsisisisisisssi}", + PyObject *cast_info = Py_BuildValue("{sOsOsisisisisiss}", "from", from_dtype, "to", to_dtype, "legacy", (cast->name != NULL && strncmp(cast->name, "legacy_", 7) == 0), - "casting", cast->casting & ~_NPY_CAST_IS_VIEW, + "casting", cast->casting, "requires_pyapi", cast->flags & NPY_METH_REQUIRES_PYAPI, "supports_unaligned", cast->flags & NPY_METH_SUPPORTS_UNALIGNED, "no_floatingpoint_errors", cast->flags & NPY_METH_NO_FLOATINGPOINT_ERRORS, - "name", cast->name, - "cast_is_view", - cast->casting & _NPY_CAST_IS_VIEW); + "name", cast->name); if (cast_info == NULL) { goto fail; } diff --git a/numpy/core/src/multiarray/array_method.c b/numpy/core/src/multiarray/array_method.c index d93dac506..b421d9e4f 100644 --- a/numpy/core/src/multiarray/array_method.c +++ b/numpy/core/src/multiarray/array_method.c @@ -48,13 +48,19 @@ * * We could allow setting the output descriptors specifically to simplify * this step. + * + * Note that the default version will indicate that the cast can be done + * as using `arr.view(new_dtype)` if the default cast-safety is + * set to "no-cast". This default function cannot be used if a view may + * be sufficient for casting but the cast is not always "no-cast". */ static NPY_CASTING default_resolve_descriptors( PyArrayMethodObject *method, PyArray_DTypeMeta **dtypes, PyArray_Descr **input_descrs, - PyArray_Descr **output_descrs) + PyArray_Descr **output_descrs, + npy_intp *view_offset) { int nin = method->nin; int nout = method->nout; @@ -76,6 +82,13 @@ default_resolve_descriptors( * abstract ones or unspecified outputs). We can use the common-dtype * operation to provide a default here. */ + if (method->casting == NPY_NO_CASTING) { + /* + * By (current) definition no-casting should imply viewable. This + * is currently indicated for example for object to object cast. + */ + *view_offset = 0; + } return method->casting; fail: @@ -102,9 +115,10 @@ is_contiguous( /** * The default method to fetch the correct loop for a cast or ufunc * (at the time of writing only casts). - * The default version can return loops explicitly registered during method - * creation. It does specialize contiguous loops, although has to check - * all descriptors itemsizes for this. + * Note that the default function provided here will only indicate that a cast + * can be done as a view (i.e., arr.view(new_dtype)) when this is trivially + * true, i.e., for cast safety "no-cast". It will not recognize view as an + * option for other casts (e.g., viewing '>i8' as '>i4' with an offset of 4). * * @param context * @param aligned @@ -166,7 +180,7 @@ validate_spec(PyArrayMethod_Spec *spec) "not exceed %d. (method: %s)", NPY_MAXARGS, spec->name); return -1; } - switch (spec->casting & ~_NPY_CAST_IS_VIEW) { + switch (spec->casting) { case NPY_NO_CASTING: case NPY_EQUIV_CASTING: case NPY_SAFE_CASTING: @@ -495,8 +509,9 @@ boundarraymethod_dealloc(PyObject *self) /* - * Calls resolve_descriptors() and returns the casting level and the resolved - * descriptors as a tuple. If the operation is impossible returns (-1, None). + * Calls resolve_descriptors() and returns the casting level, the resolved + * descriptors as a tuple, and a possible view-offset (integer or None). + * If the operation is impossible returns (-1, None, None). * May raise an error, but usually should not. * The function validates the casting attribute compared to the returned * casting level. @@ -551,14 +566,15 @@ boundarraymethod__resolve_descripors( } } + npy_intp view_offset = NPY_MIN_INTP; NPY_CASTING casting = self->method->resolve_descriptors( - self->method, self->dtypes, given_descrs, loop_descrs); + self->method, self->dtypes, given_descrs, loop_descrs, &view_offset); if (casting < 0 && PyErr_Occurred()) { return NULL; } else if (casting < 0) { - return Py_BuildValue("iO", casting, Py_None); + return Py_BuildValue("iO", casting, Py_None, Py_None); } PyObject *result_tuple = PyTuple_New(nin + nout); @@ -570,9 +586,22 @@ boundarraymethod__resolve_descripors( PyTuple_SET_ITEM(result_tuple, i, (PyObject *)loop_descrs[i]); } + PyObject *view_offset_obj; + if (view_offset == NPY_MIN_INTP) { + Py_INCREF(Py_None); + view_offset_obj = Py_None; + } + else { + view_offset_obj = PyLong_FromSsize_t(view_offset); + if (view_offset_obj == NULL) { + Py_DECREF(result_tuple); + return NULL; + } + } + /* - * The casting flags should be the most generic casting level (except the - * cast-is-view flag. If no input is parametric, it must match exactly. + * The casting flags should be the most generic casting level. + * If no input is parametric, it must match exactly. * * (Note that these checks are only debugging checks.) */ @@ -584,7 +613,7 @@ boundarraymethod__resolve_descripors( } } if (self->method->casting != -1) { - NPY_CASTING cast = casting & ~_NPY_CAST_IS_VIEW; + NPY_CASTING cast = casting; if (self->method->casting != PyArray_MinCastSafety(cast, self->method->casting)) { PyErr_Format(PyExc_RuntimeError, @@ -592,6 +621,7 @@ boundarraymethod__resolve_descripors( "(set level is %d, got %d for method %s)", self->method->casting, cast, self->method->name); Py_DECREF(result_tuple); + Py_DECREF(view_offset_obj); return NULL; } if (!parametric) { @@ -608,12 +638,13 @@ boundarraymethod__resolve_descripors( "(set level is %d, got %d for method %s)", self->method->casting, cast, self->method->name); Py_DECREF(result_tuple); + Py_DECREF(view_offset_obj); return NULL; } } } - return Py_BuildValue("iN", casting, result_tuple); + return Py_BuildValue("iNN", casting, result_tuple, view_offset_obj); } @@ -694,8 +725,9 @@ boundarraymethod__simple_strided_call( return NULL; } + npy_intp view_offset = NPY_MIN_INTP; NPY_CASTING casting = self->method->resolve_descriptors( - self->method, self->dtypes, descrs, out_descrs); + self->method, self->dtypes, descrs, out_descrs, &view_offset); if (casting < 0) { PyObject *err_type = NULL, *err_value = NULL, *err_traceback = NULL; diff --git a/numpy/core/src/multiarray/array_method.h b/numpy/core/src/multiarray/array_method.h index 7b7372bd0..35b9033e0 100644 --- a/numpy/core/src/multiarray/array_method.h +++ b/numpy/core/src/multiarray/array_method.h @@ -70,7 +70,8 @@ typedef NPY_CASTING (resolve_descriptors_function)( struct PyArrayMethodObject_tag *method, PyArray_DTypeMeta **dtypes, PyArray_Descr **given_descrs, - PyArray_Descr **loop_descrs); + PyArray_Descr **loop_descrs, + npy_intp *view_offset); typedef int (get_loop_function)( diff --git a/numpy/core/src/multiarray/convert_datatype.c b/numpy/core/src/multiarray/convert_datatype.c index 3135d6989..b21fc3cfa 100644 --- a/numpy/core/src/multiarray/convert_datatype.c +++ b/numpy/core/src/multiarray/convert_datatype.c @@ -223,14 +223,11 @@ PyArray_MinCastSafety(NPY_CASTING casting1, NPY_CASTING casting2) if (casting1 < 0 || casting2 < 0) { return -1; } - NPY_CASTING view = casting1 & casting2 & _NPY_CAST_IS_VIEW; - casting1 = casting1 & ~_NPY_CAST_IS_VIEW; - casting2 = casting2 & ~_NPY_CAST_IS_VIEW; /* larger casting values are less safe */ if (casting1 > casting2) { - return casting1 | view; + return casting1; } - return casting2 | view; + return casting2; } @@ -363,29 +360,41 @@ PyArray_CastAnyTo(PyArrayObject *out, PyArrayObject *mp) static NPY_CASTING _get_cast_safety_from_castingimpl(PyArrayMethodObject *castingimpl, - PyArray_DTypeMeta *dtypes[2], PyArray_Descr *from, PyArray_Descr *to) + PyArray_DTypeMeta *dtypes[2], PyArray_Descr *from, PyArray_Descr *to, + npy_intp *view_offset) { PyArray_Descr *descrs[2] = {from, to}; PyArray_Descr *out_descrs[2]; + *view_offset = NPY_MIN_INTP; NPY_CASTING casting = castingimpl->resolve_descriptors( - castingimpl, dtypes, descrs, out_descrs); + castingimpl, dtypes, descrs, out_descrs, view_offset); if (casting < 0) { return -1; } /* The returned descriptors may not match, requiring a second check */ if (out_descrs[0] != descrs[0]) { - NPY_CASTING from_casting = PyArray_GetCastSafety( - descrs[0], out_descrs[0], NULL); + npy_intp from_offset = NPY_MIN_INTP; + NPY_CASTING from_casting = PyArray_GetCastInfo( + descrs[0], out_descrs[0], NULL, &from_offset); casting = PyArray_MinCastSafety(casting, from_casting); + if (from_offset != *view_offset) { + /* `view_offset` differs: The multi-step cast cannot be a view. */ + *view_offset = NPY_MIN_INTP; + } if (casting < 0) { goto finish; } } if (descrs[1] != NULL && out_descrs[1] != descrs[1]) { - NPY_CASTING from_casting = PyArray_GetCastSafety( - descrs[1], out_descrs[1], NULL); + npy_intp from_offset = NPY_MIN_INTP; + NPY_CASTING from_casting = PyArray_GetCastInfo( + descrs[1], out_descrs[1], NULL, &from_offset); casting = PyArray_MinCastSafety(casting, from_casting); + if (from_offset != *view_offset) { + /* `view_offset` differs: The multi-step cast cannot be a view. */ + *view_offset = NPY_MIN_INTP; + } if (casting < 0) { goto finish; } @@ -396,15 +405,21 @@ _get_cast_safety_from_castingimpl(PyArrayMethodObject *castingimpl, Py_DECREF(out_descrs[1]); /* * Check for less harmful non-standard returns. The following two returns - * should never happen. They would be roughly equivalent, but less precise, - * versions of `(NPY_NO_CASTING|_NPY_CAST_IS_VIEW)`. - * 1. No-casting must imply cast-is-view. - * 2. Equivalent-casting + cast-is-view is (currently) the definition - * of a "no" cast (there may be reasons to relax this). - * Note that e.g. `(NPY_UNSAFE_CASTING|_NPY_CAST_IS_VIEW)` is valid. + * should never happen: + * 1. No-casting must imply a view offset of 0. + * 2. Equivalent-casting + 0 view offset is (usually) the definition + * of a "no" cast. However, changing the order of fields can also + * create descriptors that are not equivalent but views. + * Note that unsafe casts can have a view offset. For example, in + * principle, casting `<i8` to `<i4` is a cast with 0 offset. */ - assert(casting != NPY_NO_CASTING); - assert(casting != (NPY_EQUIV_CASTING|_NPY_CAST_IS_VIEW)); + if (*view_offset != 0) { + assert(casting != NPY_NO_CASTING); + } + else { + assert(casting != NPY_EQUIV_CASTING + || (PyDataType_HASFIELDS(from) && PyDataType_HASFIELDS(to))); + } return casting; } @@ -420,11 +435,13 @@ _get_cast_safety_from_castingimpl(PyArrayMethodObject *castingimpl, * @param to The descriptor to cast to (may be NULL) * @param to_dtype If `to` is NULL, must pass the to_dtype (otherwise this * is ignored). + * @param[out] view_offset * @return NPY_CASTING or -1 on error or if the cast is not possible. */ NPY_NO_EXPORT NPY_CASTING -PyArray_GetCastSafety( - PyArray_Descr *from, PyArray_Descr *to, PyArray_DTypeMeta *to_dtype) +PyArray_GetCastInfo( + PyArray_Descr *from, PyArray_Descr *to, PyArray_DTypeMeta *to_dtype, + npy_intp *view_offset) { if (to != NULL) { to_dtype = NPY_DTYPE(to); @@ -441,7 +458,7 @@ PyArray_GetCastSafety( PyArrayMethodObject *castingimpl = (PyArrayMethodObject *)meth; PyArray_DTypeMeta *dtypes[2] = {NPY_DTYPE(from), to_dtype}; NPY_CASTING casting = _get_cast_safety_from_castingimpl(castingimpl, - dtypes, from, to); + dtypes, from, to, view_offset); Py_DECREF(meth); return casting; @@ -449,8 +466,8 @@ PyArray_GetCastSafety( /** - * Check whether a cast is safe, see also `PyArray_GetCastSafety` for - * a similar function. Unlike GetCastSafety, this function checks the + * Check whether a cast is safe, see also `PyArray_GetCastInfo` for + * a similar function. Unlike GetCastInfo, this function checks the * `castingimpl->casting` when available. This allows for two things: * * 1. It avoids calling `resolve_descriptors` in some cases. @@ -493,8 +510,9 @@ PyArray_CheckCastSafety(NPY_CASTING casting, } PyArray_DTypeMeta *dtypes[2] = {NPY_DTYPE(from), to_dtype}; + npy_intp view_offset; NPY_CASTING safety = _get_cast_safety_from_castingimpl(castingimpl, - dtypes, from, to); + dtypes, from, to, &view_offset); Py_DECREF(meth); /* If casting is the smaller (or equal) safety we match */ if (safety < 0) { @@ -971,8 +989,9 @@ PyArray_CastDescrToDType(PyArray_Descr *descr, PyArray_DTypeMeta *given_DType) PyArray_Descr *loop_descrs[2]; PyArrayMethodObject *meth = (PyArrayMethodObject *)tmp; + npy_intp view_offset = NPY_MIN_INTP; NPY_CASTING casting = meth->resolve_descriptors( - meth, dtypes, given_descrs, loop_descrs); + meth, dtypes, given_descrs, loop_descrs, &view_offset); Py_DECREF(tmp); if (casting < 0) { goto error; @@ -2289,7 +2308,8 @@ legacy_same_dtype_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[2]), PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) { Py_INCREF(given_descrs[0]); loop_descrs[0] = given_descrs[0]; @@ -2315,7 +2335,8 @@ legacy_same_dtype_resolve_descriptors( */ if (PyDataType_ISNOTSWAPPED(loop_descrs[0]) == PyDataType_ISNOTSWAPPED(loop_descrs[1])) { - return NPY_NO_CASTING | _NPY_CAST_IS_VIEW; + *view_offset = 0; + return NPY_NO_CASTING; } return NPY_EQUIV_CASTING; } @@ -2354,7 +2375,8 @@ simple_cast_resolve_descriptors( PyArrayMethodObject *self, PyArray_DTypeMeta *dtypes[2], PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) { assert(NPY_DT_is_legacy(dtypes[0]) && NPY_DT_is_legacy(dtypes[1])); @@ -2378,7 +2400,8 @@ simple_cast_resolve_descriptors( } if (PyDataType_ISNOTSWAPPED(loop_descrs[0]) == PyDataType_ISNOTSWAPPED(loop_descrs[1])) { - return NPY_NO_CASTING | _NPY_CAST_IS_VIEW; + *view_offset = 0; + return NPY_NO_CASTING; } return NPY_EQUIV_CASTING; } @@ -2572,7 +2595,8 @@ cast_to_string_resolve_descriptors( PyArrayMethodObject *self, PyArray_DTypeMeta *dtypes[2], PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *NPY_UNUSED(view_offset)) { /* * NOTE: The following code used to be part of PyArray_AdaptFlexibleDType @@ -2723,7 +2747,8 @@ string_to_string_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[2]), PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) { Py_INCREF(given_descrs[0]); loop_descrs[0] = given_descrs[0]; @@ -2739,19 +2764,29 @@ string_to_string_resolve_descriptors( loop_descrs[1] = given_descrs[1]; } - if (loop_descrs[0]->elsize == loop_descrs[1]->elsize) { - if (PyDataType_ISNOTSWAPPED(loop_descrs[0]) == - PyDataType_ISNOTSWAPPED(loop_descrs[1])) { - return NPY_NO_CASTING | _NPY_CAST_IS_VIEW; + if (loop_descrs[0]->elsize < loop_descrs[1]->elsize) { + /* New string is longer: safe but cannot be a view */ + return NPY_SAFE_CASTING; + } + else { + /* New string fits into old: if the byte-order matches can be a view */ + int not_swapped = (PyDataType_ISNOTSWAPPED(loop_descrs[0]) + == PyDataType_ISNOTSWAPPED(loop_descrs[1])); + if (not_swapped) { + *view_offset = 0; + } + + if (loop_descrs[0]->elsize > loop_descrs[1]->elsize) { + return NPY_SAME_KIND_CASTING; + } + /* The strings have the same length: */ + if (not_swapped) { + return NPY_NO_CASTING; } else { return NPY_EQUIV_CASTING; } } - else if (loop_descrs[0]->elsize <= loop_descrs[1]->elsize) { - return NPY_SAFE_CASTING; - } - return NPY_SAME_KIND_CASTING; } @@ -2866,7 +2901,8 @@ PyArray_InitializeStringCasts(void) */ static NPY_CASTING cast_to_void_dtype_class( - PyArray_Descr **given_descrs, PyArray_Descr **loop_descrs) + PyArray_Descr **given_descrs, PyArray_Descr **loop_descrs, + npy_intp *view_offset) { /* `dtype="V"` means unstructured currently (compare final path) */ loop_descrs[1] = PyArray_DescrNewFromType(NPY_VOID); @@ -2876,11 +2912,13 @@ cast_to_void_dtype_class( loop_descrs[1]->elsize = given_descrs[0]->elsize; Py_INCREF(given_descrs[0]); loop_descrs[0] = given_descrs[0]; + + *view_offset = 0; if (loop_descrs[0]->type_num == NPY_VOID && loop_descrs[0]->subarray == NULL && loop_descrs[1]->names == NULL) { - return NPY_NO_CASTING | _NPY_CAST_IS_VIEW; + return NPY_NO_CASTING; } - return NPY_SAFE_CASTING | _NPY_CAST_IS_VIEW; + return NPY_SAFE_CASTING; } @@ -2889,12 +2927,13 @@ nonstructured_to_structured_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[2]), PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) { NPY_CASTING casting; if (given_descrs[1] == NULL) { - return cast_to_void_dtype_class(given_descrs, loop_descrs); + return cast_to_void_dtype_class(given_descrs, loop_descrs, view_offset); } if (given_descrs[1]->subarray != NULL) { @@ -2903,12 +2942,18 @@ nonstructured_to_structured_resolve_descriptors( * possible to allow a view if the field has exactly one element. */ casting = NPY_SAFE_CASTING; + npy_intp sub_view_offset = NPY_MIN_INTP; /* Subarray dtype */ - NPY_CASTING base_casting = PyArray_GetCastSafety( - given_descrs[0], given_descrs[1]->subarray->base, NULL); + NPY_CASTING base_casting = PyArray_GetCastInfo( + given_descrs[0], given_descrs[1]->subarray->base, NULL, + &sub_view_offset); if (base_casting < 0) { return -1; } + if (given_descrs[1]->elsize == given_descrs[1]->subarray->base->elsize) { + /* A single field, view is OK if sub-view is */ + *view_offset = sub_view_offset; + } casting = PyArray_MinCastSafety(casting, base_casting); } else if (given_descrs[1]->names != NULL) { @@ -2920,21 +2965,32 @@ nonstructured_to_structured_resolve_descriptors( else { /* Considered at most unsafe casting (but this could be changed) */ casting = NPY_UNSAFE_CASTING; - if (PyTuple_Size(given_descrs[1]->names) == 1) { - /* A view may be acceptable */ - casting |= _NPY_CAST_IS_VIEW; - } Py_ssize_t pos = 0; PyObject *key, *tuple; while (PyDict_Next(given_descrs[1]->fields, &pos, &key, &tuple)) { PyArray_Descr *field_descr = (PyArray_Descr *)PyTuple_GET_ITEM(tuple, 0); - NPY_CASTING field_casting = PyArray_GetCastSafety( - given_descrs[0], field_descr, NULL); + npy_intp field_view_off = NPY_MIN_INTP; + NPY_CASTING field_casting = PyArray_GetCastInfo( + given_descrs[0], field_descr, NULL, &field_view_off); casting = PyArray_MinCastSafety(casting, field_casting); if (casting < 0) { return -1; } + if (field_view_off != NPY_MIN_INTP) { + npy_intp to_off = PyLong_AsSsize_t(PyTuple_GET_ITEM(tuple, 1)); + if (error_converting(to_off)) { + return -1; + } + *view_offset = field_view_off - to_off; + } + } + if (PyTuple_Size(given_descrs[1]->names) != 1) { + /* + * Assume that a view is impossible when there is more than one + * field. (Fields could overlap, but that seems weird...) + */ + *view_offset = NPY_MIN_INTP; } } } @@ -2944,15 +3000,20 @@ nonstructured_to_structured_resolve_descriptors( !PyDataType_REFCHK(given_descrs[0])) { /* * A simple view, at the moment considered "safe" (the refcheck is - * probably not necessary, but more future proof + * probably not necessary, but more future proof) */ - casting = NPY_SAFE_CASTING | _NPY_CAST_IS_VIEW; + *view_offset = 0; + casting = NPY_SAFE_CASTING; } else if (given_descrs[0]->elsize <= given_descrs[1]->elsize) { casting = NPY_SAFE_CASTING; } else { casting = NPY_UNSAFE_CASTING; + /* new elsize is smaller so a view is OK (reject refs for now) */ + if (!PyDataType_REFCHK(given_descrs[0])) { + *view_offset = 0; + } } } @@ -3048,6 +3109,8 @@ PyArray_GetGenericToVoidCastingImpl(void) method->casting = -1; method->resolve_descriptors = &nonstructured_to_structured_resolve_descriptors; method->get_strided_loop = &nonstructured_to_structured_get_loop; + method->nin = 1; + method->nout = 1; return (PyObject *)method; } @@ -3058,12 +3121,19 @@ structured_to_nonstructured_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *dtypes[2], PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) { PyArray_Descr *base_descr; + /* The structured part may allow a view (and have its own offset): */ + npy_intp struct_view_offset = NPY_MIN_INTP; if (given_descrs[0]->subarray != NULL) { base_descr = given_descrs[0]->subarray->base; + /* A view is possible if the subarray has exactly one element: */ + if (given_descrs[0]->elsize == given_descrs[0]->subarray->base->elsize) { + struct_view_offset = 0; + } } else if (given_descrs[0]->names != NULL) { if (PyTuple_Size(given_descrs[0]->names) != 1) { @@ -3073,6 +3143,10 @@ structured_to_nonstructured_resolve_descriptors( PyObject *key = PyTuple_GetItem(given_descrs[0]->names, 0); PyObject *base_tup = PyDict_GetItem(given_descrs[0]->fields, key); base_descr = (PyArray_Descr *)PyTuple_GET_ITEM(base_tup, 0); + struct_view_offset = PyLong_AsSsize_t(PyTuple_GET_ITEM(base_tup, 1)); + if (error_converting(struct_view_offset)) { + return -1; + } } else { /* @@ -3080,20 +3154,29 @@ structured_to_nonstructured_resolve_descriptors( * at this time they go back to legacy behaviour using getitem/setitem. */ base_descr = NULL; + struct_view_offset = 0; } /* - * The cast is always considered unsafe, so the PyArray_GetCastSafety - * result currently does not matter. + * The cast is always considered unsafe, so the PyArray_GetCastInfo + * result currently only matters for the view_offset. */ - if (base_descr != NULL && PyArray_GetCastSafety( - base_descr, given_descrs[1], dtypes[1]) < 0) { + npy_intp base_view_offset = NPY_MIN_INTP; + if (base_descr != NULL && PyArray_GetCastInfo( + base_descr, given_descrs[1], dtypes[1], &base_view_offset) < 0) { return -1; } + if (base_view_offset != NPY_MIN_INTP + && struct_view_offset != NPY_MIN_INTP) { + *view_offset = base_view_offset + struct_view_offset; + } /* Void dtypes always do the full cast. */ if (given_descrs[1] == NULL) { loop_descrs[1] = NPY_DT_CALL_default_descr(dtypes[1]); + if (loop_descrs[1] == NULL) { + return -1; + } /* * Special case strings here, it should be useless (and only actually * work for empty arrays). Possibly this should simply raise for @@ -3187,6 +3270,8 @@ PyArray_GetVoidToGenericCastingImpl(void) method->casting = -1; method->resolve_descriptors = &structured_to_nonstructured_resolve_descriptors; method->get_strided_loop = &structured_to_nonstructured_get_loop; + method->nin = 1; + method->nout = 1; return (PyObject *)method; } @@ -3201,16 +3286,19 @@ PyArray_GetVoidToGenericCastingImpl(void) * implementations on the dtype, to avoid duplicate work. */ static NPY_CASTING -can_cast_fields_safety(PyArray_Descr *from, PyArray_Descr *to) +can_cast_fields_safety( + PyArray_Descr *from, PyArray_Descr *to, npy_intp *view_offset) { - NPY_CASTING casting = NPY_NO_CASTING | _NPY_CAST_IS_VIEW; - Py_ssize_t field_count = PyTuple_Size(from->names); if (field_count != PyTuple_Size(to->names)) { /* TODO: This should be rejected! */ return NPY_UNSAFE_CASTING; } + + NPY_CASTING casting = NPY_NO_CASTING; + *view_offset = 0; /* if there are no fields, a view is OK. */ for (Py_ssize_t i = 0; i < field_count; i++) { + npy_intp field_view_off = NPY_MIN_INTP; PyObject *from_key = PyTuple_GET_ITEM(from->names, i); PyObject *from_tup = PyDict_GetItemWithError(from->fields, from_key); if (from_tup == NULL) { @@ -3229,15 +3317,40 @@ can_cast_fields_safety(PyArray_Descr *from, PyArray_Descr *to) } PyArray_Descr *to_base = (PyArray_Descr*)PyTuple_GET_ITEM(to_tup, 0); - NPY_CASTING field_casting = PyArray_GetCastSafety(from_base, to_base, NULL); + NPY_CASTING field_casting = PyArray_GetCastInfo( + from_base, to_base, NULL, &field_view_off); if (field_casting < 0) { return -1; } casting = PyArray_MinCastSafety(casting, field_casting); + + /* Adjust the "view offset" by the field offsets: */ + if (field_view_off != NPY_MIN_INTP) { + npy_intp to_off = PyLong_AsSsize_t(PyTuple_GET_ITEM(to_tup, 1)); + if (error_converting(to_off)) { + return -1; + } + npy_intp from_off = PyLong_AsSsize_t(PyTuple_GET_ITEM(from_tup, 1)); + if (error_converting(from_off)) { + return -1; + } + field_view_off = field_view_off - to_off + from_off; + } + + /* + * If there is one field, use its field offset. After that propagate + * the view offset if they match and set to "invalid" if not. + */ + if (i == 0) { + *view_offset = field_view_off; + } + else if (*view_offset != field_view_off) { + *view_offset = NPY_MIN_INTP; + } } - if (!(casting & _NPY_CAST_IS_VIEW)) { - assert((casting & ~_NPY_CAST_IS_VIEW) != NPY_NO_CASTING); - return casting; + if (*view_offset != 0) { + /* If the calculated `view_offset` is not 0, it can only be "equiv" */ + return PyArray_MinCastSafety(casting, NPY_EQUIV_CASTING); } /* @@ -3277,38 +3390,42 @@ void_to_void_resolve_descriptors( PyArrayMethodObject *self, PyArray_DTypeMeta *dtypes[2], PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) { NPY_CASTING casting; if (given_descrs[1] == NULL) { /* This is weird, since it doesn't return the original descr, but... */ - return cast_to_void_dtype_class(given_descrs, loop_descrs); + return cast_to_void_dtype_class(given_descrs, loop_descrs, view_offset); } if (given_descrs[0]->names != NULL && given_descrs[1]->names != NULL) { /* From structured to structured, need to check fields */ - casting = can_cast_fields_safety(given_descrs[0], given_descrs[1]); + casting = can_cast_fields_safety( + given_descrs[0], given_descrs[1], view_offset); } else if (given_descrs[0]->names != NULL) { return structured_to_nonstructured_resolve_descriptors( - self, dtypes, given_descrs, loop_descrs); + self, dtypes, given_descrs, loop_descrs, view_offset); } else if (given_descrs[1]->names != NULL) { return nonstructured_to_structured_resolve_descriptors( - self, dtypes, given_descrs, loop_descrs); + self, dtypes, given_descrs, loop_descrs, view_offset); } else if (given_descrs[0]->subarray == NULL && given_descrs[1]->subarray == NULL) { /* Both are plain void dtypes */ if (given_descrs[0]->elsize == given_descrs[1]->elsize) { - casting = NPY_NO_CASTING | _NPY_CAST_IS_VIEW; + casting = NPY_NO_CASTING; + *view_offset = 0; } else if (given_descrs[0]->elsize < given_descrs[1]->elsize) { casting = NPY_SAFE_CASTING; } else { casting = NPY_SAME_KIND_CASTING; + *view_offset = 0; } } else { @@ -3322,20 +3439,51 @@ void_to_void_resolve_descriptors( /* If the shapes do not match, this is at most an unsafe cast */ casting = NPY_UNSAFE_CASTING; + /* + * We can use a view in two cases: + * 1. The shapes and elsizes matches, so any view offset applies to + * each element of the subarray identically. + * (in practice this probably implies the `view_offset` will be 0) + * 2. There is exactly one element and the subarray has no effect + * (can be tested by checking if the itemsizes of the base matches) + */ + npy_bool subarray_layout_supports_view = NPY_FALSE; if (from_sub && to_sub) { int res = PyObject_RichCompareBool(from_sub->shape, to_sub->shape, Py_EQ); if (res < 0) { return -1; } else if (res) { - /* Both are subarrays and the shape matches */ - casting = NPY_NO_CASTING | _NPY_CAST_IS_VIEW; + /* Both are subarrays and the shape matches, could be no cast */ + casting = NPY_NO_CASTING; + /* May be a view if there is one element or elsizes match */ + if (from_sub->base->elsize == to_sub->base->elsize + || given_descrs[0]->elsize == from_sub->base->elsize) { + subarray_layout_supports_view = NPY_TRUE; + } + } + } + else if (from_sub) { + /* May use a view if "from" has only a single element: */ + if (given_descrs[0]->elsize == from_sub->base->elsize) { + subarray_layout_supports_view = NPY_TRUE; + } + } + else { + /* May use a view if "from" has only a single element: */ + if (given_descrs[1]->elsize == to_sub->base->elsize) { + subarray_layout_supports_view = NPY_TRUE; } } PyArray_Descr *from_base = (from_sub == NULL) ? given_descrs[0] : from_sub->base; PyArray_Descr *to_base = (to_sub == NULL) ? given_descrs[1] : to_sub->base; - NPY_CASTING field_casting = PyArray_GetCastSafety(from_base, to_base, NULL); + /* An offset for */ + NPY_CASTING field_casting = PyArray_GetCastInfo( + from_base, to_base, NULL, view_offset); + if (!subarray_layout_supports_view) { + *view_offset = NPY_MIN_INTP; + } if (field_casting < 0) { return -1; } @@ -3443,7 +3591,8 @@ object_to_any_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *dtypes[2], PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *NPY_UNUSED(view_offset)) { if (given_descrs[1] == NULL) { /* @@ -3513,7 +3662,8 @@ any_to_object_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *dtypes[2], PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *NPY_UNUSED(view_offset)) { if (given_descrs[1] == NULL) { loop_descrs[1] = NPY_DT_CALL_default_descr(dtypes[1]); @@ -3591,10 +3741,6 @@ object_to_object_get_loop( static int PyArray_InitializeObjectToObjectCast(void) { - /* - * The object dtype does not support byte order changes, so its cast - * is always a direct view. - */ PyArray_DTypeMeta *Object = PyArray_DTypeFromTypeNum(NPY_OBJECT); PyArray_DTypeMeta *dtypes[2] = {Object, Object}; PyType_Slot slots[] = { @@ -3602,7 +3748,7 @@ PyArray_InitializeObjectToObjectCast(void) {0, NULL}}; PyArrayMethod_Spec spec = { .name = "object_to_object_cast", - .casting = NPY_NO_CASTING | _NPY_CAST_IS_VIEW, + .casting = NPY_NO_CASTING, .nin = 1, .nout = 1, .flags = NPY_METH_REQUIRES_PYAPI | NPY_METH_SUPPORTS_UNALIGNED, diff --git a/numpy/core/src/multiarray/convert_datatype.h b/numpy/core/src/multiarray/convert_datatype.h index 5e0682f22..6b4413959 100644 --- a/numpy/core/src/multiarray/convert_datatype.h +++ b/numpy/core/src/multiarray/convert_datatype.h @@ -68,8 +68,9 @@ NPY_NO_EXPORT NPY_CASTING PyArray_MinCastSafety(NPY_CASTING casting1, NPY_CASTING casting2); NPY_NO_EXPORT NPY_CASTING -PyArray_GetCastSafety( - PyArray_Descr *from, PyArray_Descr *to, PyArray_DTypeMeta *to_dtype); +PyArray_GetCastInfo( + PyArray_Descr *from, PyArray_Descr *to, PyArray_DTypeMeta *to_dtype, + npy_intp *view_offset); NPY_NO_EXPORT int PyArray_CheckCastSafety(NPY_CASTING casting, @@ -80,7 +81,8 @@ legacy_same_dtype_resolve_descriptors( PyArrayMethodObject *self, PyArray_DTypeMeta *dtypes[2], PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]); + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset); NPY_NO_EXPORT int legacy_cast_get_strided_loop( @@ -94,7 +96,8 @@ simple_cast_resolve_descriptors( PyArrayMethodObject *self, PyArray_DTypeMeta *dtypes[2], PyArray_Descr *input_descrs[2], - PyArray_Descr *loop_descrs[2]); + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset); NPY_NO_EXPORT int PyArray_InitializeCasts(void); diff --git a/numpy/core/src/multiarray/datetime.c b/numpy/core/src/multiarray/datetime.c index e0064c017..03ebaa7ce 100644 --- a/numpy/core/src/multiarray/datetime.c +++ b/numpy/core/src/multiarray/datetime.c @@ -3746,8 +3746,6 @@ find_object_datetime_type(PyObject *obj, int type_num) } - - /* * Describes casting within datetimes or timedelta */ @@ -3756,7 +3754,8 @@ time_to_time_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[2]), PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) { /* This is a within-dtype cast, which currently must handle byteswapping */ Py_INCREF(given_descrs[0]); @@ -3772,14 +3771,14 @@ time_to_time_resolve_descriptors( int is_timedelta = given_descrs[0]->type_num == NPY_TIMEDELTA; if (given_descrs[0] == given_descrs[1]) { - return NPY_NO_CASTING | _NPY_CAST_IS_VIEW; + *view_offset = 0; + return NPY_NO_CASTING; } - NPY_CASTING byteorder_may_allow_view = 0; - if (PyDataType_ISNOTSWAPPED(loop_descrs[0]) == - PyDataType_ISNOTSWAPPED(loop_descrs[1])) { - byteorder_may_allow_view = _NPY_CAST_IS_VIEW; - } + npy_bool byteorder_may_allow_view = ( + PyDataType_ISNOTSWAPPED(loop_descrs[0]) + == PyDataType_ISNOTSWAPPED(loop_descrs[1])); + PyArray_DatetimeMetaData *meta1, *meta2; meta1 = get_datetime_metadata_from_dtype(loop_descrs[0]); assert(meta1 != NULL); @@ -3798,12 +3797,16 @@ time_to_time_resolve_descriptors( ((meta2->base >= 7) && (meta1->base - meta2->base == 3) && ((meta1->num / meta2->num) == 1000000000))) { if (byteorder_may_allow_view) { - return NPY_NO_CASTING | byteorder_may_allow_view; + *view_offset = 0; + return NPY_NO_CASTING; } return NPY_EQUIV_CASTING; } else if (meta1->base == NPY_FR_GENERIC) { - return NPY_SAFE_CASTING | byteorder_may_allow_view; + if (byteorder_may_allow_view) { + *view_offset = 0; + } + return NPY_SAFE_CASTING ; } else if (meta2->base == NPY_FR_GENERIC) { /* TODO: This is actually an invalid cast (casting will error) */ @@ -3931,10 +3934,11 @@ datetime_to_timedelta_resolve_descriptors( /* In the current setup both strings and unicode casts support all outputs */ static NPY_CASTING time_to_string_resolve_descriptors( - PyArrayMethodObject *self, + PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *dtypes[2], PyArray_Descr **given_descrs, - PyArray_Descr **loop_descrs) + PyArray_Descr **loop_descrs, + npy_intp *NPY_UNUSED(view_offset)) { if (given_descrs[1] != NULL && dtypes[0]->type_num == NPY_DATETIME) { /* @@ -4013,7 +4017,8 @@ string_to_datetime_cast_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *dtypes[2], PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *NPY_UNUSED(view_offset)) { if (given_descrs[1] == NULL) { /* NOTE: This doesn't actually work, and will error during the cast */ diff --git a/numpy/core/src/multiarray/dtype_transfer.c b/numpy/core/src/multiarray/dtype_transfer.c index 8fb44c4f6..4877f8dfa 100644 --- a/numpy/core/src/multiarray/dtype_transfer.c +++ b/numpy/core/src/multiarray/dtype_transfer.c @@ -2991,7 +2991,8 @@ _strided_to_strided_multistep_cast( * transferfunction and transferdata. */ static NPY_INLINE int -init_cast_info(NPY_cast_info *cast_info, NPY_CASTING *casting, +init_cast_info( + NPY_cast_info *cast_info, NPY_CASTING *casting, npy_intp *view_offset, PyArray_Descr *src_dtype, PyArray_Descr *dst_dtype, int main_step) { PyObject *meth = PyArray_GetCastingImpl( @@ -3016,7 +3017,8 @@ init_cast_info(NPY_cast_info *cast_info, NPY_CASTING *casting, PyArray_Descr *in_descr[2] = {src_dtype, dst_dtype}; *casting = cast_info->context.method->resolve_descriptors( - cast_info->context.method, dtypes, in_descr, cast_info->descriptors); + cast_info->context.method, dtypes, + in_descr, cast_info->descriptors, view_offset); if (NPY_UNLIKELY(*casting < 0)) { if (!PyErr_Occurred()) { PyErr_Format(PyExc_TypeError, @@ -3071,6 +3073,9 @@ _clear_cast_info_after_get_loop_failure(NPY_cast_info *cast_info) * transfer function from the each casting implementation (ArrayMethod). * May set the transfer function to NULL when the cast can be achieved using * a view. + * 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 @@ -3101,9 +3106,12 @@ define_cast_for_descrs( castdata.main.func = NULL; castdata.to.func = NULL; castdata.from.func = NULL; + /* `view_offset` passed to `init_cast_info` but unused for the main cast */ + npy_intp view_offset = NPY_MIN_INTP; NPY_CASTING casting = -1; - if (init_cast_info(cast_info, &casting, src_dtype, dst_dtype, 1) < 0) { + if (init_cast_info( + cast_info, &casting, &view_offset, src_dtype, dst_dtype, 1) < 0) { return -1; } @@ -3123,17 +3131,18 @@ define_cast_for_descrs( */ if (NPY_UNLIKELY(src_dtype != cast_info->descriptors[0] || must_wrap)) { NPY_CASTING from_casting = -1; + npy_intp from_view_offset = NPY_MIN_INTP; /* Cast function may not support the input, wrap if necessary */ if (init_cast_info( - &castdata.from, &from_casting, + &castdata.from, &from_casting, &from_view_offset, src_dtype, cast_info->descriptors[0], 0) < 0) { goto fail; } casting = PyArray_MinCastSafety(casting, from_casting); /* Prepare the actual cast (if necessary): */ - if (from_casting & _NPY_CAST_IS_VIEW && !must_wrap) { - /* This step is not necessary and can be skipped. */ + if (from_view_offset == 0 && !must_wrap) { + /* This step is not necessary and can be skipped */ castdata.from.func = &_dec_src_ref_nop; /* avoid NULL */ NPY_cast_info_xfree(&castdata.from); } @@ -3161,16 +3170,17 @@ define_cast_for_descrs( */ if (NPY_UNLIKELY(dst_dtype != cast_info->descriptors[1] || must_wrap)) { NPY_CASTING to_casting = -1; + npy_intp to_view_offset = NPY_MIN_INTP; /* Cast function may not support the output, wrap if necessary */ if (init_cast_info( - &castdata.to, &to_casting, + &castdata.to, &to_casting, &to_view_offset, cast_info->descriptors[1], dst_dtype, 0) < 0) { goto fail; } casting = PyArray_MinCastSafety(casting, to_casting); /* Prepare the actual cast (if necessary): */ - if (to_casting & _NPY_CAST_IS_VIEW && !must_wrap) { + if (to_view_offset == 0 && !must_wrap) { /* This step is not necessary and can be skipped. */ castdata.to.func = &_dec_src_ref_nop; /* avoid NULL */ NPY_cast_info_xfree(&castdata.to); diff --git a/numpy/core/src/multiarray/experimental_public_dtype_api.c b/numpy/core/src/multiarray/experimental_public_dtype_api.c index 4b9c7199b..e9d191002 100644 --- a/numpy/core/src/multiarray/experimental_public_dtype_api.c +++ b/numpy/core/src/multiarray/experimental_public_dtype_api.c @@ -16,7 +16,7 @@ #include "common_dtype.h" -#define EXPERIMENTAL_DTYPE_API_VERSION 2 +#define EXPERIMENTAL_DTYPE_API_VERSION 3 typedef struct{ diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index d1627d639..789446d0c 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -1500,7 +1500,8 @@ PyArray_EquivTypes(PyArray_Descr *type1, PyArray_Descr *type2) * Do not use PyArray_CanCastTypeTo because it supports legacy flexible * dtypes as input. */ - NPY_CASTING safety = PyArray_GetCastSafety(type1, type2, NULL); + npy_intp view_offset; + NPY_CASTING safety = PyArray_GetCastInfo(type1, type2, NULL, &view_offset); if (safety < 0) { PyErr_Clear(); return 0; diff --git a/numpy/core/src/umath/_scaled_float_dtype.c b/numpy/core/src/umath/_scaled_float_dtype.c index b6c19362a..a214b32aa 100644 --- a/numpy/core/src/umath/_scaled_float_dtype.c +++ b/numpy/core/src/umath/_scaled_float_dtype.c @@ -325,7 +325,8 @@ sfloat_to_sfloat_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[2]), PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) { loop_descrs[0] = given_descrs[0]; Py_INCREF(loop_descrs[0]); @@ -341,7 +342,8 @@ sfloat_to_sfloat_resolve_descriptors( if (((PyArray_SFloatDescr *)loop_descrs[0])->scaling == ((PyArray_SFloatDescr *)loop_descrs[1])->scaling) { /* same scaling is just a view */ - return NPY_NO_CASTING | _NPY_CAST_IS_VIEW; + *view_offset = 0; + return NPY_NO_CASTING; } else if (-((PyArray_SFloatDescr *)loop_descrs[0])->scaling == ((PyArray_SFloatDescr *)loop_descrs[1])->scaling) { @@ -384,7 +386,8 @@ float_to_from_sfloat_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *dtypes[2], PyArray_Descr *NPY_UNUSED(given_descrs[2]), - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) { loop_descrs[0] = NPY_DT_CALL_default_descr(dtypes[0]); if (loop_descrs[0] == NULL) { @@ -394,7 +397,8 @@ float_to_from_sfloat_resolve_descriptors( if (loop_descrs[1] == NULL) { return -1; } - return NPY_NO_CASTING | _NPY_CAST_IS_VIEW; + *view_offset = 0; + return NPY_NO_CASTING; } @@ -422,7 +426,8 @@ sfloat_to_bool_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[2]), PyArray_Descr *given_descrs[2], - PyArray_Descr *loop_descrs[2]) + PyArray_Descr *loop_descrs[2], + npy_intp *NPY_UNUSED(view_offset)) { Py_INCREF(given_descrs[0]); loop_descrs[0] = given_descrs[0]; @@ -541,7 +546,8 @@ multiply_sfloats_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[3]), PyArray_Descr *given_descrs[3], - PyArray_Descr *loop_descrs[3]) + PyArray_Descr *loop_descrs[3], + npy_intp *NPY_UNUSED(view_offset)) { /* * Multiply the scaling for the result. If the result was passed in we @@ -602,7 +608,8 @@ add_sfloats_resolve_descriptors( PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[3]), PyArray_Descr *given_descrs[3], - PyArray_Descr *loop_descrs[3]) + PyArray_Descr *loop_descrs[3], + npy_intp *NPY_UNUSED(view_offset)) { /* * Here we accept an output descriptor (the inner loop can deal with it), diff --git a/numpy/core/src/umath/legacy_array_method.c b/numpy/core/src/umath/legacy_array_method.c index ef24edff1..f4b2aed96 100644 --- a/numpy/core/src/umath/legacy_array_method.c +++ b/numpy/core/src/umath/legacy_array_method.c @@ -103,7 +103,8 @@ NPY_NO_EXPORT NPY_CASTING wrapped_legacy_resolve_descriptors(PyArrayMethodObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[]), PyArray_Descr *NPY_UNUSED(given_descrs[]), - PyArray_Descr *NPY_UNUSED(loop_descrs[])) + PyArray_Descr *NPY_UNUSED(loop_descrs[]), + npy_intp *NPY_UNUSED(view_offset)) { PyErr_SetString(PyExc_RuntimeError, "cannot use legacy wrapping ArrayMethod without calling the ufunc " @@ -121,7 +122,8 @@ simple_legacy_resolve_descriptors( PyArrayMethodObject *method, PyArray_DTypeMeta **dtypes, PyArray_Descr **given_descrs, - PyArray_Descr **output_descrs) + PyArray_Descr **output_descrs, + npy_intp *NPY_UNUSED(view_offset)) { int i = 0; int nin = method->nin; diff --git a/numpy/core/src/umath/legacy_array_method.h b/numpy/core/src/umath/legacy_array_method.h index 0dec1fb3a..d20b4fb08 100644 --- a/numpy/core/src/umath/legacy_array_method.h +++ b/numpy/core/src/umath/legacy_array_method.h @@ -27,7 +27,7 @@ get_wrapped_legacy_ufunc_loop(PyArrayMethod_Context *context, NPY_NO_EXPORT NPY_CASTING wrapped_legacy_resolve_descriptors(PyArrayMethodObject *, - PyArray_DTypeMeta **, PyArray_Descr **, PyArray_Descr **); + PyArray_DTypeMeta **, PyArray_Descr **, PyArray_Descr **, npy_intp *); #endif /*_NPY_LEGACY_ARRAY_METHOD_H */ diff --git a/numpy/core/src/umath/ufunc_object.c b/numpy/core/src/umath/ufunc_object.c index 78f6f4b5a..52b354353 100644 --- a/numpy/core/src/umath/ufunc_object.c +++ b/numpy/core/src/umath/ufunc_object.c @@ -1073,13 +1073,15 @@ check_for_trivial_loop(PyArrayMethodObject *ufuncimpl, int must_copy = !PyArray_ISALIGNED(op[i]); if (dtypes[i] != PyArray_DESCR(op[i])) { - NPY_CASTING safety = PyArray_GetCastSafety( - PyArray_DESCR(op[i]), dtypes[i], NULL); + npy_intp view_offset; + NPY_CASTING safety = PyArray_GetCastInfo( + PyArray_DESCR(op[i]), dtypes[i], NULL, &view_offset); if (safety < 0 && PyErr_Occurred()) { /* A proper error during a cast check, should be rare */ return -1; } - if (!(safety & _NPY_CAST_IS_VIEW)) { + if (view_offset != 0) { + /* NOTE: Could possibly implement non-zero view offsets */ must_copy = 1; } @@ -4508,8 +4510,10 @@ resolve_descriptors(int nop, if (ufuncimpl->resolve_descriptors != &wrapped_legacy_resolve_descriptors) { /* The default: use the `ufuncimpl` as nature intended it */ + npy_intp view_offset = NPY_MIN_INTP; /* currently ignored */ + NPY_CASTING safety = ufuncimpl->resolve_descriptors(ufuncimpl, - signature, original_dtypes, dtypes); + signature, original_dtypes, dtypes, &view_offset); if (safety < 0) { goto finish; } diff --git a/numpy/core/tests/test_casting_unittests.py b/numpy/core/tests/test_casting_unittests.py index cb4792090..a57e46fd0 100644 --- a/numpy/core/tests/test_casting_unittests.py +++ b/numpy/core/tests/test_casting_unittests.py @@ -76,7 +76,6 @@ class Casting(enum.IntEnum): safe = 2 same_kind = 3 unsafe = 4 - cast_is_view = 1 << 16 def _get_cancast_table(): @@ -259,14 +258,14 @@ class TestCasting: del default for to_dt in [to_Dt(), to_Dt().newbyteorder()]: - casting, (from_res, to_res) = cast._resolve_descriptors( - (from_dt, to_dt)) + casting, (from_res, to_res), view_off = ( + cast._resolve_descriptors((from_dt, to_dt))) assert(type(from_res) == from_Dt) assert(type(to_res) == to_Dt) - if casting & Casting.cast_is_view: + if view_off is not None: # If a view is acceptable, this is "no" casting # and byte order must be matching. - assert casting == Casting.no | Casting.cast_is_view + assert casting == Casting.no # The above table lists this as "equivalent" assert Casting.equiv == CAST_TABLE[from_Dt][to_Dt] # Note that to_res may not be the same as from_dt @@ -299,7 +298,7 @@ class TestCasting: to_dt = to_dt.values[0] cast = get_castingimpl(type(from_dt), type(to_dt)) - casting, (from_res, to_res) = cast._resolve_descriptors( + casting, (from_res, to_res), view_off = cast._resolve_descriptors( (from_dt, to_dt)) if from_res is not from_dt or to_res is not to_dt: @@ -307,7 +306,7 @@ class TestCasting: # each of which should is tested individually. return - safe = (casting & ~Casting.cast_is_view) <= Casting.safe + safe = casting <= Casting.safe del from_res, to_res, casting arr1, arr2, values = self.get_data(from_dt, to_dt) @@ -355,14 +354,15 @@ class TestCasting: for time_dt in time_dtypes: cast = get_castingimpl(type(from_dt), type(time_dt)) - casting, (from_res, to_res) = cast._resolve_descriptors( + casting, (from_res, to_res), view_off = cast._resolve_descriptors( (from_dt, time_dt)) assert from_res is from_dt assert to_res is time_dt del from_res, to_res - assert(casting & CAST_TABLE[from_Dt][type(time_dt)]) + assert casting & CAST_TABLE[from_Dt][type(time_dt)] + assert view_off is None int64_dt = np.dtype(np.int64) arr1, arr2, values = self.get_data(from_dt, int64_dt) @@ -391,31 +391,37 @@ class TestCasting: assert arr2_o.tobytes() == arr2.tobytes() @pytest.mark.parametrize( - ["from_dt", "to_dt", "expected_casting", "nom", "denom"], - [("M8[ns]", None, - Casting.no | Casting.cast_is_view, 1, 1), - (str(np.dtype("M8[ns]").newbyteorder()), None, Casting.equiv, 1, 1), - ("M8", "M8[ms]", Casting.safe | Casting.cast_is_view, 1, 1), - ("M8[ms]", "M8", Casting.unsafe, 1, 1), # should be invalid cast - ("M8[5ms]", "M8[5ms]", Casting.no | Casting.cast_is_view, 1, 1), - ("M8[ns]", "M8[ms]", Casting.same_kind, 1, 10**6), - ("M8[ms]", "M8[ns]", Casting.safe, 10**6, 1), - ("M8[ms]", "M8[7ms]", Casting.same_kind, 1, 7), - ("M8[4D]", "M8[1M]", Casting.same_kind, None, + ["from_dt", "to_dt", "expected_casting", "expected_view_off", + "nom", "denom"], + [("M8[ns]", None, Casting.no, 0, 1, 1), + (str(np.dtype("M8[ns]").newbyteorder()), None, + Casting.equiv, None, 1, 1), + ("M8", "M8[ms]", Casting.safe, 0, 1, 1), + # should be invalid cast: + ("M8[ms]", "M8", Casting.unsafe, None, 1, 1), + ("M8[5ms]", "M8[5ms]", Casting.no, 0, 1, 1), + ("M8[ns]", "M8[ms]", Casting.same_kind, None, 1, 10**6), + ("M8[ms]", "M8[ns]", Casting.safe, None, 10**6, 1), + ("M8[ms]", "M8[7ms]", Casting.same_kind, None, 1, 7), + ("M8[4D]", "M8[1M]", Casting.same_kind, None, None, # give full values based on NumPy 1.19.x [-2**63, 0, -1, 1314, -1315, 564442610]), - ("m8[ns]", None, Casting.no | Casting.cast_is_view, 1, 1), - (str(np.dtype("m8[ns]").newbyteorder()), None, Casting.equiv, 1, 1), - ("m8", "m8[ms]", Casting.safe | Casting.cast_is_view, 1, 1), - ("m8[ms]", "m8", Casting.unsafe, 1, 1), # should be invalid cast - ("m8[5ms]", "m8[5ms]", Casting.no | Casting.cast_is_view, 1, 1), - ("m8[ns]", "m8[ms]", Casting.same_kind, 1, 10**6), - ("m8[ms]", "m8[ns]", Casting.safe, 10**6, 1), - ("m8[ms]", "m8[7ms]", Casting.same_kind, 1, 7), - ("m8[4D]", "m8[1M]", Casting.unsafe, None, + ("m8[ns]", None, Casting.no, 0, 1, 1), + (str(np.dtype("m8[ns]").newbyteorder()), None, + Casting.equiv, None, 1, 1), + ("m8", "m8[ms]", Casting.safe, 0, 1, 1), + # should be invalid cast: + ("m8[ms]", "m8", Casting.unsafe, None, 1, 1), + ("m8[5ms]", "m8[5ms]", Casting.no, 0, 1, 1), + ("m8[ns]", "m8[ms]", Casting.same_kind, None, 1, 10**6), + ("m8[ms]", "m8[ns]", Casting.safe, None, 10**6, 1), + ("m8[ms]", "m8[7ms]", Casting.same_kind, None, 1, 7), + ("m8[4D]", "m8[1M]", Casting.unsafe, None, None, # give full values based on NumPy 1.19.x [-2**63, 0, 0, 1314, -1315, 564442610])]) - def test_time_to_time(self, from_dt, to_dt, expected_casting, nom, denom): + def test_time_to_time(self, from_dt, to_dt, + expected_casting, expected_view_off, + nom, denom): from_dt = np.dtype(from_dt) if to_dt is not None: to_dt = np.dtype(to_dt) @@ -428,10 +434,12 @@ class TestCasting: DType = type(from_dt) cast = get_castingimpl(DType, DType) - casting, (from_res, to_res) = cast._resolve_descriptors((from_dt, to_dt)) + casting, (from_res, to_res), view_off = cast._resolve_descriptors( + (from_dt, to_dt)) assert from_res is from_dt assert to_res is to_dt or to_dt is None assert casting == expected_casting + assert view_off == expected_view_off if nom is not None: expected_out = (values * nom // denom).view(to_res) @@ -476,9 +484,11 @@ class TestCasting: expected_length = get_expected_stringlength(other_dt) string_dt = np.dtype(f"{string_char}{expected_length}") - safety, (res_other_dt, res_dt) = cast._resolve_descriptors((other_dt, None)) + safety, (res_other_dt, res_dt), view_off = cast._resolve_descriptors( + (other_dt, None)) assert res_dt.itemsize == expected_length * fact assert safety == Casting.safe # we consider to string casts "safe" + assert view_off is None assert isinstance(res_dt, string_DT) # These casts currently implement changing the string length, so @@ -490,19 +500,24 @@ class TestCasting: expected_safety = Casting.same_kind to_dt = self.string_with_modified_length(string_dt, change_length) - safety, (_, res_dt) = cast._resolve_descriptors((other_dt, to_dt)) + safety, (_, res_dt), view_off = cast._resolve_descriptors( + (other_dt, to_dt)) assert res_dt is to_dt assert safety == expected_safety + assert view_off is None # The opposite direction is always considered unsafe: cast = get_castingimpl(string_DT, other_DT) - safety, _ = cast._resolve_descriptors((string_dt, other_dt)) + safety, _, view_off = cast._resolve_descriptors((string_dt, other_dt)) assert safety == Casting.unsafe + assert view_off is None cast = get_castingimpl(string_DT, other_DT) - safety, (_, res_dt) = cast._resolve_descriptors((string_dt, None)) + safety, (_, res_dt), view_off = cast._resolve_descriptors( + (string_dt, None)) assert safety == Casting.unsafe + assert view_off is None assert other_dt is res_dt # returns the singleton for simple dtypes @pytest.mark.parametrize("string_char", ["S", "U"]) @@ -521,7 +536,8 @@ class TestCasting: cast = get_castingimpl(type(other_dt), string_DT) cast_back = get_castingimpl(string_DT, type(other_dt)) - _, (res_other_dt, string_dt) = cast._resolve_descriptors((other_dt, None)) + _, (res_other_dt, string_dt), _ = cast._resolve_descriptors( + (other_dt, None)) if res_other_dt is not other_dt: # do not support non-native byteorder, skip test in that case @@ -580,13 +596,16 @@ class TestCasting: expected_length = other_dt.itemsize // div string_dt = np.dtype(f"{string_char}{expected_length}") - safety, (res_other_dt, res_dt) = cast._resolve_descriptors((other_dt, None)) + safety, (res_other_dt, res_dt), view_off = cast._resolve_descriptors( + (other_dt, None)) assert res_dt.itemsize == expected_length * fact assert isinstance(res_dt, string_DT) + expected_view_off = None if other_dt.char == string_char: if other_dt.isnative: - expected_safety = Casting.no | Casting.cast_is_view + expected_safety = Casting.no + expected_view_off = 0 else: expected_safety = Casting.equiv elif string_char == "U": @@ -594,13 +613,19 @@ class TestCasting: else: expected_safety = Casting.unsafe + assert view_off == expected_view_off assert expected_safety == safety for change_length in [-1, 0, 1]: to_dt = self.string_with_modified_length(string_dt, change_length) - safety, (_, res_dt) = cast._resolve_descriptors((other_dt, to_dt)) + safety, (_, res_dt), view_off = cast._resolve_descriptors( + (other_dt, to_dt)) assert res_dt is to_dt + if change_length <= 0: + assert view_off == expected_view_off + else: + assert view_off is None if expected_safety == Casting.unsafe: assert safety == expected_safety elif change_length < 0: @@ -655,12 +680,16 @@ class TestCasting: object_dtype = type(np.dtype(object)) cast = get_castingimpl(object_dtype, type(dtype)) - safety, (_, res_dt) = cast._resolve_descriptors((np.dtype("O"), dtype)) + safety, (_, res_dt), view_off = cast._resolve_descriptors( + (np.dtype("O"), dtype)) assert safety == Casting.unsafe + assert view_off is None assert res_dt is dtype - safety, (_, res_dt) = cast._resolve_descriptors((np.dtype("O"), None)) + safety, (_, res_dt), view_off = cast._resolve_descriptors( + (np.dtype("O"), None)) assert safety == Casting.unsafe + assert view_off is None assert res_dt == dtype.newbyteorder("=") @pytest.mark.parametrize("dtype", simple_dtype_instances()) @@ -669,8 +698,10 @@ class TestCasting: object_dtype = type(np.dtype(object)) cast = get_castingimpl(type(dtype), object_dtype) - safety, (_, res_dt) = cast._resolve_descriptors((dtype, None)) + safety, (_, res_dt), view_off = cast._resolve_descriptors( + (dtype, None)) assert safety == Casting.safe + assert view_off is None assert res_dt is np.dtype("O") @pytest.mark.parametrize("casting", ["no", "unsafe"]) @@ -681,6 +712,71 @@ class TestCasting: assert np.can_cast("V4", dtype, casting=casting) == expected assert np.can_cast(dtype, "V4", casting=casting) == expected + @pytest.mark.parametrize(["to_dt", "expected_off"], + [ # Same as `from_dt` but with both fields shifted: + (np.dtype({"names": ["a", "b"], "formats": ["i4", "f4"], + "offsets": [2, 6]}), -2), + # Additional change of the names + # TODO: Tests will need changing for order vs. name based casting: + (np.dtype({"names": ["b", "a"], "formats": ["f4", "i4"], + "offsets": [6, 2]}), -2), + # Incompatible field offset change (offsets -2 and 0) + (np.dtype({"names": ["b", "a"], "formats": ["f4", "i4"], + "offsets": [6, 0]}), None)]) + def test_structured_field_offsets(self, to_dt, expected_off): + # This checks the cast-safety and view offset for swapped and "shifted" + # fields which are viewable + from_dt = np.dtype({"names": ["a", "b"], + "formats": ["i4", "f4"], + "offsets": [0, 4]}) + cast = get_castingimpl(type(from_dt), type(to_dt)) + safety, _, view_off = cast._resolve_descriptors((from_dt, to_dt)) + assert safety == Casting.equiv + # Shifting the original data pointer by -2 will align both by + # effectively adding 2 bytes of spacing before `from_dt`. + assert view_off == expected_off + + @pytest.mark.parametrize(("from_dt", "to_dt", "expected_off"), [ + # Subarray cases: + ("i", "(1,1)i", 0), + ("(1,1)i", "i", 0), + ("(2,1)i", "(2,1)i", 0), + # field cases (field to field is tested explicitly also): + ("i", dict(names=["a"], formats=["i"], offsets=[2]), -2), + (dict(names=["a"], formats=["i"], offsets=[2]), "i", 2), + # Currently considered not viewable, due to multiple fields + # even though they overlap (maybe we should not allow that?) + ("i", dict(names=["a", "b"], formats=["i", "i"], offsets=[2, 2]), + None), + # different number of fields can't work, should probably just fail + # so it never reports "viewable": + ("i,i", "i,i,i", None), + # Unstructured void cases: + ("i4", "V3", 0), # void smaller or equal + ("i4", "V4", 0), # void smaller or equal + ("i4", "V10", None), # void is larger (no view) + ("O", "V4", None), # currently reject objects for view here. + ("O", "V8", None), # currently reject objects for view here. + ("V4", "V3", 0), + ("V4", "V4", 0), + ("V3", "V4", None), + # Note that currently void-to-other cast goes via byte-strings + # and is not a "view" based cast like the opposite direction: + ("V4", "i4", None), + # completely invalid/impossible cast: + ("i,i", "i,i,i", None), + ]) + def test_structured_view_offsets_paramteric( + self, from_dt, to_dt, expected_off): + # TODO: While this test is fairly thorough, right now, it does not + # really test some paths that may have nonzero offsets (they don't + # really exists). + from_dt = np.dtype(from_dt) + to_dt = np.dtype(to_dt) + cast = get_castingimpl(type(from_dt), type(to_dt)) + _, _, view_off = cast._resolve_descriptors((from_dt, to_dt)) + assert view_off == expected_off + @pytest.mark.parametrize("dtype", np.typecodes["All"]) def test_object_casts_NULL_None_equivalence(self, dtype): # None to <other> casts may succeed or fail, but a NULL'ed array must diff --git a/numpy/testing/print_coercion_tables.py b/numpy/testing/print_coercion_tables.py index 3a447cd2d..c1d4cdff8 100755 --- a/numpy/testing/print_coercion_tables.py +++ b/numpy/testing/print_coercion_tables.py @@ -87,11 +87,12 @@ def print_new_cast_table(*, can_cast=True, legacy=False, flags=False): from numpy.core._multiarray_tests import get_all_cast_information cast_table = { - 0 : "#", # No cast (classify as equivalent here) - 1 : "#", # equivalent casting - 2 : "=", # safe casting - 3 : "~", # same-kind casting - 4 : ".", # unsafe casting + -1: " ", + 0: "#", # No cast (classify as equivalent here) + 1: "#", # equivalent casting + 2: "=", # safe casting + 3: "~", # same-kind casting + 4: ".", # unsafe casting } flags_table = { 0 : "▗", 7: "█", |
