[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
This commit is contained in:
pre-commit-ci[bot] 2024-04-13 00:00:18 +00:00
parent 72ad6dc953
commit f4cd1ba0d6
813 changed files with 66015 additions and 58839 deletions

View file

@ -1,5 +1,7 @@
# mypy: allow-untyped-defs
"""Support for presenting detailed information in failing assertions."""
from __future__ import annotations
import sys
from typing import Any
from typing import Generator
@ -22,34 +24,34 @@ if TYPE_CHECKING:
def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("debugconfig")
group = parser.getgroup('debugconfig')
group.addoption(
"--assert",
action="store",
dest="assertmode",
choices=("rewrite", "plain"),
default="rewrite",
metavar="MODE",
'--assert',
action='store',
dest='assertmode',
choices=('rewrite', 'plain'),
default='rewrite',
metavar='MODE',
help=(
"Control assertion debugging tools.\n"
'Control assertion debugging tools.\n'
"'plain' performs no assertion debugging.\n"
"'rewrite' (the default) rewrites assert statements in test modules"
" on import to provide assert expression information."
' on import to provide assert expression information.'
),
)
parser.addini(
"enable_assertion_pass_hook",
type="bool",
'enable_assertion_pass_hook',
type='bool',
default=False,
help="Enables the pytest_assertion_pass hook. "
"Make sure to delete any previously generated pyc cache files.",
help='Enables the pytest_assertion_pass hook. '
'Make sure to delete any previously generated pyc cache files.',
)
Config._add_verbosity_ini(
parser,
Config.VERBOSITY_ASSERTIONS,
help=(
"Specify a verbosity level for assertions, overriding the main level. "
"Higher levels will provide more detailed explanation when an assertion fails."
'Specify a verbosity level for assertions, overriding the main level. '
'Higher levels will provide more detailed explanation when an assertion fails.'
),
)
@ -67,7 +69,7 @@ def register_assert_rewrite(*names: str) -> None:
"""
for name in names:
if not isinstance(name, str):
msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable]
msg = 'expected module names as *args, got {0} instead' # type: ignore[unreachable]
raise TypeError(msg.format(repr(names)))
for hook in sys.meta_path:
if isinstance(hook, rewrite.AssertionRewritingHook):
@ -92,16 +94,16 @@ class AssertionState:
def __init__(self, config: Config, mode) -> None:
self.mode = mode
self.trace = config.trace.root.get("assertion")
self.hook: Optional[rewrite.AssertionRewritingHook] = None
self.trace = config.trace.root.get('assertion')
self.hook: rewrite.AssertionRewritingHook | None = None
def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
"""Try to install the rewrite hook, raise SystemError if it fails."""
config.stash[assertstate_key] = AssertionState(config, "rewrite")
config.stash[assertstate_key] = AssertionState(config, 'rewrite')
config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
sys.meta_path.insert(0, hook)
config.stash[assertstate_key].trace("installed rewrite import hook")
config.stash[assertstate_key].trace('installed rewrite import hook')
def undo() -> None:
hook = config.stash[assertstate_key].hook
@ -112,7 +114,7 @@ def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
return hook
def pytest_collection(session: "Session") -> None:
def pytest_collection(session: Session) -> None:
# This hook is only called when test modules are collected
# so for example not in the managing process of pytest-xdist
# (which does not collect test modules).
@ -132,7 +134,7 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
"""
ihook = item.ihook
def callbinrepr(op, left: object, right: object) -> Optional[str]:
def callbinrepr(op, left: object, right: object) -> str | None:
"""Call the pytest_assertrepr_compare hook and prepare the result.
This uses the first result from the hook and then ensures the
@ -148,15 +150,15 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
pretty printing.
"""
hook_result = ihook.pytest_assertrepr_compare(
config=item.config, op=op, left=left, right=right
config=item.config, op=op, left=left, right=right,
)
for new_expl in hook_result:
if new_expl:
new_expl = truncate.truncate_if_required(new_expl, item)
new_expl = [line.replace("\n", "\\n") for line in new_expl]
res = "\n~".join(new_expl)
if item.config.getvalue("assertmode") == "rewrite":
res = res.replace("%", "%%")
new_expl = [line.replace('\n', '\\n') for line in new_expl]
res = '\n~'.join(new_expl)
if item.config.getvalue('assertmode') == 'rewrite':
res = res.replace('%', '%%')
return res
return None
@ -178,7 +180,7 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
util._config = None
def pytest_sessionfinish(session: "Session") -> None:
def pytest_sessionfinish(session: Session) -> None:
assertstate = session.config.stash.get(assertstate_key, None)
if assertstate:
if assertstate.hook is not None:
@ -186,6 +188,6 @@ def pytest_sessionfinish(session: "Session") -> None:
def pytest_assertrepr_compare(
config: Config, op: str, left: Any, right: Any
) -> Optional[List[str]]:
config: Config, op: str, left: Any, right: Any,
) -> list[str] | None:
return util.assertrepr_compare(config=config, op=op, left=left, right=right)

