From 8a66f0a96d51b35f3e948ad6c5e140fb05e96c52 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 9 Aug 2020 21:20:07 +0300 Subject: [PATCH] capture: overcome a mypy limitation by making CaptureResult a regular class See the code comment for the rationale. --- src/_pytest/capture.py | 56 +++++++++++++++++++++++++++++++++++++++-- testing/test_capture.py | 31 +++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 99a587fcb..bf17e0eb8 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -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: diff --git a/testing/test_capture.py b/testing/test_capture.py index 15077a3e9..92a0a3e41 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -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+")