Skip to content

Commit 40deadd

Browse files
authored
Merge pull request #4342 from tybug/scrutineer-sort
Sort scrutineer reported lines
2 parents 61dc6d7 + 486537d commit 40deadd

File tree

4 files changed

+74
-9
lines changed

4 files changed

+74
-9
lines changed

hypothesis-python/RELEASE.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
RELEASE_TYPE: patch
2+
3+
When reporting the always-failing, never-passing lines from the |Phase.explain| phase, we now sort the reported lines so that local code shows up first, then third-party library code, then standard library code.

hypothesis-python/src/hypothesis/internal/scrutineer.py

+30-8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import re
1414
import subprocess
1515
import sys
16+
import sysconfig
1617
import types
1718
from collections import defaultdict
1819
from collections.abc import Iterable
@@ -27,12 +28,10 @@
2728

2829
if TYPE_CHECKING:
2930
from typing import TypeAlias
30-
else:
31-
TypeAlias = object
3231

33-
Location: TypeAlias = tuple[str, int]
34-
Branch: TypeAlias = tuple[Optional[Location], Location]
35-
Trace: TypeAlias = set[Branch]
32+
Location: "TypeAlias" = tuple[str, int]
33+
Branch: "TypeAlias" = tuple[Optional[Location], Location]
34+
Trace: "TypeAlias" = set[Branch]
3635

3736

3837
@lru_cache(maxsize=None)
@@ -215,18 +214,41 @@ def get_explaining_locations(traces):
215214
}
216215

217216

218-
LIB_DIR = str(Path(sys.executable).parent / "lib")
217+
# see e.g. https://docs.python.org/3/library/sysconfig.html#posix-user
218+
# for examples of these path schemes
219+
STDLIB_DIRS = [
220+
Path(sysconfig.get_path("platstdlib")).resolve(),
221+
Path(sysconfig.get_path("stdlib")).resolve(),
222+
]
223+
SITE_PACKAGES_DIRS = [
224+
Path(sysconfig.get_path("purelib")).resolve(),
225+
Path(sysconfig.get_path("platlib")).resolve(),
226+
]
227+
219228
EXPLANATION_STUB = (
220229
"Explanation:",
221230
" These lines were always and only run by failing examples:",
222231
)
223232

224233

225-
def make_report(explanations, cap_lines_at=5):
234+
# show local files first, then site-packages, then stdlib
235+
def _sort_key(path, lineno):
236+
path = Path(path).resolve()
237+
# site-packages may be a subdir of stdlib or platlib, so it's important to
238+
# check is_relative_to for this before the stdlib.
239+
if any(path.is_relative_to(p) for p in SITE_PACKAGES_DIRS):
240+
return (1, path, lineno)
241+
if any(path.is_relative_to(p) for p in STDLIB_DIRS):
242+
return (2, path, lineno)
243+
return (0, path, lineno)
244+
245+
246+
def make_report(explanations, *, cap_lines_at=5):
226247
report = defaultdict(list)
227248
for origin, locations in explanations.items():
249+
locations = list(locations)
250+
locations.sort(key=lambda v: _sort_key(v[0], v[1]))
228251
report_lines = [f" {fname}:{lineno}" for fname, lineno in locations]
229-
report_lines.sort(key=lambda line: (line.startswith(LIB_DIR), line))
230252
if len(report_lines) > cap_lines_at + 1:
231253
msg = " (and {} more with settings.verbosity >= verbose)"
232254
report_lines[cap_lines_at:] = [msg.format(len(report_lines[cap_lines_at:]))]

hypothesis-python/tests/ghostwriter/try-writing-for-installed.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
just have such weird semantics that we don't _want_ to support them.
2020
"""
2121

22-
import distutils.sysconfig as sysconfig
2322
import multiprocessing
2423
import os
2524
import subprocess
25+
import sysconfig
2626

2727
skip = (
2828
"idlelib curses antigravity pip prompt_toolkit IPython .popen_ django. .test. "

hypothesis-python/tests/nocover/test_scrutineer.py

+40
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@
88
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
99
# obtain one at https://mozilla.org/MPL/2.0/.
1010

11+
import json
1112
import sys
13+
import sysconfig
1214

1315
import pytest
1416

17+
from hypothesis import given, note, settings, strategies as st
1518
from hypothesis.internal.compat import PYPY
1619
from hypothesis.internal.scrutineer import make_report
20+
from hypothesis.vendor import pretty
1721

1822
# We skip tracing for explanations under PyPy, where it has a large performance
1923
# impact, or if there is already a trace function (e.g. coverage or a debugger)
@@ -105,3 +109,39 @@ def test(x):
105109
def test_skips_uninformative_locations(testdir):
106110
pytest_stdout, _ = get_reports(NO_SHOW_CONTEXTLIB, testdir=testdir)
107111
assert "Explanation:" not in pytest_stdout
112+
113+
114+
@given(st.randoms())
115+
@settings(max_examples=5)
116+
def test_report_sort(random):
117+
# show local files first, then site-packages, then stdlib
118+
119+
lines = [
120+
# local
121+
(__file__, 10),
122+
# site-packages
123+
(pytest.__file__, 123),
124+
(pytest.__file__, 124),
125+
# stdlib
126+
(json.__file__, 43),
127+
(json.__file__, 42),
128+
]
129+
random.shuffle(lines)
130+
explanations = {"origin": lines}
131+
report = make_report(explanations)
132+
report_lines = report["origin"][2:]
133+
report_lines = [line.strip() for line in report_lines]
134+
135+
expected_lines = [
136+
f"{__file__}:10",
137+
f"{pytest.__file__}:123",
138+
f"{pytest.__file__}:124",
139+
f"{json.__file__}:42",
140+
f"{json.__file__}:43",
141+
]
142+
143+
note(f"sysconfig.get_paths(): {pretty.pretty(sysconfig.get_paths())}")
144+
note(f"actual lines: {pretty.pretty(report_lines)}")
145+
note(f"expected lines: {pretty.pretty(expected_lines)}")
146+
147+
assert report_lines == expected_lines

0 commit comments

Comments
 (0)