Skip to content

Commit 054a0ae

Browse files
Merge branch 'main' into Improvement-catch-duplicate-values-when-determining-param-indices-in-metafunc-parametrize
2 parents 6e482f5 + 52db918 commit 054a0ae

File tree

12 files changed

+207
-124
lines changed

12 files changed

+207
-124
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ Michael Goerz
266266
Michael Krebs
267267
Michael Seifert
268268
Michal Wajszczuk
269+
Michał Górny
269270
Michał Zięba
270271
Mickey Pashov
271272
Mihai Capotă

changelog/11520.improvement.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
Improved very verbose diff output to color it as a diff instead of only red.
22

33
Improved the error reporting to better separate each section.
4+
5+
Improved the error reporting to syntax-highlight Python code when Pygments is available.

changelog/11712.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed handling ``NO_COLOR`` and ``FORCE_COLOR`` to ignore an empty value.

doc/en/reference/plugin_list.rst

Lines changed: 51 additions & 43 deletions
Large diffs are not rendered by default.

doc/en/reference/reference.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,13 +1146,13 @@ When set to ``0``, pytest will not use color.
11461146

11471147
.. envvar:: NO_COLOR
11481148

1149-
When set (regardless of value), pytest will not use color in terminal output.
1149+
When set to a non-empty string (regardless of value), pytest will not use color in terminal output.
11501150
``PY_COLORS`` takes precedence over ``NO_COLOR``, which takes precedence over ``FORCE_COLOR``.
11511151
See `no-color.org <https://no-color.org/>`__ for other libraries supporting this community standard.
11521152

11531153
.. envvar:: FORCE_COLOR
11541154

1155-
When set (regardless of value), pytest will use color in terminal output.
1155+
When set to a non-empty string (regardless of value), pytest will use color in terminal output.
11561156
``PY_COLORS`` and ``NO_COLOR`` take precedence over ``FORCE_COLOR``.
11571157

11581158
Exceptions

