Skip to content

Commit 5256542

Browse files
committed
pytester.LineMatcher: add support for matching lines consecutively
1 parent 50f81db commit 5256542

File tree

3 files changed

+47
-4
lines changed

3 files changed

+47
-4
lines changed

changelog/6653.improvement.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for matching lines consecutively with :attr:`LineMatcher <_pytest.pytester.LineMatcher>`'s :func:`~_pytest.pytester.LineMatcher.fnmatch_lines` and :func:`~_pytest.pytester.LineMatcher.re_match_lines`.

src/_pytest/pytester.py

+26-4
Original file line numberDiff line numberDiff line change
@@ -1380,19 +1380,24 @@ def _log(self, *args) -> None:
13801380
def _log_text(self) -> str:
13811381
return "\n".join(self._log_output)
13821382

1383-
def fnmatch_lines(self, lines2: Sequence[str]) -> None:
1383+
def fnmatch_lines(
1384+
self, lines2: Sequence[str], *, consecutive: bool = False
1385+
) -> None:
13841386
"""Check lines exist in the output (using :func:`python:fnmatch.fnmatch`).
13851387
13861388
The argument is a list of lines which have to match and can use glob
13871389
wildcards. If they do not match a pytest.fail() is called. The
13881390
matches and non-matches are also shown as part of the error message.
13891391
13901392
:param lines2: string patterns to match.
1393+
:param consecutive: match lines consecutive?
13911394
"""
13921395
__tracebackhide__ = True
1393-
self._match_lines(lines2, fnmatch, "fnmatch")
1396+
self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
13941397

1395-
def re_match_lines(self, lines2: Sequence[str]) -> None:
1398+
def re_match_lines(
1399+
self, lines2: Sequence[str], *, consecutive: bool = False
1400+
) -> None:
13961401
"""Check lines exist in the output (using :func:`python:re.match`).
13971402
13981403
The argument is a list of lines which have to match using ``re.match``.
@@ -1401,17 +1406,23 @@ def re_match_lines(self, lines2: Sequence[str]) -> None:
14011406
The matches and non-matches are also shown as part of the error message.
14021407
14031408
:param lines2: string patterns to match.
1409+
:param consecutive: match lines consecutively?
14041410
"""
14051411
__tracebackhide__ = True
14061412
self._match_lines(
1407-
lines2, lambda name, pat: bool(re.match(pat, name)), "re.match"
1413+
lines2,
1414+
lambda name, pat: bool(re.match(pat, name)),
1415+
"re.match",
1416+
consecutive=consecutive,
14081417
)
14091418

14101419
def _match_lines(
14111420
self,
14121421
lines2: Sequence[str],
14131422
match_func: Callable[[str, str], bool],
14141423
match_nickname: str,
1424+
*,
1425+
consecutive: bool = False
14151426
) -> None:
14161427
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
14171428
@@ -1422,6 +1433,7 @@ def _match_lines(
14221433
pattern
14231434
:param str match_nickname: the nickname for the match function that
14241435
will be logged to stdout when a match occurs
1436+
:param consecutive: match lines consecutively?
14251437
"""
14261438
if not isinstance(lines2, collections.abc.Sequence):
14271439
raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__))
@@ -1431,20 +1443,30 @@ def _match_lines(
14311443
extralines = []
14321444
__tracebackhide__ = True
14331445
wnick = len(match_nickname) + 1
1446+
started = False
14341447
for line in lines2:
14351448
nomatchprinted = False
14361449
while lines1:
14371450
nextline = lines1.pop(0)
14381451
if line == nextline:
14391452
self._log("exact match:", repr(line))
1453+
started = True
14401454
break
14411455
elif match_func(nextline, line):
14421456
self._log("%s:" % match_nickname, repr(line))
14431457
self._log(
14441458
"{:>{width}}".format("with:", width=wnick), repr(nextline)
14451459
)
1460+
started = True
14461461
break
14471462
else:
1463+
if consecutive and started:
1464+
msg = "no consecutive match: {!r}".format(line)
1465+
self._log(msg)
1466+
self._log(
1467+
"{:>{width}}".format("with:", width=wnick), repr(nextline)
1468+
)
1469+
self._fail(msg)
14481470
if not nomatchprinted:
14491471
self._log(
14501472
"{:>{width}}".format("nomatch:", width=wnick), repr(line)

testing/test_pytester.py

+20
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,26 @@ def test_linematcher_match_failure() -> None:
508508
]
509509

510510

511+
def test_linematcher_consecutive():
512+
lm = LineMatcher(["1", "", "2"])
513+
with pytest.raises(pytest.fail.Exception) as excinfo:
514+
lm.fnmatch_lines(["1", "2"], consecutive=True)
515+
assert str(excinfo.value).splitlines() == [
516+
"exact match: '1'",
517+
"no consecutive match: '2'",
518+
" with: ''",
519+
]
520+
521+
lm.re_match_lines(["1", r"\d?", "2"], consecutive=True)
522+
with pytest.raises(pytest.fail.Exception) as excinfo:
523+
lm.re_match_lines(["1", r"\d", "2"], consecutive=True)
524+
assert str(excinfo.value).splitlines() == [
525+
"exact match: '1'",
526+
r"no consecutive match: '\\d'",
527+
" with: ''",
528+
]
529+
530+
511531
@pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"])
512532
def test_linematcher_no_matching(function) -> None:
513533
if function == "no_fnmatch_line":

0 commit comments

Comments
 (0)