Skip to content

Commit 1fda861

Browse files
blueyedbluetech
authored andcommitted
Fix crash when printing while capsysbinary is active
Previously, writing to sys.stdout/stderr in text-mode (e.g. `print('foo')`) while a `capsysbinary` fixture is active, would crash with: /usr/lib/python3.7/contextlib.py:119: in __exit__ next(self.gen) E TypeError: write() argument must be str, not bytes This is due to some confusion in the types. The relevant functions are `snap()` and `writeorg()`. The function `snap()` returns what was captured, and the return type should be `bytes` for the binary captures and `str` for the regular ones. The `snap()` return value is eventually passed to `writeorg()` to be written to the original file, so it's input type should correspond to `snap()`. But this was incorrect for `SysCaptureBinary`, which handled it like `str`. To fix this, be explicit in the `snap()` and `writeorg()` implementations, also of the other Capture types. We can't add type annotations yet, because the current inheritance scheme breaks Liskov Substitution and mypy would complain. To be refactored later. Fixes: pytest-dev#6871 Co-authored-by: Ran Benita (some modifications & commit message)
1 parent 1d244b3 commit 1fda861

File tree

3 files changed

+41
-10
lines changed

3 files changed

+41
-10
lines changed

changelog/6871.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix crash with captured output when using the :fixture:`capsysbinary fixture <capsysbinary>`.

src/_pytest/capture.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -570,8 +570,6 @@ def resume(self):
570570

571571
def writeorg(self, data):
572572
""" write to original file descriptor. """
573-
if isinstance(data, str):
574-
data = data.encode("utf8") # XXX use encoding of original stream
575573
os.write(self.targetfd_save, data)
576574

577575

@@ -591,6 +589,11 @@ def snap(self):
591589
self.tmpfile.truncate()
592590
return res
593591

592+
def writeorg(self, data):
593+
""" write to original file descriptor. """
594+
data = data.encode("utf-8") # XXX use encoding of original stream
595+
os.write(self.targetfd_save, data)
596+
594597

595598
class SysCaptureBinary:
596599

@@ -642,8 +645,9 @@ def resume(self):
642645
self._state = "resumed"
643646

644647
def writeorg(self, data):
645-
self._old.write(data)
646648
self._old.flush()
649+
self._old.buffer.write(data)
650+
self._old.buffer.flush()
647651

648652

649653
class SysCapture(SysCaptureBinary):
@@ -655,6 +659,10 @@ def snap(self):
655659
self.tmpfile.truncate()
656660
return res
657661

662+
def writeorg(self, data):
663+
self._old.write(data)
664+
self._old.flush()
665+
658666

659667
class TeeSysCapture(SysCapture):
660668
def __init__(self, fd, tmpfile=None):

testing/test_capture.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -515,18 +515,40 @@ def test_hello(capfdbinary):
515515
reprec.assertoutcome(passed=1)
516516

517517
def test_capsysbinary(self, testdir):
518-
reprec = testdir.inline_runsource(
519-
"""\
518+
p1 = testdir.makepyfile(
519+
r"""
520520
def test_hello(capsysbinary):
521521
import sys
522-
# some likely un-decodable bytes
523-
sys.stdout.buffer.write(b'\\xfe\\x98\\x20')
522+
523+
sys.stdout.buffer.write(b'hello')
524+
525+
# Some likely un-decodable bytes.
526+
sys.stdout.buffer.write(b'\xfe\x98\x20')
527+
528+
sys.stdout.buffer.flush()
529+
530+
# Ensure writing in text mode still works and is captured.
531+
# https://github.com/pytest-dev/pytest/issues/6871
532+
print("world", flush=True)
533+
524534
out, err = capsysbinary.readouterr()
525-
assert out == b'\\xfe\\x98\\x20'
535+
assert out == b'hello\xfe\x98\x20world\n'
526536
assert err == b''
537+
538+
print("stdout after")
539+
print("stderr after", file=sys.stderr)
527540
"""
528541
)
529-
reprec.assertoutcome(passed=1)
542+
result = testdir.runpytest(str(p1), "-rA")
543+
result.stdout.fnmatch_lines(
544+
[
545+
"*- Captured stdout call -*",
546+
"stdout after",
547+
"*- Captured stderr call -*",
548+
"stderr after",
549+
"*= 1 passed in *",
550+
]
551+
)
530552

531553
def test_partial_setup_failure(self, testdir):
532554
p = testdir.makepyfile(
@@ -890,7 +912,7 @@ def test_writeorg(self, tmpfile):
890912
cap.start()
891913
tmpfile.write(data1)
892914
tmpfile.flush()
893-
cap.writeorg(data2)
915+
cap.writeorg(data2.decode("ascii"))
894916
scap = cap.snap()
895917
cap.done()
896918
assert scap == data1.decode("ascii")

0 commit comments

Comments
 (0)