diff --git a/xarray/core/dtypes.py b/xarray/core/dtypes.py index bb2fe26d727..d8ef4c4c6a8 100644 --- a/xarray/core/dtypes.py +++ b/xarray/core/dtypes.py @@ -278,17 +278,11 @@ def should_promote_to_object( """ np_result_types = set() for arr_or_dtype in arrays_and_dtypes: - try: - result_type = array_api_compat.result_type( - maybe_promote_to_variable_width(arr_or_dtype), xp=xp - ) - if isinstance(result_type, np.dtype): - np_result_types.add(result_type) - except TypeError: - # passing individual objects to xp.result_type (i.e., what `array_api_compat.result_type` calls) means NEP-18 implementations won't have - # a chance to intercept special values (such as NA) that numpy core cannot handle. - # Thus they are considered as types that don't need promotion i.e., the `arr_or_dtype` that rose the `TypeError` will not contribute to `np_result_types`. - pass + result_type = array_api_compat.result_type( + maybe_promote_to_variable_width(arr_or_dtype), xp=xp + ) + if isinstance(result_type, np.dtype): + np_result_types.add(result_type) if np_result_types: for left, right in PROMOTE_TO_OBJECT: @@ -326,8 +320,14 @@ def result_type( if xp is None: xp = get_array_namespace(arrays_and_dtypes) - if should_promote_to_object(arrays_and_dtypes, xp): - return np.dtype(object) + try: + if should_promote_to_object(arrays_and_dtypes, xp): + return np.dtype(object) + except TypeError: + # Unknown python objects will raise a TypeError in `xp.result_type`; + # We pass the decision on its impact on array type to after we've attempted to promote. + pass + maybe_promote = functools.partial( maybe_promote_to_variable_width, # let extension arrays handle their own str/bytes @@ -335,4 +335,12 @@ def result_type( map(utils.is_allowed_extension_array_dtype, arrays_and_dtypes) ), ) - return array_api_compat.result_type(*map(maybe_promote, arrays_and_dtypes), xp=xp) + try: + result = array_api_compat.result_type( + *map(maybe_promote, arrays_and_dtypes), xp=xp + ) + except TypeError: + # Unknown python objects will raise a TypeError in `xp.result_type`; + # We assume the user wants them to be there and therefore promote to object dtype instead of raising. + return np.dtype(object) + return result diff --git a/xarray/core/utils.py b/xarray/core/utils.py index 100c256fa9d..f94487759c2 100644 --- a/xarray/core/utils.py +++ b/xarray/core/utils.py @@ -213,7 +213,7 @@ def maybe_coerce_to_str(index, original_coords): try: result_type = dtypes.result_type(*original_coords) - except (TypeError, ValueError): + except ValueError: pass else: if result_type.kind in "SU": diff --git a/xarray/tests/test_dtypes.py b/xarray/tests/test_dtypes.py index 4ed66509725..5a772b1691f 100644 --- a/xarray/tests/test_dtypes.py +++ b/xarray/tests/test_dtypes.py @@ -32,6 +32,10 @@ class DummyArrayAPINamespace: ([np.dtype(" None: diff --git a/xarray/tests/test_utils.py b/xarray/tests/test_utils.py index 90fdcb7d561..0d42cd8d48c 100644 --- a/xarray/tests/test_utils.py +++ b/xarray/tests/test_utils.py @@ -29,31 +29,45 @@ def new_method(): old_method() -@pytest.mark.parametrize( - ["a", "b", "expected"], - [ - [np.array(["a"]), np.array(["b"]), np.array(["a", "b"])], - [np.array([1], dtype="int64"), np.array([2], dtype="int64"), pd.Index([1, 2])], - ], -) -def test_maybe_coerce_to_str(a, b, expected): - index = pd.Index(a).append(pd.Index(b)) - - actual = utils.maybe_coerce_to_str(index, [a, b]) - - assert_array_equal(expected, actual) - assert expected.dtype == actual.dtype - - -def test_maybe_coerce_to_str_minimal_str_dtype(): - a = np.array(["a", "a_long_string"]) - index = pd.Index(["a"]) - - actual = utils.maybe_coerce_to_str(index, [a]) - expected = np.array("a") - - assert_array_equal(expected, actual) - assert expected.dtype == actual.dtype +class TestMaybeCoerceToStr: + @pytest.mark.parametrize( + ["a", "b", "expected"], + [ + [np.array(["a"]), np.array(["b"]), np.array(["a", "b"])], + [ + np.array([1], dtype="int64"), + np.array([2], dtype="int64"), + pd.Index([1, 2]), + ], + ], + ) + def test_maybe_coerce_to_str(self, a, b, expected): + index = pd.Index(a).append(pd.Index(b)) + + actual = utils.maybe_coerce_to_str(index, [a, b]) + + assert_array_equal(expected, actual) + assert expected.dtype == actual.dtype + + def test_maybe_coerce_to_str_minimal_str_dtype(self): + a = np.array(["a", "a_long_string"]) + index = pd.Index(["a"]) + + actual = utils.maybe_coerce_to_str(index, [a]) + expected = np.array("a") + + assert_array_equal(expected, actual) + assert expected.dtype == actual.dtype + + def test_maybe_coerce_to_str_python_obj_dtype(self): + """No change to dtype if the array contains a custom python object.""" + a = np.array([type("Foo", (object,), {"foo": "bar"}), "a_long_string"]) + index = pd.Index(["a"], dtype=object) + + actual = utils.maybe_coerce_to_str(index, [a]) + + assert_array_equal(index, actual) + assert index.dtype == actual.dtype class TestArrayEquiv: