test_string.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. """
  2. This file contains a minimal set of tests for compliance with the extension
  3. array interface test suite, and should contain no other tests.
  4. The test suite for the full functionality of the array is located in
  5. `pandas/tests/arrays/`.
  6. The tests in this file are inherited from the BaseExtensionTests, and only
  7. minimal tweaks should be applied to get the tests passing (by overwriting a
  8. parent method).
  9. Additional tests should either be added to one of the BaseExtensionTests
  10. classes (if they are relevant for the extension interface for all dtypes), or
  11. be added to the array-specific tests in `pandas/tests/arrays/`.
  12. """
  13. from __future__ import annotations
  14. import string
  15. from typing import cast
  16. import numpy as np
  17. import pytest
  18. from pandas.compat import HAS_PYARROW
  19. from pandas.core.dtypes.base import StorageExtensionDtype
  20. import pandas as pd
  21. import pandas._testing as tm
  22. from pandas.api.types import is_string_dtype
  23. from pandas.core.arrays import ArrowStringArray
  24. from pandas.core.arrays.string_ import StringDtype
  25. from pandas.tests.arrays.string_.test_string import string_dtype_highest_priority
  26. from pandas.tests.extension import base
  27. def maybe_split_array(arr, chunked):
  28. if not chunked:
  29. return arr
  30. elif arr.dtype.storage != "pyarrow":
  31. return arr
  32. pa = pytest.importorskip("pyarrow")
  33. arrow_array = arr._pa_array
  34. split = len(arrow_array) // 2
  35. arrow_array = pa.chunked_array(
  36. [*arrow_array[:split].chunks, *arrow_array[split:].chunks]
  37. )
  38. assert arrow_array.num_chunks == 2
  39. return type(arr)(arrow_array)
  40. @pytest.fixture(params=[True, False])
  41. def chunked(request):
  42. return request.param
  43. @pytest.fixture
  44. def dtype(string_dtype_arguments):
  45. storage, na_value = string_dtype_arguments
  46. return StringDtype(storage=storage, na_value=na_value)
  47. @pytest.fixture
  48. def data(dtype, chunked):
  49. strings = np.random.default_rng(2).choice(list(string.ascii_letters), size=100)
  50. while strings[0] == strings[1]:
  51. strings = np.random.default_rng(2).choice(list(string.ascii_letters), size=100)
  52. arr = dtype.construct_array_type()._from_sequence(strings, dtype=dtype)
  53. return maybe_split_array(arr, chunked)
  54. @pytest.fixture
  55. def data_missing(dtype, chunked):
  56. """Length 2 array with [NA, Valid]"""
  57. arr = dtype.construct_array_type()._from_sequence([pd.NA, "A"], dtype=dtype)
  58. return maybe_split_array(arr, chunked)
  59. @pytest.fixture
  60. def data_for_sorting(dtype, chunked):
  61. arr = dtype.construct_array_type()._from_sequence(["B", "C", "A"], dtype=dtype)
  62. return maybe_split_array(arr, chunked)
  63. @pytest.fixture
  64. def data_missing_for_sorting(dtype, chunked):
  65. arr = dtype.construct_array_type()._from_sequence(["B", pd.NA, "A"], dtype=dtype)
  66. return maybe_split_array(arr, chunked)
  67. @pytest.fixture
  68. def data_for_grouping(dtype, chunked):
  69. arr = dtype.construct_array_type()._from_sequence(
  70. ["B", "B", pd.NA, pd.NA, "A", "A", "B", "C"], dtype=dtype
  71. )
  72. return maybe_split_array(arr, chunked)
  73. class TestStringArray(base.ExtensionTests):
  74. def test_eq_with_str(self, dtype):
  75. super().test_eq_with_str(dtype)
  76. if dtype.na_value is pd.NA:
  77. # only the NA-variant supports parametrized string alias
  78. assert dtype == f"string[{dtype.storage}]"
  79. elif dtype.storage == "pyarrow":
  80. with tm.assert_produces_warning(FutureWarning):
  81. assert dtype == "string[pyarrow_numpy]"
  82. def test_is_not_string_type(self, dtype):
  83. # Different from BaseDtypeTests.test_is_not_string_type
  84. # because StringDtype is a string type
  85. assert is_string_dtype(dtype)
  86. def test_is_dtype_from_name(self, dtype, using_infer_string):
  87. if dtype.na_value is np.nan and not using_infer_string:
  88. result = type(dtype).is_dtype(dtype.name)
  89. assert result is False
  90. else:
  91. super().test_is_dtype_from_name(dtype)
  92. def test_construct_from_string_own_name(self, dtype, using_infer_string):
  93. if dtype.na_value is np.nan and not using_infer_string:
  94. with pytest.raises(TypeError, match="Cannot construct a 'StringDtype'"):
  95. dtype.construct_from_string(dtype.name)
  96. else:
  97. super().test_construct_from_string_own_name(dtype)
  98. def test_view(self, data):
  99. if data.dtype.storage == "pyarrow":
  100. pytest.skip(reason="2D support not implemented for ArrowStringArray")
  101. super().test_view(data)
  102. def test_from_dtype(self, data):
  103. # base test uses string representation of dtype
  104. pass
  105. def test_transpose(self, data):
  106. if data.dtype.storage == "pyarrow":
  107. pytest.skip(reason="2D support not implemented for ArrowStringArray")
  108. super().test_transpose(data)
  109. def test_setitem_preserves_views(self, data):
  110. if data.dtype.storage == "pyarrow":
  111. pytest.skip(reason="2D support not implemented for ArrowStringArray")
  112. super().test_setitem_preserves_views(data)
  113. def test_dropna_array(self, data_missing):
  114. result = data_missing.dropna()
  115. expected = data_missing[[1]]
  116. tm.assert_extension_array_equal(result, expected)
  117. def test_fillna_no_op_returns_copy(self, data):
  118. data = data[~data.isna()]
  119. valid = data[0]
  120. result = data.fillna(valid)
  121. assert result is not data
  122. tm.assert_extension_array_equal(result, data)
  123. result = data.fillna(method="backfill")
  124. assert result is not data
  125. tm.assert_extension_array_equal(result, data)
  126. def _get_expected_exception(
  127. self, op_name: str, obj, other
  128. ) -> type[Exception] | tuple[type[Exception], ...] | None:
  129. if op_name in [
  130. "__mod__",
  131. "__rmod__",
  132. "__divmod__",
  133. "__rdivmod__",
  134. "__pow__",
  135. "__rpow__",
  136. ]:
  137. return TypeError
  138. elif op_name in ["__mul__", "__rmul__"]:
  139. # Can only multiply strings by integers
  140. return TypeError
  141. elif op_name in [
  142. "__truediv__",
  143. "__rtruediv__",
  144. "__floordiv__",
  145. "__rfloordiv__",
  146. "__sub__",
  147. "__rsub__",
  148. ]:
  149. return TypeError
  150. return None
  151. def _supports_reduction(self, ser: pd.Series, op_name: str) -> bool:
  152. return (
  153. op_name in ["min", "max", "sum"]
  154. or ser.dtype.na_value is np.nan # type: ignore[union-attr]
  155. and op_name in ("any", "all")
  156. )
  157. def _supports_accumulation(self, ser: pd.Series, op_name: str) -> bool:
  158. assert isinstance(ser.dtype, StorageExtensionDtype)
  159. return op_name in ["cummin", "cummax", "cumsum"]
  160. def _cast_pointwise_result(self, op_name: str, obj, other, pointwise_result):
  161. dtype = cast(StringDtype, tm.get_dtype(obj))
  162. if op_name in ["__add__", "__radd__"]:
  163. cast_to = dtype
  164. dtype_other = tm.get_dtype(other) if not isinstance(other, str) else None
  165. if isinstance(dtype_other, StringDtype):
  166. cast_to = string_dtype_highest_priority(dtype, dtype_other)
  167. elif dtype.na_value is np.nan:
  168. cast_to = np.bool_ # type: ignore[assignment]
  169. elif dtype.storage == "pyarrow":
  170. cast_to = "bool[pyarrow]" # type: ignore[assignment]
  171. else:
  172. cast_to = "boolean" # type: ignore[assignment]
  173. return pointwise_result.astype(cast_to)
  174. def test_compare_scalar(self, data, comparison_op):
  175. ser = pd.Series(data)
  176. self._compare_other(ser, data, comparison_op, "abc")
  177. def test_combine_add(self, data_repeated, using_infer_string, request):
  178. dtype = next(data_repeated(1)).dtype
  179. if using_infer_string and (
  180. (dtype.na_value is pd.NA) and dtype.storage == "python"
  181. ):
  182. mark = pytest.mark.xfail(
  183. reason="The pointwise operation result will be inferred to "
  184. "string[nan, pyarrow], which does not match the input dtype"
  185. )
  186. request.applymarker(mark)
  187. super().test_combine_add(data_repeated)
  188. def test_arith_series_with_array(
  189. self, data, all_arithmetic_operators, using_infer_string, request
  190. ):
  191. dtype = data.dtype
  192. if (
  193. using_infer_string
  194. and all_arithmetic_operators == "__radd__"
  195. and dtype.na_value is pd.NA
  196. and (HAS_PYARROW or dtype.storage == "pyarrow")
  197. ):
  198. # TODO(infer_string)
  199. mark = pytest.mark.xfail(
  200. reason="The pointwise operation result will be inferred to "
  201. "string[nan, pyarrow], which does not match the input dtype"
  202. )
  203. request.applymarker(mark)
  204. super().test_arith_series_with_array(data, all_arithmetic_operators)
  205. class Test2DCompat(base.Dim2CompatTests):
  206. @pytest.fixture(autouse=True)
  207. def arrow_not_supported(self, data):
  208. if isinstance(data, ArrowStringArray):
  209. pytest.skip(reason="2D support not implemented for ArrowStringArray")
  210. def test_searchsorted_with_na_raises(data_for_sorting, as_series):
  211. # GH50447
  212. b, c, a = data_for_sorting
  213. arr = data_for_sorting.take([2, 0, 1]) # to get [a, b, c]
  214. arr[-1] = pd.NA
  215. if as_series:
  216. arr = pd.Series(arr)
  217. msg = (
  218. "searchsorted requires array to be sorted, "
  219. "which is impossible with NAs present."
  220. )
  221. with pytest.raises(ValueError, match=msg):
  222. arr.searchsorted(b)
  223. def test_mixed_object_comparison(dtype):
  224. # GH#60228
  225. ser = pd.Series(["a", "b"], dtype=dtype)
  226. mixed = pd.Series([1, "b"], dtype=object)
  227. result = ser == mixed
  228. expected = pd.Series([False, True], dtype=bool)
  229. if dtype.storage == "python" and dtype.na_value is pd.NA:
  230. expected = expected.astype("boolean")
  231. elif dtype.storage == "pyarrow" and dtype.na_value is pd.NA:
  232. expected = expected.astype("bool[pyarrow]")
  233. tm.assert_series_equal(result, expected)