diff --git a/pandas/_libs/lib.pyx b/pandas/_libs/lib.pyx index 3a11e7fbbdf33..a78ae49b3b18f 100644 --- a/pandas/_libs/lib.pyx +++ b/pandas/_libs/lib.pyx @@ -1045,10 +1045,10 @@ def is_list_like(obj: object, allow_sets: bool = True) -> bool: cdef inline bint c_is_list_like(object obj, bint allow_sets) except -1: return ( isinstance(obj, abc.Iterable) + # avoid numpy-style scalars + and not (hasattr(obj, "ndim") and obj.ndim == 0) # we do not count strings/unicode/bytes as list-like and not isinstance(obj, (str, bytes)) - # exclude zero-dimensional numpy arrays, effectively scalars - and not (util.is_array(obj) and obj.ndim == 0) # exclude sets if allow_sets is False and not (allow_sets is False and isinstance(obj, abc.Set)) ) diff --git a/pandas/_libs/testing.pyx b/pandas/_libs/testing.pyx index 7a2fa471b9ba8..6cd5e45b73e17 100644 --- a/pandas/_libs/testing.pyx +++ b/pandas/_libs/testing.pyx @@ -11,6 +11,7 @@ from pandas._libs.lib import is_complex from pandas._libs.util cimport is_array, is_real_number_object from pandas.core.dtypes.common import is_dtype_equal +from pandas.core.dtypes.inference import is_array_like from pandas.core.dtypes.missing import array_equivalent, isna @@ -99,7 +100,9 @@ cpdef assert_almost_equal(a, b, return True a_is_ndarray = is_array(a) + a_has_size_and_shape = hasattr(a, "size") and hasattr(a, "shape") b_is_ndarray = is_array(b) + b_has_size_and_shape = hasattr(b, "size") and hasattr(b, "shape") if obj is None: if a_is_ndarray or b_is_ndarray: @@ -119,7 +122,7 @@ cpdef assert_almost_equal(a, b, f"Can't compare objects without length, one or both is invalid: ({a}, {b})" ) - if a_is_ndarray and b_is_ndarray: + if (a_is_ndarray and b_is_ndarray) or (a_has_size_and_shape and b_has_size_and_shape): na, nb = a.size, b.size if a.shape != b.shape: from pandas._testing import raise_assert_detail diff --git a/pandas/tests/dtypes/test_inference.py b/pandas/tests/dtypes/test_inference.py index 0f4cef772458f..11f88cab54fea 100644 --- a/pandas/tests/dtypes/test_inference.py +++ b/pandas/tests/dtypes/test_inference.py @@ -60,6 +60,52 @@ def coerce(request): return request.param +class MockNumpyLikeArray: + """ + A class which is numpy-like (e.g. Pint's Quantity) but not actually numpy + + The key is that it is not actually a numpy array so + ``util.is_array(mock_numpy_like_array_instance)`` returns ``False``. Other + important properties are that the class defines a :meth:`__iter__` method + (so that ``isinstance(abc.Iterable)`` returns ``True``) and has a + :meth:`ndim` property which can be used as a check for whether it is a + scalar or not. + """ + + def __init__(self, values): + self._values = values + + def __iter__(self): + iter_values = iter(self._values) + + def it_outer(): + yield from iter_values + + return it_outer() + + def __len__(self): + return len(self._values) + + def __array__(self, t=None): + return self._values + + @property + def ndim(self): + return self._values.ndim + + @property + def dtype(self): + return self._values.dtype + + @property + def size(self): + return self._values.size + + @property + def shape(self): + return self._values.shape + + # collect all objects to be tested for list-like-ness; use tuples of objects, # whether they are list-like or not (special casing for sets), and their ID ll_params = [ @@ -94,6 +140,15 @@ def coerce(request): (np.ndarray((2,) * 4), True, "ndarray-4d"), (np.array([[[[]]]]), True, "ndarray-4d-empty"), (np.array(2), False, "ndarray-0d"), + (MockNumpyLikeArray(np.ndarray((2,) * 1)), True, "duck-ndarray-1d"), + (MockNumpyLikeArray(np.array([])), True, "duck-ndarray-1d-empty"), + (MockNumpyLikeArray(np.ndarray((2,) * 2)), True, "duck-ndarray-2d"), + (MockNumpyLikeArray(np.array([[]])), True, "duck-ndarray-2d-empty"), + (MockNumpyLikeArray(np.ndarray((2,) * 3)), True, "duck-ndarray-3d"), + (MockNumpyLikeArray(np.array([[[]]])), True, "duck-ndarray-3d-empty"), + (MockNumpyLikeArray(np.ndarray((2,) * 4)), True, "duck-ndarray-4d"), + (MockNumpyLikeArray(np.array([[[[]]]])), True, "duck-ndarray-4d-empty"), + (MockNumpyLikeArray(np.array(2)), False, "duck-ndarray-0d"), (1, False, "int"), (b"123", False, "bytes"), (b"", False, "bytes-empty"), @@ -154,6 +209,8 @@ def test_is_array_like(): assert inference.is_array_like(Series([1, 2])) assert inference.is_array_like(np.array(["a", "b"])) assert inference.is_array_like(Index(["2016-01-01"])) + assert inference.is_array_like(np.array([2, 3])) + assert inference.is_array_like(MockNumpyLikeArray(np.array([2, 3]))) class DtypeList(list): dtype = "special" @@ -166,6 +223,23 @@ class DtypeList(list): assert not inference.is_array_like(123) +def test_assert_almost_equal(): + tm.assert_almost_equal(np.array(2), np.array(2)) + eg = MockNumpyLikeArray(np.array(2)) + tm.assert_almost_equal(eg, eg) + + +@pytest.mark.parametrize( + "eg", + ( + np.array(2), + MockNumpyLikeArray(np.array(2)), + ), +) +def test_assert_almost_equal(eg): + tm.assert_almost_equal(eg, eg) + + @pytest.mark.parametrize( "inner", [ @@ -1427,34 +1501,52 @@ def test_is_scalar_builtin_nonscalars(self): assert not is_scalar(slice(None)) assert not is_scalar(Ellipsis) - def test_is_scalar_numpy_array_scalars(self): - assert is_scalar(np.int64(1)) - assert is_scalar(np.float64(1.0)) - assert is_scalar(np.int32(1)) - assert is_scalar(np.complex64(2)) - assert is_scalar(np.object_("foobar")) - assert is_scalar(np.str_("foobar")) - assert is_scalar(np.unicode_("foobar")) - assert is_scalar(np.bytes_(b"foobar")) - assert is_scalar(np.datetime64("2014-01-01")) - assert is_scalar(np.timedelta64(1, "h")) - - def test_is_scalar_numpy_zerodim_arrays(self): - for zerodim in [ - np.array(1), - np.array("foobar"), - np.array(np.datetime64("2014-01-01")), - np.array(np.timedelta64(1, "h")), - np.array(np.datetime64("NaT")), - ]: - assert not is_scalar(zerodim) - assert is_scalar(lib.item_from_zerodim(zerodim)) - + @pytest.mark.parametrize("start", ( + np.int64(1), + np.float64(1.0), + np.int32(1), + np.complex64(2), + np.object_("foobar"), + np.str_("foobar"), + np.unicode_("foobar"), + np.bytes_(b"foobar"), + np.datetime64("2014-01-01"), + np.timedelta64(1, "h"), + )) + @pytest.mark.parametrize("numpy_like", (True, False)) + def test_is_scalar_numpy_array_scalars(self, start, numpy_like): + if numpy_like: + start = MockNumpyLikeArray(start) + + assert is_scalar(start) + + @pytest.mark.parametrize("zerodim", ( + np.array(1), + np.array("foobar"), + np.array(np.datetime64("2014-01-01")), + np.array(np.timedelta64(1, "h")), + np.array(np.datetime64("NaT")), + )) + @pytest.mark.parametrize("numpy_like", (True, False)) + def test_is_scalar_numpy_zerodim_arrays(self, zerodim, numpy_like): + if numpy_like: + zerodim = MockNumpyLikeArray(zerodim) + + assert not is_scalar(zerodim) + assert is_scalar(lib.item_from_zerodim(zerodim)) + + @pytest.mark.parametrize("start", ( + np.array([]), + np.array([[]]), + np.matrix("1; 2"), + )) + @pytest.mark.parametrize("numpy_like", (True, False)) @pytest.mark.filterwarnings("ignore::PendingDeprecationWarning") - def test_is_scalar_numpy_arrays(self): - assert not is_scalar(np.array([])) - assert not is_scalar(np.array([[]])) - assert not is_scalar(np.matrix("1; 2")) + def test_is_scalar_numpy_arrays(self, start, numpy_like): + if numpy_like: + start = MockNumpyLikeArray(start) + + assert not is_scalar(start) def test_is_scalar_pandas_scalars(self): assert is_scalar(Timestamp("2014-01-01"))