-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Expand file tree
/
Copy pathutil.py
More file actions
215 lines (184 loc) · 7.53 KB
/
Copy pathutil.py
File metadata and controls
215 lines (184 loc) · 7.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# mypy: allow-untyped-defs
"""Utilities for assertion debugging."""
from __future__ import annotations
from collections.abc import Callable
from collections.abc import Iterator
from collections.abc import Sequence
from typing import Literal
from unicodedata import normalize
from _pytest import outcomes
import _pytest._code
from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
from _pytest.assertion._compare_any import _compare_eq_any
from _pytest.assertion._compare_set import SET_COMPARISON_FUNCTIONS
from _pytest.assertion._guards import isset
from _pytest.assertion._guards import istext
from _pytest.assertion._typing import _AssertionTextDiffStyle
from _pytest.assertion._typing import _HighlightFunc
from _pytest.assertion.compare_text import _notin_text
from _pytest.assertion.highlight import dummy_highlighter as dummy_highlighter
from _pytest.config import Config
from _pytest.config import UsageError
# The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was
# loaded and in turn call the hooks defined here as part of the
# DebugInterpreter.
_reprcompare: Callable[[str, object, object], str | None] | None = None
# Works similarly as _reprcompare attribute. Is populated with the hook call
# when pytest_runtest_setup is called.
_assertion_pass: Callable[[int, str, str], None] | None = None
# Config object which is assigned during pytest_runtest_protocol.
_config: Config | None = None
ASSERTION_TEXT_DIFF_STYLE_INI = "assertion_text_diff_style"
ASSERTION_TEXT_DIFF_STYLE_NDIFF: Literal["ndiff"] = "ndiff"
ASSERTION_TEXT_DIFF_STYLE_BLOCK: Literal["block"] = "block"
ASSERTION_TEXT_DIFF_STYLE_CHOICES = (
ASSERTION_TEXT_DIFF_STYLE_NDIFF,
ASSERTION_TEXT_DIFF_STYLE_BLOCK,
)
def get_assertion_text_diff_style(config: Config) -> _AssertionTextDiffStyle:
style = str(config.getini(ASSERTION_TEXT_DIFF_STYLE_INI))
match style:
case "ndiff" | "block":
return style
case _:
choices = ", ".join(
repr(choice) for choice in ASSERTION_TEXT_DIFF_STYLE_CHOICES
)
raise UsageError(
f"{ASSERTION_TEXT_DIFF_STYLE_INI} must be one of {choices}; got {style!r}"
)
def validate_assertion_text_diff_style(config: Config) -> None:
get_assertion_text_diff_style(config)
def format_explanation(explanation: str) -> str:
r"""Format an explanation.
Normally all embedded newlines are escaped, however there are
three exceptions: \n{, \n} and \n~. The first two are intended
cover nested explanations, see function and attribute explanations
for examples (.visit_Call(), visit_Attribute()). The last one is
for when one explanation needs to span multiple lines, e.g. when
displaying diffs.
"""
lines = _split_explanation(explanation)
result = _format_lines(lines)
return "\n".join(result)
def _split_explanation(explanation: str) -> list[str]:
r"""Return a list of individual lines in the explanation.
This will return a list of lines split on '\n{', '\n}' and '\n~'.
Any other newlines will be escaped and appear in the line as the
literal '\n' characters.
"""
raw_lines = (explanation or "").split("\n")
lines = [raw_lines[0]]
for values in raw_lines[1:]:
if values and values[0] in ["{", "}", "~", ">"]:
lines.append(values)
else:
lines[-1] += "\\n" + values
return lines
def _format_lines(lines: Sequence[str]) -> list[str]:
"""Format the individual lines.
This will replace the '{', '}' and '~' characters of our mini formatting
language with the proper 'where ...', 'and ...' and ' + ...' text, taking
care of indentation along the way.
Return a list of formatted lines.
"""
result = list(lines[:1])
stack = [0]
stackcnt = [0]
for line in lines[1:]:
if line.startswith("{"):
if stackcnt[-1]:
s = "and "
else:
s = "where "
stack.append(len(result))
stackcnt[-1] += 1
stackcnt.append(0)
result.append(" +" + " " * (len(stack) - 1) + s + line[1:])
elif line.startswith("}"):
stack.pop()
stackcnt.pop()
result[stack[-1]] += line[1:]
else:
assert line[0] in ["~", ">"]
stack[-1] += 1
indent = len(stack) if line.startswith("~") else len(stack) - 1
result.append(" " * indent + line[1:])
assert len(stack) == 1
return result
def assertrepr_compare(
op: str,
left: object,
right: object,
*,
verbose: int,
highlighter: _HighlightFunc,
assertion_text_diff_style: _AssertionTextDiffStyle,
) -> Iterator[str]:
"""Yield specialised explanations for some operators/operands.
The first line yielded is always the summary (``left op right``);
subsequent lines are the detailed explanation. Yields nothing when no
specialised explanation applies, which lets consumers map an empty
iterator to "no explanation" without materialising anything.
The iterator is lazy on purpose: a streaming consumer can stop pulling
lines as soon as it has enough to show, so an enormous diff doesn't
have to be built in full just to be thrown away.
"""
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
# See issue #3246.
use_ascii = (
isinstance(left, str)
and isinstance(right, str)
and normalize("NFD", left) == normalize("NFD", right)
)
if verbose > 1:
left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
else:
# XXX: "15 chars indentation" is wrong
# ("E AssertionError: assert "); should use term width.
maxsize = (
80 - 15 - len(op) - 2
) // 2 # 15 chars indentation, 1 space around op
left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
summary = f"{left_repr} {op} {right_repr}"
try:
if op == "==":
source = _compare_eq_any(
left,
right,
highlighter,
verbose,
assertion_text_diff_style,
)
elif op == "not in" and istext(left) and istext(right):
source = _notin_text(left, right, verbose)
elif op in {"!=", ">=", "<=", ">", "<"} and isset(left) and isset(right):
source = SET_COMPARISON_FUNCTIONS[op](left, right, highlighter, verbose)
else:
source = iter(())
# Only yield the summary if there is a detailed explanation.
# Make sure there's a separating empty line after the summary.
summary_yielded = False
for line in source:
if not summary_yielded:
yield summary
if line != "":
yield ""
summary_yielded = True
yield line
except outcomes.Exit:
raise
except Exception:
repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
if not summary_yielded:
yield summary
yield ""
summary_yielded = True
yield (
f"(pytest_assertion plugin: representation of details failed: {repr_crash}."
)
yield " Probably an object has a faulty __repr__.)"