|
10 | 10 |
|
11 | 11 | import functools
|
12 | 12 | import os
|
| 13 | +import re |
13 | 14 | import subprocess
|
14 | 15 | import sys
|
15 | 16 | import types
|
@@ -58,15 +59,18 @@ def __init__(self):
|
58 | 59 | self._previous_location = None
|
59 | 60 |
|
60 | 61 | def trace(self, frame, event, arg):
|
61 |
| - if event == "call": |
62 |
| - return self.trace |
63 |
| - elif event == "line": |
64 |
| - # manual inlining of self.trace_line for performance. |
65 |
| - fname = frame.f_code.co_filename |
66 |
| - if should_trace_file(fname): |
67 |
| - current_location = (fname, frame.f_lineno) |
68 |
| - self.branches.add((self._previous_location, current_location)) |
69 |
| - self._previous_location = current_location |
| 62 | + try: |
| 63 | + if event == "call": |
| 64 | + return self.trace |
| 65 | + elif event == "line": |
| 66 | + # manual inlining of self.trace_line for performance. |
| 67 | + fname = frame.f_code.co_filename |
| 68 | + if should_trace_file(fname): |
| 69 | + current_location = (fname, frame.f_lineno) |
| 70 | + self.branches.add((self._previous_location, current_location)) |
| 71 | + self._previous_location = current_location |
| 72 | + except RecursionError: |
| 73 | + pass |
70 | 74 |
|
71 | 75 | def trace_line(self, code: types.CodeType, line_number: int) -> None:
|
72 | 76 | fname = code.co_filename
|
@@ -104,19 +108,38 @@ def __exit__(self, *args, **kwargs):
|
104 | 108 | # a contextmanager; this is probably after the fault has been triggered.
|
105 | 109 | # Similar reasoning applies to a few other standard-library modules: even
|
106 | 110 | # if the fault was later, these still aren't useful locations to report!
|
107 |
| - f"{sep}contextlib.py", |
108 |
| - f"{sep}inspect.py", |
109 |
| - f"{sep}re.py", |
110 |
| - f"{sep}re{sep}__init__.py", # refactored in Python 3.11 |
111 |
| - f"{sep}warnings.py", |
| 111 | + # Note: The list is post-processed, so use plain "/" for separator here. |
| 112 | + "/contextlib.py", |
| 113 | + "/inspect.py", |
| 114 | + "/re.py", |
| 115 | + "/re/__init__.py", # refactored in Python 3.11 |
| 116 | + "/warnings.py", |
112 | 117 | # Quite rarely, the first AFNP line is in Pytest's internals.
|
113 |
| - f"{sep}_pytest{sep}assertion{sep}__init__.py", |
114 |
| - f"{sep}_pytest{sep}assertion{sep}rewrite.py", |
115 |
| - f"{sep}_pytest{sep}_io{sep}saferepr.py", |
116 |
| - f"{sep}pluggy{sep}_result.py", |
| 118 | + "/_pytest/_io/saferepr.py", |
| 119 | + "/_pytest/assertion/*.py", |
| 120 | + "/_pytest/config/__init__.py", |
| 121 | + "/_pytest/pytester.py", |
| 122 | + "/pluggy/_*.py", |
| 123 | + "/reprlib.py", |
| 124 | + "/typing.py", |
| 125 | + "/conftest.py", |
117 | 126 | )
|
118 | 127 |
|
119 | 128 |
|
| 129 | +def _glob_to_re(locs): |
| 130 | + """Translate a list of glob patterns to a combined regular expression. |
| 131 | + Only the * wildcard is supported, and patterns including special |
| 132 | + characters will only work by chance.""" |
| 133 | + # fnmatch.translate is not an option since its "*" consumes path sep |
| 134 | + return "|".join( |
| 135 | + loc.replace("*", r"[^/]+") |
| 136 | + .replace(".", re.escape(".")) |
| 137 | + .replace("/", re.escape(sep)) |
| 138 | + + r"\Z" # right anchored |
| 139 | + for loc in locs |
| 140 | + ) |
| 141 | + |
| 142 | + |
120 | 143 | def get_explaining_locations(traces):
|
121 | 144 | # Traces is a dict[interesting_origin | None, set[frozenset[tuple[str, int]]]]
|
122 | 145 | # Each trace in the set might later become a Counter instead of frozenset.
|
@@ -159,8 +182,9 @@ def get_explaining_locations(traces):
|
159 | 182 | # The last step is to filter out explanations that we know would be uninformative.
|
160 | 183 | # When this is the first AFNP location, we conclude that Scrutineer missed the
|
161 | 184 | # real divergence (earlier in the trace) and drop that unhelpful explanation.
|
| 185 | + filter_regex = re.compile(_glob_to_re(UNHELPFUL_LOCATIONS)) |
162 | 186 | return {
|
163 |
| - origin: {loc for loc in afnp_locs if not loc[0].endswith(UNHELPFUL_LOCATIONS)} |
| 187 | + origin: {loc for loc in afnp_locs if not filter_regex.search(loc[0])} |
164 | 188 | for origin, afnp_locs in explanations.items()
|
165 | 189 | }
|
166 | 190 |
|
|
0 commit comments