From 687be4bbc1bb88eca023c2a1ac0778cfc0ea82e0 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 7 May 2024 18:37:16 -0700 Subject: [PATCH 1/4] CLN: callable to Series.iloc returning a tuple --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/_testing/__init__.py | 13 +++---------- pandas/core/indexing.py | 15 ++++++--------- pandas/tests/frame/indexing/test_indexing.py | 10 +++++----- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index c932d793038c2..e0d16cc7bae6b 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -223,6 +223,7 @@ Removal of prior version deprecations/changes - All arguments except ``name`` in :meth:`Index.rename` are now keyword only (:issue:`56493`) - All arguments except the first ``path``-like argument in IO writers are now keyword only (:issue:`54229`) - Changed behavior of :meth:`Series.__getitem__` and :meth:`Series.__setitem__` to always treat integer keys as labels, never as positional, consistent with :class:`DataFrame` behavior (:issue:`50617`) +- Disallow allowing a callable argument to :meth:`Series.iloc` to return a ``tuple`` (:issue:`53769`) - Disallow allowing logical operations (``||``, ``&``, ``^``) between pandas objects and dtype-less sequences (e.g. ``list``, ``tuple``); wrap the objects in :class:`Series`, :class:`Index`, or ``np.array`` first instead (:issue:`52264`) - Disallow automatic casting to object in :class:`Series` logical operations (``&``, ``^``, ``||``) between series with mismatched indexes and dtypes other than ``object`` or ``bool`` (:issue:`52538`) - Disallow calling :meth:`Series.replace` or :meth:`DataFrame.replace` without a ``value`` and with non-dict-like ``to_replace`` (:issue:`33302`) diff --git a/pandas/_testing/__init__.py b/pandas/_testing/__init__.py index 12395b42bba19..a757ef6fc1a29 100644 --- a/pandas/_testing/__init__.py +++ b/pandas/_testing/__init__.py @@ -10,7 +10,6 @@ ContextManager, cast, ) -import warnings import numpy as np @@ -290,17 +289,11 @@ def box_expected(expected, box_cls, transpose: bool = True): else: expected = pd.array(expected, copy=False) elif box_cls is Index: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", "Dtype inference", category=FutureWarning) - expected = Index(expected) + expected = Index(expected) elif box_cls is Series: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", "Dtype inference", category=FutureWarning) - expected = Series(expected) + expected = Series(expected) elif box_cls is DataFrame: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", "Dtype inference", category=FutureWarning) - expected = Series(expected).to_frame() + expected = Series(expected).to_frame() if transpose: # for vector operations, we need a DataFrame to be a single-row, # not a single-column, in order to operate against non-DataFrame diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index b3ae53272cae4..91aa9cee6be8b 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -159,7 +159,7 @@ def iloc(self) -> _iLocIndexer: """ Purely integer-location based indexing for selection by position. - .. deprecated:: 2.2.0 + .. versionchanged:: 3.0 Returning a tuple from a callable is deprecated. @@ -905,7 +905,7 @@ def __setitem__(self, key, value) -> None: key = tuple(com.apply_if_callable(x, self.obj) for x in key) else: maybe_callable = com.apply_if_callable(key, self.obj) - key = self._check_deprecated_callable_usage(key, maybe_callable) + key = self._raise_callable_usage(key, maybe_callable) indexer = self._get_setitem_indexer(key) self._has_valid_setitem_indexer(key) @@ -1164,14 +1164,11 @@ def _contains_slice(x: object) -> bool: def _convert_to_indexer(self, key, axis: AxisInt): raise AbstractMethodError(self) - def _check_deprecated_callable_usage(self, key: Any, maybe_callable: T) -> T: + def _raise_callable_usage(self, key: Any, maybe_callable: T) -> T: # GH53533 if self.name == "iloc" and callable(key) and isinstance(maybe_callable, tuple): - warnings.warn( - "Returning a tuple from a callable with iloc " - "is deprecated and will be removed in a future version", - FutureWarning, - stacklevel=find_stack_level(), + raise ValueError( + "Returning a tuple from a callable with iloc " "is not allowed.", ) return maybe_callable @@ -1189,7 +1186,7 @@ def __getitem__(self, key): axis = self.axis or 0 maybe_callable = com.apply_if_callable(key, self.obj) - maybe_callable = self._check_deprecated_callable_usage(key, maybe_callable) + maybe_callable = self._raise_callable_usage(key, maybe_callable) return self._getitem_axis(maybe_callable, axis=axis) def _is_scalar_access(self, key: tuple): diff --git a/pandas/tests/frame/indexing/test_indexing.py b/pandas/tests/frame/indexing/test_indexing.py index ee08c10f96ae7..bc233c5b86b4b 100644 --- a/pandas/tests/frame/indexing/test_indexing.py +++ b/pandas/tests/frame/indexing/test_indexing.py @@ -1019,13 +1019,13 @@ def test_single_element_ix_dont_upcast(self, float_frame): result = df.loc[[0], "b"] tm.assert_series_equal(result, expected) - def test_iloc_callable_tuple_return_value(self): - # GH53769 + def test_iloc_callable_tuple_return_value_raises(self): + # GH53769: Enforced pandas 3.0 df = DataFrame(np.arange(40).reshape(10, 4), index=range(0, 20, 2)) - msg = "callable with iloc" - with tm.assert_produces_warning(FutureWarning, match=msg): + msg = "Returning a tuple from" + with pytest.raises(ValueError, match=msg): df.iloc[lambda _: (0,)] - with tm.assert_produces_warning(FutureWarning, match=msg): + with pytest.raises(ValueError, match=msg): df.iloc[lambda _: (0,)] = 1 def test_iloc_row(self): From 4d2a2ad2ae05f1fee7dba9ca79889b6966eda5c7 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 7 May 2024 18:38:04 -0700 Subject: [PATCH 2/4] undo other change --- pandas/_testing/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pandas/_testing/__init__.py b/pandas/_testing/__init__.py index a757ef6fc1a29..12395b42bba19 100644 --- a/pandas/_testing/__init__.py +++ b/pandas/_testing/__init__.py @@ -10,6 +10,7 @@ ContextManager, cast, ) +import warnings import numpy as np @@ -289,11 +290,17 @@ def box_expected(expected, box_cls, transpose: bool = True): else: expected = pd.array(expected, copy=False) elif box_cls is Index: - expected = Index(expected) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Dtype inference", category=FutureWarning) + expected = Index(expected) elif box_cls is Series: - expected = Series(expected) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Dtype inference", category=FutureWarning) + expected = Series(expected) elif box_cls is DataFrame: - expected = Series(expected).to_frame() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Dtype inference", category=FutureWarning) + expected = Series(expected).to_frame() if transpose: # for vector operations, we need a DataFrame to be a single-row, # not a single-column, in order to operate against non-DataFrame From 3196fb92d2c4c92e58e265d55f1a3a6fd2ba45f0 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 7 May 2024 18:38:53 -0700 Subject: [PATCH 3/4] concat string --- pandas/core/indexing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 91aa9cee6be8b..d29bef02a16cf 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -1168,7 +1168,7 @@ def _raise_callable_usage(self, key: Any, maybe_callable: T) -> T: # GH53533 if self.name == "iloc" and callable(key) and isinstance(maybe_callable, tuple): raise ValueError( - "Returning a tuple from a callable with iloc " "is not allowed.", + "Returning a tuple from a callable with iloc is not allowed.", ) return maybe_callable From 4b1b868acbf1c3abbc8a10bc40fd5243ba6e3c29 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 7 May 2024 19:15:43 -0700 Subject: [PATCH 4/4] Update doc/source/whatsnew/v3.0.0.rst Co-authored-by: William Ayd --- 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 e0d16cc7bae6b..376ca4b3f7dd3 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -223,7 +223,7 @@ Removal of prior version deprecations/changes - All arguments except ``name`` in :meth:`Index.rename` are now keyword only (:issue:`56493`) - All arguments except the first ``path``-like argument in IO writers are now keyword only (:issue:`54229`) - Changed behavior of :meth:`Series.__getitem__` and :meth:`Series.__setitem__` to always treat integer keys as labels, never as positional, consistent with :class:`DataFrame` behavior (:issue:`50617`) -- Disallow allowing a callable argument to :meth:`Series.iloc` to return a ``tuple`` (:issue:`53769`) +- Disallow a callable argument to :meth:`Series.iloc` to return a ``tuple`` (:issue:`53769`) - Disallow allowing logical operations (``||``, ``&``, ``^``) between pandas objects and dtype-less sequences (e.g. ``list``, ``tuple``); wrap the objects in :class:`Series`, :class:`Index`, or ``np.array`` first instead (:issue:`52264`) - Disallow automatic casting to object in :class:`Series` logical operations (``&``, ``^``, ``||``) between series with mismatched indexes and dtypes other than ``object`` or ``bool`` (:issue:`52538`) - Disallow calling :meth:`Series.replace` or :meth:`DataFrame.replace` without a ``value`` and with non-dict-like ``to_replace`` (:issue:`33302`)