[refactor] Make base assertion comparisons return an iterator instead of a list of string#14546
[refactor] Make base assertion comparisons return an iterator instead of a list of string#14546Pierre-Sassoulas wants to merge 3 commits into
Conversation
Following Ronny's review comment on pytest-dev#13762, switch the set comparison helpers in ``_compare_set.py`` to return ``Iterator[str]`` so the composition is direct: ``_set_one_sided_diff`` ``yield``s, and the other helpers ``yield from`` it. This avoids the manual ``explanation = []; .append/.extend`` boilerplate. The "equal sets" branch of ``_compare_gt_set`` / ``_compare_lt_set`` used to peek at the diff for emptiness; replace that with a direct ``left == right`` check so the generator form stays idiomatic. ``SET_COMPARISON_FUNCTIONS`` and ``_compare_eq_set`` now return ``Iterable[str]`` / ``Iterator[str]``; the consumers in ``_compare_eq_any`` materialise with ``list(...)``.
Drop the ``list(...)`` wraps around each per-type comparator call in the match dispatch and ``yield from`` instead. ``_compare_eq_any`` becomes an ``Iterator[str]`` that yields nothing when no specialised explanation applies (replaces the previous ``list[str] | None`` sentinel). The two callers materialise: * ``util.assertrepr_compare`` does ``list(_compare_eq_any(...))`` before its empty/summary check. * ``_compare_eq_cls`` iterates the generator directly via ``for line in _compare_eq_any(...)``. No behavior change yet — this is the stepping stone for letting the truncator upstream consume the iterator lazily so huge diffs don't materialise just to be thrown away.
Turn ``assertrepr_compare`` into a generator. The first line yielded is the summary; subsequent lines are the explanation produced by ``_compare_eq_any``. Yields nothing when no specialised explanation applies — the consumer maps an empty iterator to ``None``. The ``pytest_assertrepr_compare`` hook impl in ``assertion/__init__`` materialises the iterator and returns ``list[str] | None`` so the public hook contract is unchanged. A follow-up commit replaces the ``list(...)`` call with a streaming truncator so an enormous diff doesn't have to be built in full just to be discarded. Behaviour change: previously, if an exception was raised while building the explanation (e.g. a faulty ``__repr__``), the partial output was discarded and only the failure notice was returned. The generator can't unyield lines it has already produced, so the new form preserves the partial output and appends the failure notice after it. This is arguably more useful — the reader sees what was being compared at the point the comparison failed. ``test_list_bad_repr`` is updated to assert that the failure notice appears at the end of the explanation instead of replacing the body.
bluetech
left a comment
There was a problem hiding this comment.
LGTM.
Regarding the behavior change on exception, I do think the previous behavior was a bit preferable, because I think showing a partial explanation and then the failed message can be confusing. But I think it's acceptable given the advantage.
| 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 (e.g. the |
There was a problem hiding this comment.
We should drop the "e.g." part since it doesn't hold as of this commit.
| """Yield specialised explanations for some operators/operands. | ||
|
|
||
| The first line yielded is always the summary (``left op right``); | ||
| subsequent lines are the per-line explanation. Yields nothing when no |
There was a problem hiding this comment.
"are the per-line explanation" -> "is the detailed explanation".
I don't think "per-line" is entirely accurate.
| summary = f"{left_repr} {op} {right_repr}" | ||
|
|
||
| explanation: list[str] | None = None | ||
| summary_yielded = False |
There was a problem hiding this comment.
Would move this above the for line in source, since it's not set or used in this block of code.
| else: | ||
| source = iter(()) | ||
|
|
||
| for line in source: |
There was a problem hiding this comment.
I suggest a comment like this: Only yield summary if there is a detailed explanation. Make sure there's a separating empty line after the summary.
Usually I avoid comments which just describe what the code does, but in this case it took me a bit of effort to understand, so I think a comment would be helpful.
The consensual refactor prior to #14523 to make the next review easier. No performance increase yet, because we call list on it in the end.