8 ACCEPT = os.getenv(
'EXPECTTEST_ACCEPT')
11 def nth_line(src, lineno):
13 Compute the starting index of the n-th line (where n is 1-indexed) 15 >>> nth_line("aaa\\nbb\\nc", 2) 20 for _
in range(lineno - 1):
21 pos = src.find(
'\n', pos) + 1
25 def nth_eol(src, lineno):
27 Compute the ending index of the n-th line (before the newline, 30 >>> nth_eol("aaa\\nbb\\nc", 2) 35 for _
in range(lineno):
36 pos = src.find(
'\n', pos + 1)
43 return t.replace(
'\r\n',
'\n').replace(
'\r',
'\n')
46 def escape_trailing_quote(s, quote):
47 if s
and s[-1] == quote:
48 return s[:-1] +
'\\' + quote
57 def adjust_lineno(self, fn, lineno):
58 if fn
not in self.
state:
60 for edit_loc, edit_diff
in self.
state[fn]:
65 def seen_file(self, fn):
66 return fn
in self.
state 68 def record_edit(self, fn, lineno, delta):
69 self.state.setdefault(fn, []).append((lineno, delta))
75 def ok_for_raw_triple_quoted_string(s, quote):
77 Is this string representable inside a raw triple-quoted string? 78 Due to the fact that backslashes are always treated literally, 79 some strings are not representable. 81 >>> ok_for_raw_triple_quoted_string("blah", quote="'") 83 >>> ok_for_raw_triple_quoted_string("'", quote="'") 85 >>> ok_for_raw_triple_quoted_string("a ''' b", quote="'") 88 return quote * 3
not in s
and (
not s
or s[-1]
not in [quote,
'\\'])
92 RE_EXPECT = re.compile(
r"^(?P<suffix>[^\n]*?)" 93 r"(?P<quote>'''|" r'""")' 96 r"(?P<raw>r?)", re.DOTALL)
99 def replace_string_literal(src, lineno, new_string):
101 Replace a triple quoted string literal with new contents. 102 Only handles printable ASCII correctly at the moment. This 103 will preserve the quote style of the original string, and 104 makes a best effort to preserve raw-ness (unless it is impossible 107 Returns a tuple of the replaced string, as well as a delta of 108 number of lines added/removed. 110 >>> replace_string_literal("'''arf'''", 1, "barf") 112 >>> r = replace_string_literal(" moo = '''arf'''", 1, "'a'\n\\b\n") 120 >>> replace_string_literal(" moo = '''\\\narf'''", 2, "'a'\n\\b\n")[1] 122 >>> print(replace_string_literal(" f('''\"\"\"''')", 1, "a ''' b")[0]) 126 assert all(c
in string.printable
for c
in new_string)
127 i = nth_eol(src, lineno)
128 new_string = normalize_nl(new_string)
130 delta = [new_string.count(
"\n")]
136 raw = m.group(
'raw') ==
'r' 137 if not raw
or not ok_for_raw_triple_quoted_string(s, quote=m.group(
'quote')[0]):
139 s = s.replace(
'\\',
'\\\\')
140 if m.group(
'quote') ==
"'''":
141 s = escape_trailing_quote(s,
"'").replace(
"'''",
r"\'\'\'")
143 s = escape_trailing_quote(s,
'"').replace(
'"""',
r'\"\"\"')
145 new_body =
"\\\n" + s
if "\n" in s
and not raw
else s
146 delta[0] -= m.group(
'body').count(
"\n")
148 return ''.join([m.group(
'suffix'),
157 return (RE_EXPECT.sub(replace, src[:i][::-1], count=1)[::-1] + src[i:], delta[0])
163 def assertExpectedInline(self, actual, expect, skip=0):
167 tb = traceback.extract_stack(limit=2 + skip)
168 fn, lineno, _, _ = tb[0]
169 print(
"Accepting new output for {} at {}:{}".format(self.id(), fn, lineno))
170 with open(fn,
'r+')
as f:
174 lineno = EDIT_HISTORY.adjust_lineno(fn, lineno)
175 new, delta = replace_string_literal(old, lineno, actual)
177 assert old != new,
"Failed to substitute string at {}:{}".format(fn, lineno)
181 if not EDIT_HISTORY.seen_file(fn):
182 with open(fn +
".bak",
'w')
as f_bak:
189 EDIT_HISTORY.record_edit(fn, lineno, delta)
191 help_text = (
"To accept the new output, re-run test with " 192 "envvar EXPECTTEST_ACCEPT=1 (we recommend " 193 "staging/committing your changes before doing this)")
194 if hasattr(self,
"assertMultiLineEqual"):
195 self.assertMultiLineEqual(expect, actual, msg=help_text)
197 self.assertEqual(expect, actual, msg=help_text)
200 if __name__ ==
"__main__":