Repository: pandas-dev/pandas
Pull request #58235 (2024-04-12 16:55:51)

DOC: Enforce Numpy Docstring Validation for pandas.ExcelFile, pandas.ExcelFile.parse and pandas.ExcelWriter
- [ ] xref #58067
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


tuhinsharma121: This PR is ready for review. Thanks.

Patch:
diff --git a/ci/code_checks.sh b/ci/code_checks.sh
index b76ef69efa..01baadab67 100755
--- a/ci/code_checks.sh
+++ b/ci/code_checks.sh
@@ -153,9 +153,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then
         -i "pandas.DatetimeTZDtype SA01" \
         -i "pandas.DatetimeTZDtype.tz SA01" \
         -i "pandas.DatetimeTZDtype.unit SA01" \
-        -i "pandas.ExcelFile PR01,SA01" \
-        -i "pandas.ExcelFile.parse PR01,SA01" \
-        -i "pandas.ExcelWriter SA01" \
         -i "pandas.Float32Dtype SA01" \
         -i "pandas.Float64Dtype SA01" \
         -i "pandas.Grouper PR02,SA01" \
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index e05cc87d1a..f709bec842 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -208,6 +208,8 @@ Removal of prior version deprecations/changes
 - :meth:`SeriesGroupBy.agg` no longer pins the name of the group to the input passed to the provided ``func`` (:issue:`51703`)
 - 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`)
+- 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`)
 - Disallow constructing a :class:`arrays.SparseArray` with scalar data (:issue:`53039`)
 - Disallow non-standard (``np.ndarray``, :class:`Index`, :class:`ExtensionArray`, or :class:`Series`) to :func:`isin`, :func:`unique`, :func:`factorize` (:issue:`52986`)
diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py
index 2d8e490f02..98e6895287 100644
--- a/pandas/core/dtypes/dtypes.py
+++ b/pandas/core/dtypes/dtypes.py
@@ -698,8 +698,8 @@ class DatetimeTZDtype(PandasExtensionDtype):
     Parameters
     ----------
     unit : str, default "ns"
-        The precision of the datetime data. Currently limited
-        to ``"ns"``.
+        The precision of the datetime data. Valid options are
+        ``"s"``, ``"ms"``, ``"us"``, ``"ns"``.
     tz : str, int, or datetime.tzinfo
         The timezone.
 
diff --git a/pandas/core/ops/array_ops.py b/pandas/core/ops/array_ops.py
index 810e30d369..983a3df57e 100644
--- a/pandas/core/ops/array_ops.py
+++ b/pandas/core/ops/array_ops.py
@@ -12,7 +12,6 @@ from typing import (
     TYPE_CHECKING,
     Any,
 )
-import warnings
 
 import numpy as np
 
@@ -29,7 +28,6 @@ from pandas._libs.tslibs import (
     is_supported_dtype,
     is_unitless,
 )
