[py-svn] py-trunk commit 3afeedc7eba5: fix issue96 - make capturing more resilient against KeyboardInterrupt
commits-noreply at bitbucket.org
commits-noreply at bitbucket.org
Mon May 17 18:58:21 CEST 2010
# HG changeset patch -- Bitbucket.org
# Project py-trunk
# URL http://bitbucket.org/hpk42/py-trunk/overview
# User holger krekel <holger at merlinux.eu>
# Date 1274115639 -7200
# Node ID 3afeedc7eba5e2da4b2940fd8e99ec99d1a88a14
# Parent d699a5f29cdd04524f985568303a6918aaa5df8c
fix issue96 - make capturing more resilient against KeyboardInterrupt
--- a/testing/plugin/test_pytest_capture.py
+++ b/testing/plugin/test_pytest_capture.py
@@ -39,6 +39,10 @@ class TestCaptureManager:
old = sys.stdout, sys.stderr, sys.stdin
try:
capman = CaptureManager()
+ # call suspend without resume or start
+ outerr = capman.suspendcapture()
+ outerr = capman.suspendcapture()
+ assert outerr == ("", "")
capman.resumecapture(method)
print ("hello")
out, err = capman.suspendcapture()
--- a/py/_io/capture.py
+++ b/py/_io/capture.py
@@ -29,21 +29,25 @@ except ImportError:
class FDCapture:
""" Capture IO to/from a given os-level filedescriptor. """
- def __init__(self, targetfd, tmpfile=None):
+ def __init__(self, targetfd, tmpfile=None, now=True):
""" save targetfd descriptor, and open a new
temporary file there. If no tmpfile is
specified a tempfile.Tempfile() will be opened
in text mode.
"""
self.targetfd = targetfd
+ self._patched = []
if tmpfile is None:
f = tempfile.TemporaryFile('wb+')
tmpfile = dupfile(f, encoding="UTF-8")
f.close()
self.tmpfile = tmpfile
- self._savefd = os.dup(targetfd)
- os.dup2(self.tmpfile.fileno(), targetfd)
- self._patched = []
+ if now:
+ self.start()
+
+ def start(self):
+ self._savefd = os.dup(self.targetfd)
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
def setasfile(self, name, module=sys):
""" patch <module>.<name> to self.tmpfile
@@ -62,10 +66,13 @@ class FDCapture:
def done(self):
""" unpatch and clean up, returns the self.tmpfile (file object)
"""
- os.dup2(self._savefd, self.targetfd)
+ try:
+ os.dup2(self._savefd, self.targetfd)
+ os.close(self._savefd)
+ self.tmpfile.seek(0)
+ except (AttributeError, ValueError, OSError):
+ pass
self.unsetfiles()
- os.close(self._savefd)
- self.tmpfile.seek(0)
return self.tmpfile
def writeorg(self, data):
@@ -146,89 +153,92 @@ class Capture(object):
def reset(self):
""" reset sys.stdout/stderr and return captured output as strings. """
- if hasattr(self, '_suspended'):
- outfile = self._kwargs['out']
- errfile = self._kwargs['err']
- del self._kwargs
- else:
- outfile, errfile = self.done()
+ outfile, errfile = self.done()
out, err = "", ""
- if outfile:
+ if outfile and not outfile.closed:
out = outfile.read()
outfile.close()
- if errfile and errfile != outfile:
+ if errfile and errfile != outfile and not errfile.closed:
err = errfile.read()
errfile.close()
return out, err
def suspend(self):
""" return current snapshot captures, memorize tempfiles. """
- assert not hasattr(self, '_suspended')
- self._suspended = True
outerr = self.readouterr()
outfile, errfile = self.done()
- self._kwargs['out'] = outfile
- self._kwargs['err'] = errfile
return outerr
- def resume(self):
- """ resume capturing with original temp files. """
- assert self._suspended
- self._initialize(**self._kwargs)
- del self._suspended
-
class StdCaptureFD(Capture):
""" This class allows to capture writes to FD1 and FD2
and may connect a NULL file to FD0 (and prevent
reads from sys.stdin)
"""
- def __init__(self, out=True, err=True,
- mixed=False, in_=True, patchsys=True):
- self._kwargs = locals().copy()
- del self._kwargs['self']
- self._initialize(**self._kwargs)
-
- def _initialize(self, out=True, err=True,
- mixed=False, in_=True, patchsys=True):
+ def __init__(self, out=True, err=True, mixed=False,
+ in_=True, patchsys=True, now=True):
+ self.in_ = in_
if in_:
self._oldin = (sys.stdin, os.dup(0))
- sys.stdin = DontReadFromInput()
- fd = os.open(devnullpath, os.O_RDONLY)
- os.dup2(fd, 0)
- os.close(fd)
- if out:
+ if out:
tmpfile = None
if hasattr(out, 'write'):
- tmpfile = out
- self.out = py.io.FDCapture(1, tmpfile=tmpfile)
- if patchsys:
- self.out.setasfile('stdout')
- if err:
- if mixed and out:
+ tmpfile = None
+ self.out = py.io.FDCapture(1, tmpfile=tmpfile, now=False)
+ self.out_tmpfile = tmpfile
+ if err:
+ if out and mixed:
tmpfile = self.out.tmpfile
elif hasattr(err, 'write'):
tmpfile = err
else:
tmpfile = None
- self.err = py.io.FDCapture(2, tmpfile=tmpfile)
- if patchsys:
- self.err.setasfile('stderr')
+ self.err = py.io.FDCapture(2, tmpfile=tmpfile, now=False)
+ self.err_tmpfile = tmpfile
+ self.patchsys = patchsys
+ if now:
+ self.startall()
+
+ def startall(self):
+ if self.in_:
+ sys.stdin = DontReadFromInput()
+ fd = os.open(devnullpath, os.O_RDONLY)
+ os.dup2(fd, 0)
+ os.close(fd)
+
+ out = getattr(self, 'out', None)
+ if out:
+ out.start()
+ if self.patchsys:
+ out.setasfile('stdout')
+ err = getattr(self, 'err', None)
+ if err:
+ err.start()
+ if self.patchsys:
+ err.setasfile('stderr')
+
+ def resume(self):
+ """ resume capturing with original temp files. """
+ #if hasattr(self, 'out'):
+ # self.out.restart()
+ #if hasattr(self, 'err'):
+ # self.err.restart()
+ self.startall()
def done(self):
""" return (outfile, errfile) and stop capturing. """
+ outfile = errfile = None
if hasattr(self, 'out'):
outfile = self.out.done()
- else:
- outfile = None
if hasattr(self, 'err'):
errfile = self.err.done()
- else:
- errfile = None
if hasattr(self, '_oldin'):
oldsys, oldfd = self._oldin
- os.dup2(oldfd, 0)
- os.close(oldfd)
+ try:
+ os.dup2(oldfd, 0)
+ os.close(oldfd)
+ except OSError:
+ pass
sys.stdin = oldsys
return outfile, errfile
@@ -252,69 +262,61 @@ class StdCapture(Capture):
modifies sys.stdout|stderr|stdin attributes and does not
touch underlying File Descriptors (use StdCaptureFD for that).
"""
- def __init__(self, out=True, err=True, in_=True, mixed=False):
- self._kwargs = locals().copy()
- del self._kwargs['self']
- self._initialize(**self._kwargs)
-
- def _initialize(self, out, err, in_, mixed):
- self._out = out
- self._err = err
- self._in = in_
- if out:
- self._oldout = sys.stdout
- if not hasattr(out, 'write'):
- out = TextIO()
- sys.stdout = self.out = out
- if err:
- self._olderr = sys.stderr
- if out and mixed:
- err = self.out
+ def __init__(self, out=True, err=True, in_=True, mixed=False, now=True):
+ self._oldout = sys.stdout
+ self._olderr = sys.stderr
+ self._oldin = sys.stdin
+ if out and not hasattr(out, 'file'):
+ out = TextIO()
+ self.out = out
+ if err:
+ if mixed:
+ err = out
elif not hasattr(err, 'write'):
err = TextIO()
- sys.stderr = self.err = err
- if in_:
- self._oldin = sys.stdin
- sys.stdin = self.newin = DontReadFromInput()
+ self.err = err
+ self.in_ = in_
+ if now:
+ self.startall()
+
+ def startall(self):
+ if self.out:
+ sys.stdout = self.out
+ if self.err:
+ sys.stderr = self.err
+ if self.in_:
+ sys.stdin = self.in_ = DontReadFromInput()
def done(self):
""" return (outfile, errfile) and stop capturing. """
- o,e = sys.stdout, sys.stderr
- if self._out:
- try:
- sys.stdout = self._oldout
- except AttributeError:
- raise IOError("stdout capturing already reset")
- del self._oldout
+ outfile = errfile = None
+ if self.out and not self.out.closed:
+ sys.stdout = self._oldout
outfile = self.out
outfile.seek(0)
- else:
- outfile = None
- if self._err:
- try:
- sys.stderr = self._olderr
- except AttributeError:
- raise IOError("stderr capturing already reset")
- del self._olderr
+ if self.err and not self.err.closed:
+ sys.stderr = self._olderr
errfile = self.err
errfile.seek(0)
- else:
- errfile = None
- if self._in:
+ if self.in_:
sys.stdin = self._oldin
return outfile, errfile
+ def resume(self):
+ """ resume capturing with original temp files. """
+ self.startall()
+
def readouterr(self):
""" return snapshot value of stdout/stderr capturings. """
out = err = ""
- if self._out:
- out = sys.stdout.getvalue()
- sys.stdout.truncate(0)
- sys.stdout.seek(0)
- if self._err:
- err = sys.stderr.getvalue()
- sys.stderr.truncate(0)
- sys.stderr.seek(0)
+ if self.out:
+ out = self.out.getvalue()
+ self.out.truncate(0)
+ self.out.seek(0)
+ if self.err:
+ err = self.err.getvalue()
+ self.err.truncate(0)
+ self.err.seek(0)
return out, err
class DontReadFromInput:
@@ -344,5 +346,3 @@ except AttributeError:
devnullpath = 'NUL'
else:
devnullpath = '/dev/null'
-
-
--- a/testing/io_/test_capture.py
+++ b/testing/io_/test_capture.py
@@ -96,6 +96,21 @@ def test_dupfile(tmpfile):
class TestFDCapture:
pytestmark = needsdup
+ def test_not_now(self, tmpfile):
+ fd = tmpfile.fileno()
+ cap = py.io.FDCapture(fd, now=False)
+ data = tobytes("hello")
+ os.write(fd, data)
+ f = cap.done()
+ s = f.read()
+ assert not s
+ cap = py.io.FDCapture(fd, now=False)
+ cap.start()
+ os.write(fd, data)
+ f = cap.done()
+ s = f.read()
+ assert s == "hello"
+
def test_stdout(self, tmpfile):
fd = tmpfile.fileno()
cap = py.io.FDCapture(fd)
@@ -185,8 +200,11 @@ class TestStdCapture:
def test_capturing_twice_error(self):
cap = self.getcapture()
print ("hello")
- cap.reset()
- py.test.raises(Exception, "cap.reset()")
+ out, err = cap.reset()
+ print ("world")
+ out2, err = cap.reset()
+ assert out == "hello\n"
+ assert not err
def test_capturing_modify_sysouterr_in_between(self):
oldout = sys.stdout
@@ -210,7 +228,6 @@ class TestStdCapture:
cap2 = self.getcapture()
print ("cap2")
out2, err2 = cap2.reset()
- py.test.raises(Exception, "cap2.reset()")
out1, err1 = cap1.reset()
assert out1 == "cap1\n"
assert out2 == "cap2\n"
@@ -265,6 +282,13 @@ class TestStdCapture:
assert out == "after\n"
assert not err
+class TestStdCaptureNotNow(TestStdCapture):
+ def getcapture(self, **kw):
+ kw['now'] = False
+ cap = py.io.StdCapture(**kw)
+ cap.startall()
+ return cap
+
class TestStdCaptureFD(TestStdCapture):
pytestmark = needsdup
@@ -296,6 +320,22 @@ class TestStdCaptureFD(TestStdCapture):
assert out.startswith("3")
assert err.startswith("4")
+class TestStdCaptureFDNotNow(TestStdCaptureFD):
+ pytestmark = needsdup
+
+ def getcapture(self, **kw):
+ kw['now'] = False
+ cap = py.io.StdCaptureFD(**kw)
+ cap.startall()
+ return cap
+
+def test_capture_not_started_but_reset():
+ capsys = py.io.StdCapture(now=False)
+ capsys.done()
+ capsys.done()
+ capsys.reset()
+ capsys.reset()
+
@needsdup
def test_capture_no_sys():
capsys = py.io.StdCapture()
--- a/py/_plugin/pytest_capture.py
+++ b/py/_plugin/pytest_capture.py
@@ -106,6 +106,14 @@ def addouterr(rep, outerr):
def pytest_configure(config):
config.pluginmanager.register(CaptureManager(), 'capturemanager')
+class NoCapture:
+ def startall(self):
+ pass
+ def resume(self):
+ pass
+ def suspend(self):
+ return "", ""
+
class CaptureManager:
def __init__(self):
self._method2capture = {}
@@ -118,15 +126,17 @@ class CaptureManager:
def _makestringio(self):
return py.io.TextIO()
- def _startcapture(self, method):
+ def _getcapture(self, method):
if method == "fd":
- return py.io.StdCaptureFD(
+ return py.io.StdCaptureFD(now=False,
out=self._maketempfile(), err=self._maketempfile()
)
elif method == "sys":
- return py.io.StdCapture(
+ return py.io.StdCapture(now=False,
out=self._makestringio(), err=self._makestringio()
)
+ elif method == "no":
+ return NoCapture()
else:
raise ValueError("unknown capturing method: %r" % method)
@@ -152,27 +162,25 @@ class CaptureManager:
if hasattr(self, '_capturing'):
raise ValueError("cannot resume, already capturing with %r" %
(self._capturing,))
- if method != "no":
- cap = self._method2capture.get(method)
- if cap is None:
- cap = self._startcapture(method)
- self._method2capture[method] = cap
- else:
- cap.resume()
+ cap = self._method2capture.get(method)
self._capturing = method
+ if cap is None:
+ self._method2capture[method] = cap = self._getcapture(method)
+ cap.startall()
+ else:
+ cap.resume()
def suspendcapture(self, item=None):
self.deactivate_funcargs()
if hasattr(self, '_capturing'):
method = self._capturing
- if method != "no":
- cap = self._method2capture[method]
+ cap = self._method2capture.get(method)
+ if cap is not None:
outerr = cap.suspend()
- else:
- outerr = "", ""
del self._capturing
if item:
- outerr = (item.outerr[0] + outerr[0], item.outerr[1] + outerr[1])
+ outerr = (item.outerr[0] + outerr[0],
+ item.outerr[1] + outerr[1])
return outerr
return "", ""
@@ -180,19 +188,17 @@ class CaptureManager:
if not hasattr(pyfuncitem, 'funcargs'):
return
assert not hasattr(self, '_capturing_funcargs')
- l = []
- for name, obj in pyfuncitem.funcargs.items():
- if name == 'capfd' and not hasattr(os, 'dup'):
- py.test.skip("capfd funcarg needs os.dup")
+ self._capturing_funcargs = capturing_funcargs = []
+ for name, capfuncarg in pyfuncitem.funcargs.items():
if name in ('capsys', 'capfd'):
- obj._start()
- l.append(obj)
- if l:
- self._capturing_funcargs = l
+ capturing_funcargs.append(capfuncarg)
+ capfuncarg._start()
def deactivate_funcargs(self):
- if hasattr(self, '_capturing_funcargs'):
- for capfuncarg in self._capturing_funcargs:
+ capturing_funcargs = getattr(self, '_capturing_funcargs', None)
+ if capturing_funcargs is not None:
+ while capturing_funcargs:
+ capfuncarg = capturing_funcargs.pop()
capfuncarg._finalize()
del self._capturing_funcargs
@@ -256,16 +262,19 @@ def pytest_funcarg__capfd(request):
platform does not have ``os.dup`` (e.g. Jython) tests using
this funcarg will automatically skip.
"""
+ if not hasattr(os, 'dup'):
+ py.test.skip("capfd funcarg needs os.dup")
return CaptureFuncarg(request, py.io.StdCaptureFD)
class CaptureFuncarg:
def __init__(self, request, captureclass):
self._cclass = captureclass
+ self.capture = self._cclass(now=False)
#request.addfinalizer(self._finalize)
def _start(self):
- self.capture = self._cclass()
+ self.capture.startall()
def _finalize(self):
if hasattr(self, 'capture'):
@@ -276,6 +285,4 @@ class CaptureFuncarg:
return self.capture.readouterr()
def close(self):
- self.capture.reset()
- del self.capture
-
+ self._finalize()
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,11 @@
Changes between 1.3.0 and 1.3.1
==================================================
+- fix issue96: make capturing more resilient against Control-C
+ interruptions (involved somewhat substantial refactoring
+ to the underlying capturing functionality to avoid race
+ conditions).
+
- make py.test.cmdline.main() return the exitstatus
instead of raising (which is still done by py.cmdline.pytest())
and make it so that py.test.cmdline.main() can be called
More information about the pytest-commit
mailing list