From c874b4f40843a3236870e9b65adcbe8cd4360b68 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 13:12:39 +0100 Subject: [PATCH 1/7] capture: resume when stdin is being read --- src/_pytest/capture.py | 44 ++++++++++++++++++++++++++++++++++++----- testing/test_capture.py | 23 +++++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index c140757c4fd..400b32fb0b6 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -450,7 +450,7 @@ class MultiCapture(object): def __init__(self, out=True, err=True, in_=True, Capture=None): if in_: - self.in_ = Capture(0) + self.in_ = Capture(0, multicapture=self) if out: self.out = Capture(1) if err: @@ -527,7 +527,7 @@ class FDCaptureBinary(object): EMPTY_BUFFER = b"" - def __init__(self, targetfd, tmpfile=None): + def __init__(self, targetfd, tmpfile=None, multicapture=None): self.targetfd = targetfd try: self.targetfd_save = os.dup(self.targetfd) @@ -538,7 +538,7 @@ def __init__(self, targetfd, tmpfile=None): if targetfd == 0: assert not tmpfile, "cannot set tmpfile with stdin" tmpfile = open(os.devnull, "r") - self.syscapture = SysCapture(targetfd) + self.syscapture = SysCapture(targetfd, tmpfile=None, multicapture=multicapture) else: if tmpfile is None: f = TemporaryFile() @@ -617,13 +617,14 @@ class SysCapture(object): EMPTY_BUFFER = str() - def __init__(self, fd, tmpfile=None): + def __init__(self, fd, tmpfile=None, multicapture=None): name = patchsysdict[fd] self._old = getattr(sys, name) self.name = name if tmpfile is None: if name == "stdin": - tmpfile = DontReadFromInput() + tmpfile = SysStdinCapture(wrapped_capture=self, + multicapture=multicapture) else: tmpfile = CaptureIO() self.tmpfile = tmpfile @@ -663,6 +664,39 @@ def snap(self): return res +class SysStdinCapture(CaptureIO): + """Wrap CaptureIO to suspend on read.""" + def __init__(self, wrapped_capture, multicapture, *args): + self.wrapped_capture = wrapped_capture + self.multicapture = multicapture + assert isinstance(wrapped_capture, SysCapture) + assert isinstance(multicapture, MultiCapture), multicapture + + super(SysStdinCapture, self).__init__(*args) + + def __repr__(self): + return "" % ( + self.wrapped_capture, self.multicapture, + ) + + def _suspend_on_read(self, method, *args): + # TODO: would be nice to have this in the same order! + self.multicapture.pop_outerr_to_orig() + self.multicapture.suspend_capturing(in_=True) + + try: + syscapture = self.multicapture.in_.syscapture + except AttributeError: + syscapture = self.multicapture.in_ + + f = getattr(syscapture._old, method) + r = f(*args) + return r + + def read(self, *args): + return self._suspend_on_read("read", *args) + + class DontReadFromInput(six.Iterator): """Temporary stub class. Ideally when stdin is accessed, the capturing should be turned off, with possibly all data captured diff --git a/testing/test_capture.py b/testing/test_capture.py index fa0bad5fca0..f062e352614 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1565,3 +1565,26 @@ def test_fails(): ) else: assert result_with_capture.ret == 0 + + +def test_suspend_on_read_from_stdin(testdir): + p1 = testdir.makepyfile( + """ + import sys + + def test(): + print("before") + assert sys.stdin.read() == "asdf\\n" + assert sys.stdin.readline() == "asdf\\n" + print("after") + """ + ) + child = testdir.spawn_pytest("%s" % p1) + child.expect("before") + child.sendline("asdf") + child.sendeof() + child.sendline("asdf") + child.sendeof() + child.expect("after") + rest = child.read().decode("utf8") + assert "1 passed in" in rest From 3a58ed017a661f41485f01a59650d45af78cf899 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 14:17:19 +0100 Subject: [PATCH 2/7] add ini --- src/_pytest/capture.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 400b32fb0b6..7bae9e9f15d 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -40,6 +40,12 @@ def pytest_addoption(parser): dest="capture", help="shortcut for --capture=no.", ) + parser.addini( + "capture_suspend_on_stdin", + "Suspend capturing when stdin is being read from.", + type="bool", + default=False, + ) @pytest.hookimpl(hookwrapper=True) @@ -50,7 +56,8 @@ def pytest_load_initial_conftests(early_config, parser, args): _colorama_workaround() _readline_workaround() pluginmanager = early_config.pluginmanager - capman = CaptureManager(ns.capture) + suspend_on_stdin = early_config.getini("capture_suspend_on_stdin") + capman = CaptureManager(ns.capture, suspend_on_stdin) pluginmanager.register(capman, "capturemanager") # make sure that capturemanager is properly reset at final shutdown @@ -86,10 +93,11 @@ class CaptureManager(object): case special handling is needed to ensure the fixtures take precedence over the global capture. """ - def __init__(self, method): + def __init__(self, method, suspend_on_stdin=False): self._method = method self._global_capturing = None self._current_item = None + self._suspend_on_stdin = suspend_on_stdin def __repr__(self): return "" % ( @@ -538,7 +546,9 @@ def __init__(self, targetfd, tmpfile=None, multicapture=None): if targetfd == 0: assert not tmpfile, "cannot set tmpfile with stdin" tmpfile = open(os.devnull, "r") - self.syscapture = SysCapture(targetfd, tmpfile=None, multicapture=multicapture) + self.syscapture = SysCapture( + targetfd, tmpfile=None, multicapture=multicapture + ) else: if tmpfile is None: f = TemporaryFile() @@ -623,8 +633,9 @@ def __init__(self, fd, tmpfile=None, multicapture=None): self.name = name if tmpfile is None: if name == "stdin": - tmpfile = SysStdinCapture(wrapped_capture=self, - multicapture=multicapture) + tmpfile = SysStdinCapture( + wrapped_capture=self, multicapture=multicapture + ) else: tmpfile = CaptureIO() self.tmpfile = tmpfile @@ -666,6 +677,7 @@ def snap(self): class SysStdinCapture(CaptureIO): """Wrap CaptureIO to suspend on read.""" + def __init__(self, wrapped_capture, multicapture, *args): self.wrapped_capture = wrapped_capture self.multicapture = multicapture @@ -676,7 +688,8 @@ def __init__(self, wrapped_capture, multicapture, *args): def __repr__(self): return "" % ( - self.wrapped_capture, self.multicapture, + self.wrapped_capture, + self.multicapture, ) def _suspend_on_read(self, method, *args): From 1dd4cbc38f311a219a9b335986ff93b4b44f38cb Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 14:52:40 +0100 Subject: [PATCH 3/7] refactor, still messy --- src/_pytest/capture.py | 58 +++++++++++++++++++++++++++-------------- testing/test_capture.py | 2 +- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 7bae9e9f15d..00bcde85056 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -108,9 +108,32 @@ def __repr__(self): def _getcapture(self, method): if method == "fd": - return MultiCapture(out=True, err=True, Capture=FDCapture) + if self._suspend_on_stdin: + + def in_(fd, multicapture): + assert fd == 0, fd + syscapture = SysCapture( + fd, tmpfile=SysStdinCapture(multicapture=multicapture) + ) + return FDCapture(0, stdin_syscapture=syscapture) + + else: + in_ = True + return MultiCapture(out=True, err=True, in_=in_, Capture=FDCapture) + elif method == "sys": - return MultiCapture(out=True, err=True, Capture=SysCapture) + if self._suspend_on_stdin: + + def in_(fd, multicapture): + assert fd == 0, fd + return SysCapture( + fd, tmpfile=SysStdinCapture(multicapture=multicapture) + ) + + else: + in_ = True + return MultiCapture(out=True, err=True, in_=in_, Capture=SysCapture) + elif method == "no": return MultiCapture(out=False, err=False, in_=False) raise ValueError("unknown capturing method: %r" % method) # pragma: no cover @@ -458,7 +481,10 @@ class MultiCapture(object): def __init__(self, out=True, err=True, in_=True, Capture=None): if in_: - self.in_ = Capture(0, multicapture=self) + if in_ is True: + self.in_ = Capture(0) + else: + self.in_ = in_(0, multicapture=self) if out: self.out = Capture(1) if err: @@ -535,7 +561,7 @@ class FDCaptureBinary(object): EMPTY_BUFFER = b"" - def __init__(self, targetfd, tmpfile=None, multicapture=None): + def __init__(self, targetfd, tmpfile=None, stdin_syscapture=None): self.targetfd = targetfd try: self.targetfd_save = os.dup(self.targetfd) @@ -546,9 +572,10 @@ def __init__(self, targetfd, tmpfile=None, multicapture=None): if targetfd == 0: assert not tmpfile, "cannot set tmpfile with stdin" tmpfile = open(os.devnull, "r") - self.syscapture = SysCapture( - targetfd, tmpfile=None, multicapture=multicapture - ) + if stdin_syscapture is None: + self.syscapture = SysCapture(targetfd) + else: + self.syscapture = stdin_syscapture else: if tmpfile is None: f = TemporaryFile() @@ -627,15 +654,13 @@ class SysCapture(object): EMPTY_BUFFER = str() - def __init__(self, fd, tmpfile=None, multicapture=None): + def __init__(self, fd, tmpfile=None): name = patchsysdict[fd] self._old = getattr(sys, name) self.name = name if tmpfile is None: if name == "stdin": - tmpfile = SysStdinCapture( - wrapped_capture=self, multicapture=multicapture - ) + tmpfile = DontReadFromInput() else: tmpfile = CaptureIO() self.tmpfile = tmpfile @@ -678,22 +703,17 @@ def snap(self): class SysStdinCapture(CaptureIO): """Wrap CaptureIO to suspend on read.""" - def __init__(self, wrapped_capture, multicapture, *args): - self.wrapped_capture = wrapped_capture + def __init__(self, multicapture, *args): self.multicapture = multicapture - assert isinstance(wrapped_capture, SysCapture) assert isinstance(multicapture, MultiCapture), multicapture super(SysStdinCapture, self).__init__(*args) def __repr__(self): - return "" % ( - self.wrapped_capture, - self.multicapture, - ) + return "" % (self.multicapture,) def _suspend_on_read(self, method, *args): - # TODO: would be nice to have this in the same order! + # TODO: would be nice to have this in the original order! self.multicapture.pop_outerr_to_orig() self.multicapture.suspend_capturing(in_=True) diff --git a/testing/test_capture.py b/testing/test_capture.py index f062e352614..2df0a1924f1 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1579,7 +1579,7 @@ def test(): print("after") """ ) - child = testdir.spawn_pytest("%s" % p1) + child = testdir.spawn_pytest("-o capture_suspend_on_stdin=1 %s" % p1) child.expect("before") child.sendline("asdf") child.sendeof() From c3b992c6e89968d65691f2e8d74c8211ca421921 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 15:06:59 +0100 Subject: [PATCH 4/7] add readline --- src/_pytest/capture.py | 3 +++ testing/test_capture.py | 29 ++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 00bcde85056..4090c2a5b43 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -729,6 +729,9 @@ def _suspend_on_read(self, method, *args): def read(self, *args): return self._suspend_on_read("read", *args) + def readline(self, *args): + return self._suspend_on_read("readline", *args) + class DontReadFromInput(six.Iterator): """Temporary stub class. Ideally when stdin is accessed, the diff --git a/testing/test_capture.py b/testing/test_capture.py index 2df0a1924f1..df301a3fc03 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1573,18 +1573,29 @@ def test_suspend_on_read_from_stdin(testdir): import sys def test(): - print("before") - assert sys.stdin.read() == "asdf\\n" - assert sys.stdin.readline() == "asdf\\n" - print("after") + print("prompt_1") + assert input() == "input_1" + + print("prompt_2") + assert sys.stdin.read() == "input_2\\nsecond_line\\n" + + print("prompt_3") + assert sys.stdin.readline() == "input_3\\n" + print("after_" + "input: OK") """ ) child = testdir.spawn_pytest("-o capture_suspend_on_stdin=1 %s" % p1) - child.expect("before") - child.sendline("asdf") - child.sendeof() - child.sendline("asdf") + child.expect("prompt_1") + child.sendline("input_1") + + child.expect("prompt_2") + child.sendline("input_2") + child.sendline("second_line") child.sendeof() - child.expect("after") + + child.expect("prompt_3") + child.sendline("input_3") + + child.expect("after_input: OK") rest = child.read().decode("utf8") assert "1 passed in" in rest From ef90e4892d8b7b949456a3de1db0a3dd1f9bfd6f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 15:56:00 +0100 Subject: [PATCH 5/7] more tests, isatty --- src/_pytest/capture.py | 20 +++++++++++----- testing/test_capture.py | 52 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 4090c2a5b43..1e27589889b 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -712,26 +712,34 @@ def __init__(self, multicapture, *args): def __repr__(self): return "" % (self.multicapture,) + @property + def _syscapture(self): + try: + return self.multicapture.in_.syscapture + except AttributeError: + return self.multicapture.in_ + def _suspend_on_read(self, method, *args): # TODO: would be nice to have this in the original order! self.multicapture.pop_outerr_to_orig() self.multicapture.suspend_capturing(in_=True) - try: - syscapture = self.multicapture.in_.syscapture - except AttributeError: - syscapture = self.multicapture.in_ - - f = getattr(syscapture._old, method) + f = getattr(self._syscapture._old, method) r = f(*args) return r + def isatty(self): + return self._syscapture._old.isatty() + def read(self, *args): return self._suspend_on_read("read", *args) def readline(self, *args): return self._suspend_on_read("readline", *args) + def fileno(self): + return self._syscapture._old.fileno() + class DontReadFromInput(six.Iterator): """Temporary stub class. Ideally when stdin is accessed, the diff --git a/testing/test_capture.py b/testing/test_capture.py index df301a3fc03..855b2c29366 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1581,7 +1581,13 @@ def test(): print("prompt_3") assert sys.stdin.readline() == "input_3\\n" + + print("prompt_4") + assert sys.stdin.readlines() == ["input_4\\n", "second_line\\n"] + print("after_" + "input: OK") + + print("is_atty: %d" % sys.stdin.isatty()) """ ) child = testdir.spawn_pytest("-o capture_suspend_on_stdin=1 %s" % p1) @@ -1596,6 +1602,52 @@ def test(): child.expect("prompt_3") child.sendline("input_3") + child.expect("prompt_4") + child.sendline("input_4") + child.sendline("second_line") + child.sendeof() + child.expect("after_input: OK") rest = child.read().decode("utf8") + assert "is_atty: 1" in rest assert "1 passed in" in rest + + +@pytest.mark.parametrize("method", ("fd", "sys")) +def test_sysstdincapture(method, testdir): + p1 = testdir.makepyfile( + """ + import pytest + from _pytest.capture import CaptureManager, MultiCapture, SysStdinCapture + + def test_inner(): + method = {method!r} + + capman = CaptureManager(method, suspend_on_stdin=True) + multicapture = capman._getcapture(method) + in_ = multicapture.in_ + if method == "sys": + f = in_.tmpfile + else: + f = in_.syscapture.tmpfile + assert isinstance(f, SysStdinCapture) + + assert not f.isatty() + + assert f.read() == "" + assert f.readlines() == [] + iter_f = iter(f) + with pytest.raises(StopIteration): + next(iter_f) + + assert f.fileno() == 0 + f.close() + """.format( + method=method + ) + ) + result = testdir.runpytest_subprocess( + str(p1), "-s" # Pass through stdin, we're not testing resuming here. + ) + assert result.ret == 0 + assert "1 passed in" in result.stdout.str() From fac626813934c774f717e4ed01621dc2454b327b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 16:11:27 +0100 Subject: [PATCH 6/7] _suspend_on_read: headers --- src/_pytest/capture.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 1e27589889b..b3f666d8ca6 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -720,8 +720,22 @@ def _syscapture(self): return self.multicapture.in_ def _suspend_on_read(self, method, *args): - # TODO: would be nice to have this in the original order! - self.multicapture.pop_outerr_to_orig() + # TODO: would be nice to have this in the original order, not split + # by stdout/stderr. Let's have stderr first at least, given that + # prompts go to stdout usually. + + # self.multicapture.pop_outerr_to_orig() + self.multicapture.out.writeorg( + "=== Suspending capturing due to stdin being read ===\n" + ) + out, err = self.multicapture.readouterr() + if err: + # NOTE: this writes to stderr. Not sure about this. + self.multicapture.err.writeorg("=== stderr ===\n") + self.multicapture.err.writeorg(err) + if out: + self.multicapture.out.writeorg("=== stdout ===\n") + self.multicapture.out.writeorg(out) self.multicapture.suspend_capturing(in_=True) f = getattr(self._syscapture._old, method) From 99cfef47d76cc7049e0df7aef8442c0a4cd5abbe Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 20:06:38 +0100 Subject: [PATCH 7/7] py2: needs readlines, skip next-test --- src/_pytest/capture.py | 3 +++ testing/test_capture.py | 14 ++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index b3f666d8ca6..482483ff9d9 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -751,6 +751,9 @@ def read(self, *args): def readline(self, *args): return self._suspend_on_read("readline", *args) + def readlines(self, *args): # Required for py2. + return self._suspend_on_read("readlines", *args) + def fileno(self): return self._syscapture._old.fileno() diff --git a/testing/test_capture.py b/testing/test_capture.py index 855b2c29366..29a32e7ddf4 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1636,9 +1636,15 @@ def test_inner(): assert f.read() == "" assert f.readlines() == [] - iter_f = iter(f) - with pytest.raises(StopIteration): - next(iter_f) + + # XXX: fails on py2. + # > next(iter_f) + # E IOError: readline() should have returned an str object, not 'str' + import sys + if sys.version_info > (3,): + iter_f = iter(f) + with pytest.raises(StopIteration): + next(iter_f) assert f.fileno() == 0 f.close() @@ -1647,7 +1653,7 @@ def test_inner(): ) ) result = testdir.runpytest_subprocess( - str(p1), "-s" # Pass through stdin, we're not testing resuming here. + str(p1), "-s" # Pass through stdin, we're not testing suspending here. ) assert result.ret == 0 assert "1 passed in" in result.stdout.str()