-from pandas.util._exceptions import find_stack_level
 
 from pandas.core.dtypes.cast import (
     construct_1d_object_array_from_listlike,
@@ -424,15 +422,13 @@ def logical_op(left: ArrayLike, right: Any, op) -> ArrayLike:
     right = lib.item_from_zerodim(right)
     if is_list_like(right) and not hasattr(right, "dtype"):
         # e.g. list, tuple
-        warnings.warn(
+        raise TypeError(
+            # GH#52264
             "Logical ops (and, or, xor) between Pandas objects and dtype-less "
-            "sequences (e.g. list, tuple) are deprecated and will raise in a "
-            "future version. Wrap the object in a Series, Index, or np.array "
+            "sequences (e.g. list, tuple) are no longer supported. "
+            "Wrap the object in a Series, Index, or np.array "
             "before operating instead.",
-            FutureWarning,
-            stacklevel=find_stack_level(),
         )
-        right = construct_1d_object_array_from_listlike(right)
 
     # NB: We assume extract_array has already been called on left and right
     lvalues = ensure_wrapped_if_datetimelike(left)
diff --git a/pandas/core/series.py b/pandas/core/series.py
index 0f796964eb..ab24b224b0 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -5859,17 +5859,12 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
                         object,
                         np.bool_,
                     ):
-                        warnings.warn(
-                            "Operation between non boolean Series with different "
-                            "indexes will no longer return a boolean result in "
-                            "a future version. Cast both Series to object type "
-                            "to maintain the prior behavior.",
-                            FutureWarning,
-                            stacklevel=find_stack_level(),
-                        )
-                    # to keep original value's dtype for bool ops
-                    left = left.astype(object)
-                    right = right.astype(object)
+                        pass
+                        # GH#52538 no longer cast in these cases
+                    else:
+                        # to keep original value's dtype for bool ops
+                        left = left.astype(object)
+                        right = right.astype(object)
 
                 left, right = left.align(right)
 
diff --git a/pandas/io/excel/_base.py b/pandas/io/excel/_base.py
index a9da95054b..2b35cfa044 100644
--- a/pandas/io/excel/_base.py
+++ b/pandas/io/excel/_base.py
@@ -979,6 +979,12 @@ class ExcelWriter(Generic[_WorkbookT]):
 
         .. versionadded:: 1.3.0
 
+    See Also
+    --------
+    read_excel : Read an Excel sheet values (xlsx) file into DataFrame.
+    read_csv : Read a comma-separated values (csv) file into DataFrame.
+    read_fwf : Read a table of fixed-width formatted lines into DataFrame.
+
     Notes
     -----
     For compatibility with CSV writers, ExcelWriter serializes lists
@@ -1434,6 +1440,7 @@ def inspect_excel_format(
         return "zip"
 
 
+@doc(storage_options=_shared_docs["storage_options"])
 class ExcelFile:
     """
     Class for parsing tabular Excel sheets into DataFrame objects.
@@ -1472,19 +1479,27 @@ class ExcelFile:
            - Otherwise if ``path_or_buffer`` is in xlsb format,
              `pyxlsb <https://pypi.org/project/pyxlsb/>`_ will be used.
 
-           .. versionadded:: 1.3.0
+        .. versionadded:: 1.3.0
 
            - Otherwise if `openpyxl <https://pypi.org/project/openpyxl/>`_ is installed,
              then ``openpyxl`` will be used.
            - Otherwise if ``xlrd >= 2.0`` is installed, a ``ValueError`` will be raised.
 
-           .. warning::
+        .. warning::
 
-            Please do not report issues when using ``xlrd`` to read ``.xlsx`` files.
-            This is not supported, switch to using ``openpyxl`` instead.
+           Please do not report issues when using ``xlrd`` to read ``.xlsx`` files.
+           This is not supported, switch to using ``openpyxl`` instead.
+    {storage_options}
     engine_kwargs : dict, optional
         Arbitrary keyword arguments passed to excel engine.
 
+    See Also
+    --------
+    DataFrame.to_excel : Write DataFrame to an Excel file.
+    DataFrame.to_csv : Write DataFrame to a comma-separated values (csv) file.
+    read_csv : Read a comma-separated values (csv) file into DataFrame.
+    read_fwf : Read a table of fixed-width formatted lines into DataFrame.
+
     Examples
     --------
     >>> file = pd.ExcelFile("myfile.xlsx")  # doctest: +SKIP
@@ -1595,11 +1610,134 @@ class ExcelFile:
         Equivalent to read_excel(ExcelFile, ...)  See the read_excel
         docstring for more info on accepted parameters.
 
+        Parameters
+        ----------
+        sheet_name : str, int, list, or None, default 0
+            Strings are used for sheet names. Integers are used in zero-indexed
+            sheet positions (chart sheets do not count as a sheet position).
+            Lists of strings/integers are used to request multiple sheets.
+            Specify ``None`` to get all worksheets.
+        header : int, list of int, default 0
+            Row (0-indexed) to use for the column labels of the parsed
+            DataFrame. If a list of integers is passed those row positions will
+            be combined into a ``MultiIndex``. Use None if there is no header.
+        names : array-like, default None
+            List of column names to use. If file contains no header row,
+            then you should explicitly pass header=None.
+        index_col : int, str, list of int, default None
+            Column (0-indexed) to use as the row labels of the DataFrame.
+            Pass None if there is no such column.  If a list is passed,
+            those columns will be combined into a ``MultiIndex``.  If a
+            subset of data is selected with ``usecols``, index_col
+            is based on the subset.
+
+            Missing values will be forward filled to allow roundtripping with
+            ``to_excel`` for ``merged_cells=True``. To avoid forward filling the
+            missing values use ``set_index`` after reading the data instead of
+            ``index_col``.
+        usecols : str, list-like, or callable, default None
+            * If None, then parse all columns.
+            * If str, then indicates comma separated list of Excel column letters
+              and column ranges (e.g. "A:E" or "A,C,E:F"). Ranges are inclusive of
+              both sides.
+            * If list of int, then indicates list of column numbers to be parsed
+              (0-indexed).
+            * If list of string, then indicates list of column names to be parsed.
+            * If callable, then evaluate each column name against it and parse the
+              column if the callable returns ``True``.
+
+            Returns a subset of the columns according to behavior above.
+        converters : dict, default None
+            Dict of functions for converting values in certain columns. Keys can
+            either be integers or column labels, values are functions that take one
+            input argument, the Excel cell content, and return the transformed
+            content.
+        true_values : list, default None
+            Values to consider as True.
+        false_values : list, default None
+            Values to consider as False.
+        skiprows : list-like, int, or callable, optional
+            Line numbers to skip (0-indexed) or number of lines to skip (int) at the
+            start of the file. If callable, the callable function will be evaluated
+            against the row indices, returning True if the row should be skipped and
+            False otherwise. An example of a valid callable argument would be ``lambda
+            x: x in [0, 2]``.
+        nrows : int, default None
+            Number of rows to parse.
+        na_values : scalar, str, list-like, or dict, default None
+            Additional strings to recognize as NA/NaN. If dict passed, specific
+            per-column NA values.
+        parse_dates : bool, list-like, or dict, default False
+            The behavior is as follows:
+
+            * ``bool``. If True -> try parsing the index.
+            * ``list`` of int or names. e.g. If [1, 2, 3] -> try parsing columns 1, 2, 3
+              each as a separate date column.
+            * ``list`` of lists. e.g.  If [[1, 3]] -> combine columns 1 and 3 and
+              parse as a single date column.
+            * ``dict``, e.g. {{'foo' : [1, 3]}} -> parse columns 1, 3 as date and call
+              result 'foo'
+
+            If a column or index contains an unparsable date, the entire column or
+            index will be returned unaltered as an object data type. If you
+            don`t want to parse some cells as date just change their type
+            in Excel to "Text".For non-standard datetime parsing, use
+            ``pd.to_datetime`` after ``pd.read_excel``.
+
+            Note: A fast-path exists for iso8601-formatted dates.
+        date_parser : function, optional
+            Function to use for converting a sequence of string columns to an array of
+            datetime instances. The default uses ``dateutil.parser.parser`` to do the
+            conversion. Pandas will try to call `date_parser` in three different ways,
+            advancing to the next if an exception occurs: 1) Pass one or more arrays
+            (as defined by `parse_dates`) as arguments; 2) concatenate (row-wise) the
+            string values from the columns defined by `parse_dates` into a single array
+            and pass that; and 3) call `date_parser` once for each row using one or
+            more strings (corresponding to the columns defined by `parse_dates`) as
+            arguments.
+
+            .. deprecated:: 2.0.0
+               Use ``date_format`` instead, or read in as ``object`` and then apply
+               :func:`to_datetime` as-needed.
+        date_format : str or dict of column -> format, default ``None``
+           If used in conjunction with ``parse_dates``, will parse dates
+           according to this format. For anything more complex,
+           please read in as ``object`` and then apply :func:`to_datetime` as-needed.
+        thousands : str, default None
+            Thousands separator for parsing string columns to numeric.  Note that
+            this parameter is only necessary for columns stored as TEXT in Excel,
+            any numeric columns will automatically be parsed, regardless of display
+            format.
+        comment : str, default None
+            Comments out remainder of line. Pass a character or characters to this
+            argument to indicate comments in the input file. Any data between the
+            comment string and the end of the current line is ignored.
+        skipfooter : int, default 0
+            Rows at the end to skip (0-indexed).
+        dtype_backend : {{'numpy_nullable', 'pyarrow'}}, default 'numpy_nullable'
+            Back-end data type applied to the resultant :class:`DataFrame`
+            (still experimental). Behaviour is as follows:
+
+            * ``"numpy_nullable"``: returns nullable-dtype-backed :class:`DataFrame`
+              (default).
+            * ``"pyarrow"``: returns pyarrow-backed nullable :class:`ArrowDtype`
+              DataFrame.
+
+            .. versionadded:: 2.0
+        **kwds : dict, optional
+            Arbitrary keyword arguments passed to excel engine.
+
         Returns
         -------
         DataFrame or dict of DataFrames
             DataFrame from the passed in Excel file.
 
+        See Also
+        --------
+        read_excel : Read an Excel sheet values (xlsx) file into DataFrame.
+        read_csv : Read a comma-separated values (csv) file into DataFrame.
+        read_fwf : Read a table of fixed-width formatted lines into DataFrame.
+
         Examples
         --------
         >>> df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=["A", "B", "C"])


Test patch:
diff --git a/pandas/_testing/_warnings.py b/pandas/_testing/_warnings.py
index d951607778..cd2e2b4141 100644
--- a/pandas/_testing/_warnings.py
+++ b/pandas/_testing/_warnings.py
@@ -11,6 +11,7 @@ import sys
 from typing import (
     TYPE_CHECKING,
     Literal,
+    Union,
     cast,
 )
 import warnings
@@ -32,7 +33,8 @@ def assert_produces_warning(
     ] = "always",
     check_stacklevel: bool = True,
     raise_on_extra_warnings: bool = True,
-    match: str | None = None,
+    match: str | tuple[str | None, ...] | None = None,
+    must_find_all_warnings: bool = True,
 ) -> Generator[list[warnings.WarningMessage], None, None]:
     """
     Context manager for running code expected to either raise a specific warning,
@@ -68,8 +70,15 @@ def assert_produces_warning(
     raise_on_extra_warnings : bool, default True
         Whether extra warnings not of the type `expected_warning` should
         cause the test to fail.
-    match : str, optional
-        Match warning message.
+    match : {str, tuple[str, ...]}, optional
+        Match warning message. If it's a tuple, it has to be the size of
+        `expected_warning`. If additionally `must_find_all_warnings` is
+        True, each expected warning's message gets matched with a respective
+        match. Otherwise, multiple values get treated as an alternative.
+    must_find_all_warnings : bool, default True
+        If True and `expected_warning` is a tuple, each expected warning
+        type must get encountered. Otherwise, even one expected warning
+        results in success.
 
     Examples
     --------
@@ -97,13 +106,35 @@ def assert_produces_warning(
             yield w
         finally:
             if expected_warning:
-                expected_warning = cast(type[Warning], expected_warning)
-                _assert_caught_expected_warning(
-                    caught_warnings=w,
-                    expected_warning=expected_warning,
-                    match=match,
-                    check_stacklevel=check_stacklevel,
-                )
+                if isinstance(expected_warning, tuple) and must_find_all_warnings:
+                    match = (
+                        match
+                        if isinstance(match, tuple)
+                        else (match,) * len(expected_warning)
+                    )
+                    for warning_type, warning_match in zip(expected_warning, match):
+                        _assert_caught_expected_warnings(
+                            caught_warnings=w,
+                            expected_warning=warning_type,
+                            match=warning_match,
+                            check_stacklevel=check_stacklevel,
+                        )
+                else:
+                    expected_warning = cast(
+                        Union[type[Warning], tuple[type[Warning], ...]],
+                        expected_warning,
+                    )
+                    match = (
+                        "|".join(m for m in match if m)
+                        if isinstance(match, tuple)
+                        else match
+                    )
+                    _assert_caught_expected_warnings(
+                        caught_warnings=w,
+                        expected_warning=expected_warning,
+                        match=match,
+                        check_stacklevel=check_stacklevel,
+                    )
             if raise_on_extra_warnings:
                 _assert_caught_no_extra_warnings(
                     caught_warnings=w,
@@ -123,10 +154,10 @@ def maybe_produces_warning(
         return nullcontext()
 
 
-def _assert_caught_expected_warning(
+def _assert_caught_expected_warnings(
     *,
     caught_warnings: Sequence[warnings.WarningMessage],
-    expected_warning: type[Warning],
+    expected_warning: type[Warning] | tuple[type[Warning], ...],
     match: str | None,
     check_stacklevel: bool,
 ) -> None:
@@ -134,6 +165,11 @@ def _assert_caught_expected_warning(
     saw_warning = False
     matched_message = False
     unmatched_messages = []
+    warning_name = (
+        tuple(x.__name__ for x in expected_warning)
+        if isinstance(expected_warning, tuple)
+        else expected_warning.__name__
+    )
 
     for actual_warning in caught_warnings:
         if issubclass(actual_warning.category, expected_warning):
@@ -149,13 +185,11 @@ def _assert_caught_expected_warning(
                     unmatched_messages.append(actual_warning.message)
 
     if not saw_warning:
-        raise AssertionError(
-            f"Did not see expected warning of class {expected_warning.__name__!r}"
-        )
+        raise AssertionError(f"Did not see expected warning of class {warning_name!r}")
 
     if match and not matched_message:
         raise AssertionError(
-            f"Did not see warning {expected_warning.__name__!r} "
+            f"Did not see warning {warning_name!r} "
             f"matching '{match}'. The emitted warning messages are "
             f"{unmatched_messages}"
         )
diff --git a/pandas/_testing/contexts.py b/pandas/_testing/contexts.py
index b986e03e25..7ebed8857f 100644
--- a/pandas/_testing/contexts.py
+++ b/pandas/_testing/contexts.py
@@ -173,7 +173,7 @@ def raises_chained_assignment_error(warn=True, extra_warnings=(), extra_match=()
     elif PYPY and extra_warnings:
         return assert_produces_warning(
             extra_warnings,
-            match="|".join(extra_match),
+            match=extra_match,
         )
     else:
         if using_copy_on_write():
@@ -190,5 +190,5 @@ def raises_chained_assignment_error(warn=True, extra_warnings=(), extra_match=()
             warning = (warning, *extra_warnings)  # type: ignore[assignment]
         return assert_produces_warning(
             warning,
-            match="|".join((match, *extra_match)),
+            match=(match, *extra_match),
         )
diff --git a/pandas/tests/indexing/test_chaining_and_caching.py b/pandas/tests/indexing/test_chaining_and_caching.py
index 2a2772d1b3..b28c3cba7d 100644
--- a/pandas/tests/indexing/test_chaining_and_caching.py
+++ b/pandas/tests/indexing/test_chaining_and_caching.py
@@ -284,7 +284,9 @@ class TestChaining:
         with tm.raises_chained_assignment_error():
             df.loc[2]["C"] = "foo"
         tm.assert_frame_equal(df, df_original)
-        with tm.raises_chained_assignment_error(extra_warnings=(FutureWarning,)):
+        with tm.raises_chained_assignment_error(
+            extra_warnings=(FutureWarning,), extra_match=(None,)
+        ):
             df["C"][2] = "foo"
         tm.assert_frame_equal(df, df_original)
 
diff --git a/pandas/tests/io/parser/common/test_read_errors.py b/pandas/tests/io/parser/common/test_read_errors.py
index 0827f64dcc..bd47e04541 100644
--- a/pandas/tests/io/parser/common/test_read_errors.py
+++ b/pandas/tests/io/parser/common/test_read_errors.py
@@ -196,7 +196,6 @@ def test_warn_bad_lines(all_parsers):
     expected_warning = ParserWarning
     if parser.engine == "pyarrow":
         match_msg = "Expected 1 columns, but found 3: 1,2,3"
-        expected_warning = (ParserWarning, DeprecationWarning)
 
     with tm.assert_produces_warning(
         expected_warning, match=match_msg, check_stacklevel=False
@@ -315,7 +314,6 @@ a,b
     expected_warning = ParserWarning
     if parser.engine == "pyarrow":
         match_msg = "Expected 2 columns, but found 3: a,b,c"
-        expected_warning = (ParserWarning, DeprecationWarning)
 
     with tm.assert_produces_warning(
         expected_warning, match=match_msg, check_stacklevel=False
diff --git a/pandas/tests/io/parser/test_parse_dates.py b/pandas/tests/io/parser/test_parse_dates.py
index 0bc0c3e744..8968948df5 100644
--- a/pandas/tests/io/parser/test_parse_dates.py
+++ b/pandas/tests/io/parser/test_parse_dates.py
@@ -343,7 +343,7 @@ KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000
         "names": ["X0", "X1", "X2", "X3", "X4", "X5", "X6", "X7", "X8"],
     }
     with tm.assert_produces_warning(
-        (DeprecationWarning, FutureWarning), match=depr_msg, check_stacklevel=False
+        FutureWarning, match=depr_msg, check_stacklevel=False
     ):
         result = parser.read_csv(StringIO(data), **kwds)
 
@@ -724,7 +724,7 @@ def test_multiple_date_col_name_collision(all_parsers, data, parse_dates, msg):
     )
     with pytest.raises(ValueError, match=msg):
         with tm.assert_produces_warning(
-            (FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
+            FutureWarning, match=depr_msg, check_stacklevel=False
         ):
             parser.read_csv(StringIO(data), parse_dates=parse_dates)
 
@@ -1248,14 +1248,14 @@ KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000
         "Support for nested sequences for 'parse_dates' in pd.read_csv is deprecated"
     )
     with tm.assert_produces_warning(
-        (FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
+        FutureWarning, match=depr_msg, check_stacklevel=False
     ):
         with_indices = parser.read_csv(
             StringIO(data), parse_dates={"nominal": [1, 2]}, index_col="nominal"
         )
 
     with tm.assert_produces_warning(
-        (FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
+        FutureWarning, match=depr_msg, check_stacklevel=False
     ):
         with_names = parser.read_csv(
             StringIO(data),
@@ -1280,13 +1280,13 @@ KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000
         "Support for nested sequences for 'parse_dates' in pd.read_csv is deprecated"
     )
     with tm.assert_produces_warning(
-        (FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
+        FutureWarning, match=depr_msg, check_stacklevel=False
     ):
         result = parser.read_csv(
             StringIO(data), index_col=["nominal", "ID"], parse_dates={"nominal": [1, 2]}
         )
     with tm.assert_produces_warning(
-        (FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
+        FutureWarning, match=depr_msg, check_stacklevel=False
     ):
         expected = parser.read_csv(StringIO(data), parse_dates={"nominal": [1, 2]})
 
@@ -2267,7 +2267,7 @@ def test_parse_dates_dict_format_two_columns(all_parsers, key, parse_dates):
         "Support for nested sequences for 'parse_dates' in pd.read_csv is deprecated"
     )
     with tm.assert_produces_warning(
-        (FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
+        FutureWarning, match=depr_msg, check_stacklevel=False
     ):
         result = parser.read_csv(
             StringIO(data), date_format={key: "%d- %m-%Y"}, parse_dates=parse_dates
diff --git a/pandas/tests/io/parser/usecols/test_parse_dates.py b/pandas/tests/io/parser/usecols/test_parse_dates.py
index 75efe87c40..ab98857e0c 100644
--- a/pandas/tests/io/parser/usecols/test_parse_dates.py
+++ b/pandas/tests/io/parser/usecols/test_parse_dates.py
@@ -146,7 +146,7 @@ def test_usecols_with_parse_dates4(all_parsers):
         "Support for nested sequences for 'parse_dates' in pd.read_csv is deprecated"
     )
     with tm.assert_produces_warning(
-        (FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
+        FutureWarning, match=depr_msg, check_stacklevel=False
     ):
         result = parser.read_csv(
             StringIO(data),
@@ -187,7 +187,7 @@ def test_usecols_with_parse_dates_and_names(all_parsers, usecols, names, request
         "Support for nested sequences for 'parse_dates' in pd.read_csv is deprecated"
     )
     with tm.assert_produces_warning(
-        (FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
+        FutureWarning, match=depr_msg, check_stacklevel=False
     ):
         result = parser.read_csv(
             StringIO(s), names=names, parse_dates=parse_dates, usecols=usecols
diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py
index 00f48bf3b1..44bf3475b8 100644
--- a/pandas/tests/series/test_arithmetic.py
+++ b/pandas/tests/series/test_arithmetic.py
@@ -807,9 +807,6 @@ class TestNamePreservation:
             r"Logical ops \(and, or, xor\) between Pandas objects and "
             "dtype-less sequences"
         )
-        warn = None
-        if box in [list, tuple] and is_logical:
-            warn = FutureWarning
 
         right = box(right)
         if flex:
@@ -818,9 +815,12 @@ class TestNamePreservation:
                 return
             result = getattr(left, name)(right)
         else:
-            # GH#37374 logical ops behaving as set ops deprecated
-            with tm.assert_produces_warning(warn, match=msg):
-                result = op(left, right)
+            if is_logical and box in [list, tuple]:
+                with pytest.raises(TypeError, match=msg):
+                    # GH#52264 logical ops with dtype-less sequences deprecated
+                    op(left, right)
+                return
+            result = op(left, right)
 
         assert isinstance(result, Series)
         if box in [Index, Series]:
diff --git a/pandas/tests/series/test_logical_ops.py b/pandas/tests/series/test_logical_ops.py
index b76b69289b..f59eacea3f 100644
--- a/pandas/tests/series/test_logical_ops.py
+++ b/pandas/tests/series/test_logical_ops.py
@@ -86,7 +86,7 @@ class TestSeriesLogicalOps:
         # GH#9016: support bitwise op for integer types
         s_0123 = Series(range(4), dtype="int64")
 
-        warn_msg = (
+        err_msg = (
             r"Logical ops \(and, or, xor\) between Pandas objects and "
             "dtype-less sequences"
         )
@@ -97,9 +97,8 @@ class TestSeriesLogicalOps:
         with pytest.raises(TypeError, match=msg):
             s_0123 & 3.14
         msg = "unsupported operand type.+for &:"
-        with pytest.raises(TypeError, match=msg):
-            with tm.assert_produces_warning(FutureWarning, match=warn_msg):
-                s_0123 & [0.1, 4, 3.14, 2]
+        with pytest.raises(TypeError, match=err_msg):
+            s_0123 & [0.1, 4, 3.14, 2]
         with pytest.raises(TypeError, match=msg):
             s_0123 & np.array([0.1, 4, 3.14, 2])
         with pytest.raises(TypeError, match=msg):
@@ -108,7 +107,7 @@ class TestSeriesLogicalOps:
     def test_logical_operators_int_dtype_with_str(self):
         s_1111 = Series([1] * 4, dtype="int8")
 
-        warn_msg = (
+        err_msg = (
             r"Logical ops \(and, or, xor\) between Pandas objects and "
             "dtype-less sequences"
         )
@@ -116,9 +115,8 @@ class TestSeriesLogicalOps:
         msg = "Cannot perform 'and_' with a dtyped.+array and scalar of type"
         with pytest.raises(TypeError, match=msg):
             s_1111 & "a"
-        with pytest.raises(TypeError, match="unsupported operand.+for &"):
-            with tm.assert_produces_warning(FutureWarning, match=warn_msg):
-                s_1111 & ["a", "b", "c", "d"]
+        with pytest.raises(TypeError, match=err_msg):
+            s_1111 & ["a", "b", "c", "d"]
 
     def test_logical_operators_int_dtype_with_bool(self):
         # GH#9016: support bitwise op for integer types
@@ -129,17 +127,15 @@ class TestSeriesLogicalOps:
         result = s_0123 & False
         tm.assert_series_equal(result, expected)
 
-        warn_msg = (
+        msg = (
             r"Logical ops \(and, or, xor\) between Pandas objects and "
             "dtype-less sequences"
         )
-        with tm.assert_produces_warning(FutureWarning, match=warn_msg):
-            result = s_0123 & [False]
-        tm.assert_series_equal(result, expected)
+        with pytest.raises(TypeError, match=msg):
+            s_0123 & [False]
 
-        with tm.assert_produces_warning(FutureWarning, match=warn_msg):
-            result = s_0123 & (False,)
-        tm.assert_series_equal(result, expected)
+        with pytest.raises(TypeError, match=msg):
+            s_0123 & (False,)
 
         result = s_0123 ^ False
         expected = Series([False, True, True, True])
@@ -188,9 +184,8 @@ class TestSeriesLogicalOps:
         )
 
         expected = Series([True, False, False, False, False])
-        with tm.assert_produces_warning(FutureWarning, match=msg):
-            result = left & right
-        tm.assert_series_equal(result, expected)
+        with pytest.raises(TypeError, match=msg):
+            left & right
         result = left & np.array(right)
         tm.assert_series_equal(result, expected)
         result = left & Index(right)
@@ -199,9 +194,8 @@ class TestSeriesLogicalOps:
         tm.assert_series_equal(result, expected)
 
         expected = Series([True, True, True, True, True])
-        with tm.assert_produces_warning(FutureWarning, match=msg):
-            result = left | right
-        tm.assert_series_equal(result, expected)
+        with pytest.raises(TypeError, match=msg):
+            left | right
         result = left | np.array(right)
         tm.assert_series_equal(result, expected)
         result = left | Index(right)
@@ -210,9 +204,8 @@ class TestSeriesLogicalOps:
         tm.assert_series_equal(result, expected)
 
         expected = Series([False, True, True, True, True])
-        with tm.assert_produces_warning(FutureWarning, match=msg):
-            result = left ^ right
-        tm.assert_series_equal(result, expected)
+        with pytest.raises(TypeError, match=msg):
+            left ^ right
         result = left ^ np.array(right)
         tm.assert_series_equal(result, expected)
         result = left ^ Index(right)
@@ -233,26 +226,22 @@ class TestSeriesLogicalOps:
 
         # s_0123 will be all false now because of reindexing like s_tft
         expected = Series([False] * 7, index=[0, 1, 2, 3, "a", "b", "c"])
-        with tm.assert_produces_warning(FutureWarning):
-            result = s_tft & s_0123
+        result = s_tft & s_0123
         tm.assert_series_equal(result, expected)
 
-        # GH 52538: Deprecate casting to object type when reindex is needed;
+        # GH#52538: no longer to object type when reindex is needed;
         # matches DataFrame behavior
-        expected = Series([False] * 7, index=[0, 1, 2, 3, "a", "b", "c"])
-        with tm.assert_produces_warning(FutureWarning):
-            result = s_0123 & s_tft
-        tm.assert_series_equal(result, expected)
+        msg = r"unsupported operand type\(s\) for &: 'float' and 'bool'"
+        with pytest.raises(TypeError, match=msg):
+            s_0123 & s_tft
 
         s_a0b1c0 = Series([1], list("b"))
 
-        with tm.assert_produces_warning(FutureWarning):
-            res = s_tft & s_a0b1c0
+        res = s_tft & s_a0b1c0
         expected = s_tff.reindex(list("abc"))
         tm.assert_series_equal(res, expected)
 
-        with tm.assert_produces_warning(FutureWarning):
-            res = s_tft | s_a0b1c0
+        res = s_tft | s_a0b1c0
         expected = s_tft.reindex(list("abc"))
         tm.assert_series_equal(res, expected)
 
@@ -273,9 +262,8 @@ class TestSeriesLogicalOps:
             r"Logical ops \(and, or, xor\) between Pandas objects and "
             "dtype-less sequences"
         )
-        with tm.assert_produces_warning(FutureWarning, match=msg):
-            result = s & list(s)
-        tm.assert_series_equal(result, expected)
+        with pytest.raises(TypeError, match=msg):
+            s & list(s)
 
     def test_scalar_na_logical_ops_corners_aligns(self):
         s = Series([2, 3, 4, 5, 6, 7, 8, 9, datetime(2005, 1, 1)])
@@ -405,27 +393,24 @@ class TestSeriesLogicalOps:
         tm.assert_series_equal(result, expected)
 
         # vs non-matching
-        with tm.assert_produces_warning(FutureWarning):
-            result = a & Series([1], ["z"])
+        result = a & Series([1], ["z"])
         expected = Series([False, False, False, False], list("abcz"))
         tm.assert_series_equal(result, expected)
 
-        with tm.assert_produces_warning(FutureWarning):
-            result = a | Series([1], ["z"])
+        result = a | Series([1], ["z"])
         expected = Series([True, True, False, False], list("abcz"))
         tm.assert_series_equal(result, expected)
 
         # identity
         # we would like s[s|e] == s to hold for any e, whether empty or not
-        with tm.assert_produces_warning(FutureWarning):
-            for e in [
-                empty.copy(),
-                Series([1], ["z"]),
-                Series(np.nan, b.index),
-                Series(np.nan, a.index),
-            ]:
-                result = a[a | e]
-                tm.assert_series_equal(result, a[a])
+        for e in [
+            empty.copy(),
+            Series([1], ["z"]),
+            Series(np.nan, b.index),
+            Series(np.nan, a.index),
+        ]:
+            result = a[a | e]
+            tm.assert_series_equal(result, a[a])
 
         for e in [Series(["z"])]:
             warn = FutureWarning if using_infer_string else None
@@ -519,7 +504,6 @@ class TestSeriesLogicalOps:
         tm.assert_frame_equal(s3.to_frame() | s4.to_frame(), exp_or1.to_frame())
         tm.assert_frame_equal(s4.to_frame() | s3.to_frame(), exp_or.to_frame())
 
-    @pytest.mark.xfail(reason="Will pass once #52839 deprecation is enforced")
     def test_int_dtype_different_index_not_bool(self):
         # GH 52500
         ser1 = Series([1, 2, 3], index=[10, 11, 23], name="a")
diff --git a/pandas/tests/util/test_assert_produces_warning.py b/pandas/tests/util/test_assert_produces_warning.py
index 80e3264690..5b917dbbe7 100644
--- a/pandas/tests/util/test_assert_produces_warning.py
+++ b/pandas/tests/util/test_assert_produces_warning.py
@@ -42,7 +42,6 @@ def f():
     warnings.warn("f2", RuntimeWarning)
 
 
-@pytest.mark.filterwarnings("ignore:f1:FutureWarning")
 def test_assert_produces_warning_honors_filter():
     # Raise by default.
     msg = r"Caused unexpected warning\(s\)"
@@ -180,6 +179,44 @@ def test_match_multiple_warnings():
         warnings.warn("Match this too", UserWarning)
 
 
+def test_must_match_multiple_warnings():
+    # https://github.com/pandas-dev/pandas/issues/56555
+    category = (FutureWarning, UserWarning)
+    msg = "Did not see expected warning of class 'UserWarning'"
+    with pytest.raises(AssertionError, match=msg):
+        with tm.assert_produces_warning(category, match=r"^Match this"):
+            warnings.warn("Match this", FutureWarning)
+
+
+def test_must_match_multiple_warnings_messages():
+    # https://github.com/pandas-dev/pandas/issues/56555
+    category = (FutureWarning, UserWarning)
+    msg = r"The emitted warning messages are \[UserWarning\('Not this'\)\]"
+    with pytest.raises(AssertionError, match=msg):
+        with tm.assert_produces_warning(category, match=r"^Match this"):
+            warnings.warn("Match this", FutureWarning)
+            warnings.warn("Not this", UserWarning)
+
+
+def test_allow_partial_match_for_multiple_warnings():
+    # https://github.com/pandas-dev/pandas/issues/56555
+    category = (FutureWarning, UserWarning)
+    with tm.assert_produces_warning(
+        category, match=r"^Match this", must_find_all_warnings=False
+    ):
+        warnings.warn("Match this", FutureWarning)
+
+
+def test_allow_partial_match_for_multiple_warnings_messages():
+    # https://github.com/pandas-dev/pandas/issues/56555
+    category = (FutureWarning, UserWarning)
+    with tm.assert_produces_warning(
+        category, match=r"^Match this", must_find_all_warnings=False
+    ):
+        warnings.warn("Match this", FutureWarning)
+        warnings.warn("Not this", UserWarning)
+
+
 def test_right_category_wrong_match_raises(pair_different_warnings):
     target_category, other_category = pair_different_warnings
     with pytest.raises(AssertionError, match="Did not see warning.*matching"):