From 11251998c7c29037806e68e78764832f1605a0fe Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Aug 2023 17:12:25 +0100 Subject: [PATCH 01/32] add round abc --- pandas/core/arrays/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 7babce46a3977..4d1e4917c5a60 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -2340,6 +2340,11 @@ def _add_logical_ops(cls) -> None: setattr(cls, "__ror__", cls._create_logical_method(roperator.ror_)) setattr(cls, "__xor__", cls._create_logical_method(operator.xor)) setattr(cls, "__rxor__", cls._create_logical_method(roperator.rxor)) + + def round(self, decimals: int = 0, *args, **kwargs) -> self: + raise AbstractMethodError(self) + + class ExtensionScalarOpsMixin(ExtensionOpsMixin): From a4ef5632ae029c81f491f169401c7cc6c0dc5b35 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Aug 2023 17:17:59 +0100 Subject: [PATCH 02/32] add round test --- pandas/tests/extension/base/ops.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pandas/tests/extension/base/ops.py b/pandas/tests/extension/base/ops.py index 064242f3649f4..071c9cc916a64 100644 --- a/pandas/tests/extension/base/ops.py +++ b/pandas/tests/extension/base/ops.py @@ -263,3 +263,9 @@ def test_unary_ufunc_dunder_equivalence(self, data, ufunc): else: alt = ufunc(data) tm.assert_extension_array_equal(result, alt) + +class BaseRoundingTests: + def test_round(self, data): + result = pd.Series(data).round() + expected = pd.Series([np.round(item) for item in data], dtype = self.dtype) + tm.assert_series_equal(result, expected) From 9f6ebc9524e6c2cc474be3cece23ba82d8a4457f Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Aug 2023 20:05:26 +0100 Subject: [PATCH 03/32] round tests --- pandas/tests/extension/base/__init__.py | 2 ++ pandas/tests/extension/base/ops.py | 2 +- pandas/tests/extension/test_masked.py | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pandas/tests/extension/base/__init__.py b/pandas/tests/extension/base/__init__.py index 7cd55b7240d54..7c02cc319c561 100644 --- a/pandas/tests/extension/base/__init__.py +++ b/pandas/tests/extension/base/__init__.py @@ -54,6 +54,7 @@ class TestMyDtype(BaseDtypeTests): BaseComparisonOpsTests, BaseOpsUtil, BaseUnaryOpsTests, + BaseRoundingTests, ) from pandas.tests.extension.base.printing import BasePrintingTests from pandas.tests.extension.base.reduce import ( # noqa: F401 @@ -86,6 +87,7 @@ class ExtensionTests( BaseArithmeticOpsTests, BaseComparisonOpsTests, BaseUnaryOpsTests, + BaseRoundingTests, BasePrintingTests, BaseReduceTests, BaseReshapingTests, diff --git a/pandas/tests/extension/base/ops.py b/pandas/tests/extension/base/ops.py index 071c9cc916a64..e62c45701597a 100644 --- a/pandas/tests/extension/base/ops.py +++ b/pandas/tests/extension/base/ops.py @@ -267,5 +267,5 @@ def test_unary_ufunc_dunder_equivalence(self, data, ufunc): class BaseRoundingTests: def test_round(self, data): result = pd.Series(data).round() - expected = pd.Series([np.round(item) for item in data], dtype = self.dtype) + expected = pd.Series(np.round(data), dtype = data.dtype) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/extension/test_masked.py b/pandas/tests/extension/test_masked.py index c4195be8ea121..5e8512b30d3aa 100644 --- a/pandas/tests/extension/test_masked.py +++ b/pandas/tests/extension/test_masked.py @@ -433,3 +433,12 @@ class TestParsing(base.BaseParsingTests): class Test2DCompat(base.Dim2CompatTests): pass + +class TestRounding(base.BaseRoundingTests): + def test_round(self, data, request): + if data.dtype == "boolean": + mark = pytest.mark.xfail( + reason="Cannot round boolean dtype" + ) + request.node.add_marker(mark) + super().test_round(data) \ No newline at end of file From 2f0f100d291137234b36dc5b2557283a00105c10 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Aug 2023 20:47:12 +0100 Subject: [PATCH 04/32] lint --- pandas/tests/extension/base/__init__.py | 2 +- pandas/tests/extension/base/ops.py | 3 ++- pandas/tests/extension/test_masked.py | 7 +++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pandas/tests/extension/base/__init__.py b/pandas/tests/extension/base/__init__.py index 7c02cc319c561..53acf95c4e7d6 100644 --- a/pandas/tests/extension/base/__init__.py +++ b/pandas/tests/extension/base/__init__.py @@ -53,8 +53,8 @@ class TestMyDtype(BaseDtypeTests): BaseArithmeticOpsTests, BaseComparisonOpsTests, BaseOpsUtil, - BaseUnaryOpsTests, BaseRoundingTests, + BaseUnaryOpsTests, ) from pandas.tests.extension.base.printing import BasePrintingTests from pandas.tests.extension.base.reduce import ( # noqa: F401 diff --git a/pandas/tests/extension/base/ops.py b/pandas/tests/extension/base/ops.py index e62c45701597a..b43e192e33187 100644 --- a/pandas/tests/extension/base/ops.py +++ b/pandas/tests/extension/base/ops.py @@ -264,8 +264,9 @@ def test_unary_ufunc_dunder_equivalence(self, data, ufunc): alt = ufunc(data) tm.assert_extension_array_equal(result, alt) + class BaseRoundingTests: def test_round(self, data): result = pd.Series(data).round() - expected = pd.Series(np.round(data), dtype = data.dtype) + expected = pd.Series(np.round(data), dtype=data.dtype) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/extension/test_masked.py b/pandas/tests/extension/test_masked.py index 5e8512b30d3aa..73959d793c553 100644 --- a/pandas/tests/extension/test_masked.py +++ b/pandas/tests/extension/test_masked.py @@ -434,11 +434,10 @@ class TestParsing(base.BaseParsingTests): class Test2DCompat(base.Dim2CompatTests): pass + class TestRounding(base.BaseRoundingTests): def test_round(self, data, request): if data.dtype == "boolean": - mark = pytest.mark.xfail( - reason="Cannot round boolean dtype" - ) + mark = pytest.mark.xfail(reason="Cannot round boolean dtype") request.node.add_marker(mark) - super().test_round(data) \ No newline at end of file + super().test_round(data) From 241ba6c23d85b33cc3bbd64b087791595d72d876 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Aug 2023 22:12:40 +0100 Subject: [PATCH 05/32] lint --- pandas/core/arrays/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 4d1e4917c5a60..b57077d4c5ba8 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -2340,11 +2340,9 @@ def _add_logical_ops(cls) -> None: setattr(cls, "__ror__", cls._create_logical_method(roperator.ror_)) setattr(cls, "__xor__", cls._create_logical_method(operator.xor)) setattr(cls, "__rxor__", cls._create_logical_method(roperator.rxor)) - - def round(self, decimals: int = 0, *args, **kwargs) -> self: - raise AbstractMethodError(self) - + def round(self, decimals: int = 0, *args, **kwargs) -> Self: + raise AbstractMethodError(self) class ExtensionScalarOpsMixin(ExtensionOpsMixin): From e2a4168a46a0431b0cf440db28762efb24956705 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 26 Aug 2023 10:22:29 +0100 Subject: [PATCH 06/32] tests --- pandas/tests/extension/decimal/test_decimal.py | 6 ++++++ pandas/tests/extension/test_interval.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/pandas/tests/extension/decimal/test_decimal.py b/pandas/tests/extension/decimal/test_decimal.py index baa056550624f..58e4531a08efa 100644 --- a/pandas/tests/extension/decimal/test_decimal.py +++ b/pandas/tests/extension/decimal/test_decimal.py @@ -226,6 +226,12 @@ def test_invert(self, data): @pytest.mark.parametrize("ufunc", [np.positive, np.negative, np.abs]) def test_unary_ufunc_dunder_equivalence(self, data, ufunc): super().test_unary_ufunc_dunder_equivalence(data, ufunc) + + @pytest.mark.xfail( + reason="DecimalArray.round is not implemented." + ) + def test_round(self, data): + super().test_round(data) def test_take_na_value_other_decimal(): diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py index 66b25abb55961..0b3c8062ddd4d 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -95,6 +95,12 @@ def test_EA_types(self, engine, data): def test_invert(self, data): super().test_invert(data) + @pytest.mark.xfail( + reason="IntervalArray.round is not implemented." + ) + def test_round(self, data): + super().test_round(data) + # TODO: either belongs in tests.arrays.interval or move into base tests. def test_fillna_non_scalar_raises(data_missing): From a908700b327b2f8cbf0741b241f4e028d63a0fe7 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 5 Sep 2023 00:53:07 +0100 Subject: [PATCH 07/32] move methods tests, elementwise rounding --- pandas/tests/extension/base/__init__.py | 2 -- pandas/tests/extension/base/methods.py | 7 +++++++ pandas/tests/extension/base/ops.py | 7 ------- pandas/tests/extension/test_arrow.py | 13 +++++++++++++ pandas/tests/extension/test_interval.py | 2 +- pandas/tests/extension/test_masked.py | 12 ++++++------ pandas/tests/extension/test_numpy.py | 6 ++++++ pandas/tests/extension/test_sparse.py | 6 ++++++ 8 files changed, 39 insertions(+), 16 deletions(-) diff --git a/pandas/tests/extension/base/__init__.py b/pandas/tests/extension/base/__init__.py index 53acf95c4e7d6..7cd55b7240d54 100644 --- a/pandas/tests/extension/base/__init__.py +++ b/pandas/tests/extension/base/__init__.py @@ -53,7 +53,6 @@ class TestMyDtype(BaseDtypeTests): BaseArithmeticOpsTests, BaseComparisonOpsTests, BaseOpsUtil, - BaseRoundingTests, BaseUnaryOpsTests, ) from pandas.tests.extension.base.printing import BasePrintingTests @@ -87,7 +86,6 @@ class ExtensionTests( BaseArithmeticOpsTests, BaseComparisonOpsTests, BaseUnaryOpsTests, - BaseRoundingTests, BasePrintingTests, BaseReduceTests, BaseReshapingTests, diff --git a/pandas/tests/extension/base/methods.py b/pandas/tests/extension/base/methods.py index 2dd62a4ca7538..be2ba3a292982 100644 --- a/pandas/tests/extension/base/methods.py +++ b/pandas/tests/extension/base/methods.py @@ -698,3 +698,10 @@ def test_equals(self, data, na_value, as_series, box): def test_equals_same_data_different_object(self, data): # https://github.com/pandas-dev/pandas/issues/34660 assert pd.Series(data).equals(pd.Series(data)) + + def test_round(self, data): + if not data.dtype._is_numeric: + pytest.skip("Round is only valid for numeric dtypes") + result = pd.Series(data).round() + expected = pd.Series([np.round(element) if pd.notna(element) else element for element in data], dtype=data.dtype) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/extension/base/ops.py b/pandas/tests/extension/base/ops.py index b43e192e33187..064242f3649f4 100644 --- a/pandas/tests/extension/base/ops.py +++ b/pandas/tests/extension/base/ops.py @@ -263,10 +263,3 @@ def test_unary_ufunc_dunder_equivalence(self, data, ufunc): else: alt = ufunc(data) tm.assert_extension_array_equal(result, alt) - - -class BaseRoundingTests: - def test_round(self, data): - result = pd.Series(data).round() - expected = pd.Series(np.round(data), dtype=data.dtype) - tm.assert_series_equal(result, expected) diff --git a/pandas/tests/extension/test_arrow.py b/pandas/tests/extension/test_arrow.py index 4c05049ddfcf5..c490bb5b28fc1 100644 --- a/pandas/tests/extension/test_arrow.py +++ b/pandas/tests/extension/test_arrow.py @@ -1011,6 +1011,19 @@ def _get_arith_xfail_marker(self, opname, pa_dtype): ) return mark + + def test_round(self, data, request): + mark = pytest.mark.xfail( + # raises=pa.ArrowInvalid, + reason="ArrowArray.round converts dtype to double", + ) + if pa.types.is_float32(data.dtype.pyarrow_dtype) or pa.types.is_float64( + data.dtype.pyarrow_dtype + ): + mark = None + if mark is not None: + request.node.add_marker(mark) + super().test_round(data) def test_arith_series_with_scalar(self, data, all_arithmetic_operators, request): pa_dtype = data.dtype.pyarrow_dtype diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py index 0b3c8062ddd4d..4604478d77538 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -96,7 +96,7 @@ def test_invert(self, data): super().test_invert(data) @pytest.mark.xfail( - reason="IntervalArray.round is not implemented." + reason="Round is not valid for IntervalArray." ) def test_round(self, data): super().test_round(data) diff --git a/pandas/tests/extension/test_masked.py b/pandas/tests/extension/test_masked.py index ccaed7a262500..7895a7fdab881 100644 --- a/pandas/tests/extension/test_masked.py +++ b/pandas/tests/extension/test_masked.py @@ -358,15 +358,15 @@ def test_invert(self, data, request): ) request.node.add_marker(mark) super().test_invert(data) + + def test_round(self, data, request): + if data.dtype == "boolean": + mark = pytest.mark.xfail(reason="Cannot round boolean dtype") + request.node.add_marker(mark) + super().test_round(data) class Test2DCompat(base.Dim2CompatTests): pass -class TestRounding(base.BaseRoundingTests): - def test_round(self, data, request): - if data.dtype == "boolean": - mark = pytest.mark.xfail(reason="Cannot round boolean dtype") - request.node.add_marker(mark) - super().test_round(data) diff --git a/pandas/tests/extension/test_numpy.py b/pandas/tests/extension/test_numpy.py index a54729de57a97..b264875822adb 100644 --- a/pandas/tests/extension/test_numpy.py +++ b/pandas/tests/extension/test_numpy.py @@ -270,6 +270,12 @@ def test_insert_invalid(self, data, invalid_scalar): # NumpyExtensionArray[object] can hold anything, so skip super().test_insert_invalid(data, invalid_scalar) + @pytest.mark.xfail( + reason="NumpyExtensionArray.round is not implemented." + ) + def test_round(self, data): + super().test_round(data) + class TestArithmetics(BaseNumPyTests, base.BaseArithmeticOpsTests): divmod_exc = None diff --git a/pandas/tests/extension/test_sparse.py b/pandas/tests/extension/test_sparse.py index 01448a2f83f75..ae6409b9e5ded 100644 --- a/pandas/tests/extension/test_sparse.py +++ b/pandas/tests/extension/test_sparse.py @@ -346,6 +346,12 @@ def test_map_raises(self, data, na_action): with pytest.raises(ValueError, match=msg): data.map(lambda x: np.nan, na_action=na_action) + @pytest.mark.xfail( + reason="SpareArray.round not implemented." + ) + def test_round(self, data): + super().test_round(data) + class TestCasting(BaseSparseTests, base.BaseCastingTests): @pytest.mark.xfail(raises=TypeError, reason="no sparse StringDtype") From fbbfe46794f46247e0042ea91c0f7c1c19ac457f Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 5 Sep 2023 01:04:29 +0100 Subject: [PATCH 08/32] lint --- pandas/tests/extension/base/methods.py | 5 ++++- pandas/tests/extension/decimal/test_decimal.py | 6 ++---- pandas/tests/extension/test_arrow.py | 2 +- pandas/tests/extension/test_interval.py | 4 +--- pandas/tests/extension/test_masked.py | 4 +--- pandas/tests/extension/test_numpy.py | 4 +--- pandas/tests/extension/test_sparse.py | 4 +--- 7 files changed, 11 insertions(+), 18 deletions(-) diff --git a/pandas/tests/extension/base/methods.py b/pandas/tests/extension/base/methods.py index be2ba3a292982..296f038e33cdf 100644 --- a/pandas/tests/extension/base/methods.py +++ b/pandas/tests/extension/base/methods.py @@ -703,5 +703,8 @@ def test_round(self, data): if not data.dtype._is_numeric: pytest.skip("Round is only valid for numeric dtypes") result = pd.Series(data).round() - expected = pd.Series([np.round(element) if pd.notna(element) else element for element in data], dtype=data.dtype) + expected = pd.Series( + [np.round(element) if pd.notna(element) else element for element in data], + dtype=data.dtype, + ) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/extension/decimal/test_decimal.py b/pandas/tests/extension/decimal/test_decimal.py index 58e4531a08efa..a166772461d67 100644 --- a/pandas/tests/extension/decimal/test_decimal.py +++ b/pandas/tests/extension/decimal/test_decimal.py @@ -226,10 +226,8 @@ def test_invert(self, data): @pytest.mark.parametrize("ufunc", [np.positive, np.negative, np.abs]) def test_unary_ufunc_dunder_equivalence(self, data, ufunc): super().test_unary_ufunc_dunder_equivalence(data, ufunc) - - @pytest.mark.xfail( - reason="DecimalArray.round is not implemented." - ) + + @pytest.mark.xfail(reason="DecimalArray.round is not implemented.") def test_round(self, data): super().test_round(data) diff --git a/pandas/tests/extension/test_arrow.py b/pandas/tests/extension/test_arrow.py index c490bb5b28fc1..a82cc8ccb4362 100644 --- a/pandas/tests/extension/test_arrow.py +++ b/pandas/tests/extension/test_arrow.py @@ -1011,7 +1011,7 @@ def _get_arith_xfail_marker(self, opname, pa_dtype): ) return mark - + def test_round(self, data, request): mark = pytest.mark.xfail( # raises=pa.ArrowInvalid, diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py index 4604478d77538..1cb2513d6f103 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -95,9 +95,7 @@ def test_EA_types(self, engine, data): def test_invert(self, data): super().test_invert(data) - @pytest.mark.xfail( - reason="Round is not valid for IntervalArray." - ) + @pytest.mark.xfail(reason="Round is not valid for IntervalArray.") def test_round(self, data): super().test_round(data) diff --git a/pandas/tests/extension/test_masked.py b/pandas/tests/extension/test_masked.py index 7895a7fdab881..3dd155817d060 100644 --- a/pandas/tests/extension/test_masked.py +++ b/pandas/tests/extension/test_masked.py @@ -358,7 +358,7 @@ def test_invert(self, data, request): ) request.node.add_marker(mark) super().test_invert(data) - + def test_round(self, data, request): if data.dtype == "boolean": mark = pytest.mark.xfail(reason="Cannot round boolean dtype") @@ -368,5 +368,3 @@ def test_round(self, data, request): class Test2DCompat(base.Dim2CompatTests): pass - - diff --git a/pandas/tests/extension/test_numpy.py b/pandas/tests/extension/test_numpy.py index b264875822adb..b7f61f41c8efc 100644 --- a/pandas/tests/extension/test_numpy.py +++ b/pandas/tests/extension/test_numpy.py @@ -270,9 +270,7 @@ def test_insert_invalid(self, data, invalid_scalar): # NumpyExtensionArray[object] can hold anything, so skip super().test_insert_invalid(data, invalid_scalar) - @pytest.mark.xfail( - reason="NumpyExtensionArray.round is not implemented." - ) + @pytest.mark.xfail(reason="NumpyExtensionArray.round is not implemented.") def test_round(self, data): super().test_round(data) diff --git a/pandas/tests/extension/test_sparse.py b/pandas/tests/extension/test_sparse.py index ae6409b9e5ded..9319f9be198fd 100644 --- a/pandas/tests/extension/test_sparse.py +++ b/pandas/tests/extension/test_sparse.py @@ -346,9 +346,7 @@ def test_map_raises(self, data, na_action): with pytest.raises(ValueError, match=msg): data.map(lambda x: np.nan, na_action=na_action) - @pytest.mark.xfail( - reason="SpareArray.round not implemented." - ) + @pytest.mark.xfail(reason="SpareArray.round not implemented.") def test_round(self, data): super().test_round(data) From 15915dc1337972b1be5068f6a6996151f8bab835 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 5 Sep 2023 01:14:16 +0100 Subject: [PATCH 09/32] remove test --- pandas/tests/extension/test_interval.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py index 1cb2513d6f103..7f8c4e6881e6a 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -89,12 +89,6 @@ def test_EA_types(self, engine, data): with pytest.raises(NotImplementedError, match=expected_msg): super().test_EA_types(engine, data) - @pytest.mark.xfail( - reason="Looks like the test (incorrectly) implicitly assumes int/bool dtype" - ) - def test_invert(self, data): - super().test_invert(data) - @pytest.mark.xfail(reason="Round is not valid for IntervalArray.") def test_round(self, data): super().test_round(data) From e31d06a998285b0776cadb6a139ccfad78413a18 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 6 Sep 2023 18:11:38 +0100 Subject: [PATCH 10/32] add default round method --- pandas/core/arrays/base.py | 12 +++++++++++- pandas/tests/extension/base/methods.py | 6 +++--- pandas/tests/extension/test_arrow.py | 6 ++++-- pandas/tests/extension/test_interval.py | 4 ---- pandas/tests/extension/test_masked.py | 6 ------ 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 0c73777a336bf..1a5dc695da524 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -2343,7 +2343,17 @@ def _add_logical_ops(cls) -> None: setattr(cls, "__rxor__", cls._create_logical_method(roperator.rxor)) def round(self, decimals: int = 0, *args, **kwargs) -> Self: - raise AbstractMethodError(self) + # Implementer note: This is a non-optimized default implementation. + # Implementers are encouraged to override this method to avoid + # elementwise rounding. + if not self.dtype._is_numeric or self.dtype._is_boolean: + raise TypeError( + f"Cannot round {self.dtype} dtype as it is non-numeric or boolean" + ) + return self._from_sequence( + [round(element) if not isna(element) else element for element in self.data], + dtype=self.dtype, + ) class ExtensionScalarOpsMixin(ExtensionOpsMixin): diff --git a/pandas/tests/extension/base/methods.py b/pandas/tests/extension/base/methods.py index 296f038e33cdf..420e3434b17eb 100644 --- a/pandas/tests/extension/base/methods.py +++ b/pandas/tests/extension/base/methods.py @@ -700,11 +700,11 @@ def test_equals_same_data_different_object(self, data): assert pd.Series(data).equals(pd.Series(data)) def test_round(self, data): - if not data.dtype._is_numeric: - pytest.skip("Round is only valid for numeric dtypes") + if not data.dtype._is_numeric or data.dtype._is_boolean: + pytest.skip("Round is only valid for numeric non-boolean dtypes") result = pd.Series(data).round() expected = pd.Series( - [np.round(element) if pd.notna(element) else element for element in data], + [round(element) if pd.notna(element) else element for element in data], dtype=data.dtype, ) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/extension/test_arrow.py b/pandas/tests/extension/test_arrow.py index a82cc8ccb4362..b7ee47bd0ac5a 100644 --- a/pandas/tests/extension/test_arrow.py +++ b/pandas/tests/extension/test_arrow.py @@ -1017,8 +1017,10 @@ def test_round(self, data, request): # raises=pa.ArrowInvalid, reason="ArrowArray.round converts dtype to double", ) - if pa.types.is_float32(data.dtype.pyarrow_dtype) or pa.types.is_float64( - data.dtype.pyarrow_dtype + if ( + pa.types.is_float32(data.dtype.pyarrow_dtype) + or pa.types.is_float64(data.dtype.pyarrow_dtype) + or pa.types.is_decimal(data.dtype.pyarrow_dtype) ): mark = None if mark is not None: diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py index 7f8c4e6881e6a..9a27cf6fbd0eb 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -89,10 +89,6 @@ def test_EA_types(self, engine, data): with pytest.raises(NotImplementedError, match=expected_msg): super().test_EA_types(engine, data) - @pytest.mark.xfail(reason="Round is not valid for IntervalArray.") - def test_round(self, data): - super().test_round(data) - # TODO: either belongs in tests.arrays.interval or move into base tests. def test_fillna_non_scalar_raises(data_missing): diff --git a/pandas/tests/extension/test_masked.py b/pandas/tests/extension/test_masked.py index 3dd155817d060..bed406e902483 100644 --- a/pandas/tests/extension/test_masked.py +++ b/pandas/tests/extension/test_masked.py @@ -359,12 +359,6 @@ def test_invert(self, data, request): request.node.add_marker(mark) super().test_invert(data) - def test_round(self, data, request): - if data.dtype == "boolean": - mark = pytest.mark.xfail(reason="Cannot round boolean dtype") - request.node.add_marker(mark) - super().test_round(data) - class Test2DCompat(base.Dim2CompatTests): pass From bc4d8c6b77b3de47528cce0cb45f9ebbc81cec7b Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 6 Sep 2023 18:14:44 +0100 Subject: [PATCH 11/32] remove invert --- pandas/tests/extension/test_masked.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pandas/tests/extension/test_masked.py b/pandas/tests/extension/test_masked.py index 8c0f4e27055f1..f5b0b6f4efa98 100644 --- a/pandas/tests/extension/test_masked.py +++ b/pandas/tests/extension/test_masked.py @@ -361,15 +361,6 @@ def check_accumulate(self, ser: pd.Series, op_name: str, skipna: bool): else: raise NotImplementedError(f"{op_name} not supported") - def test_invert(self, data, request): - if data.dtype.kind == "f": - mark = pytest.mark.xfail( - reason="Looks like the base class test implicitly assumes " - "boolean/integer dtypes" - ) - request.node.add_marker(mark) - super().test_invert(data) - class Test2DCompat(base.Dim2CompatTests): pass From 362cd7390e223608912f0e5afe068cb3109b6e6a Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 6 Sep 2023 18:17:05 +0100 Subject: [PATCH 12/32] typo --- pandas/tests/extension/test_sparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/extension/test_sparse.py b/pandas/tests/extension/test_sparse.py index d37a7127ed0f4..051ebed466f62 100644 --- a/pandas/tests/extension/test_sparse.py +++ b/pandas/tests/extension/test_sparse.py @@ -349,7 +349,7 @@ def test_map_raises(self, data, na_action): with pytest.raises(ValueError, match=msg): data.map(lambda x: np.nan, na_action=na_action) - @pytest.mark.xfail(reason="SpareArray.round not implemented.") + @pytest.mark.xfail(reason="SparseArray.round not implemented.") def test_round(self, data): super().test_round(data) From aa7986938f44ef1c8c94e39b749a13fe5d38f6a1 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 6 Sep 2023 18:19:32 +0100 Subject: [PATCH 13/32] decimal array works now --- pandas/tests/extension/decimal/test_decimal.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pandas/tests/extension/decimal/test_decimal.py b/pandas/tests/extension/decimal/test_decimal.py index 3597921bd2ce2..8dbd1c4c511d0 100644 --- a/pandas/tests/extension/decimal/test_decimal.py +++ b/pandas/tests/extension/decimal/test_decimal.py @@ -259,10 +259,6 @@ def test_series_repr(self, data): def test_unary_ufunc_dunder_equivalence(self, data, ufunc): super().test_unary_ufunc_dunder_equivalence(data, ufunc) - @pytest.mark.xfail(reason="DecimalArray.round is not implemented.") - def test_round(self, data): - super().test_round(data) - def test_take_na_value_other_decimal(): arr = DecimalArray([decimal.Decimal("1.0"), decimal.Decimal("2.0")]) From 2b694bf1a2e06b3ef8cd0f7bc6dfc524232a5f72 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 24 Sep 2023 11:12:17 +0100 Subject: [PATCH 14/32] move round to base calss --- pandas/core/arrays/base.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 843b89b4d0d69..cb64a257db5f3 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -2141,6 +2141,19 @@ def _mode(self, dropna: bool = True) -> Self: # error: Incompatible return value type (got "Union[ExtensionArray, # ndarray[Any, Any]]", expected "Self") return mode(self, dropna=dropna) # type: ignore[return-value] + + def round(self, decimals: int = 0, *args, **kwargs) -> Self: + # Implementer note: This is a non-optimized default implementation. + # Implementers are encouraged to override this method to avoid + # elementwise rounding. + if not self.dtype._is_numeric or self.dtype._is_boolean: + raise TypeError( + f"Cannot round {self.dtype} dtype as it is non-numeric or boolean" + ) + return self._from_sequence( + [round(element) if not isna(element) else element for element in self.data], + dtype=self.dtype, + ) def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs): if any( @@ -2337,19 +2350,6 @@ def _add_logical_ops(cls) -> None: setattr(cls, "__xor__", cls._create_logical_method(operator.xor)) setattr(cls, "__rxor__", cls._create_logical_method(roperator.rxor)) - def round(self, decimals: int = 0, *args, **kwargs) -> Self: - # Implementer note: This is a non-optimized default implementation. - # Implementers are encouraged to override this method to avoid - # elementwise rounding. - if not self.dtype._is_numeric or self.dtype._is_boolean: - raise TypeError( - f"Cannot round {self.dtype} dtype as it is non-numeric or boolean" - ) - return self._from_sequence( - [round(element) if not isna(element) else element for element in self.data], - dtype=self.dtype, - ) - class ExtensionScalarOpsMixin(ExtensionOpsMixin): """ From 01d145364f29806d384a04e51789050a74ce84b3 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 24 Sep 2023 11:20:00 +0100 Subject: [PATCH 15/32] move pytest mark into if statement --- pandas/tests/extension/test_arrow.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pandas/tests/extension/test_arrow.py b/pandas/tests/extension/test_arrow.py index 4d55ef156b8e4..d4c63c47e51b8 100644 --- a/pandas/tests/extension/test_arrow.py +++ b/pandas/tests/extension/test_arrow.py @@ -1027,16 +1027,15 @@ def _get_arith_xfail_marker(self, opname, pa_dtype): return mark def test_round(self, data, request): - mark = pytest.mark.xfail( - # raises=pa.ArrowInvalid, - reason="ArrowArray.round converts dtype to double", - ) - if ( + mark = None + if not ( pa.types.is_float32(data.dtype.pyarrow_dtype) or pa.types.is_float64(data.dtype.pyarrow_dtype) or pa.types.is_decimal(data.dtype.pyarrow_dtype) ): - mark = None + pytest.mark.xfail( + reason="ArrowArray.round converts dtype to double", + ) if mark is not None: request.node.add_marker(mark) super().test_round(data) From e5d500dac201717d901826c98ceff2c84c8764e5 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 24 Sep 2023 11:28:59 +0100 Subject: [PATCH 16/32] nitpick --- pandas/core/arrays/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index cb64a257db5f3..8ea11f93ff430 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -2141,14 +2141,14 @@ def _mode(self, dropna: bool = True) -> Self: # error: Incompatible return value type (got "Union[ExtensionArray, # ndarray[Any, Any]]", expected "Self") return mode(self, dropna=dropna) # type: ignore[return-value] - + def round(self, decimals: int = 0, *args, **kwargs) -> Self: # Implementer note: This is a non-optimized default implementation. # Implementers are encouraged to override this method to avoid # elementwise rounding. if not self.dtype._is_numeric or self.dtype._is_boolean: raise TypeError( - f"Cannot round {self.dtype} dtype as it is non-numeric or boolean" + f"Cannot round {type(self)} dtype as it is non-numeric or boolean" ) return self._from_sequence( [round(element) if not isna(element) else element for element in self.data], From 95d07faf48ed807a72e192051b216a1b737c5b00 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 24 Sep 2023 12:06:45 +0100 Subject: [PATCH 17/32] fix pyarrow round dtypes --- pandas/core/arrays/arrow/array.py | 3 ++- pandas/tests/extension/test_arrow.py | 15 --------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index 4d887ecd1510f..1e03f755e43b8 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -1085,7 +1085,8 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Self: DataFrame.round : Round values of a DataFrame. Series.round : Round values of a Series. """ - return type(self)(pc.round(self._pa_array, ndigits=decimals)) + result = pc.round(self._pa_array, ndigits=decimals) + return type(self)(result.cast(self._pa_array.type)) @doc(ExtensionArray.searchsorted) def searchsorted( diff --git a/pandas/tests/extension/test_arrow.py b/pandas/tests/extension/test_arrow.py index d4c63c47e51b8..b317984198e47 100644 --- a/pandas/tests/extension/test_arrow.py +++ b/pandas/tests/extension/test_arrow.py @@ -1023,23 +1023,8 @@ def _get_arith_xfail_marker(self, opname, pa_dtype): raises=pa.ArrowInvalid, reason="Invalid decimal function: power_checked", ) - return mark - def test_round(self, data, request): - mark = None - if not ( - pa.types.is_float32(data.dtype.pyarrow_dtype) - or pa.types.is_float64(data.dtype.pyarrow_dtype) - or pa.types.is_decimal(data.dtype.pyarrow_dtype) - ): - pytest.mark.xfail( - reason="ArrowArray.round converts dtype to double", - ) - if mark is not None: - request.node.add_marker(mark) - super().test_round(data) - def test_arith_series_with_scalar(self, data, all_arithmetic_operators, request): pa_dtype = data.dtype.pyarrow_dtype From f28d82f565e12317940e4dc4ae881280358a1bad Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 19 Oct 2023 03:10:15 +0100 Subject: [PATCH 18/32] implement sparsearray round --- pandas/core/arrays/sparse/array.py | 4 ++++ pandas/tests/extension/test_sparse.py | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/core/arrays/sparse/array.py b/pandas/core/arrays/sparse/array.py index 00cbe1286c195..c73633b36997f 100644 --- a/pandas/core/arrays/sparse/array.py +++ b/pandas/core/arrays/sparse/array.py @@ -1647,6 +1647,10 @@ def argmin(self, skipna: bool = True) -> int: if not skipna and self._hasna: raise NotImplementedError return self._argmin_argmax("argmin") + + def round(self, decimals: int = 0, *args, **kwargs) -> Self: + new_values = np.array([round(element) if not isna(element) else element for element in self.sp_values]) + return self._simple_new(new_values, self._sparse_index, self.dtype) # ------------------------------------------------------------------------ # Ufuncs diff --git a/pandas/tests/extension/test_sparse.py b/pandas/tests/extension/test_sparse.py index 051ebed466f62..f56dea3f43de7 100644 --- a/pandas/tests/extension/test_sparse.py +++ b/pandas/tests/extension/test_sparse.py @@ -349,10 +349,6 @@ def test_map_raises(self, data, na_action): with pytest.raises(ValueError, match=msg): data.map(lambda x: np.nan, na_action=na_action) - @pytest.mark.xfail(reason="SparseArray.round not implemented.") - def test_round(self, data): - super().test_round(data) - class TestCasting(BaseSparseTests, base.BaseCastingTests): @pytest.mark.xfail(raises=TypeError, reason="no sparse StringDtype") From 2299134ba4bebc14015d9fad5831ba8fcf6c0479 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 19 Oct 2023 03:11:12 +0100 Subject: [PATCH 19/32] lint --- pandas/core/arrays/sparse/array.py | 9 +++++++-- pandas/core/generic.py.isorted | 0 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 pandas/core/generic.py.isorted diff --git a/pandas/core/arrays/sparse/array.py b/pandas/core/arrays/sparse/array.py index c73633b36997f..fff82ae9091e5 100644 --- a/pandas/core/arrays/sparse/array.py +++ b/pandas/core/arrays/sparse/array.py @@ -1647,9 +1647,14 @@ def argmin(self, skipna: bool = True) -> int: if not skipna and self._hasna: raise NotImplementedError return self._argmin_argmax("argmin") - + def round(self, decimals: int = 0, *args, **kwargs) -> Self: - new_values = np.array([round(element) if not isna(element) else element for element in self.sp_values]) + new_values = np.array( + [ + round(element) if not isna(element) else element + for element in self.sp_values + ] + ) return self._simple_new(new_values, self._sparse_index, self.dtype) # ------------------------------------------------------------------------ diff --git a/pandas/core/generic.py.isorted b/pandas/core/generic.py.isorted new file mode 100644 index 0000000000000..e69de29bb2d1d From 15a7cacc84e9cdabec89eec468b2af754be236b9 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 19 Oct 2023 03:11:17 +0100 Subject: [PATCH 20/32] lint --- pandas/core/generic.py.isorted | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pandas/core/generic.py.isorted diff --git a/pandas/core/generic.py.isorted b/pandas/core/generic.py.isorted deleted file mode 100644 index e69de29bb2d1d..0000000000000 From ee37b4dc95a6b88a27585c8e6f839c5d796f8ea8 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 19 Oct 2023 13:41:17 +0100 Subject: [PATCH 21/32] mypy --- pandas/core/arrays/base.py | 2 +- pandas/tests/extension/test_numpy.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 8ea11f93ff430..42b84c609cf54 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -2151,7 +2151,7 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Self: f"Cannot round {type(self)} dtype as it is non-numeric or boolean" ) return self._from_sequence( - [round(element) if not isna(element) else element for element in self.data], + [round(element) if not isna(element) else element for element in self], dtype=self.dtype, ) diff --git a/pandas/tests/extension/test_numpy.py b/pandas/tests/extension/test_numpy.py index a212ab1bdc210..542e938d1a40a 100644 --- a/pandas/tests/extension/test_numpy.py +++ b/pandas/tests/extension/test_numpy.py @@ -270,10 +270,6 @@ def test_insert_invalid(self, data, invalid_scalar): # NumpyExtensionArray[object] can hold anything, so skip super().test_insert_invalid(data, invalid_scalar) - @pytest.mark.xfail(reason="NumpyExtensionArray.round is not implemented.") - def test_round(self, data): - super().test_round(data) - class TestArithmetics(BaseNumPyTests, base.BaseArithmeticOpsTests): divmod_exc = None From 70ebf5a1317c16a2bf2b9a5fecaf0bac54ef272d Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 7 Nov 2023 20:02:56 +0000 Subject: [PATCH 22/32] ignore my py error --- pandas/core/arrays/datetimelike.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 52596f29ffc0c..49e7d7c78ec81 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -2140,7 +2140,7 @@ def _round(self, freq, mode, ambiguous, nonexistent): return self._simple_new(result, dtype=self.dtype) @Appender((_round_doc + _round_example).format(op="round")) - def round( + def round( # type: ignore[override] self, freq, ambiguous: TimeAmbiguous = "raise", From e01130dc912f0dcc9d5d0c4115b1e9f1324daff2 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 7 Nov 2023 21:17:01 +0000 Subject: [PATCH 23/32] remove blocks ignore mypyr --- pandas/core/internals/blocks.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 6399f85723ae5..0e83c7fbce02e 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -1644,11 +1644,7 @@ def round(self, decimals: int, using_cow: bool = False) -> Self: if not self.is_numeric or self.is_bool: return self.copy(deep=not using_cow) refs = None - # TODO: round only defined on BaseMaskedArray - # Series also does this, so would need to fix both places - # error: Item "ExtensionArray" of "Union[ndarray[Any, Any], ExtensionArray]" - # has no attribute "round" - values = self.values.round(decimals) # type: ignore[union-attr] + values = self.values.round(decimals) if values is self.values: refs = self.refs if not using_cow: From 2e9cddfcc03ac4d2fd99c8edb2eed46da8a4e38d Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 8 Nov 2023 14:26:43 +0000 Subject: [PATCH 24/32] revert cast --- pandas/core/arrays/arrow/array.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index e9a5c7d4bcbf2..4bcc03643dac8 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -1094,8 +1094,7 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Self: DataFrame.round : Round values of a DataFrame. Series.round : Round values of a Series. """ - result = pc.round(self._pa_array, ndigits=decimals) - return type(self)(result.cast(self._pa_array.type)) + return type(self)(pc.round(self._pa_array, ndigits=decimals)) @doc(ExtensionArray.searchsorted) def searchsorted( From 76c9ba7ee9d412d89000ef4e968bce294364766c Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 8 Nov 2023 14:42:42 +0000 Subject: [PATCH 25/32] typerror for non numerics --- pandas/core/arrays/arrow/array.py | 2 ++ pandas/tests/extension/base/methods.py | 16 +++++++++------- pandas/tests/extension/test_datetime.py | 5 +++++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index 4bcc03643dac8..852bc7458151c 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -1094,6 +1094,8 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Self: DataFrame.round : Round values of a DataFrame. Series.round : Round values of a Series. """ + if not self.dtype._is_numeric or self.dtype._is_boolean: + raise TypeError("Cannot round non-numeric type.") return type(self)(pc.round(self._pa_array, ndigits=decimals)) @doc(ExtensionArray.searchsorted) diff --git a/pandas/tests/extension/base/methods.py b/pandas/tests/extension/base/methods.py index 0bfe56fb9e34f..835b5e0be713b 100644 --- a/pandas/tests/extension/base/methods.py +++ b/pandas/tests/extension/base/methods.py @@ -719,10 +719,12 @@ def test_equals_same_data_different_object(self, data): def test_round(self, data): if not data.dtype._is_numeric or data.dtype._is_boolean: - pytest.skip("Round is only valid for numeric non-boolean dtypes") - result = pd.Series(data).round() - expected = pd.Series( - [round(element) if pd.notna(element) else element for element in data], - dtype=data.dtype, - ) - tm.assert_series_equal(result, expected) + with pytest.raises(TypeError): + pd.Series(data).round() + else: + result = pd.Series(data).round() + expected = pd.Series( + [round(element) if pd.notna(element) else element for element in data], + dtype=data.dtype, + ) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/extension/test_datetime.py b/pandas/tests/extension/test_datetime.py index 5a7b15ddb01ce..1cc362bd3c67e 100644 --- a/pandas/tests/extension/test_datetime.py +++ b/pandas/tests/extension/test_datetime.py @@ -138,6 +138,11 @@ def check_reduce(self, ser: pd.Series, op_name: str, skipna: bool): else: return super().check_reduce(ser, op_name, skipna) + + @pytest.mark.skip("DatetimeArray uses a different function signature for round") + def test_round(self): + pass + class Test2DCompat(base.NDArrayBacked2DTests): From 044f0a1030e94179110167e3dfcaa85fd44be469 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 8 Nov 2023 14:53:30 +0000 Subject: [PATCH 26/32] isna --- pandas/core/arrays/base.py | 5 ++++- pandas/tests/extension/test_datetime.py | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index a9caeb3265f7d..7fe44c99b3205 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -2265,7 +2265,10 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Self: f"Cannot round {type(self)} dtype as it is non-numeric or boolean" ) return self._from_sequence( - [round(element) if not isna(element) else element for element in self], + [ + round(element) if not element_isna else element + for (element, element_isna) in zip(self, self.isna()) + ], dtype=self.dtype, ) diff --git a/pandas/tests/extension/test_datetime.py b/pandas/tests/extension/test_datetime.py index 1cc362bd3c67e..8e0f573a472b6 100644 --- a/pandas/tests/extension/test_datetime.py +++ b/pandas/tests/extension/test_datetime.py @@ -138,12 +138,11 @@ def check_reduce(self, ser: pd.Series, op_name: str, skipna: bool): else: return super().check_reduce(ser, op_name, skipna) - + @pytest.mark.skip("DatetimeArray uses a different function signature for round") def test_round(self): pass - class Test2DCompat(base.NDArrayBacked2DTests): pass From 770edf34ceef9e8543110e4f2021902390deb87a Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 9 Nov 2023 11:36:24 +0000 Subject: [PATCH 27/32] cast pyarrow --- pandas/core/arrays/arrow/array.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index 852bc7458151c..3be8f6e04764a 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -1096,7 +1096,8 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Self: """ if not self.dtype._is_numeric or self.dtype._is_boolean: raise TypeError("Cannot round non-numeric type.") - return type(self)(pc.round(self._pa_array, ndigits=decimals)) + result = pc.round(self._pa_array, ndigits=decimals) + return type(self)(result.cast(self._pa_array.type)) @doc(ExtensionArray.searchsorted) def searchsorted( From f3d452029ad6744ec4bd317c8636505cef4e448d Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Mon, 4 Mar 2024 10:07:17 +0000 Subject: [PATCH 28/32] Update blocks.py --- pandas/core/internals/blocks.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 803d5eee94571..abb49948cdd21 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -1522,9 +1522,8 @@ def round(self, decimals: int) -> Self: Caller is responsible for validating this """ if not self.is_numeric or self.is_bool: - return self.copy(deep=False) - - values = self.values.round(decimals) # type: ignore[union-attr] + return self.copy(deep=False) + values = self.values.round(decimals) refs = None if values is self.values: From 398e64b30b45346472410f72fe6b8746a26b3524 Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Mon, 4 Mar 2024 10:50:20 +0000 Subject: [PATCH 29/32] Update blocks.py --- pandas/core/internals/blocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index abb49948cdd21..14abcab6eb3f9 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -1522,7 +1522,7 @@ def round(self, decimals: int) -> Self: Caller is responsible for validating this """ if not self.is_numeric or self.is_bool: - return self.copy(deep=False) + return self.copy(deep=False) values = self.values.round(decimals) refs = None From b0af597a95c9184b2026004a445d17a6ad464f67 Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Wed, 13 Mar 2024 10:52:58 +0000 Subject: [PATCH 30/32] rounding boolean shouldnt error --- pandas/core/arrays/base.py | 8 ++++---- pandas/tests/extension/base/methods.py | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 741f9aa86094f..668fa67777c84 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -2282,10 +2282,10 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Self: # Implementer note: This is a non-optimized default implementation. # Implementers are encouraged to override this method to avoid # elementwise rounding. - if not self.dtype._is_numeric or self.dtype._is_boolean: - raise TypeError( - f"Cannot round {type(self)} dtype as it is non-numeric or boolean" - ) + if self.dtype._is_boolean: + return self + if not self.dtype._is_numeric: + raise TypeError(f"Cannot round {type(self)} dtype as it is non-numeric") return self._from_sequence( [ round(element) if not element_isna else element diff --git a/pandas/tests/extension/base/methods.py b/pandas/tests/extension/base/methods.py index bba45baf7fba0..b247fb0ebe2fa 100644 --- a/pandas/tests/extension/base/methods.py +++ b/pandas/tests/extension/base/methods.py @@ -720,9 +720,13 @@ def test_equals_same_data_different_object(self, data): assert pd.Series(data).equals(pd.Series(data)) def test_round(self, data): - if not data.dtype._is_numeric or data.dtype._is_boolean: + if not data.dtype._is_numeric: with pytest.raises(TypeError): - pd.Series(data).round() + data.round() + elif data.dtype._is_boolean: + result = pd.Series(data).round() + expected = pd.Series(data) + tm.assert_series_equal(result, expected) else: result = pd.Series(data).round() expected = pd.Series( From 70b6c896984b6354539b35196d2b9a73201a8b22 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 13 Mar 2024 22:08:45 +0000 Subject: [PATCH 31/32] whatsnew --- doc/source/whatsnew/v3.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 0f125af599b12..cf51583e63aa6 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -360,7 +360,7 @@ Sparse ExtensionArray ^^^^^^^^^^^^^^ - Fixed bug in :meth:`api.types.is_datetime64_any_dtype` where a custom :class:`ExtensionDtype` would return ``False`` for array-likes (:issue:`57055`) -- +- A default, unoptimised :meth:`ExtensionArray.round` method is now provided for numeric ExtensionArrays (:issue:`49387`) Styler ^^^^^^ From bebe0ac716360cac56ab9c724e321245fd82722d Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Sat, 15 Jun 2024 23:14:23 +0100 Subject: [PATCH 32/32] Update v3.0.0.rst --- doc/source/whatsnew/v3.0.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 1044a5f4d221e..041a082bc4e16 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -586,6 +586,7 @@ Sparse - ExtensionArray +^^^^^^^^^^^^^^ - A default, unoptimised :meth:`ExtensionArray.round` method is now provided for numeric ExtensionArrays (:issue:`49387`) - Bug in :meth:`.arrays.ArrowExtensionArray.__setitem__` which caused wrong behavior when using an integer array with repeated values as a key (:issue:`58530`) - Bug in :meth:`api.types.is_datetime64_any_dtype` where a custom :class:`ExtensionDtype` would return ``False`` for array-likes (:issue:`57055`)