Skip to content

Commit e313538

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 e313538

File tree

2 files changed

+84
-2
lines changed

2 files changed

+84
-2
lines changed

src/_pytest/capture.py

Lines changed: 53 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,58 @@ 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+
# Can't use slots due to bug in Python<=3.5.2: https://bugs.python.org/issue31272
502+
# __slots__ = ("out", "err")
503+
504+
def __init__(self, out, err) -> None:
505+
self.out = out
506+
self.err = err
507+
508+
def __len__(self) -> int:
509+
return 2
510+
511+
def __iter__(self):
512+
return iter((self.out, self.err))
513+
514+
def __getitem__(self, item: int):
515+
return tuple(self)[item]
516+
517+
def _replace(self, out=None, err=None) -> "CaptureResult":
518+
return CaptureResult(
519+
out=self.out if out is None else out, err=self.err if err is None else err
520+
)
521+
522+
def count(self, value) -> int:
523+
return tuple(self).count(value)
524+
525+
def index(self, value) -> int:
526+
return tuple(self).index(value)
527+
528+
def __eq__(self, other: object) -> bool:
529+
if not isinstance(other, (CaptureResult, tuple)):
530+
return NotImplemented
531+
return tuple(self) == tuple(other)
532+
533+
def __hash__(self) -> int:
534+
return hash(tuple(self))
535+
536+
def __lt__(self, other: object) -> bool:
537+
if not isinstance(other, (CaptureResult, tuple)):
538+
return NotImplemented
539+
return tuple(self) < tuple(other)
540+
541+
def __repr__(self) -> str:
542+
return "CaptureResult(out={!r}, err={!r})".format(self.out, self.err)
492543

493544

494545
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)