Skip to content

Commit f665070

Browse files
committed
capture: overcome a mypy limitation by making CaptureResult a regular class
See the code comment for the rationale.
1 parent bee72e1 commit f665070

File tree

2 files changed

+83
-2
lines changed

2 files changed

+83
-2
lines changed

src/_pytest/capture.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Per-test stdout/stderr capturing mechanism."""
2-
import collections
32
import contextlib
3+
import functools
44
import io
55
import os
66
import sys
@@ -488,7 +488,57 @@ def writeorg(self, data):
488488

489489
# MultiCapture
490490

491-
CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"])
491+
492+
# This class was a namedtuple, but due to mypy limitation[0] it could not be
493+
# made generic, so was replaced by a regular class which tries to emulate the
494+
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
495+
# make it a namedtuple again.
496+
# [0]: https://github.com/python/mypy/issues/685
497+
@functools.total_ordering
498+
class CaptureResult:
499+
"""The result of :method:`CaptureFixture.readouterr`."""
500+
501+
__slots__ = ("out", "err")
502+
503+
def __init__(self, out, err) -> None:
504+
self.out = out
505+
self.err = err
506+
507+
def __len__(self) -> int:
508+
return 2
509+
510+
def __iter__(self):
511+
return iter((self.out, self.err))
512+
513+
def __getitem__(self, item: int):
514+
return tuple(self)[item]
515+
516+
def _replace(self, out=None, err=None) -> "CaptureResult":
517+
return CaptureResult(
518+
out=self.out if out is None else out, err=self.err if err is None else err
519+
)
520+
521+
def count(self, value) -> int:
522+
return tuple(self).count(value)
523+
524+
def index(self, value) -> int:
525+
return tuple(self).index(value)
526+
527+
def __eq__(self, other: object) -> bool:
528+
if not isinstance(other, (CaptureResult, tuple)):
529+
return NotImplemented
530+
return tuple(self) == tuple(other)
531+
532+
def __hash__(self) -> int:
533+
return hash(tuple(self))
534+
535+
def __lt__(self, other: object) -> bool:
536+
if not isinstance(other, (CaptureResult, tuple)):
537+
return NotImplemented
538+
return tuple(self) < tuple(other)
539+
540+
def __repr__(self) -> str:
541+
return "CaptureResult(out={!r}, err={!r})".format(self.out, self.err)
492542

493543

494544
class MultiCapture:

testing/test_capture.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from _pytest import capture
1515
from _pytest.capture import _get_multicapture
1616
from _pytest.capture import CaptureManager
17+
from _pytest.capture import CaptureResult
1718
from _pytest.capture import MultiCapture
1819
from _pytest.config import ExitCode
1920

@@ -856,6 +857,36 @@ def test_dontreadfrominput():
856857
f.close() # just for completeness
857858

858859

860+
def test_captureresult() -> None:
861+
cr = CaptureResult("out", "err")
862+
assert len(cr) == 2
863+
assert cr.out == "out"
864+
assert cr.err == "err"
865+
out, err = cr
866+
assert out == "out"
867+
assert err == "err"
868+
assert cr[0] == "out"
869+
assert cr[1] == "err"
870+
assert cr == cr
871+
assert cr == CaptureResult("out", "err")
872+
assert cr != CaptureResult("wrong", "err")
873+
assert cr == ("out", "err")
874+
assert cr != ("out", "wrong")
875+
assert hash(cr) == hash(CaptureResult("out", "err"))
876+
assert hash(cr) == hash(("out", "err"))
877+
assert hash(cr) != hash(("out", "wrong"))
878+
assert cr < ("z",)
879+
assert cr < ("z", "b")
880+
assert cr < ("z", "b", "c")
881+
assert cr.count("err") == 1
882+
assert cr.count("wrong") == 0
883+
assert cr.index("err") == 1
884+
with pytest.raises(ValueError):
885+
assert cr.index("wrong") == 0
886+
assert next(iter(cr)) == "out"
887+
assert cr._replace(err="replaced") == ("out", "replaced")
888+
889+
859890
@pytest.fixture
860891
def tmpfile(testdir) -> Generator[BinaryIO, None, None]:
861892
f = testdir.makepyfile("").open("wb+")

0 commit comments

Comments
 (0)