Skip to content

Commit 65963d2

Browse files
committed
warnings: speed up work done in catch_warnings_for_item()
When setting up the warnings capture, filter strings (with the general form `action:message:category:module:line`) are collected from the cmdline, ini and item and applied. This happens for every test and other cases. To apply a string it needs to be parsed into a tuple, and it turns out this is slow. Since we already vendor the parsing code from Python's warnings.py, we can speed it up by caching the result. After splitting the parsing part from the applying part, the parsing is pure and is straightforward to cache. An alternative is to parse ahead of time and reuse the result, however the caching solution turns out cleaner and more general in this case. On this benchmark: import pytest @pytest.mark.parametrize("x", range(5000)) def test_foo(x): pass Before: ============================ 5000 passed in 14.11s ============================= 14365646 function calls (13450775 primitive calls) in 14.536 seconds After: ============================ 5000 passed in 13.61s ============================= 13290372 function calls (12375498 primitive calls) in 14.034 seconds
1 parent 81da5da commit 65963d2

File tree

1 file changed

+32
-14
lines changed

1 file changed

+32
-14
lines changed

src/_pytest/warnings.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,52 @@
1+
import re
12
import sys
23
import warnings
34
from contextlib import contextmanager
5+
from functools import lru_cache
46
from typing import Generator
7+
from typing import Tuple
58

69
import pytest
10+
from _pytest.compat import TYPE_CHECKING
711
from _pytest.main import Session
812

13+
if TYPE_CHECKING:
14+
from typing_extensions import Type
915

10-
def _setoption(wmod, arg):
11-
"""
12-
Copy of the warning._setoption function but does not escape arguments.
16+
17+
@lru_cache(maxsize=50)
18+
def _parse_filter(
19+
arg: str, *, escape: bool
20+
) -> "Tuple[str, str, Type[Warning], str, int]":
21+
"""Parse a warnings filter string.
22+
23+
This is copied from warnings._setoption, but does not apply the filter,
24+
only parses it, and makes the escaping optional.
1325
"""
1426
parts = arg.split(":")
1527
if len(parts) > 5:
16-
raise wmod._OptionError("too many fields (max 5): {!r}".format(arg))
28+
raise warnings._OptionError("too many fields (max 5): {!r}".format(arg))
1729
while len(parts) < 5:
1830
parts.append("")
19-
action, message, category, module, lineno = [s.strip() for s in parts]
20-
action = wmod._getaction(action)
21-
category = wmod._getcategory(category)
22-
if lineno:
31+
action_, message, category_, module, lineno_ = [s.strip() for s in parts]
32+
action = warnings._getaction(action_) # type: str # type: ignore[attr-defined]
33+
category = warnings._getcategory(
34+
category_
35+
) # type: Type[Warning] # type: ignore[attr-defined]
36+
if message and escape:
37+
message = re.escape(message)
38+
if module and escape:
39+
module = re.escape(module) + r"\Z"
40+
if lineno_:
2341
try:
24-
lineno = int(lineno)
42+
lineno = int(lineno_)
2543
if lineno < 0:
2644
raise ValueError
2745
except (ValueError, OverflowError):
28-
raise wmod._OptionError("invalid lineno {!r}".format(lineno))
46+
raise warnings._OptionError("invalid lineno {!r}".format(lineno_))
2947
else:
3048
lineno = 0
31-
wmod.filterwarnings(action, message, category, module, lineno)
49+
return (action, message, category, module, lineno)
3250

3351

3452
def pytest_addoption(parser):
@@ -79,15 +97,15 @@ def catch_warnings_for_item(config, ihook, when, item):
7997
# filters should have this precedence: mark, cmdline options, ini
8098
# filters should be applied in the inverse order of precedence
8199
for arg in inifilters:
82-
_setoption(warnings, arg)
100+
warnings.filterwarnings(*_parse_filter(arg, escape=False))
83101

84102
for arg in cmdline_filters:
85-
warnings._setoption(arg)
103+
warnings.filterwarnings(*_parse_filter(arg, escape=True))
86104

87105
if item is not None:
88106
for mark in item.iter_markers(name="filterwarnings"):
89107
for arg in mark.args:
90-
_setoption(warnings, arg)
108+
warnings.filterwarnings(*_parse_filter(arg, escape=False))
91109

92110
yield
93111

0 commit comments

Comments
 (0)