Skip to content

Commit 00b4eb2

Browse files
committed
Remove safe_text_dupfile() and simplify EncodedFile
I tried to understand what the `safe_text_dupfile()` function and `EncodedFile` class do. Outside tests, `EncodedFile` is only used by `safe_text_dupfile`, and `safe_text_dupfile` is only used by `FDCaptureBinary.__init__()`. I then started to eliminate always-true conditions based on the single call site, and in the end nothing was left except of a couple workarounds that are still needed.
1 parent e1b3a68 commit 00b4eb2

File tree

2 files changed

+25
-114
lines changed

2 files changed

+25
-114
lines changed

src/_pytest/capture.py

Lines changed: 14 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
@@ -390,54 +388,21 @@ def disabled(self):
390388
yield
391389

392390

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

429394
@property
430395
def name(self) -> str:
431-
"""Ensure that file.name is a string."""
396+
# Ensure that file.name is a string. Workaround for a Python bug
397+
# fixed in >=3.7.4: https://bugs.python.org/issue36015
432398
return repr(self.buffer)
433399

434400
@property
435401
def mode(self) -> str:
402+
# TextIOWrapper doesn't expose a mode, but at least some of our
403+
# tests check it.
436404
return self.buffer.mode.replace("b", "")
437405

438-
def __getattr__(self, name):
439-
return getattr(object.__getattribute__(self, "buffer"), name)
440-
441406

442407
CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"])
443408

@@ -552,9 +517,9 @@ def __init__(self, targetfd, tmpfile=None):
552517
self.syscapture = SysCapture(targetfd)
553518
else:
554519
if tmpfile is None:
555-
f = TemporaryFile()
556-
with f:
557-
tmpfile = safe_text_dupfile(f, mode="wb+")
520+
tmpfile = EncodedFile(
521+
TemporaryFile(buffering=0), encoding="utf-8", errors="replace"
522+
)
558523
if targetfd in patchsysdict:
559524
self.syscapture = SysCapture(targetfd, tmpfile)
560525
else:
@@ -583,7 +548,7 @@ def _start(self):
583548

584549
def snap(self):
585550
self.tmpfile.seek(0)
586-
res = self.tmpfile.read()
551+
res = self.tmpfile.buffer.read()
587552
self.tmpfile.seek(0)
588553
self.tmpfile.truncate()
589554
return res
@@ -625,10 +590,10 @@ class FDCapture(FDCaptureBinary):
625590
EMPTY_BUFFER = str() # type: ignore
626591

627592
def snap(self):
628-
res = super().snap()
629-
enc = getattr(self.tmpfile, "encoding", None)
630-
if enc and isinstance(res, bytes):
631-
res = str(res, enc, "replace")
593+
self.tmpfile.seek(0)
594+
res = self.tmpfile.read()
595+
self.tmpfile.seek(0)
596+
self.tmpfile.truncate()
632597
return res
633598

634599

testing/test_capture.py

Lines changed: 11 additions & 65 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
@@ -865,49 +861,6 @@ def tmpfile(testdir) -> Generator[BinaryIO, None, None]:
865861
f.close()
866862

867863

868-
@needsosdup
869-
def test_dupfile(tmpfile) -> None:
870-
flist = [] # type: List[TextIO]
871-
for i in range(5):
872-
nf = capture.safe_text_dupfile(tmpfile, "wb")
873-
assert nf != tmpfile
874-
assert nf.fileno() != tmpfile.fileno()
875-
assert nf not in flist
876-
print(i, end="", file=nf)
877-
flist.append(nf)
878-
879-
fname_open = flist[0].name
880-
assert fname_open == repr(flist[0].buffer)
881-
882-
for i in range(5):
883-
f = flist[i]
884-
f.close()
885-
fname_closed = flist[0].name
886-
assert fname_closed == repr(flist[0].buffer)
887-
assert fname_closed != fname_open
888-
tmpfile.seek(0)
889-
s = tmpfile.read()
890-
assert "01234" in repr(s)
891-
tmpfile.close()
892-
assert fname_closed == repr(flist[0].buffer)
893-
894-
895-
def test_dupfile_on_bytesio():
896-
bio = io.BytesIO()
897-
f = capture.safe_text_dupfile(bio, "wb")
898-
f.write("hello")
899-
assert bio.getvalue() == b"hello"
900-
assert "BytesIO object" in f.name
901-
902-
903-
def test_dupfile_on_textio():
904-
sio = StringIO()
905-
f = capture.safe_text_dupfile(sio, "wb")
906-
f.write("hello")
907-
assert sio.getvalue() == "hello"
908-
assert not hasattr(f, "name")
909-
910-
911864
@contextlib.contextmanager
912865
def lsof_check():
913866
pid = os.getpid()
@@ -1355,8 +1308,8 @@ def test_error_attribute_issue555(testdir):
13551308
"""
13561309
import sys
13571310
def test_capattr():
1358-
assert sys.stdout.errors == "strict"
1359-
assert sys.stderr.errors == "strict"
1311+
assert sys.stdout.errors == "replace"
1312+
assert sys.stderr.errors == "replace"
13601313
"""
13611314
)
13621315
reprec = testdir.inline_run()
@@ -1431,15 +1384,6 @@ def test_spam_in_thread():
14311384
result.stdout.no_fnmatch_line("*IOError*")
14321385

14331386

1434-
def test_pickling_and_unpickling_encoded_file():
1435-
# See https://bitbucket.org/pytest-dev/pytest/pull-request/194
1436-
# pickle.loads() raises infinite recursion if
1437-
# EncodedFile.__getattr__ is not implemented properly
1438-
ef = capture.EncodedFile(None, None)
1439-
ef_as_str = pickle.dumps(ef)
1440-
pickle.loads(ef_as_str)
1441-
1442-
14431387
def test_global_capture_with_live_logging(testdir):
14441388
# Issue 3819
14451389
# capture should work with live cli logging
@@ -1545,8 +1489,9 @@ def test_fails():
15451489
result_with_capture = testdir.runpytest(str(p))
15461490

15471491
assert result_with_capture.ret == result_without_capture.ret
1548-
result_with_capture.stdout.fnmatch_lines(
1549-
["E * TypeError: write() argument must be str, not bytes"]
1492+
out = result_with_capture.stdout.str()
1493+
assert ("TypeError: write() argument must be str, not bytes" in out) or (
1494+
"TypeError: unicode argument expected, got 'bytes'" in out
15501495
)
15511496

15521497

@@ -1556,12 +1501,13 @@ def test_stderr_write_returns_len(capsys):
15561501

15571502

15581503
def test_encodedfile_writelines(tmpfile: BinaryIO) -> None:
1559-
ef = capture.EncodedFile(tmpfile, "utf-8")
1560-
with pytest.raises(AttributeError):
1561-
ef.writelines([b"line1", b"line2"]) # type: ignore[list-item] # noqa: F821
1562-
assert ef.writelines(["line1", "line2"]) is None # type: ignore[func-returns-value] # noqa: F821
1504+
ef = capture.EncodedFile(tmpfile, encoding="utf-8")
1505+
with pytest.raises(TypeError):
1506+
ef.writelines([b"line1", b"line2"])
1507+
assert ef.writelines(["line3", "line4"]) is None # type: ignore[func-returns-value] # noqa: F821
1508+
ef.flush()
15631509
tmpfile.seek(0)
1564-
assert tmpfile.read() == b"line1line2"
1510+
assert tmpfile.read() == b"line3line4"
15651511
tmpfile.close()
15661512
with pytest.raises(ValueError):
15671513
ef.read()

0 commit comments

Comments
 (0)