Repository: numpy/numpy
Pull request: BUG: safer bincount casting
* Fixes #28354

* Allow some uint types to be cast a bit more gracefully for `np.bincount()`.


Problem statement:
BUG: `np.bincount` casting `uint64` to `int64`
### Describe the issue:

When passing a `uint64` array to `np.bincount`, it appears to try to cast the input (or some internally created array) to `int64`. This in turn raises an exception.

### Reproduce the code example:

```python
import numpy as np

a = np.arange(64, dtype=np.uint64)
np.bincount(a)
```

### Error message:

```python
Traceback (most recent call last):
  File "...", line 4, in <module>
    np.bincount(a)
TypeError: Cannot cast array data from dtype('uint64') to dtype('int64') according to the rule 'safe'
```

### Python and NumPy Versions:

```
2.2.2
3.12.5 | packaged by conda-forge | (main, Aug  8 2024, 18:32:50) [Clang 16.0.6 ]
```

### Runtime Environment:

```python
[{'numpy_version': '2.2.2',
  'python': '3.12.5 | packaged by conda-forge | (main, Aug  8 2024, 18:32:50) '
            '[Clang 16.0.6 ]',
  'uname': uname_result(system='Darwin', node='jkirkham-mlt', release='23.6.0', version='Darwin Kernel Version 23.6.0: Thu Sep 12 23:35:29 PDT 2024; root:xnu-10063.141.1.701.1~1/RELEASE_ARM64_T6000', machine='arm64')},
 {'simd_extensions': {'baseline': ['NEON', 'NEON_FP16', 'NEON_VFPV4', 'ASIMD'],
                      'found': ['ASIMDHP'],
                      'not_found': ['ASIMDFHM']}},
 {'architecture': 'VORTEX',
  'filepath': '/Users/jkirkham/miniforge/lib/libopenblas.0.dylib',
  'internal_api': 'openblas',
  'num_threads': 10,
  'prefix': 'libopenblas',
  'threading_layer': 'openmp',
  'user_api': 'blas',
  'version': '0.3.28'},
 {'filepath': '/Users/jkirkham/miniforge/lib/libomp.dylib',
  'internal_api': 'openmp',
  'num_threads': 10,
  'prefix': 'libomp',
  'user_api': 'openmp',
  'version': None}]
```

### Context for the issue:

_No response_

Hints from issue discussion:
Just to check - is this a regression?
>  is this a regression?

It  is also true for NumPy 1.26.4, so I suspect it has been around for a while.

Commit message: MAINT, BUG: PR 28355 revisions

* Based on reviewer feedback, narrow the scope of the `flags`
variable in `arr_bincount()`.

* Based on reviewer feedback, add an array-like test for the `uint64`
casting issue, which indeed fails before and passes after adding
a similar shim to the array-like code path in `arr_bincount()`.

* Based on reviewer feedback, switch the patching from `PyArray_Size`
to `PyArray_Check` in a few places.

Patch to review:
diff --git a/numpy/_core/src/multiarray/compiled_base.c b/numpy/_core/src/multiarray/compiled_base.c
index 523ccf40cef9..d47180c0a890 100644
--- a/numpy/_core/src/multiarray/compiled_base.c
+++ b/numpy/_core/src/multiarray/compiled_base.c
@@ -150,8 +150,13 @@ arr_bincount(PyObject *NPY_UNUSED(self), PyObject *const *args,
         }
         if (PyArray_SIZE(tmp1) > 0) {
             /* The input is not empty, so convert it to NPY_INTP. */
-            lst = (PyArrayObject *)PyArray_ContiguousFromAny((PyObject *)tmp1,
-                                                             NPY_INTP, 1, 1);
+            int flags = NPY_ARRAY_WRITEABLE | NPY_ARRAY_ALIGNED | NPY_ARRAY_C_CONTIGUOUS;
+            if (PyArray_Check((PyObject *)tmp1) &&
+                PyArray_ISINTEGER((PyArrayObject *)tmp1)) {
+                flags = flags | NPY_ARRAY_FORCECAST;
+            }
+            PyArray_Descr* local_dtype = PyArray_DescrFromType(NPY_INTP);
+            lst = (PyArrayObject *)PyArray_FromAny((PyObject *)tmp1, local_dtype, 1, 1, flags, NULL);
             Py_DECREF(tmp1);
             if (lst == NULL) {
                 /* Failed converting to NPY_INTP. */
@@ -177,7 +182,13 @@ arr_bincount(PyObject *NPY_UNUSED(self), PyObject *const *args,
     }
 
     if (lst == NULL) {
-        lst = (PyArrayObject *)PyArray_ContiguousFromAny(list, NPY_INTP, 1, 1);
+        int flags = NPY_ARRAY_WRITEABLE | NPY_ARRAY_ALIGNED | NPY_ARRAY_C_CONTIGUOUS;
+        if (PyArray_Check((PyObject *)list) &&
+            PyArray_ISINTEGER((PyArrayObject *)list)) {
+            flags = flags | NPY_ARRAY_FORCECAST;
+        }
+        PyArray_Descr* local_dtype = PyArray_DescrFromType(NPY_INTP);
+        lst = (PyArrayObject *)PyArray_FromAny(list, local_dtype, 1, 1, flags, NULL);
         if (lst == NULL) {
             goto fail;
         }
diff --git a/numpy/lib/tests/test_function_base.py b/numpy/lib/tests/test_function_base.py
index 63a61901f0df..e6efc4cf0a32 100644
--- a/numpy/lib/tests/test_function_base.py
+++ b/numpy/lib/tests/test_function_base.py
@@ -2959,6 +2959,27 @@ def test_error_not_1d(self, vals):
         with assert_raises(ValueError):
             np.bincount(vals)
 
+    @pytest.mark.parametrize("dt", np.typecodes["AllInteger"])
+    def test_gh_28354(self, dt):
+        a = np.array([0, 1, 1, 3, 2, 1, 7], dtype=dt)
+        actual = np.bincount(a)
+        expected = [1, 3, 1, 1, 0, 0, 0, 1]
+        assert_array_equal(actual, expected)
+
+    def test_contiguous_handling(self):
+        # check for absence of hard crash
+        np.bincount(np.arange(10000)[::2])
+
+    def test_gh_28354_array_like(self):
+        class A:
+            def __array__(self):
+                return np.array([0, 1, 1, 3, 2, 1, 7], dtype=np.uint64)
+
+        a = A()
+        actual = np.bincount(a)
+        expected = [1, 3, 1, 1, 0, 0, 0, 1]
+        assert_array_equal(actual, expected)
+
 
 class TestInterp:
 


Code review comments:
diff_hunk: @@ -150,8 +150,13 @@ arr_bincount(PyObject *NPY_UNUSED(self), PyObject *const *args,
         }
         if (PyArray_SIZE(tmp1) > 0) {
             /* The input is not empty, so convert it to NPY_INTP. */
-            lst = (PyArrayObject *)PyArray_ContiguousFromAny((PyObject *)tmp1,
-                                                             NPY_INTP, 1, 1);
+            int flags = NPY_ARRAY_WRITEABLE | NPY_ARRAY_ALIGNED | NPY_ARRAY_C_CONTIGUOUS;
+            if (PyArray_Check((PyObject *)tmp1) &&
+                PyArray_ISINTEGER((PyArrayObject *)tmp1)) {
path: numpy/_core/src/multiarray/compiled_base.c
text: @user1:
```suggestion
            if (PyArray_ISINTEGER(tmp1)) {
```
`tmp1` is already an array (not that the check is expensive).