Skip to content

Commit d8c7e85

Browse files
Jorewinrhshadrach
andauthored
ENH: Add all warnings check to the assert_produces_warnings, and separate messages for each warning. (#57222)
* ENH: Add all warnings check to the `assert_produces_warnings`, and separate messages for each warning. * Fix typing errors * Fix typing errors * Remove unnecessary documentation * Change `assert_produces_warning behavior` to check for all warnings by default * Refactor typing * Fix tests expecting a Warning that is not raised * Adjust `raises_chained_assignment_error` and its dependencies to the new API of `assert_produces_warning` * Fix `_assert_caught_expected_warning` typing not including tuple of warnings * fixup! Refactor typing * fixup! Fix `_assert_caught_expected_warning` typing not including tuple of warnings * fixup! Fix `_assert_caught_expected_warning` typing not including tuple of warnings * Add tests --------- Co-authored-by: Richard Shadrach <[email protected]>
1 parent 72fa623 commit d8c7e85

File tree

7 files changed

+102
-31
lines changed

7 files changed

+102
-31
lines changed

pandas/_testing/_warnings.py

+50-16
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import (
1212
TYPE_CHECKING,
1313
Literal,
14+
Union,
1415
cast,
1516
)
1617
import warnings
@@ -32,7 +33,8 @@ def assert_produces_warning(
3233
] = "always",
3334
check_stacklevel: bool = True,
3435
raise_on_extra_warnings: bool = True,
35-
match: str | None = None,
36+
match: str | tuple[str | None, ...] | None = None,
37+
must_find_all_warnings: bool = True,
3638
) -> Generator[list[warnings.WarningMessage], None, None]:
3739
"""
3840
Context manager for running code expected to either raise a specific warning,
@@ -68,8 +70,15 @@ class for all warnings. To raise multiple types of exceptions,
6870
raise_on_extra_warnings : bool, default True
6971
Whether extra warnings not of the type `expected_warning` should
7072
cause the test to fail.
71-
match : str, optional
72-
Match warning message.
73+
match : {str, tuple[str, ...]}, optional
74+
Match warning message. If it's a tuple, it has to be the size of
75+
`expected_warning`. If additionally `must_find_all_warnings` is
76+
True, each expected warning's message gets matched with a respective
77+
match. Otherwise, multiple values get treated as an alternative.
78+
must_find_all_warnings : bool, default True
79+
If True and `expected_warning` is a tuple, each expected warning
80+
type must get encountered. Otherwise, even one expected warning
81+
results in success.
7382
7483
Examples
7584
--------
@@ -97,13 +106,35 @@ class for all warnings. To raise multiple types of exceptions,
97106
yield w
98107
finally:
99108
if expected_warning:
100-
expected_warning = cast(type[Warning], expected_warning)
101-
_assert_caught_expected_warning(
102-
caught_warnings=w,
103-
expected_warning=expected_warning,
104-
match=match,
105-
check_stacklevel=check_stacklevel,
106-
)
109+
if isinstance(expected_warning, tuple) and must_find_all_warnings:
110+
match = (
111+
match
112+
if isinstance(match, tuple)
113+
else (match,) * len(expected_warning)
114+
)
115+
for warning_type, warning_match in zip(expected_warning, match):
116+
_assert_caught_expected_warnings(
117+
caught_warnings=w,
118+
expected_warning=warning_type,
119+
match=warning_match,
120+
check_stacklevel=check_stacklevel,
121+
)
122+
else:
123+
expected_warning = cast(
124+
Union[type[Warning], tuple[type[Warning], ...]],
125+
expected_warning,
126+
)
127+
match = (
128+
"|".join(m for m in match if m)
129+
if isinstance(match, tuple)
130+
else match
131+
)
132+
_assert_caught_expected_warnings(
133+
caught_warnings=w,
134+
expected_warning=expected_warning,
135+
match=match,
136+
check_stacklevel=check_stacklevel,
137+
)
107138
if raise_on_extra_warnings:
108139
_assert_caught_no_extra_warnings(
109140
caught_warnings=w,
@@ -123,17 +154,22 @@ def maybe_produces_warning(
123154
return nullcontext()
124155

125156

126-
def _assert_caught_expected_warning(
157+
def _assert_caught_expected_warnings(
127158
*,
128159
caught_warnings: Sequence[warnings.WarningMessage],
129-
expected_warning: type[Warning],
160+
expected_warning: type[Warning] | tuple[type[Warning], ...],
130161
match: str | None,
131162
check_stacklevel: bool,
132163
) -> None:
133164
"""Assert that there was the expected warning among the caught warnings."""
134165
saw_warning = False
135166
matched_message = False
136167
unmatched_messages = []
168+
warning_name = (
169+
tuple(x.__name__ for x in expected_warning)
170+
if isinstance(expected_warning, tuple)
171+
else expected_warning.__name__
172+
)
137173

138174
for actual_warning in caught_warnings:
139175
if issubclass(actual_warning.category, expected_warning):
@@ -149,13 +185,11 @@ def _assert_caught_expected_warning(
149185
unmatched_messages.append(actual_warning.message)
150186

151187
if not saw_warning:
152-
raise AssertionError(
153-
f"Did not see expected warning of class {expected_warning.__name__!r}"
154-
)
188+
raise AssertionError(f"Did not see expected warning of class {warning_name!r}")
155189

156190
if match and not matched_message:
157191
raise AssertionError(
158-
f"Did not see warning {expected_warning.__name__!r} "
192+
f"Did not see warning {warning_name!r} "
159193
f"matching '{match}'. The emitted warning messages are "
160194
f"{unmatched_messages}"
161195
)

pandas/_testing/contexts.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def raises_chained_assignment_error(warn=True, extra_warnings=(), extra_match=()
173173
elif PYPY and extra_warnings:
174174
return assert_produces_warning(
175175
extra_warnings,
176-
match="|".join(extra_match),
176+
match=extra_match,
177177
)
178178
else:
179179
if using_copy_on_write():
@@ -190,5 +190,5 @@ def raises_chained_assignment_error(warn=True, extra_warnings=(), extra_match=()
190190
warning = (warning, *extra_warnings) # type: ignore[assignment]
191191
return assert_produces_warning(
192192
warning,
193-
match="|".join((match, *extra_match)),
193+
match=(match, *extra_match),
194194
)

pandas/tests/indexing/test_chaining_and_caching.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,9 @@ def test_detect_chained_assignment_changing_dtype(self):
284284
with tm.raises_chained_assignment_error():
285285
df.loc[2]["C"] = "foo"
286286
tm.assert_frame_equal(df, df_original)
287-
with tm.raises_chained_assignment_error(extra_warnings=(FutureWarning,)):
287+
with tm.raises_chained_assignment_error(
288+
extra_warnings=(FutureWarning,), extra_match=(None,)
289+
):
288290
df["C"][2] = "foo"
289291
tm.assert_frame_equal(df, df_original)
290292

pandas/tests/io/parser/common/test_read_errors.py

-2
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ def test_warn_bad_lines(all_parsers):
196196
expected_warning = ParserWarning
197197
if parser.engine == "pyarrow":
198198
match_msg = "Expected 1 columns, but found 3: 1,2,3"
199-
expected_warning = (ParserWarning, DeprecationWarning)
200199

201200
with tm.assert_produces_warning(
202201
expected_warning, match=match_msg, check_stacklevel=False
@@ -315,7 +314,6 @@ def test_on_bad_lines_warn_correct_formatting(all_parsers):
315314
expected_warning = ParserWarning
316315
if parser.engine == "pyarrow":
317316
match_msg = "Expected 2 columns, but found 3: a,b,c"
318-
expected_warning = (ParserWarning, DeprecationWarning)
319317

320318
with tm.assert_produces_warning(
321319
expected_warning, match=match_msg, check_stacklevel=False

pandas/tests/io/parser/test_parse_dates.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ def test_multiple_date_col(all_parsers, keep_date_col, request):
343343
"names": ["X0", "X1", "X2", "X3", "X4", "X5", "X6", "X7", "X8"],
344344
}
345345
with tm.assert_produces_warning(
346-
(DeprecationWarning, FutureWarning), match=depr_msg, check_stacklevel=False
346+
FutureWarning, match=depr_msg, check_stacklevel=False
347347
):
348348
result = parser.read_csv(StringIO(data), **kwds)
349349

@@ -724,7 +724,7 @@ def test_multiple_date_col_name_collision(all_parsers, data, parse_dates, msg):
724724
)
725725
with pytest.raises(ValueError, match=msg):
726726
with tm.assert_produces_warning(
727-
(FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
727+
FutureWarning, match=depr_msg, check_stacklevel=False
728728
):
729729
parser.read_csv(StringIO(data), parse_dates=parse_dates)
730730

@@ -1248,14 +1248,14 @@ def test_multiple_date_col_named_index_compat(all_parsers):
12481248
"Support for nested sequences for 'parse_dates' in pd.read_csv is deprecated"
12491249
)
12501250
with tm.assert_produces_warning(
1251-
(FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
1251+
FutureWarning, match=depr_msg, check_stacklevel=False
12521252
):
12531253
with_indices = parser.read_csv(
12541254
StringIO(data), parse_dates={"nominal": [1, 2]}, index_col="nominal"
12551255
)
12561256

12571257
with tm.assert_produces_warning(
1258-
(FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
1258+
FutureWarning, match=depr_msg, check_stacklevel=False
12591259
):
12601260
with_names = parser.read_csv(
12611261
StringIO(data),
@@ -1280,13 +1280,13 @@ def test_multiple_date_col_multiple_index_compat(all_parsers):
12801280
"Support for nested sequences for 'parse_dates' in pd.read_csv is deprecated"
12811281
)
12821282
with tm.assert_produces_warning(
1283-
(FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
1283+
FutureWarning, match=depr_msg, check_stacklevel=False
12841284
):
12851285
result = parser.read_csv(
12861286
StringIO(data), index_col=["nominal", "ID"], parse_dates={"nominal": [1, 2]}
12871287
)
12881288
with tm.assert_produces_warning(
1289-
(FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
1289+
FutureWarning, match=depr_msg, check_stacklevel=False
12901290
):
12911291
expected = parser.read_csv(StringIO(data), parse_dates={"nominal": [1, 2]})
12921292

@@ -2267,7 +2267,7 @@ def test_parse_dates_dict_format_two_columns(all_parsers, key, parse_dates):
22672267
"Support for nested sequences for 'parse_dates' in pd.read_csv is deprecated"
22682268
)
22692269
with tm.assert_produces_warning(
2270-
(FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
2270+
FutureWarning, match=depr_msg, check_stacklevel=False
22712271
):
22722272
result = parser.read_csv(
22732273
StringIO(data), date_format={key: "%d- %m-%Y"}, parse_dates=parse_dates

pandas/tests/io/parser/usecols/test_parse_dates.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def test_usecols_with_parse_dates4(all_parsers):
146146
"Support for nested sequences for 'parse_dates' in pd.read_csv is deprecated"
147147
)
148148
with tm.assert_produces_warning(
149-
(FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
149+
FutureWarning, match=depr_msg, check_stacklevel=False
150150
):
151151
result = parser.read_csv(
152152
StringIO(data),
@@ -187,7 +187,7 @@ def test_usecols_with_parse_dates_and_names(all_parsers, usecols, names, request
187187
"Support for nested sequences for 'parse_dates' in pd.read_csv is deprecated"
188188
)
189189
with tm.assert_produces_warning(
190-
(FutureWarning, DeprecationWarning), match=depr_msg, check_stacklevel=False
190+
FutureWarning, match=depr_msg, check_stacklevel=False
191191
):
192192
result = parser.read_csv(
193193
StringIO(s), names=names, parse_dates=parse_dates, usecols=usecols

pandas/tests/util/test_assert_produces_warning.py

+38-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ def f():
4242
warnings.warn("f2", RuntimeWarning)
4343

4444

45-
@pytest.mark.filterwarnings("ignore:f1:FutureWarning")
4645
def test_assert_produces_warning_honors_filter():
4746
# Raise by default.
4847
msg = r"Caused unexpected warning\(s\)"
@@ -180,6 +179,44 @@ def test_match_multiple_warnings():
180179
warnings.warn("Match this too", UserWarning)
181180

182181

182+
def test_must_match_multiple_warnings():
183+
# https://github.com/pandas-dev/pandas/issues/56555
184+
category = (FutureWarning, UserWarning)
185+
msg = "Did not see expected warning of class 'UserWarning'"
186+
with pytest.raises(AssertionError, match=msg):
187+
with tm.assert_produces_warning(category, match=r"^Match this"):
188+
warnings.warn("Match this", FutureWarning)
189+
190+
191+
def test_must_match_multiple_warnings_messages():
192+
# https://github.com/pandas-dev/pandas/issues/56555
193+
category = (FutureWarning, UserWarning)
194+
msg = r"The emitted warning messages are \[UserWarning\('Not this'\)\]"
195+
with pytest.raises(AssertionError, match=msg):
196+
with tm.assert_produces_warning(category, match=r"^Match this"):
197+
warnings.warn("Match this", FutureWarning)
198+
warnings.warn("Not this", UserWarning)
199+
200+
201+
def test_allow_partial_match_for_multiple_warnings():
202+
# https://github.com/pandas-dev/pandas/issues/56555
203+
category = (FutureWarning, UserWarning)
204+
with tm.assert_produces_warning(
205+
category, match=r"^Match this", must_find_all_warnings=False
206+
):
207+
warnings.warn("Match this", FutureWarning)
208+
209+
210+
def test_allow_partial_match_for_multiple_warnings_messages():
211+
# https://github.com/pandas-dev/pandas/issues/56555
212+
category = (FutureWarning, UserWarning)
213+
with tm.assert_produces_warning(
214+
category, match=r"^Match this", must_find_all_warnings=False
215+
):
216+
warnings.warn("Match this", FutureWarning)
217+
warnings.warn("Not this", UserWarning)
218+
219+
183220
def test_right_category_wrong_match_raises(pair_different_warnings):
184221
target_category, other_category = pair_different_warnings
185222
with pytest.raises(AssertionError, match="Did not see warning.*matching"):

0 commit comments

Comments
 (0)