View file

@ -3,6 +3,7 @@
Current default behaviour is to truncate assertion explanations at
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
"""
from __future__ import annotations
from typing import List
from typing import Optional
@ -18,8 +19,8 @@ USAGE_MSG = "use '-vv' to show"
def truncate_if_required(
explanation: List[str], item: Item, max_length: Optional[int] = None
) -> List[str]:
explanation: list[str], item: Item, max_length: int | None = None,
) -> list[str]:
"""Truncate this assertion explanation if the given test item is eligible."""
if _should_truncate_item(item):
return _truncate_explanation(explanation)
@ -33,10 +34,10 @@ def _should_truncate_item(item: Item) -> bool:
def _truncate_explanation(
input_lines: List[str],
max_lines: Optional[int] = None,
max_chars: Optional[int] = None,
) -> List[str]:
input_lines: list[str],
max_lines: int | None = None,
max_chars: int | None = None,
) -> list[str]:
"""Truncate given list of strings that makes up the assertion explanation.
Truncates to either 8 lines, or 640 characters - whichever the input reaches
@ -49,7 +50,7 @@ def _truncate_explanation(
max_chars = DEFAULT_MAX_CHARS
# Check if truncation required
input_char_count = len("".join(input_lines))
input_char_count = len(''.join(input_lines))
# The length of the truncation explanation depends on the number of lines
# removed but is at least 68 characters:
# The real value is
@ -67,17 +68,17 @@ def _truncate_explanation(
# The truncation explanation add two lines to the output
tolerable_max_lines = max_lines + 2
if (
len(input_lines) <= tolerable_max_lines
and input_char_count <= tolerable_max_chars
len(input_lines) <= tolerable_max_lines and
input_char_count <= tolerable_max_chars
):
return input_lines
# Truncate first to max_lines, and then truncate to max_chars if necessary
truncated_explanation = input_lines[:max_lines]
truncated_char = True
# We reevaluate the need to truncate chars following removal of some lines
if len("".join(truncated_explanation)) > tolerable_max_chars:
if len(''.join(truncated_explanation)) > tolerable_max_chars:
truncated_explanation = _truncate_by_char_count(
truncated_explanation, max_chars
truncated_explanation, max_chars,
)
else:
truncated_char = False
@ -85,22 +86,22 @@ def _truncate_explanation(
truncated_line_count = len(input_lines) - len(truncated_explanation)
if truncated_explanation[-1]:
# Add ellipsis and take into account part-truncated final line
truncated_explanation[-1] = truncated_explanation[-1] + "..."
truncated_explanation[-1] = truncated_explanation[-1] + '...'
if truncated_char:
# It's possible that we did not remove any char from this line
truncated_line_count += 1
else:
# Add proper ellipsis when we were able to fit a full line exactly
truncated_explanation[-1] = "..."
truncated_explanation[-1] = '...'
return [
*truncated_explanation,
"",
f"...Full output truncated ({truncated_line_count} line"
'',
f'...Full output truncated ({truncated_line_count} line'
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
]
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]:
# Find point at which input length exceeds total allowed length
iterated_char_count = 0
for iterated_index, input_line in enumerate(input_lines):

View file

@ -1,5 +1,7 @@
# mypy: allow-untyped-defs
"""Utilities for assertion debugging."""
from __future__ import annotations
import collections.abc
import os
import pprint
@ -15,8 +17,8 @@ from typing import Protocol
from typing import Sequence
from unicodedata import normalize
from _pytest import outcomes
import _pytest._code
from _pytest import outcomes
from _pytest._io.pprint import PrettyPrinter
from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
@ -27,18 +29,18 @@ from _pytest.config import Config
# 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: 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: Optional[Callable[[int, str, str], None]] = None
_assertion_pass: Callable[[int, str, str], None] | None = None
# Config object which is assigned during pytest_runtest_protocol.
_config: Optional[Config] = None
_config: Config | None = None
class _HighlightFunc(Protocol):
def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str:
def __call__(self, source: str, lexer: Literal['diff', 'python'] = 'python') -> str:
"""Apply highlighting to the given source."""
@ -54,27 +56,27 @@ 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]:
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 ["{", "}", "~", ">"]:
if values and values[0] in ['{', '}', '~', '>']:
lines.append(values)
else:
lines[-1] += "\\n" + values
lines[-1] += '\\n' + values
return lines
def _format_lines(lines: Sequence[str]) -> List[str]:
def _format_lines(lines: Sequence[str]) -> list[str]:
"""Format the individual lines.
This will replace the '{', '}' and '~' characters of our mini formatting
@ -87,24 +89,24 @@ def _format_lines(lines: Sequence[str]) -> List[str]:
stack = [0]
stackcnt = [0]
for line in lines[1:]:
if line.startswith("{"):
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:])
elif line.startswith("}"):
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 ["~", ">"]
assert line[0] in ['~', '>']
stack[-1] += 1
indent = len(stack) if line.startswith("~") else len(stack) - 1
result.append(" " * indent + line[1:])
indent = len(stack) if line.startswith('~') else len(stack) - 1
result.append(' ' * indent + line[1:])
assert len(stack) == 1
return result
@ -126,15 +128,15 @@ def isset(x: Any) -> bool:
def isnamedtuple(obj: Any) -> bool:
return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
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
return getattr(obj, '__dataclass_fields__', None) is not None
def isattrs(obj: Any) -> bool:
return getattr(obj, "__attrs_attrs__", None) is not None
return getattr(obj, '__attrs_attrs__', None) is not None
def isiterable(obj: Any) -> bool:
@ -156,28 +158,28 @@ def has_default_eq(
for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
"""
# inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
if hasattr(obj.__eq__, '__code__') and hasattr(obj.__eq__.__code__, 'co_filename'):
code_filename = obj.__eq__.__code__.co_filename
if isattrs(obj):
return "attrs generated eq" in code_filename
return 'attrs generated eq' in code_filename
return code_filename == "<string>" # data class
return code_filename == '<string>' # data class
return True
def assertrepr_compare(
config, op: str, left: Any, right: Any, use_ascii: bool = False
) -> Optional[List[str]]:
config, op: str, left: Any, right: Any, use_ascii: bool = False,
) -> list[str] | None:
"""Return specialised explanations for some operators/operands."""
verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
# 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)
isinstance(left, str) and
isinstance(right, str) and
normalize('NFD', left) == normalize('NFD', right)
)
if verbose > 1:
@ -193,29 +195,29 @@ def assertrepr_compare(
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}"
summary = f'{left_repr} {op} {right_repr}'
highlighter = config.get_terminal_writer()._highlight
explanation = None
try:
if op == "==":
if op == '==':
explanation = _compare_eq_any(left, right, highlighter, verbose)
elif op == "not in":
elif op == 'not in':
if istext(left) and istext(right):
explanation = _notin_text(left, right, verbose)
elif op == "!=":
elif op == '!=':
if isset(left) and isset(right):
explanation = ["Both sets are equal"]
elif op == ">=":
explanation = ['Both sets are equal']
elif op == '>=':
if isset(left) and isset(right):
explanation = _compare_gte_set(left, right, highlighter, verbose)
elif op == "<=":
elif op == '<=':
if isset(left) and isset(right):
explanation = _compare_lte_set(left, right, highlighter, verbose)
elif op == ">":
elif op == '>':
if isset(left) and isset(right):
explanation = _compare_gt_set(left, right, highlighter, verbose)
elif op == "<":
elif op == '<':
if isset(left) and isset(right):
explanation = _compare_lt_set(left, right, highlighter, verbose)
@ -223,23 +225,23 @@ def assertrepr_compare(
raise
except Exception:
explanation = [
"(pytest_assertion plugin: representation of details failed: {}.".format(
_pytest._code.ExceptionInfo.from_current()._getreprcrash()
'(pytest_assertion plugin: representation of details failed: {}.'.format(
_pytest._code.ExceptionInfo.from_current()._getreprcrash(),
),
" Probably an object has a faulty __repr__.)",
' Probably an object has a faulty __repr__.)',
]
if not explanation:
return None
if explanation[0] != "":
explanation = ["", *explanation]
if explanation[0] != '':
explanation = ['', *explanation]
return [summary, *explanation]
def _compare_eq_any(
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0
) -> List[str]:
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0,
) -> list[str]:
explanation = []
if istext(left) and istext(right):
explanation = _diff_text(left, right, verbose)
@ -274,7 +276,7 @@ def _compare_eq_any(
return explanation
def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
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
@ -282,7 +284,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
"""
from difflib import ndiff
explanation: List[str] = []
explanation: list[str] = []
if verbose < 1:
i = 0 # just in case left or right has zero length
@ -292,7 +294,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:]
@ -303,8 +305,8 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
if i > 42:
i -= 10 # Provide some context
explanation += [
f"Skipping {i} identical trailing "
"characters in diff, use -v to show"
f'Skipping {i} identical trailing '
'characters in diff, use -v to show',
]
left = left[:-i]
right = right[:-i]
@ -312,11 +314,11 @@ 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()"]
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")
line.strip('\n')
for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
]
return explanation
@ -327,26 +329,26 @@ def _compare_eq_iterable(
right: Iterable[Any],
highligher: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
if verbose <= 0 and not running_on_ci():
return ["Use -v to get more diff"]
return ['Use -v to get more diff']
# dynamic import to speedup pytest
import difflib
left_formatting = PrettyPrinter().pformat(left).splitlines()
right_formatting = PrettyPrinter().pformat(right).splitlines()
explanation = ["", "Full diff:"]
explanation = ['', 'Full diff:']
# "right" is the expected base against which we compare "left",
# see https://github.com/pytest-dev/pytest/issues/3333
explanation.extend(
highligher(
"\n".join(
'\n'.join(
line.rstrip()
for line in difflib.ndiff(right_formatting, left_formatting)
),
lexer="diff",
).splitlines()
lexer='diff',
).splitlines(),
)
return explanation
@ -356,9 +358,9 @@ def _compare_eq_sequence(
right: Sequence[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
explanation: List[str] = []
explanation: list[str] = []
len_left = len(left)
len_right = len(right)
for i in range(min(len_left, len_right)):
@ -372,15 +374,15 @@ def _compare_eq_sequence(
# 102
# >>> s[0:1]
# b'f'
left_value = left[i : i + 1]
right_value = right[i : i + 1]
left_value = left[i: i + 1]
right_value = right[i: i + 1]
else:
left_value = left[i]
right_value = right[i]
explanation.append(
f"At index {i} diff:"
f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}"
f'At index {i} diff:'
f' {highlighter(repr(left_value))} != {highlighter(repr(right_value))}',
)
break
@ -393,21 +395,21 @@ def _compare_eq_sequence(
len_diff = len_left - len_right
if len_diff:
if len_diff > 0:
dir_with_more = "Left"
dir_with_more = 'Left'
extra = saferepr(left[len_right])
else:
len_diff = 0 - len_diff
dir_with_more = "Right"
dir_with_more = 'Right'
extra = saferepr(right[len_left])
if len_diff == 1:
explanation += [
f"{dir_with_more} contains one more item: {highlighter(extra)}"
f'{dir_with_more} contains one more item: {highlighter(extra)}',
]
else:
explanation += [
"%s contains %d more items, first extra item: %s"
% (dir_with_more, len_diff, highlighter(extra))
'%s contains %d more items, first extra item: %s'
% (dir_with_more, len_diff, highlighter(extra)),
]
return explanation
@ -417,10 +419,10 @@ def _compare_eq_set(
right: AbstractSet[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
explanation = []
explanation.extend(_set_one_sided_diff("left", left, right, highlighter))
explanation.extend(_set_one_sided_diff("right", right, left, highlighter))
explanation.extend(_set_one_sided_diff('left', left, right, highlighter))
explanation.extend(_set_one_sided_diff('right', right, left, highlighter))
return explanation
@ -429,10 +431,10 @@ def _compare_gt_set(
right: AbstractSet[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
explanation = _compare_gte_set(left, right, highlighter)
if not explanation:
return ["Both sets are equal"]
return ['Both sets are equal']
return explanation
@ -441,10 +443,10 @@ def _compare_lt_set(
right: AbstractSet[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
explanation = _compare_lte_set(left, right, highlighter)
if not explanation:
return ["Both sets are equal"]
return ['Both sets are equal']
return explanation
@ -453,8 +455,8 @@ def _compare_gte_set(
right: AbstractSet[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
return _set_one_sided_diff("right", right, left, highlighter)
) -> list[str]:
return _set_one_sided_diff('right', right, left, highlighter)
def _compare_lte_set(
@ -462,8 +464,8 @@ def _compare_lte_set(
right: AbstractSet[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
return _set_one_sided_diff("left", left, right, highlighter)
) -> list[str]:
return _set_one_sided_diff('left', left, right, highlighter)
def _set_one_sided_diff(
@ -471,11 +473,11 @@ def _set_one_sided_diff(
set1: AbstractSet[Any],
set2: AbstractSet[Any],
highlighter: _HighlightFunc,
) -> List[str]:
) -> list[str]:
explanation = []
diff = set1 - set2
if diff:
explanation.append(f"Extra items in the {posn} set:")
explanation.append(f'Extra items in the {posn} set:')
for item in diff:
explanation.append(highlighter(saferepr(item)))
return explanation
@ -486,52 +488,52 @@ def _compare_eq_dict(
right: Mapping[Any, Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
explanation: List[str] = []
) -> 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 += highlighter(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 += [
highlighter(saferepr({k: left[k]}))
+ " != "
+ highlighter(saferepr({k: right[k]}))
highlighter(saferepr({k: left[k]})) +
' != ' +
highlighter(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")
'Left contains %d more item%s:'
% (len_extra_left, '' if len_extra_left == 1 else 's'),
)
explanation.extend(
highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines()
highlighter(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")
'Right contains %d more item%s:'
% (len_extra_right, '' if len_extra_right == 1 else 's'),
)
explanation.extend(
highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines()
highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines(),
)
return explanation
def _compare_eq_cls(
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int
) -> List[str]:
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int,
) -> list[str]:
if not has_default_eq(left):
return []
if isdatacls(left):
@ -541,13 +543,13 @@ def _compare_eq_cls(
fields_to_check = [info.name for info in all_fields 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")]
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 = " "
indent = ' '
same = []
diff = []
for field in fields_to_check:
@ -558,46 +560,46 @@ def _compare_eq_cls(
explanation = []
if same or diff:
explanation += [""]
explanation += ['']
if same and verbose < 2:
explanation.append("Omitting %s identical items, use -vv to show" % len(same))
explanation.append('Omitting %s identical items, use -vv to show' % len(same))
elif same:
explanation += ["Matching attributes:"]
explanation += ['Matching attributes:']
explanation += highlighter(pprint.pformat(same)).splitlines()
if diff:
explanation += ["Differing attributes:"]
explanation += ['Differing attributes:']
explanation += highlighter(pprint.pformat(diff)).splitlines()
for field in diff:
field_left = getattr(left, field)
field_right = getattr(right, field)
explanation += [
"",
f"Drill down into differing attribute {field}:",
f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}",
'',
f'Drill down into differing attribute {field}:',
f'{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}',
]
explanation += [
indent + line
for line in _compare_eq_any(
field_left, field_right, highlighter, verbose
field_left, field_right, highlighter, verbose,
)
]
return explanation
def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]:
index = text.find(term)
head = text[:index]
tail = text[index + len(term) :]
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)]
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
@ -605,5 +607,5 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
def running_on_ci() -> bool:
"""Check if we're currently running on a CI system."""
env_vars = ["CI", "BUILD_NUMBER"]
env_vars = ['CI', 'BUILD_NUMBER']
return any(var in os.environ for var in env_vars)