Skip to content

Commit 1d244b3

Browse files
authored
Merge pull request #6899 from bluetech/rm-dupfile
Remove safe_text_dupfile() and simplify EncodedFile
2 parents d7f01a9 + 29e4cb5 commit 1d244b3

File tree

2 files changed

+28
-113
lines changed

2 files changed

+28
-113
lines changed

src/_pytest/capture.py

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
import sys
1010
from io import UnsupportedOperation
1111
from tempfile import TemporaryFile
12-
from typing import BinaryIO
1312
from typing import Generator
14-
from typing import Iterable
1513
from typing import Optional
1614

1715
import pytest
@@ -382,54 +380,21 @@ def disabled(self):
382380
yield
383381

384382

385-
def safe_text_dupfile(f, mode, default_encoding="UTF8"):
386-
""" return an open text file object that's a duplicate of f on the
387-
FD-level if possible.
388-
"""
389-
encoding = getattr(f, "encoding", None)
390-
try:
391-
fd = f.fileno()
392-
except Exception:
393-
if "b" not in getattr(f, "mode", "") and hasattr(f, "encoding"):
394-
# we seem to have a text stream, let's just use it
395-
return f
396-
else:
397-
newfd = os.dup(fd)
398-
if "b" not in mode:
399-
mode += "b"
400-
f = os.fdopen(newfd, mode, 0) # no buffering
401-
return EncodedFile(f, encoding or default_encoding)
402-
403-
404-
class EncodedFile:
405-
errors = "strict" # possibly needed by py3 code (issue555)
406-
407-
def __init__(self, buffer: BinaryIO, encoding: str) -> None:
408-
self.buffer = buffer
409-
self.encoding = encoding
410-
411-
def write(self, s: str) -> int:
412-
if not isinstance(s, str):
413-
raise TypeError(
414-
"write() argument must be str, not {}".format(type(s).__name__)
415-
)
416-
return self.buffer.write(s.encode(self.encoding, "replace"))
417-
418-
def writelines(self, lines: Iterable[str]) -> None:
419-
self.buffer.writelines(x.encode(self.encoding, "replace") for x in lines)
383+
class EncodedFile(io.TextIOWrapper):
384+
__slots__ = ()
420385

421386
@property
422387
def name(self) -> str:
423-
"""Ensure that file.name is a string."""
388+
# Ensure that file.name is a string. Workaround for a Python bug
389+
# fixed in >=3.7.4: https://bugs.python.org/issue36015
424390
return repr(self.buffer)
425391

426392
@property
427393
def mode(self) -> str:
394+
# TextIOWrapper doesn't expose a mode, but at least some of our
395+
# tests check it.
428396
return self.buffer.mode.replace("b", "")
429397

430-
def __getattr__(self, name):
431-
return getattr(object.__getattribute__(self, "buffer"), name)
432-
433398

434399
CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"])
435400

@@ -544,9 +509,12 @@ def __init__(self, targetfd, tmpfile=None):
544509
self.syscapture = SysCapture(targetfd)
545510
else:
546511
if tmpfile is None:
547-
f = TemporaryFile()
548-
with f:
549-
tmpfile = safe_text_dupfile(f, mode="wb+")
512+
tmpfile = EncodedFile(
513+
TemporaryFile(buffering=0),
514+
encoding="utf-8",
515+
errors="replace",
516+
write_through=True,
517+
)
550518
if targetfd in patchsysdict:
551519
self.syscapture = SysCapture(targetfd, tmpfile)
552520
else:
@@ -575,7 +543,7 @@ def _start(self):
575543

576544
def snap(self):
577545
self.tmpfile.seek(0)
578-
res = self.tmpfile.read()
546+
res = self.tmpfile.buffer.read()
579547
self.tmpfile.seek(0)
580548
self.tmpfile.truncate()
581549
return res
@@ -617,10 +585,10 @@ class FDCapture(FDCaptureBinary):
617585
EMPTY_BUFFER = str() # type: ignore
618586

619587
def snap(self):
620-
res = super().snap()
621-
enc = getattr(self.tmpfile, "encoding", None)
622-
if enc and isinstance(res, bytes):
623-
res = str(res, enc, "replace")
588+
self.tmpfile.seek(0)
589+
res = self.tmpfile.read()
590+
self.tmpfile.seek(0)
591+
self.tmpfile.truncate()
624592
return res
625593

626594

testing/test_capture.py

Lines changed: 11 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import contextlib
22
import io
33
import os
4-
import pickle
54
import subprocess
65
import sys
76
import textwrap
8-
from io import StringIO
97
from io import UnsupportedOperation
108
from typing import BinaryIO
119
from typing import Generator
12-
from typing import List
13-
from typing import TextIO
1410

