typing: pytester: LineMatcher

This commit is contained in:
Daniel Hahler 2020-02-01 22:30:34 +01:00
parent b10ab0211c
commit 2681b0aed7
2 changed files with 45 additions and 37 deletions

View File

@ -413,8 +413,8 @@ class RunResult:
def __init__( def __init__(
self, self,
ret: Union[int, ExitCode], ret: Union[int, ExitCode],
outlines: Sequence[str], outlines: List[str],
errlines: Sequence[str], errlines: List[str],
duration: float, duration: float,
) -> None: ) -> None:
try: try:
@ -1327,48 +1327,42 @@ class LineMatcher:
The constructor takes a list of lines without their trailing newlines, i.e. The constructor takes a list of lines without their trailing newlines, i.e.
``text.splitlines()``. ``text.splitlines()``.
""" """
def __init__(self, lines): def __init__(self, lines: List[str]) -> None:
self.lines = lines self.lines = lines
self._log_output = [] self._log_output = [] # type: List[str]
def str(self): def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]:
"""Return the entire original text."""
return "\n".join(self.lines)
def _getlines(self, lines2):
if isinstance(lines2, str): if isinstance(lines2, str):
lines2 = Source(lines2) lines2 = Source(lines2)
if isinstance(lines2, Source): if isinstance(lines2, Source):
lines2 = lines2.strip().lines lines2 = lines2.strip().lines
return lines2 return lines2
def fnmatch_lines_random(self, lines2): def fnmatch_lines_random(self, lines2: Sequence[str]) -> None:
"""Check lines exist in the output using in any order. """Check lines exist in the output using in any order.
Lines are checked using ``fnmatch.fnmatch``. The argument is a list of Lines are checked using ``fnmatch.fnmatch``. The argument is a list of
lines which have to occur in the output, in any order. lines which have to occur in the output, in any order.
""" """
self._match_lines_random(lines2, fnmatch) self._match_lines_random(lines2, fnmatch)
def re_match_lines_random(self, lines2): def re_match_lines_random(self, lines2: Sequence[str]) -> None:
"""Check lines exist in the output using ``re.match``, in any order. """Check lines exist in the output using ``re.match``, in any order.
The argument is a list of lines which have to occur in the output, in The argument is a list of lines which have to occur in the output, in
any order. any order.
""" """
self._match_lines_random(lines2, lambda name, pat: re.match(pat, name)) self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name)))
def _match_lines_random(self, lines2, match_func): def _match_lines_random(
self, lines2: Sequence[str], match_func: Callable[[str, str], bool]
) -> None:
"""Check lines exist in the output. """Check lines exist in the output.
The argument is a list of lines which have to occur in the output, in The argument is a list of lines which have to occur in the output, in
any order. Each line can contain glob whildcards. any order. Each line can contain glob whildcards.
""" """
lines2 = self._getlines(lines2) lines2 = self._getlines(lines2)
for line in lines2: for line in lines2:
@ -1380,25 +1374,24 @@ class LineMatcher:
self._log("line %r not found in output" % line) self._log("line %r not found in output" % line)
raise ValueError(self._log_text) raise ValueError(self._log_text)
def get_lines_after(self, fnline): def get_lines_after(self, fnline: str) -> Sequence[str]:
"""Return all lines following the given line in the text. """Return all lines following the given line in the text.
The given line can contain glob wildcards. The given line can contain glob wildcards.
""" """
for i, line in enumerate(self.lines): for i, line in enumerate(self.lines):
if fnline == line or fnmatch(line, fnline): if fnline == line or fnmatch(line, fnline):
return self.lines[i + 1 :] return self.lines[i + 1 :]
raise ValueError("line %r not found in output" % fnline) raise ValueError("line %r not found in output" % fnline)
def _log(self, *args): def _log(self, *args) -> None:
self._log_output.append(" ".join(str(x) for x in args)) self._log_output.append(" ".join(str(x) for x in args))
@property @property
def _log_text(self): def _log_text(self) -> str:
return "\n".join(self._log_output) return "\n".join(self._log_output)
def fnmatch_lines(self, lines2): def fnmatch_lines(self, lines2: Sequence[str]) -> None:
"""Search captured text for matching lines using ``fnmatch.fnmatch``. """Search captured text for matching lines using ``fnmatch.fnmatch``.
The argument is a list of lines which have to match and can use glob The argument is a list of lines which have to match and can use glob
@ -1408,7 +1401,7 @@ class LineMatcher:
__tracebackhide__ = True __tracebackhide__ = True
self._match_lines(lines2, fnmatch, "fnmatch") self._match_lines(lines2, fnmatch, "fnmatch")
def re_match_lines(self, lines2): def re_match_lines(self, lines2: Sequence[str]) -> None:
"""Search captured text for matching lines using ``re.match``. """Search captured text for matching lines using ``re.match``.
The argument is a list of lines which have to match using ``re.match``. The argument is a list of lines which have to match using ``re.match``.
@ -1417,9 +1410,16 @@ class LineMatcher:
The matches and non-matches are also shown as part of the error message. The matches and non-matches are also shown as part of the error message.
""" """
__tracebackhide__ = True __tracebackhide__ = True
self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match") self._match_lines(
lines2, lambda name, pat: bool(re.match(pat, name)), "re.match"
)
def _match_lines(self, lines2, match_func, match_nickname): def _match_lines(
self,
lines2: Sequence[str],
match_func: Callable[[str, str], bool],
match_nickname: str,
) -> None:
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
:param list[str] lines2: list of string patterns to match. The actual :param list[str] lines2: list of string patterns to match. The actual
@ -1465,7 +1465,7 @@ class LineMatcher:
self._fail(msg) self._fail(msg)
self._log_output = [] self._log_output = []
def no_fnmatch_line(self, pat): def no_fnmatch_line(self, pat: str) -> None:
"""Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``. """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
:param str pat: the pattern to match lines. :param str pat: the pattern to match lines.
@ -1473,15 +1473,19 @@ class LineMatcher:
__tracebackhide__ = True __tracebackhide__ = True
self._no_match_line(pat, fnmatch, "fnmatch") self._no_match_line(pat, fnmatch, "fnmatch")
def no_re_match_line(self, pat): def no_re_match_line(self, pat: str) -> None:
"""Ensure captured lines do not match the given pattern, using ``re.match``. """Ensure captured lines do not match the given pattern, using ``re.match``.
:param str pat: the regular expression to match lines. :param str pat: the regular expression to match lines.
""" """
__tracebackhide__ = True __tracebackhide__ = True
self._no_match_line(pat, lambda name, pat: re.match(pat, name), "re.match") self._no_match_line(
pat, lambda name, pat: bool(re.match(pat, name)), "re.match"
)
def _no_match_line(self, pat, match_func, match_nickname): def _no_match_line(
self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str
) -> None:
"""Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch`` """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``
:param str pat: the pattern to match lines :param str pat: the pattern to match lines
@ -1502,8 +1506,12 @@ class LineMatcher:
self._log("{:>{width}}".format("and:", width=wnick), repr(line)) self._log("{:>{width}}".format("and:", width=wnick), repr(line))
self._log_output = [] self._log_output = []
def _fail(self, msg): def _fail(self, msg: str) -> None:
__tracebackhide__ = True __tracebackhide__ = True
log_text = self._log_text log_text = self._log_text
self._log_output = [] self._log_output = []
pytest.fail(log_text) pytest.fail(log_text)
def str(self) -> str:
"""Return the entire original text."""
return "\n".join(self.lines)

