Skip to content

Commit 77811c7

Browse files
authored
ENH: implement matching warning message (#37263)
1 parent ce5a112 commit 77811c7

File tree

2 files changed

+178
-19
lines changed

2 files changed

+178
-19
lines changed

pandas/_testing.py

+46-19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import gzip
77
import operator
88
import os
9+
import re
910
from shutil import rmtree
1011
import string
1112
import tempfile
@@ -2546,10 +2547,11 @@ def wrapper(*args, **kwargs):
25462547

25472548
@contextmanager
25482549
def assert_produces_warning(
2549-
expected_warning=Warning,
2550+
expected_warning: Optional[Union[Type[Warning], bool]] = Warning,
25502551
filter_level="always",
2551-
check_stacklevel=True,
2552-
raise_on_extra_warnings=True,
2552+
check_stacklevel: bool = True,
2553+
raise_on_extra_warnings: bool = True,
2554+
match: Optional[str] = None,
25532555
):
25542556
"""
25552557
Context manager for running code expected to either raise a specific
@@ -2584,6 +2586,8 @@ class for all warnings. To check that no warning is returned,
25842586
raise_on_extra_warnings : bool, default True
25852587
Whether extra warnings not of the type `expected_warning` should
25862588
cause the test to fail.
2589+
match : str, optional
2590+
Match warning message.
25872591
25882592
Examples
25892593
--------
@@ -2610,28 +2614,28 @@ class for all warnings. To check that no warning is returned,
26102614
with warnings.catch_warnings(record=True) as w:
26112615

26122616
saw_warning = False
2617+
matched_message = False
2618+
26132619
warnings.simplefilter(filter_level)
26142620
yield w
26152621
extra_warnings = []
26162622

26172623
for actual_warning in w:
2618-
if expected_warning and issubclass(
2619-
actual_warning.category, expected_warning
2620-
):
2624+
if not expected_warning:
2625+
continue
2626+
2627+
expected_warning = cast(Type[Warning], expected_warning)
2628+
if issubclass(actual_warning.category, expected_warning):
26212629
saw_warning = True
26222630

26232631
if check_stacklevel and issubclass(
26242632
actual_warning.category, (FutureWarning, DeprecationWarning)
26252633
):
2626-
from inspect import getframeinfo, stack
2634+
_assert_raised_with_correct_stacklevel(actual_warning)
2635+
2636+
if match is not None and re.search(match, str(actual_warning.message)):
2637+
matched_message = True
26272638

2628-
caller = getframeinfo(stack()[2][0])
2629-
msg = (
2630-
"Warning not set with correct stacklevel. "
2631-
f"File where warning is raised: {actual_warning.filename} != "
2632-
f"{caller.filename}. Warning message: {actual_warning.message}"
2633-
)
2634-
assert actual_warning.filename == caller.filename, msg
26352639
else:
26362640
extra_warnings.append(
26372641
(
@@ -2641,18 +2645,41 @@ class for all warnings. To check that no warning is returned,
26412645
actual_warning.lineno,
26422646
)
26432647
)
2648+
26442649
if expected_warning:
2645-
msg = (
2646-
f"Did not see expected warning of class "
2647-
f"{repr(expected_warning.__name__)}"
2648-
)
2649-
assert saw_warning, msg
2650+
expected_warning = cast(Type[Warning], expected_warning)
2651+
if not saw_warning:
2652+
raise AssertionError(
2653+
f"Did not see expected warning of class "
2654+
f"{repr(expected_warning.__name__)}"
2655+
)
2656+
2657+
if match and not matched_message:
2658+
raise AssertionError(
2659+
f"Did not see warning {repr(expected_warning.__name__)} "
2660+
f"matching {match}"
2661+
)
2662+
26502663
if raise_on_extra_warnings and extra_warnings:
26512664
raise AssertionError(
26522665
f"Caused unexpected warning(s): {repr(extra_warnings)}"
26532666
)
26542667

26552668

2669+
def _assert_raised_with_correct_stacklevel(
2670+
actual_warning: warnings.WarningMessage,
2671+
) -> None:
2672+
from inspect import getframeinfo, stack
2673+
2674+
caller = getframeinfo(stack()[3][0])
2675+
msg = (
2676+
"Warning not set with correct stacklevel. "
2677+
f"File where warning is raised: {actual_warning.filename} != "
2678+
f"{caller.filename}. Warning message: {actual_warning.message}"
2679+
)
2680+
assert actual_warning.filename == caller.filename, msg
2681+
2682+
26562683
class RNGContext:
26572684
"""
26582685
Context manager to set the numpy random number generator speed. Returns

pandas/tests/util/test_assert_produces_warning.py

+132
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,58 @@
1+
""""
2+
Test module for testing ``pandas._testing.assert_produces_warning``.
3+
"""
14
import warnings
25

36
import pytest
47

8+
from pandas.errors import DtypeWarning, PerformanceWarning
9+
510
import pandas._testing as tm
611

712

13+
@pytest.fixture(
14+
params=[
15+
RuntimeWarning,
16+
ResourceWarning,
17+
UserWarning,
18+
FutureWarning,
19+
DeprecationWarning,
20+
PerformanceWarning,
21+
DtypeWarning,
22+
],
23+
)
24+
def category(request):
25+
"""
26+
Return unique warning.
27+
28+
Useful for testing behavior of tm.assert_produces_warning with various categories.
29+
"""
30+
return request.param
31+
32+
33+
@pytest.fixture(
34+
params=[
35+
(RuntimeWarning, UserWarning),
36+
(UserWarning, FutureWarning),
37+
(FutureWarning, RuntimeWarning),
38+
(DeprecationWarning, PerformanceWarning),
39+
(PerformanceWarning, FutureWarning),
40+
(DtypeWarning, DeprecationWarning),
41+
(ResourceWarning, DeprecationWarning),
42+
(FutureWarning, DeprecationWarning),
43+
],
44+
ids=lambda x: type(x).__name__,
45+
)
46+
def pair_different_warnings(request):
47+
"""
48+
Return pair or different warnings.
49+
50+
Useful for testing how several different warnings are handled
51+
in tm.assert_produces_warning.
52+
"""
53+
return request.param
54+
55+
856
def f():
957
warnings.warn("f1", FutureWarning)
1058
warnings.warn("f2", RuntimeWarning)
@@ -20,3 +68,87 @@ def test_assert_produces_warning_honors_filter():
2068

2169
with tm.assert_produces_warning(RuntimeWarning, raise_on_extra_warnings=False):
2270
f()
71+
72+
73+
@pytest.mark.parametrize(
74+
"message, match",
75+
[
76+
("", None),
77+
("", ""),
78+
("Warning message", r".*"),
79+
("Warning message", "War"),
80+
("Warning message", r"[Ww]arning"),
81+
("Warning message", "age"),
82+
("Warning message", r"age$"),
83+
("Message 12-234 with numbers", r"\d{2}-\d{3}"),
84+
("Message 12-234 with numbers", r"^Mes.*\d{2}-\d{3}"),
85+
("Message 12-234 with numbers", r"\d{2}-\d{3}\s\S+"),
86+
("Message, which we do not match", None),
87+
],
88+
)
89+
def test_catch_warning_category_and_match(category, message, match):
90+
with tm.assert_produces_warning(category, match=match):
91+
warnings.warn(message, category)
92+
93+
94+
@pytest.mark.parametrize(
95+
"message, match",
96+
[
97+
("Warning message", "Not this message"),
98+
("Warning message", "warning"),
99+
("Warning message", r"\d+"),
100+
],
101+
)
102+
def test_fail_to_match(category, message, match):
103+
msg = f"Did not see warning {repr(category.__name__)} matching"
104+
with pytest.raises(AssertionError, match=msg):
105+
with tm.assert_produces_warning(category, match=match):
106+
warnings.warn(message, category)
107+
108+
109+
def test_fail_to_catch_actual_warning(pair_different_warnings):
110+
expected_category, actual_category = pair_different_warnings
111+
match = "Did not see expected warning of class"
112+
with pytest.raises(AssertionError, match=match):
113+
with tm.assert_produces_warning(expected_category):
114+
warnings.warn("warning message", actual_category)
115+
116+
117+
def test_ignore_extra_warning(pair_different_warnings):
118+
expected_category, extra_category = pair_different_warnings
119+
with tm.assert_produces_warning(expected_category, raise_on_extra_warnings=False):
120+
warnings.warn("Expected warning", expected_category)
121+
warnings.warn("Unexpected warning OK", extra_category)
122+
123+
124+
def test_raise_on_extra_warning(pair_different_warnings):
125+
expected_category, extra_category = pair_different_warnings
126+
match = r"Caused unexpected warning\(s\)"
127+
with pytest.raises(AssertionError, match=match):
128+
with tm.assert_produces_warning(expected_category):
129+
warnings.warn("Expected warning", expected_category)
130+
warnings.warn("Unexpected warning NOT OK", extra_category)
131+
132+
133+
def test_same_category_different_messages_first_match():
134+
category = UserWarning
135+
with tm.assert_produces_warning(category, match=r"^Match this"):
136+
warnings.warn("Match this", category)
137+
warnings.warn("Do not match that", category)
138+
warnings.warn("Do not match that either", category)
139+
140+
141+
def test_same_category_different_messages_last_match():
142+
category = DeprecationWarning
143+
with tm.assert_produces_warning(category, match=r"^Match this"):
144+
warnings.warn("Do not match that", category)
145+
warnings.warn("Do not match that either", category)
146+
warnings.warn("Match this", category)
147+
148+
149+
def test_right_category_wrong_match_raises(pair_different_warnings):
150+
target_category, other_category = pair_different_warnings
151+
with pytest.raises(AssertionError, match="Did not see warning.*matching"):
152+
with tm.assert_produces_warning(target_category, match=r"^Match this"):
153+
warnings.warn("Do not match it", target_category)
154+
warnings.warn("Match this", other_category)

0 commit comments

Comments
 (0)