1511
import pytest
1612
from _pytest import capture
@@ -827,48 +823,6 @@ def tmpfile(testdir) -> Generator[BinaryIO, None, None]:
827823
f.close()
828824

829825

830-
def test_dupfile(tmpfile) -> None:
831-
flist = [] # type: List[TextIO]
832-
for i in range(5):
833-
nf = capture.safe_text_dupfile(tmpfile, "wb")
834-
assert nf != tmpfile
835-
assert nf.fileno() != tmpfile.fileno()
836-
assert nf not in flist
837-
print(i, end="", file=nf)
838-
flist.append(nf)
839-
840-
fname_open = flist[0].name
841-
assert fname_open == repr(flist[0].buffer)
842-
843-
for i in range(5):
844-
f = flist[i]
845-
f.close()
846-
fname_closed = flist[0].name
847-
assert fname_closed == repr(flist[0].buffer)
848-
assert fname_closed != fname_open
849-
tmpfile.seek(0)
850-
s = tmpfile.read()
851-
assert "01234" in repr(s)
852-
tmpfile.close()
853-
assert fname_closed == repr(flist[0].buffer)
854-
855-
856-
def test_dupfile_on_bytesio():
857-
bio = io.BytesIO()
858-
f = capture.safe_text_dupfile(bio, "wb")
859-
f.write("hello")
860-
assert bio.getvalue() == b"hello"
861-
assert "BytesIO object" in f.name
862-
863-
864-
def test_dupfile_on_textio():
865-
sio = StringIO()
866-
f = capture.safe_text_dupfile(sio, "wb")
867-
f.write("hello")
868-
assert sio.getvalue() == "hello"
869-
assert not hasattr(f, "name")
870-
871-
872826
@contextlib.contextmanager
873827
def lsof_check():
874828
pid = os.getpid()
@@ -1307,8 +1261,8 @@ def test_error_attribute_issue555(testdir):
13071261
"""
13081262
import sys
13091263
def test_capattr():
1310-
assert sys.stdout.errors == "strict"
1311-
assert sys.stderr.errors == "strict"
1264+
assert sys.stdout.errors == "replace"
1265+
assert sys.stderr.errors == "replace"
13121266
"""
13131267
)
13141268
reprec = testdir.inline_run()
@@ -1383,15 +1337,6 @@ def test_spam_in_thread():
13831337
result.stdout.no_fnmatch_line("*IOError*")
13841338

13851339

1386-
def test_pickling_and_unpickling_encoded_file():
1387-
# See https://bitbucket.org/pytest-dev/pytest/pull-request/194
1388-
# pickle.loads() raises infinite recursion if
1389-
# EncodedFile.__getattr__ is not implemented properly
1390-
ef = capture.EncodedFile(None, None)
1391-
ef_as_str = pickle.dumps(ef)
1392-
pickle.loads(ef_as_str)
1393-
1394-
13951340
def test_global_capture_with_live_logging(testdir):
13961341
# Issue 3819
13971342
# capture should work with live cli logging
@@ -1497,8 +1442,9 @@ def test_fails():
14971442
result_with_capture = testdir.runpytest(str(p))
14981443

14991444
assert result_with_capture.ret == result_without_capture.ret
1500-
result_with_capture.stdout.fnmatch_lines(
1501-
["E * TypeError: write() argument must be str, not bytes"]
1445+
out = result_with_capture.stdout.str()
1446+
assert ("TypeError: write() argument must be str, not bytes" in out) or (
1447+
"TypeError: unicode argument expected, got 'bytes'" in out
15021448
)
15031449

15041450

@@ -1508,12 +1454,13 @@ def test_stderr_write_returns_len(capsys):
15081454

15091455

15101456
def test_encodedfile_writelines(tmpfile: BinaryIO) -> None:
1511-
ef = capture.EncodedFile(tmpfile, "utf-8")
1512-
with pytest.raises(AttributeError):
1513-
ef.writelines([b"line1", b"line2"]) # type: ignore[list-item] # noqa: F821
1514-
assert ef.writelines(["line1", "line2"]) is None # type: ignore[func-returns-value] # noqa: F821
1457+
ef = capture.EncodedFile(tmpfile, encoding="utf-8")
1458+
with pytest.raises(TypeError):
1459+
ef.writelines([b"line1", b"line2"])
1460+
assert ef.writelines(["line3", "line4"]) is None # type: ignore[func-returns-value] # noqa: F821
1461+
ef.flush()
15151462
tmpfile.seek(0)
1516-
assert tmpfile.read() == b"line1line2"
1463+
assert tmpfile.read() == b"line3line4"
15171464
tmpfile.close()
15181465
with pytest.raises(ValueError):
15191466
ef.read()

0 commit comments

Comments
 (0)