capture: overcome a mypy limitation by making CaptureResult a regular class

See the code comment for the rationale.
This commit is contained in:
Ran Benita 2020-08-09 21:20:07 +03:00
parent bee72e1925
commit 8a66f0a96d
2 changed files with 85 additions and 2 deletions

View File

@ -1,6 +1,6 @@
"""Per-test stdout/stderr capturing mechanism."""
import collections
import contextlib
import functools
import io
import os
import sys
@ -488,7 +488,59 @@ class FDCapture(FDCaptureBinary):
# MultiCapture
CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"])
# This class was a namedtuple, but due to mypy limitation[0] it could not be
# made generic, so was replaced by a regular class which tries to emulate the
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
# make it a namedtuple again.
# [0]: https://github.com/python/mypy/issues/685
@functools.total_ordering
class CaptureResult:
"""The result of :method:`CaptureFixture.readouterr`."""
# Can't use slots in Python<3.5.3 due to https://bugs.python.org/issue31272
if sys.version_info >= (3, 5, 3):
__slots__ = ("out", "err")
def __init__(self, out, err) -> None:
self.out = out
self.err = err
def __len__(self) -> int:
return 2
def __iter__(self):
return iter((self.out, self.err))
def __getitem__(self, item: int):
return tuple(self)[item]
def _replace(self, out=None, err=None) -> "CaptureResult":
return CaptureResult(
out=self.out if out is None else out, err=self.err if err is None else err
)
def count(self, value) -> int:
return tuple(self).count(value)
def index(self, value) -> int:
return tuple(self).index(value)
def __eq__(self, other: object) -> bool:
if not isinstance(other, (CaptureResult, tuple)):
return NotImplemented
return tuple(self) == tuple(other)
def __hash__(self) -> int:
return hash(tuple(self))
def __lt__(self, other: object) -> bool:
if not isinstance(other, (CaptureResult, tuple)):
return NotImplemented
return tuple(self) < tuple(other)
def __repr__(self) -> str:
return "CaptureResult(out={!r}, err={!r})".format(self.out, self.err)
class MultiCapture:

View File

@ -14,6 +14,7 @@ import pytest
from _pytest import capture
from _pytest.capture import _get_multicapture
from _pytest.capture import CaptureManager
from _pytest.capture import CaptureResult
from _pytest.capture import MultiCapture
from _pytest.config import ExitCode
@ -856,6 +857,36 @@ def test_dontreadfrominput():
f.close() # just for completeness
def test_captureresult() -> None:
cr = CaptureResult("out", "err")
assert len(cr) == 2
assert cr.out == "out"
assert cr.err == "err"
out, err = cr
assert out == "out"
assert err == "err"
assert cr[0] == "out"
assert cr[1] == "err"
assert cr == cr
assert cr == CaptureResult("out", "err")
assert cr != CaptureResult("wrong", "err")
assert cr == ("out", "err")
assert cr != ("out", "wrong")
assert hash(cr) == hash(CaptureResult("out", "err"))
assert hash(cr) == hash(("out", "err"))
assert hash(cr) != hash(("out", "wrong"))
assert cr < ("z",)
assert cr < ("z", "b")
assert cr < ("z", "b", "c")
assert cr.count("err") == 1
assert cr.count("wrong") == 0
assert cr.index("err") == 1
with pytest.raises(ValueError):
assert cr.index("wrong") == 0
assert next(iter(cr)) == "out"
assert cr._replace(err="replaced") == ("out", "replaced")
@pytest.fixture
def tmpfile(testdir) -> Generator[BinaryIO, None, None]:
f = testdir.makepyfile("").open("wb+")