From 446e146c773d5d8dfe5018659e86171119f19c50 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Fri, 10 Jul 2026 20:34:08 +0200 Subject: [PATCH] BUG: do not change the dtype in asarray(array_from_another_library) Consider >>> x = np.asarray(3, dtype=np.float32) >>> y = xp.asarray(x, device=xp.Device("device1")) >>> y.dtype == xp.float32 Since `x` is an array, the dtype of `y` must match the dtype of `x`, not be the default dtype of the device of `y`! In fact, this is the behavior in 2.5, so restore it. The fix follows from the difference between 2.5 and 2.6: - in 2.5, we called `res = np.array(...)` and wrapped `res` into an xp-array - in 2.6, we additionally select adjust the dtype to match the default for the device. The last part should be gated: - if `obj` is an array_like python object, it does not have a .dtype attribute, thus we adjust res.dtype (this is new in 2.6) - if `obj` is a array from numpy or pytorch CPU (i.e. numpy conversion succeeds), `res.dtype` is a numpy analog of `obj.dtype`, so we honor that (this is what 2.6 broke, and we fix here) - if `obj` has a dtype attribute but numpy fails to convert it (so `obj` is a GPU array or a pydata sparse array etc), calling `np.array(obj)` fails with an exception, and we never reach the dtype adjustment stage anyway (and this is unchanged from 2.5). $ git diff 2.5..2.6 -- array_api_strict/_creation_functions.py ... snip ... @@ -108,6 +106,27 @@ def asarray( raise OverflowError("Integer out of bounds for array dtypes") res = np.array(obj, dtype=_np_dtype, copy=copy) + + # numpy default dtype may differ; if so, adjust the dtype + if dtype is None and device is not None: + res_dtype = DType(res.dtype) + # The dtype selected by Numpy might not be the default dtype + # on this device. We thus find the default dtype for the dtype "kind", and + # cast to the device-appropriate default. + from ._data_type_functions import isdtype + if isdtype(res_dtype, "bool"): + target_dtype = DType("bool") + elif isdtype(res_dtype, "integral"): + target_dtype = get_default_dtypes(device)["integral"] + elif isdtype(res_dtype, "real floating"): + target_dtype = get_default_dtypes(device)["real floating"] + elif isdtype(res_dtype, "complex floating"): + target_dtype = get_default_dtypes(device)["complex floating"] + else: + raise ValueError(f"{res_dtype = } not understood.") + + res = res.astype(target_dtype._np_dtype) + return Array._new(res, device=device) --- array_api_strict/_creation_functions.py | 8 ++++++-- array_api_strict/tests/test_creation_functions.py | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/array_api_strict/_creation_functions.py b/array_api_strict/_creation_functions.py index b34af5a..63bc9c4 100644 --- a/array_api_strict/_creation_functions.py +++ b/array_api_strict/_creation_functions.py @@ -107,8 +107,12 @@ def asarray( res = np.array(obj, dtype=_np_dtype, copy=copy) - # numpy default dtype may differ; if so, adjust the dtype - if dtype is None and device is not None: + # numpy default dtype may differ; if so, adjust the dtype--- + # unless `obj` is already an array, potentially from some other library. + # In the latter case, `obj.dtype` is a thing, numpy has already done the + # conversion and `res.dtype` is the numpy analog of `obj.dtype` + # (if numpy failed to convert `obj`, it has already raised an exception). + if dtype is None and device is not None and not hasattr(obj, "dtype"): res_dtype = DType(res.dtype) # The dtype selected by Numpy might not be the default dtype # on this device. We thus find the default dtype for the dtype "kind", and diff --git a/array_api_strict/tests/test_creation_functions.py b/array_api_strict/tests/test_creation_functions.py index 7031120..06c5d87 100644 --- a/array_api_strict/tests/test_creation_functions.py +++ b/array_api_strict/tests/test_creation_functions.py @@ -368,6 +368,13 @@ def test_asarray_device_2(): assert y.dtype == float64 +def test_asarray_device_1(): + # regression test for https://github.com/data-apis/array-api-strict/issues/222 + x = np.ones(3, dtype=np.float32) + y = asarray(x, device=Device('device1')) + assert y.dtype == float32 + + @pytest.mark.parametrize("api_version", ['2021.12', '2022.12', '2023.12']) def from_dlpack_2023_12(api_version): if api_version != '2022.12':