diff options
author | shadchin <shadchin@yandex-team.ru> | 2022-02-10 16:44:30 +0300 |
---|---|---|
committer | Daniil Cherednik <dcherednik@yandex-team.ru> | 2022-02-10 16:44:30 +0300 |
commit | 2598ef1d0aee359b4b6d5fdd1758916d5907d04f (patch) | |
tree | 012bb94d777798f1f56ac1cec429509766d05181 /contrib/python/pytest/py3/_pytest/assertion/util.py | |
parent | 6751af0b0c1b952fede40b19b71da8025b5d8bcf (diff) | |
download | ydb-2598ef1d0aee359b4b6d5fdd1758916d5907d04f.tar.gz |
Restoring authorship annotation for <shadchin@yandex-team.ru>. Commit 1 of 2.
Diffstat (limited to 'contrib/python/pytest/py3/_pytest/assertion/util.py')
-rw-r--r-- | contrib/python/pytest/py3/_pytest/assertion/util.py | 624 |
1 files changed, 312 insertions, 312 deletions
diff --git a/contrib/python/pytest/py3/_pytest/assertion/util.py b/contrib/python/pytest/py3/_pytest/assertion/util.py index da1ffd15e3..60e8f3a656 100644 --- a/contrib/python/pytest/py3/_pytest/assertion/util.py +++ b/contrib/python/pytest/py3/_pytest/assertion/util.py @@ -1,34 +1,34 @@ -"""Utilities for assertion debugging.""" -import collections.abc +"""Utilities for assertion debugging.""" +import collections.abc import pprint -from typing import AbstractSet -from typing import Any -from typing import Callable -from typing import Iterable -from typing import List -from typing import Mapping -from typing import Optional -from typing import Sequence +from typing import AbstractSet +from typing import Any +from typing import Callable +from typing import Iterable +from typing import List +from typing import Mapping +from typing import Optional +from typing import Sequence import _pytest._code -from _pytest import outcomes -from _pytest._io.saferepr import _pformat_dispatch -from _pytest._io.saferepr import safeformat -from _pytest._io.saferepr import saferepr +from _pytest import outcomes +from _pytest._io.saferepr import _pformat_dispatch +from _pytest._io.saferepr import safeformat +from _pytest._io.saferepr import saferepr # 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: Optional[Callable[[str, object, object], Optional[str]]] = None +_reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None -# Works similarly as _reprcompare attribute. Is populated with the hook call -# when pytest_runtest_setup is called. -_assertion_pass: Optional[Callable[[int, str, str], None]] = None +# Works similarly as _reprcompare attribute. Is populated with the hook call +# when pytest_runtest_setup is called. +_assertion_pass: Optional[Callable[[int, str, str], None]] = None -def format_explanation(explanation: str) -> str: - r"""Format an explanation. +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 @@ -39,17 +39,17 @@ def format_explanation(explanation: str) -> str: """ lines = _split_explanation(explanation) result = _format_lines(lines) - return "\n".join(result) + return "\n".join(result) -def _split_explanation(explanation: str) -> List[str]: - r"""Return a list of individual lines in the explanation. +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") + raw_lines = (explanation or "").split("\n") lines = [raw_lines[0]] for values in raw_lines[1:]: if values and values[0] in ["{", "}", "~", ">"]: @@ -59,28 +59,28 @@ def _split_explanation(explanation: str) -> List[str]: return lines -def _format_lines(lines: Sequence[str]) -> List[str]: - """Format the individual 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. + 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]) + result = list(lines[:1]) stack = [0] stackcnt = [0] for line in lines[1:]: if line.startswith("{"): if stackcnt[-1]: - s = "and " + s = "and " else: - s = "where " + s = "where " stack.append(len(result)) stackcnt[-1] += 1 stackcnt.append(0) - result.append(" +" + " " * (len(stack) - 1) + s + line[1:]) + result.append(" +" + " " * (len(stack) - 1) + s + line[1:]) elif line.startswith("}"): stack.pop() stackcnt.pop() @@ -89,79 +89,79 @@ def _format_lines(lines: Sequence[str]) -> List[str]: assert line[0] in ["~", ">"] stack[-1] += 1 indent = len(stack) if line.startswith("~") else len(stack) - 1 - result.append(" " * indent + line[1:]) + result.append(" " * indent + line[1:]) assert len(stack) == 1 return result -def issequence(x: Any) -> bool: - return isinstance(x, collections.abc.Sequence) and not isinstance(x, str) - - -def istext(x: Any) -> bool: - return isinstance(x, str) - - -def isdict(x: Any) -> bool: - return isinstance(x, dict) - - -def isset(x: Any) -> bool: - return isinstance(x, (set, frozenset)) - - -def isnamedtuple(obj: Any) -> bool: - return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None - - -def isdatacls(obj: Any) -> bool: - return getattr(obj, "__dataclass_fields__", None) is not None - - -def isattrs(obj: Any) -> bool: - return getattr(obj, "__attrs_attrs__", None) is not None - - -def isiterable(obj: Any) -> bool: - try: - iter(obj) - return not istext(obj) - except TypeError: - return False - - -def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]: - """Return specialised explanations for some operators/operands.""" - verbose = config.getoption("verbose") - if verbose > 1: - left_repr = safeformat(left) - right_repr = safeformat(right) - 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) - right_repr = saferepr(right, maxsize=maxsize) - - summary = f"{left_repr} {op} {right_repr}" - +def issequence(x: Any) -> bool: + return isinstance(x, collections.abc.Sequence) and not isinstance(x, str) + + +def istext(x: Any) -> bool: + return isinstance(x, str) + + +def isdict(x: Any) -> bool: + return isinstance(x, dict) + + +def isset(x: Any) -> bool: + return isinstance(x, (set, frozenset)) + + +def isnamedtuple(obj: Any) -> bool: + return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None + + +def isdatacls(obj: Any) -> bool: + return getattr(obj, "__dataclass_fields__", None) is not None + + +def isattrs(obj: Any) -> bool: + return getattr(obj, "__attrs_attrs__", None) is not None + + +def isiterable(obj: Any) -> bool: + try: + iter(obj) + return not istext(obj) + except TypeError: + return False + + +def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]: + """Return specialised explanations for some operators/operands.""" + verbose = config.getoption("verbose") + if verbose > 1: + left_repr = safeformat(left) + right_repr = safeformat(right) + 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) + right_repr = saferepr(right, maxsize=maxsize) + + summary = f"{left_repr} {op} {right_repr}" + explanation = None try: if op == "==": - explanation = _compare_eq_any(left, right, verbose) + explanation = _compare_eq_any(left, right, verbose) elif op == "not in": if istext(left) and istext(right): explanation = _notin_text(left, right, verbose) - except outcomes.Exit: - raise + except outcomes.Exit: + raise except Exception: explanation = [ - "(pytest_assertion plugin: representation of details failed: {}.".format( - _pytest._code.ExceptionInfo.from_current()._getreprcrash() - ), - " Probably an object has a faulty __repr__.)", + "(pytest_assertion plugin: representation of details failed: {}.".format( + _pytest._code.ExceptionInfo.from_current()._getreprcrash() + ), + " Probably an object has a faulty __repr__.)", ] if not explanation: @@ -170,44 +170,44 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[ return [summary] + explanation -def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: - explanation = [] - if istext(left) and istext(right): - explanation = _diff_text(left, right, verbose) - else: - if type(left) == type(right) and ( - isdatacls(left) or isattrs(left) or isnamedtuple(left) - ): - # Note: unlike dataclasses/attrs, namedtuples compare only the - # field values, not the type or field names. But this branch - # intentionally only handles the same-type case, which was often - # used in older code bases before dataclasses/attrs were available. - explanation = _compare_eq_cls(left, right, verbose) - elif issequence(left) and issequence(right): - explanation = _compare_eq_sequence(left, right, verbose) - elif isset(left) and isset(right): - explanation = _compare_eq_set(left, right, verbose) - elif isdict(left) and isdict(right): - explanation = _compare_eq_dict(left, right, verbose) - elif verbose > 0: - explanation = _compare_eq_verbose(left, right) - if isiterable(left) and isiterable(right): - expl = _compare_eq_iterable(left, right, verbose) - explanation.extend(expl) - return explanation - - -def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: - """Return the explanation for the diff between text. +def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: + explanation = [] + if istext(left) and istext(right): + explanation = _diff_text(left, right, verbose) + else: + if type(left) == type(right) and ( + isdatacls(left) or isattrs(left) or isnamedtuple(left) + ): + # Note: unlike dataclasses/attrs, namedtuples compare only the + # field values, not the type or field names. But this branch + # intentionally only handles the same-type case, which was often + # used in older code bases before dataclasses/attrs were available. + explanation = _compare_eq_cls(left, right, verbose) + elif issequence(left) and issequence(right): + explanation = _compare_eq_sequence(left, right, verbose) + elif isset(left) and isset(right): + explanation = _compare_eq_set(left, right, verbose) + elif isdict(left) and isdict(right): + explanation = _compare_eq_dict(left, right, verbose) + elif verbose > 0: + explanation = _compare_eq_verbose(left, right) + if isiterable(left) and isiterable(right): + expl = _compare_eq_iterable(left, right, verbose) + explanation.extend(expl) + return explanation + + +def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: + """Return the explanation for the diff between text. Unless --verbose is used this will skip leading and trailing characters which are identical to keep the diff minimal. """ from difflib import ndiff - explanation: List[str] = [] + explanation: List[str] = [] - if verbose < 1: + if verbose < 1: i = 0 # just in case left or right has zero length for i in range(min(len(left), len(right))): if left[i] != right[i]: @@ -215,7 +215,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: if i > 42: i -= 10 # Provide some context explanation = [ - "Skipping %s identical leading characters in diff, use -v to show" % i + "Skipping %s identical leading characters in diff, use -v to show" % i ] left = left[i:] right = right[i:] @@ -226,8 +226,8 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: if i > 42: i -= 10 # Provide some context explanation += [ - "Skipping {} identical trailing " - "characters in diff, use -v to show".format(i) + "Skipping {} identical trailing " + "characters in diff, use -v to show".format(i) ] left = left[:-i] right = right[:-i] @@ -235,243 +235,243 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: if left.isspace() or right.isspace(): left = repr(str(left)) right = repr(str(right)) - explanation += ["Strings contain only whitespace, escaping them using repr()"] - # "right" is the expected base against which we compare "left", - # see https://github.com/pytest-dev/pytest/issues/3333 + explanation += ["Strings contain only whitespace, escaping them using repr()"] + # "right" is the expected base against which we compare "left", + # see https://github.com/pytest-dev/pytest/issues/3333 explanation += [ line.strip("\n") - for line in ndiff(right.splitlines(keepends), left.splitlines(keepends)) + for line in ndiff(right.splitlines(keepends), left.splitlines(keepends)) ] return explanation -def _compare_eq_verbose(left: Any, right: Any) -> List[str]: - keepends = True - left_lines = repr(left).splitlines(keepends) - right_lines = repr(right).splitlines(keepends) - - explanation: List[str] = [] - explanation += ["+" + line for line in left_lines] - explanation += ["-" + line for line in right_lines] - - return explanation - - -def _surrounding_parens_on_own_lines(lines: List[str]) -> None: - """Move opening/closing parenthesis/bracket to own lines.""" - opening = lines[0][:1] - if opening in ["(", "[", "{"]: - lines[0] = " " + lines[0][1:] - lines[:] = [opening] + lines - closing = lines[-1][-1:] - if closing in [")", "]", "}"]: - lines[-1] = lines[-1][:-1] + "," - lines[:] = lines + [closing] - - -def _compare_eq_iterable( - left: Iterable[Any], right: Iterable[Any], verbose: int = 0 -) -> List[str]: +def _compare_eq_verbose(left: Any, right: Any) -> List[str]: + keepends = True + left_lines = repr(left).splitlines(keepends) + right_lines = repr(right).splitlines(keepends) + + explanation: List[str] = [] + explanation += ["+" + line for line in left_lines] + explanation += ["-" + line for line in right_lines] + + return explanation + + +def _surrounding_parens_on_own_lines(lines: List[str]) -> None: + """Move opening/closing parenthesis/bracket to own lines.""" + opening = lines[0][:1] + if opening in ["(", "[", "{"]: + lines[0] = " " + lines[0][1:] + lines[:] = [opening] + lines + closing = lines[-1][-1:] + if closing in [")", "]", "}"]: + lines[-1] = lines[-1][:-1] + "," + lines[:] = lines + [closing] + + +def _compare_eq_iterable( + left: Iterable[Any], right: Iterable[Any], verbose: int = 0 +) -> List[str]: if not verbose: - return ["Use -v to get the full diff"] + return ["Use -v to get the full diff"] # dynamic import to speedup pytest import difflib - left_formatting = pprint.pformat(left).splitlines() - right_formatting = pprint.pformat(right).splitlines() - - # Re-format for different output lengths. - lines_left = len(left_formatting) - lines_right = len(right_formatting) - if lines_left != lines_right: - left_formatting = _pformat_dispatch(left).splitlines() - right_formatting = _pformat_dispatch(right).splitlines() - - if lines_left > 1 or lines_right > 1: - _surrounding_parens_on_own_lines(left_formatting) - _surrounding_parens_on_own_lines(right_formatting) - - explanation = ["Full diff:"] - # "right" is the expected base against which we compare "left", - # see https://github.com/pytest-dev/pytest/issues/3333 + left_formatting = pprint.pformat(left).splitlines() + right_formatting = pprint.pformat(right).splitlines() + + # Re-format for different output lengths. + lines_left = len(left_formatting) + lines_right = len(right_formatting) + if lines_left != lines_right: + left_formatting = _pformat_dispatch(left).splitlines() + right_formatting = _pformat_dispatch(right).splitlines() + + if lines_left > 1 or lines_right > 1: + _surrounding_parens_on_own_lines(left_formatting) + _surrounding_parens_on_own_lines(right_formatting) + + explanation = ["Full diff:"] + # "right" is the expected base against which we compare "left", + # see https://github.com/pytest-dev/pytest/issues/3333 explanation.extend( - line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting) + line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting) ) return explanation -def _compare_eq_sequence( - left: Sequence[Any], right: Sequence[Any], verbose: int = 0 -) -> List[str]: - comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes) - explanation: List[str] = [] - len_left = len(left) - len_right = len(right) - for i in range(min(len_left, len_right)): +def _compare_eq_sequence( + left: Sequence[Any], right: Sequence[Any], verbose: int = 0 +) -> List[str]: + comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes) + explanation: List[str] = [] + len_left = len(left) + len_right = len(right) + for i in range(min(len_left, len_right)): if left[i] != right[i]: - if comparing_bytes: - # when comparing bytes, we want to see their ascii representation - # instead of their numeric values (#5260) - # using a slice gives us the ascii representation: - # >>> s = b'foo' - # >>> s[0] - # 102 - # >>> s[0:1] - # b'f' - left_value = left[i : i + 1] - right_value = right[i : i + 1] - else: - left_value = left[i] - right_value = right[i] - - explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"] + if comparing_bytes: + # when comparing bytes, we want to see their ascii representation + # instead of their numeric values (#5260) + # using a slice gives us the ascii representation: + # >>> s = b'foo' + # >>> s[0] + # 102 + # >>> s[0:1] + # b'f' + left_value = left[i : i + 1] + right_value = right[i : i + 1] + else: + left_value = left[i] + right_value = right[i] + + explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"] break - - if comparing_bytes: - # when comparing bytes, it doesn't help to show the "sides contain one or more - # items" longer explanation, so skip it - - return explanation - - len_diff = len_left - len_right - if len_diff: - if len_diff > 0: - dir_with_more = "Left" - extra = saferepr(left[len_right]) - else: - len_diff = 0 - len_diff - dir_with_more = "Right" - extra = saferepr(right[len_left]) - - if len_diff == 1: - explanation += [f"{dir_with_more} contains one more item: {extra}"] - else: - explanation += [ - "%s contains %d more items, first extra item: %s" - % (dir_with_more, len_diff, extra) - ] + + if comparing_bytes: + # when comparing bytes, it doesn't help to show the "sides contain one or more + # items" longer explanation, so skip it + + return explanation + + len_diff = len_left - len_right + if len_diff: + if len_diff > 0: + dir_with_more = "Left" + extra = saferepr(left[len_right]) + else: + len_diff = 0 - len_diff + dir_with_more = "Right" + extra = saferepr(right[len_left]) + + if len_diff == 1: + explanation += [f"{dir_with_more} contains one more item: {extra}"] + else: + explanation += [ + "%s contains %d more items, first extra item: %s" + % (dir_with_more, len_diff, extra) + ] return explanation -def _compare_eq_set( - left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 -) -> List[str]: +def _compare_eq_set( + left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 +) -> List[str]: explanation = [] diff_left = left - right diff_right = right - left if diff_left: - explanation.append("Extra items in the left set:") + explanation.append("Extra items in the left set:") for item in diff_left: - explanation.append(saferepr(item)) + explanation.append(saferepr(item)) if diff_right: - explanation.append("Extra items in the right set:") + explanation.append("Extra items in the right set:") for item in diff_right: - explanation.append(saferepr(item)) + explanation.append(saferepr(item)) return explanation -def _compare_eq_dict( - left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0 -) -> List[str]: - explanation: List[str] = [] - set_left = set(left) - set_right = set(right) - common = set_left.intersection(set_right) +def _compare_eq_dict( + left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0 +) -> List[str]: + explanation: List[str] = [] + set_left = set(left) + set_right = set(right) + common = set_left.intersection(set_right) same = {k: left[k] for k in common if left[k] == right[k]} if same and verbose < 2: - explanation += ["Omitting %s identical items, use -vv to show" % len(same)] + explanation += ["Omitting %s identical items, use -vv to show" % len(same)] elif same: - explanation += ["Common items:"] + explanation += ["Common items:"] explanation += pprint.pformat(same).splitlines() diff = {k for k in common if left[k] != right[k]} if diff: - explanation += ["Differing items:"] + explanation += ["Differing items:"] for k in diff: - explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})] - extra_left = set_left - set_right - len_extra_left = len(extra_left) - if len_extra_left: - explanation.append( - "Left contains %d more item%s:" - % (len_extra_left, "" if len_extra_left == 1 else "s") - ) + explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})] + extra_left = set_left - set_right + len_extra_left = len(extra_left) + if len_extra_left: + explanation.append( + "Left contains %d more item%s:" + % (len_extra_left, "" if len_extra_left == 1 else "s") + ) explanation.extend( pprint.pformat({k: left[k] for k in extra_left}).splitlines() ) - extra_right = set_right - set_left - len_extra_right = len(extra_right) - if len_extra_right: - explanation.append( - "Right contains %d more item%s:" - % (len_extra_right, "" if len_extra_right == 1 else "s") - ) + extra_right = set_right - set_left + len_extra_right = len(extra_right) + if len_extra_right: + explanation.append( + "Right contains %d more item%s:" + % (len_extra_right, "" if len_extra_right == 1 else "s") + ) explanation.extend( pprint.pformat({k: right[k] for k in extra_right}).splitlines() ) return explanation -def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: - if isdatacls(left): - all_fields = left.__dataclass_fields__ - fields_to_check = [field for field, info in all_fields.items() if info.compare] - elif isattrs(left): - all_fields = left.__attrs_attrs__ - fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] - elif isnamedtuple(left): - fields_to_check = left._fields - else: - assert False - - indent = " " - same = [] - diff = [] - for field in fields_to_check: - if getattr(left, field) == getattr(right, field): - same.append(field) - else: - diff.append(field) - - explanation = [] - if same or diff: - explanation += [""] - if same and verbose < 2: - explanation.append("Omitting %s identical items, use -vv to show" % len(same)) - elif same: - explanation += ["Matching attributes:"] - explanation += pprint.pformat(same).splitlines() - if diff: - explanation += ["Differing attributes:"] - explanation += pprint.pformat(diff).splitlines() - for field in diff: - field_left = getattr(left, field) - field_right = getattr(right, field) - explanation += [ - "", - "Drill down into differing attribute %s:" % field, - ("%s%s: %r != %r") % (indent, field, field_left, field_right), - ] - explanation += [ - indent + line - for line in _compare_eq_any(field_left, field_right, verbose) - ] - return explanation - - -def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]: +def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: + if isdatacls(left): + all_fields = left.__dataclass_fields__ + fields_to_check = [field for field, info in all_fields.items() if info.compare] + elif isattrs(left): + all_fields = left.__attrs_attrs__ + fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] + elif isnamedtuple(left): + fields_to_check = left._fields + else: + assert False + + indent = " " + same = [] + diff = [] + for field in fields_to_check: + if getattr(left, field) == getattr(right, field): + same.append(field) + else: + diff.append(field) + + explanation = [] + if same or diff: + explanation += [""] + if same and verbose < 2: + explanation.append("Omitting %s identical items, use -vv to show" % len(same)) + elif same: + explanation += ["Matching attributes:"] + explanation += pprint.pformat(same).splitlines() + if diff: + explanation += ["Differing attributes:"] + explanation += pprint.pformat(diff).splitlines() + for field in diff: + field_left = getattr(left, field) + field_right = getattr(right, field) + explanation += [ + "", + "Drill down into differing attribute %s:" % field, + ("%s%s: %r != %r") % (indent, field, field_left, field_right), + ] + explanation += [ + indent + line + for line in _compare_eq_any(field_left, field_right, verbose) + ] + return explanation + + +def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]: index = text.find(term) head = text[:index] tail = text[index + len(term) :] correct_text = head + tail - diff = _diff_text(text, correct_text, verbose) - newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)] + diff = _diff_text(text, correct_text, verbose) + newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)] for line in diff: - if line.startswith("Skipping"): + if line.startswith("Skipping"): continue - if line.startswith("- "): + if line.startswith("- "): continue - if line.startswith("+ "): - newdiff.append(" " + line[2:]) + if line.startswith("+ "): + newdiff.append(" " + line[2:]) else: newdiff.append(line) return newdiff |