View File

@ -462,20 +462,20 @@ def test_linematcher_with_nonlist() -> None:
lm = LineMatcher([]) lm = LineMatcher([])
with pytest.raises(TypeError, match="invalid type for lines2: set"): with pytest.raises(TypeError, match="invalid type for lines2: set"):
lm.fnmatch_lines(set()) lm.fnmatch_lines(set()) # type: ignore[arg-type] # noqa: F821
with pytest.raises(TypeError, match="invalid type for lines2: dict"): with pytest.raises(TypeError, match="invalid type for lines2: dict"):
lm.fnmatch_lines({}) lm.fnmatch_lines({}) # type: ignore[arg-type] # noqa: F821
with pytest.raises(TypeError, match="invalid type for lines2: set"): with pytest.raises(TypeError, match="invalid type for lines2: set"):
lm.re_match_lines(set()) lm.re_match_lines(set()) # type: ignore[arg-type] # noqa: F821
with pytest.raises(TypeError, match="invalid type for lines2: dict"): with pytest.raises(TypeError, match="invalid type for lines2: dict"):
lm.re_match_lines({}) lm.re_match_lines({}) # type: ignore[arg-type] # noqa: F821
with pytest.raises(TypeError, match="invalid type for lines2: Source"): with pytest.raises(TypeError, match="invalid type for lines2: Source"):
lm.fnmatch_lines(Source()) lm.fnmatch_lines(Source()) # type: ignore[arg-type] # noqa: F821
lm.fnmatch_lines([]) lm.fnmatch_lines([])
lm.fnmatch_lines(()) lm.fnmatch_lines(())
lm.fnmatch_lines("") lm.fnmatch_lines("")
assert lm._getlines({}) == {} assert lm._getlines({}) == {} # type: ignore[arg-type,comparison-overlap] # noqa: F821
assert lm._getlines(set()) == set() assert lm._getlines(set()) == set() # type: ignore[arg-type,comparison-overlap] # noqa: F821
assert lm._getlines(Source()) == [] assert lm._getlines(Source()) == []
assert lm._getlines(Source("pass\npass")) == ["pass", "pass"] assert lm._getlines(Source("pass\npass")) == ["pass", "pass"]