diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ab5e63a1e0c..aca8cf5014e 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -435,6 +435,24 @@ def _call_reprcompare(ops, results, expls, each_obj): return expl +def _call_reprcall(callable, args, kwargs, result, expl_callable, expl_result): + import re, regex + regex_callables = { + re.match, regex.match, + re.search, regex.search, + re.fullmatch, regex.fullmatch, + } + if callable in regex_callables: + # I'll have to figure out how to deal with kwargs/optional flags. + pattern, string = args + return util._call_regex(callable, pattern, string, 0, result).replace('\n', '\n~') + + else: + return "{}\n{{{} = {}\n}}".format(expl_result, expl_result, expl_callable) + + + + def _call_assertion_pass(lineno, orig, expl): # type: (int, str, str) -> None if util._assertion_pass is not None: @@ -965,8 +983,16 @@ def visit_Call(self, call): new_call = ast.Call(new_func, new_args, new_kwargs) res = self.assign(new_call) res_expl = self.explanation_param(self.display(res)) - outer_expl = "{}\n{{{} = {}\n}}".format(res_expl, res_expl, expl) - return res, outer_expl + expl_call = self.helper( + "_call_reprcall", + new_func, + ast.List(new_args, ast.Load()), + ast.List(new_kwargs, ast.Load()), + res, + ast.Str(expl), + ast.Str(res_expl), + ) + return res, self.explanation_param(expl_call) def visit_Starred(self, starred): # From Python 3.5, a Starred node can appear in a function call diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 67f8d46185e..ad7bd2c82ff 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -438,6 +438,50 @@ def _compare_eq_cls( return explanation +def _call_regex(callable, pattern, string, flags, result): + import re, regex + + def mark_errors(errors): + markers = [' '] * (max(errors) + 1) + for i in errors: + markers[i] = '^' + return ''.join(markers) + + fuzzy_matchers = { + re.match: regex.match, + re.search: regex.search, + re.fullmatch: regex.fullmatch, + } + fuzzy_matcher = fuzzy_matchers.get(callable, callable) + fuzzy_pattern = '(?:%s){e}' % pattern + fuzzy_match = fuzzy_matcher( + fuzzy_pattern, + string, + flags | regex.ENHANCEMATCH, + ) + errors = fuzzy_match.fuzzy_changes + error_names = ( + f'insertions: ', + f'deletions: ', + f'substitutions:', + ) + + n = 8 + trunc = lambda x: f"'{x[:n-1]}…'" if len(x) > n else repr(x) + lines = [ + f'{callable.__module__}.{callable.__qualname__}({trunc(pattern)}, {trunc(string)}, flags={flags})' + f'', + f'', + f'pattern: {pattern}', + f'string: {string}', + ] + for error, name in zip(errors, error_names): + if error: + lines += [f'{name} {mark_errors(error)}'] + + return '\n'.join(lines) + + def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]: index = text.find(term) head = text[:index]