src/_pytest/_io/terminalwriter.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ def should_do_markup(file: TextIO) -> bool:
2929
return True
3030
if os.environ.get("PY_COLORS") == "0":
3131
return False
32-
if "NO_COLOR" in os.environ:
32+
if os.environ.get("NO_COLOR"):
3333
return False
34-
if "FORCE_COLOR" in os.environ:
34+
if os.environ.get("FORCE_COLOR"):
3535
return True
3636
return (
3737
hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb"
@@ -223,7 +223,15 @@ def _highlight(
223223
style=os.getenv("PYTEST_THEME"),
224224
),
225225
)
226-
return highlighted
226+
# pygments terminal formatter may add a newline when there wasn't one.
227+
# We don't want this, remove.
228+
if highlighted[-1] == "\n" and source[-1] != "\n":
229+
highlighted = highlighted[:-1]
230+
231+
# Some lexers will not set the initial color explicitly
232+
# which may lead to the previous color being propagated to the
233+
# start of the expression, so reset first.
234+
return "\x1b[0m" + highlighted
227235
except pygments.util.ClassNotFound:
228236
raise UsageError(
229237
"PYTEST_THEME environment variable had an invalid value: '{}'. "

src/_pytest/assertion/util.py

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,12 @@ def assertrepr_compare(
192192
right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
193193

194194
summary = f"{left_repr} {op} {right_repr}"
195+
highlighter = config.get_terminal_writer()._highlight
195196

196197
explanation = None
197198
try:
198199
if op == "==":
199-
writer = config.get_terminal_writer()
200-
explanation = _compare_eq_any(left, right, writer._highlight, verbose)
200+
explanation = _compare_eq_any(left, right, highlighter, verbose)
201201
elif op == "not in":
202202
if istext(left) and istext(right):
203203
explanation = _notin_text(left, right, verbose)
@@ -206,16 +206,16 @@ def assertrepr_compare(
206206
explanation = ["Both sets are equal"]
207207
elif op == ">=":
208208
if isset(left) and isset(right):
209-
explanation = _compare_gte_set(left, right, verbose)
209+
explanation = _compare_gte_set(left, right, highlighter, verbose)
210210
elif op == "<=":
211211
if isset(left) and isset(right):
212-
explanation = _compare_lte_set(left, right, verbose)
212+
explanation = _compare_lte_set(left, right, highlighter, verbose)
213213
elif op == ">":
214214
if isset(left) and isset(right):
215-
explanation = _compare_gt_set(left, right, verbose)
215+
explanation = _compare_gt_set(left, right, highlighter, verbose)
216216
elif op == "<":
217217
if isset(left) and isset(right):
218-
explanation = _compare_lt_set(left, right, verbose)
218+
explanation = _compare_lt_set(left, right, highlighter, verbose)
219219

220220
except outcomes.Exit:
221221
raise
@@ -259,11 +259,11 @@ def _compare_eq_any(
259259
# used in older code bases before dataclasses/attrs were available.
260260
explanation = _compare_eq_cls(left, right, highlighter, verbose)
261261
elif issequence(left) and issequence(right):
262-
explanation = _compare_eq_sequence(left, right, verbose)
262+
explanation = _compare_eq_sequence(left, right, highlighter, verbose)
263263
elif isset(left) and isset(right):
264-
explanation = _compare_eq_set(left, right, verbose)
264+
explanation = _compare_eq_set(left, right, highlighter, verbose)
265265
elif isdict(left) and isdict(right):
266-
explanation = _compare_eq_dict(left, right, verbose)
266+
explanation = _compare_eq_dict(left, right, highlighter, verbose)
267267

268268
if isiterable(left) and isiterable(right):
269269
expl = _compare_eq_iterable(left, right, highlighter, verbose)
@@ -350,7 +350,10 @@ def _compare_eq_iterable(
350350

351351

352352
def _compare_eq_sequence(
353-
left: Sequence[Any], right: Sequence[Any], verbose: int = 0
353+
left: Sequence[Any],
354+
right: Sequence[Any],
355+
highlighter: _HighlightFunc,
356+
verbose: int = 0,
354357
) -> List[str]:
355358
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
356359
explanation: List[str] = []
@@ -373,7 +376,10 @@ def _compare_eq_sequence(
373376
left_value = left[i]
374377
right_value = right[i]
375378

376-
explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"]
379+
explanation.append(
380+
f"At index {i} diff:"
381+
f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}"
382+
)
377383
break
378384

379385
if comparing_bytes:
@@ -393,68 +399,91 @@ def _compare_eq_sequence(
393399
extra = saferepr(right[len_left])
394400

395401
if len_diff == 1:
396-
explanation += [f"{dir_with_more} contains one more item: {extra}"]
402+
explanation += [
403+
f"{dir_with_more} contains one more item: {highlighter(extra)}"
404+
]
397405
else:
398406
explanation += [
399407
"%s contains %d more items, first extra item: %s"
400-
% (dir_with_more, len_diff, extra)
408+
% (dir_with_more, len_diff, highlighter(extra))
401409
]
402410
return explanation
403411

404412

405413
def _compare_eq_set(
406-
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
414+
left: AbstractSet[Any],
415+
right: AbstractSet[Any],
416+
highlighter: _HighlightFunc,
417+
verbose: int = 0,
407418
) -> List[str]:
408419
explanation = []
409-
explanation.extend(_set_one_sided_diff("left", left, right))
410-
explanation.extend(_set_one_sided_diff("right", right, left))
420+
explanation.extend(_set_one_sided_diff("left", left, right, highlighter))
421+
explanation.extend(_set_one_sided_diff("right", right, left, highlighter))
411422
return explanation
412423

413424

414425
def _compare_gt_set(
415-
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
426+
left: AbstractSet[Any],
427+
right: AbstractSet[Any],
428+
highlighter: _HighlightFunc,
429+
verbose: int = 0,
416430
) -> List[str]:
417-
explanation = _compare_gte_set(left, right, verbose)
431+
explanation = _compare_gte_set(left, right, highlighter)
418432
if not explanation:
419433
return ["Both sets are equal"]
420434
return explanation
421435

422436

423437
def _compare_lt_set(
424-
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
438+
left: AbstractSet[Any],
439+
right: AbstractSet[Any],
440+
highlighter: _HighlightFunc,
441+
verbose: int = 0,
425442
) -> List[str]:
426-
explanation = _compare_lte_set(left, right, verbose)
443+
explanation = _compare_lte_set(left, right, highlighter)
427444
if not explanation:
428445
return ["Both sets are equal"]
429446
return explanation
430447

431448

432449
def _compare_gte_set(
433-
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
450+
left: AbstractSet[Any],
451+
right: AbstractSet[Any],
452+
highlighter: _HighlightFunc,
453+
verbose: int = 0,
434454
) -> List[str]:
435-
return _set_one_sided_diff("right", right, left)
455+
return _set_one_sided_diff("right", right, left, highlighter)
436456

437457

438458
def _compare_lte_set(
439-
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
459+
left: AbstractSet[Any],
460+
right: AbstractSet[Any],
461+
highlighter: _HighlightFunc,
462+
verbose: int = 0,
440463
) -> List[str]:
441-
return _set_one_sided_diff("left", left, right)
464+
return _set_one_sided_diff("left", left, right, highlighter)
442465

443466

444467
def _set_one_sided_diff(
445-
posn: str, set1: AbstractSet[Any], set2: AbstractSet[Any]
468+
posn: str,
469+
set1: AbstractSet[Any],
470+
set2: AbstractSet[Any],
471+
highlighter: _HighlightFunc,
446472
) -> List[str]:
447473
explanation = []
448474
diff = set1 - set2
449475
if diff:
450476
explanation.append(f"Extra items in the {posn} set:")
451477
for item in diff:
452-
explanation.append(saferepr(item))
478+
explanation.append(highlighter(saferepr(item)))
453479
return explanation
454480

455481

456482
def _compare_eq_dict(
457-
left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0
483+
left: Mapping[Any, Any],
484+
right: Mapping[Any, Any],
485+
highlighter: _HighlightFunc,
486+
verbose: int = 0,
458487
) -> List[str]:
459488
explanation: List[str] = []
460489
set_left = set(left)
@@ -465,12 +494,16 @@ def _compare_eq_dict(
465494
explanation += ["Omitting %s identical items, use -vv to show" % len(same)]
466495
elif same:
467496
explanation += ["Common items:"]
468-
explanation += pprint.pformat(same).splitlines()
497+
explanation += highlighter(pprint.pformat(same)).splitlines()
469498
diff = {k for k in common if left[k] != right[k]}
470499
if diff:
471500
explanation += ["Differing items:"]
472501
for k in diff:
473-
explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})]
502+
explanation += [
503+
highlighter(saferepr({k: left[k]}))
504+
+ " != "
505+
+ highlighter(saferepr({k: right[k]}))
506+
]
474507
extra_left = set_left - set_right
475508
len_extra_left = len(extra_left)
476509
if len_extra_left:
@@ -479,7 +512,7 @@ def _compare_eq_dict(
479512
% (len_extra_left, "" if len_extra_left == 1 else "s")
480513
)
481514
explanation.extend(
482-
pprint.pformat({k: left[k] for k in extra_left}).splitlines()
515+
highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines()
483516
)
484517
extra_right = set_right - set_left
485518
len_extra_right = len(extra_right)
@@ -489,7 +522,7 @@ def _compare_eq_dict(
489522
% (len_extra_right, "" if len_extra_right == 1 else "s")
490523
)
491524
explanation.extend(
492-
pprint.pformat({k: right[k] for k in extra_right}).splitlines()
525+
highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines()
493526
)
494527
return explanation
495528

@@ -528,17 +561,17 @@ def _compare_eq_cls(
528561
explanation.append("Omitting %s identical items, use -vv to show" % len(same))
529562
elif same:
530563
explanation += ["Matching attributes:"]
531-
explanation += pprint.pformat(same).splitlines()
564+
explanation += highlighter(pprint.pformat(same)).splitlines()
532565
if diff:
533566
explanation += ["Differing attributes:"]
534-
explanation += pprint.pformat(diff).splitlines()
567+
explanation += highlighter(pprint.pformat(diff)).splitlines()
535568
for field in diff:
536569
field_left = getattr(left, field)
537570
field_right = getattr(right, field)
538571
explanation += [
539572
"",
540-
"Drill down into differing attribute %s:" % field,
541-
("%s%s: %r != %r") % (indent, field, field_left, field_right),
573+
f"Drill down into differing attribute {field}:",
574+
f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}",
542575
]
543576
explanation += [
544577
indent + line

src/_pytest/logging.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@
1111
from io import StringIO
1212
from logging import LogRecord
1313
from pathlib import Path
14+
from types import TracebackType
1415
from typing import AbstractSet
1516
from typing import Dict
1617
from typing import final
1718
from typing import Generator
19+
from typing import Generic
1820
from typing import List
1921
from typing import Literal
2022
from typing import Mapping
2123
from typing import Optional
2224
from typing import Tuple
25+
from typing import Type
2326
from typing import TYPE_CHECKING
2427
from typing import TypeVar
2528
from typing import Union
@@ -62,7 +65,7 @@ class DatetimeFormatter(logging.Formatter):
6265
:func:`time.strftime` in case of microseconds in format string.
6366
"""
6467

65-
def formatTime(self, record: LogRecord, datefmt=None) -> str:
68+
def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str:
6669
if datefmt and "%f" in datefmt:
6770
ct = self.converter(record.created)
6871
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
@@ -331,7 +334,7 @@ def add_option_ini(option, dest, default=None, type=None, **kwargs):
331334

332335

333336
# Not using @contextmanager for performance reasons.
334-
class catching_logs:
337+
class catching_logs(Generic[_HandlerType]):
335338
"""Context manager that prepares the whole logging machinery properly."""
336339

337340
__slots__ = ("handler", "level", "orig_level")
@@ -340,7 +343,7 @@ def __init__(self, handler: _HandlerType, level: Optional[int] = None) -> None:
340343
self.handler = handler
341344
self.level = level
342345

343-
def __enter__(self):
346+
def __enter__(self) -> _HandlerType:
344347
root_logger = logging.getLogger()
345348
if self.level is not None:
346349
self.handler.setLevel(self.level)
@@ -350,7 +353,12 @@ def __enter__(self):
350353
root_logger.setLevel(min(self.orig_level, self.level))
351354
return self.handler
352355

353-
def __exit__(self, type, value, traceback):
356+
def __exit__(
357+
self,
358+
exc_type: Optional[Type[BaseException]],
359+
exc_val: Optional[BaseException],
360+
exc_tb: Optional[TracebackType],
361+
) -> None:
354362
root_logger = logging.getLogger()
355363
if self.level is not None:
356364
root_logger.setLevel(self.orig_level)
@@ -421,7 +429,7 @@ def handler(self) -> LogCaptureHandler:
421429
return self._item.stash[caplog_handler_key]
422430

423431
def get_records(
424-
self, when: "Literal['setup', 'call', 'teardown']"
432+
self, when: Literal["setup", "call", "teardown"]
425433
) -> List[logging.LogRecord]:
426434
"""Get the logging records for one of the possible test phases.
427435
@@ -742,7 +750,7 @@ def set_log_path(self, fname: str) -> None:
742750
if old_stream:
743751
old_stream.close()
744752

745-
def _log_cli_enabled(self):
753+
def _log_cli_enabled(self) -> bool:
746754
"""Return whether live logging is enabled."""
747755
enabled = self._config.getoption(
748756
"--log-cli-level"

src/_pytest/runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def result(self) -> TResult:
317317
@classmethod
318318
def from_call(
319319
cls,
320-
func: "Callable[[], TResult]",
320+
func: Callable[[], TResult],
321321
when: Literal["collect", "setup", "call", "teardown"],
322322
reraise: Optional[
323323
Union[Type[BaseException], Tuple[Type[BaseException], ...]]

0 commit comments

Comments
 (0)