|
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
|
@@ -107,30 +108,38 @@ def __exit__(self, *args, **kwargs):
|
107 | 108 | # a contextmanager; this is probably after the fault has been triggered.
|
108 | 109 | # Similar reasoning applies to a few other standard-library modules: even
|
109 | 110 | # if the fault was later, these still aren't useful locations to report!
|
110 |
| - f"{sep}contextlib.py", |
111 |
| - f"{sep}inspect.py", |
112 |
| - f"{sep}re.py", |
113 |
| - f"{sep}re{sep}__init__.py", # refactored in Python 3.11 |
114 |
| - 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", |
115 | 117 | # Quite rarely, the first AFNP line is in Pytest's internals.
|
116 |
| - f"{sep}_pytest{sep}assertion{sep}__init__.py", |
117 |
| - f"{sep}_pytest{sep}assertion{sep}rewrite.py", |
118 |
| - f"{sep}_pytest{sep}_io{sep}saferepr.py", |
119 |
| - f"{sep}pluggy{sep}_result.py", |
120 |
| - # These are triggered by gc callbacks |
121 |
| - f"{sep}reprlib.py", |
122 |
| - f"{sep}_pytest{sep}assertion{sep}util.py", |
123 |
| - f"{sep}_pytest{sep}config{sep}__init__.py", |
124 |
| - f"{sep}pluggy{sep}_result.py", |
125 |
| - f"{sep}pluggy{sep}_manager.py", |
126 |
| - f"{sep}typing.py", |
127 |
| - f"{sep}pluggy{sep}_callers.py", |
128 |
| - f"{sep}pluggy{sep}_hooks.py", |
129 |
| - f"{sep}_pytest{sep}pytester.py", |
130 |
| - f"{sep}tests{sep}conftest.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", |
131 | 126 | )
|
132 | 127 |
|
133 | 128 |
|
| 129 | +def _glob_to_re(locs): |
| 130 | + """Translate a list of glob patterns to a combined regular expression. |
| 131 | + Only * and ** wildcards are 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 | + |
134 | 143 | def get_explaining_locations(traces):
|
135 | 144 | # Traces is a dict[interesting_origin | None, set[frozenset[tuple[str, int]]]]
|
136 | 145 | # Each trace in the set might later become a Counter instead of frozenset.
|
@@ -173,8 +182,9 @@ def get_explaining_locations(traces):
|
173 | 182 | # The last step is to filter out explanations that we know would be uninformative.
|
174 | 183 | # When this is the first AFNP location, we conclude that Scrutineer missed the
|
175 | 184 | # real divergence (earlier in the trace) and drop that unhelpful explanation.
|
| 185 | + filter_regex = re.compile(_glob_to_re(UNHELPFUL_LOCATIONS)) |
176 | 186 | return {
|
177 |
| - 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])} |
178 | 188 | for origin, afnp_locs in explanations.items()
|
179 | 189 | }
|
180 | 190 |
|
|
0 commit comments