Skip to content

Commit 8a66f0a

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 8a66f0a

File tree

2 files changed

+85
-2
lines changed

2 files changed

+85
-2
lines changed

src/_pytest/capture.py

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

493545

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