From 039d582b52795e1682ec98370439081483920a95 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 25 Jan 2020 10:58:26 +0100 Subject: [PATCH 1/4] Fix `EncodedFile.writelines` This is implemented by the underlying stream already, which additionally checks if the stream is not closed, and calls `write` per line. Ref/via: https://github.com/pytest-dev/pytest/pull/6558#issuecomment-578210807 --- changelog/6566.bugfix.rst | 1 + src/_pytest/capture.py | 6 +++--- testing/test_capture.py | 12 ++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 changelog/6566.bugfix.rst diff --git a/changelog/6566.bugfix.rst b/changelog/6566.bugfix.rst new file mode 100644 index 00000000000..4af976f2268 --- /dev/null +++ b/changelog/6566.bugfix.rst @@ -0,0 +1 @@ +Fix ``EncodedFile.writelines`` to call the underlying buffer's ``writelines`` method. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index c79bfeef024..e51fe2b6784 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -9,6 +9,7 @@ import sys from io import UnsupportedOperation from tempfile import TemporaryFile +from typing import List import pytest from _pytest.compat import CaptureIO @@ -426,9 +427,8 @@ def write(self, obj): ) return self.buffer.write(obj) - def writelines(self, linelist): - data = "".join(linelist) - self.write(data) + def writelines(self, linelist: List[str]) -> None: + self.buffer.writelines([x.encode(self.encoding, "replace") for x in linelist]) @property def name(self): diff --git a/testing/test_capture.py b/testing/test_capture.py index 7d459e91c75..ebe30703ba4 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1497,3 +1497,15 @@ def test_fails(): def test_stderr_write_returns_len(capsys): """Write on Encoded files, namely captured stderr, should return number of characters written.""" assert sys.stderr.write("Foo") == 3 + + +def test_encodedfile_writelines(tmpfile) -> None: + ef = capture.EncodedFile(tmpfile, "utf-8") + with pytest.raises(AttributeError): + ef.writelines([b"line1", b"line2"]) # type: ignore[list-item] # noqa: F821 + assert ef.writelines(["line1", "line2"]) is None # type: ignore[func-returns-value] # noqa: F821 + tmpfile.seek(0) + assert tmpfile.read() == b"line1line2" + tmpfile.close() + with pytest.raises(ValueError): + ef.read() From 3f8f3952107998f03a7fd1826427a9262a267f6c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 25 Jan 2020 18:11:38 +0100 Subject: [PATCH 2/4] typing: EncodedFile --- src/_pytest/capture.py | 15 +++++++-------- testing/test_capture.py | 3 ++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index e51fe2b6784..33d2243b3a7 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -9,6 +9,7 @@ import sys from io import UnsupportedOperation from tempfile import TemporaryFile +from typing import BinaryIO from typing import List import pytest @@ -414,29 +415,27 @@ def safe_text_dupfile(f, mode, default_encoding="UTF8"): class EncodedFile: errors = "strict" # possibly needed by py3 code (issue555) - def __init__(self, buffer, encoding): + def __init__(self, buffer: BinaryIO, encoding: str) -> None: self.buffer = buffer self.encoding = encoding - def write(self, obj): - if isinstance(obj, str): - obj = obj.encode(self.encoding, "replace") - else: + def write(self, obj: str) -> int: + if not isinstance(obj, str): raise TypeError( "write() argument must be str, not {}".format(type(obj).__name__) ) - return self.buffer.write(obj) + return self.buffer.write(obj.encode(self.encoding, "replace")) def writelines(self, linelist: List[str]) -> None: self.buffer.writelines([x.encode(self.encoding, "replace") for x in linelist]) @property - def name(self): + def name(self) -> str: """Ensure that file.name is a string.""" return repr(self.buffer) @property - def mode(self): + def mode(self) -> str: return self.buffer.mode.replace("b", "") def __getattr__(self, name): diff --git a/testing/test_capture.py b/testing/test_capture.py index ebe30703ba4..e6862f31316 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -7,6 +7,7 @@ import textwrap from io import StringIO from io import UnsupportedOperation +from typing import BinaryIO from typing import List from typing import TextIO @@ -1499,7 +1500,7 @@ def test_stderr_write_returns_len(capsys): assert sys.stderr.write("Foo") == 3 -def test_encodedfile_writelines(tmpfile) -> None: +def test_encodedfile_writelines(tmpfile: BinaryIO) -> None: ef = capture.EncodedFile(tmpfile, "utf-8") with pytest.raises(AttributeError): ef.writelines([b"line1", b"line2"]) # type: ignore[list-item] # noqa: F821 From d678d380cb407bea0b80e0246752b24e61267478 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 25 Jan 2020 19:21:19 +0100 Subject: [PATCH 3/4] typing: tests: tmpfile --- testing/test_capture.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index e6862f31316..9261c8441ed 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -8,6 +8,7 @@ from io import StringIO from io import UnsupportedOperation from typing import BinaryIO +from typing import Generator from typing import List from typing import TextIO @@ -832,7 +833,7 @@ def test_dontreadfrominput(): @pytest.fixture -def tmpfile(testdir): +def tmpfile(testdir) -> Generator[BinaryIO, None, None]: f = testdir.makepyfile("").open("wb+") yield f if not f.closed: From bf5c76359cf9dce7a94989d7f9edd4e22e6ffa3a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 26 Jan 2020 23:14:32 +0100 Subject: [PATCH 4/4] fixup! typing: tests: tmpfile --- src/_pytest/capture.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 33d2243b3a7..ccbeb0884e0 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -10,7 +10,7 @@ from io import UnsupportedOperation from tempfile import TemporaryFile from typing import BinaryIO -from typing import List +from typing import Iterable import pytest from _pytest.compat import CaptureIO @@ -419,15 +419,15 @@ def __init__(self, buffer: BinaryIO, encoding: str) -> None: self.buffer = buffer self.encoding = encoding - def write(self, obj: str) -> int: - if not isinstance(obj, str): + def write(self, s: str) -> int: + if not isinstance(s, str): raise TypeError( - "write() argument must be str, not {}".format(type(obj).__name__) + "write() argument must be str, not {}".format(type(s).__name__) ) - return self.buffer.write(obj.encode(self.encoding, "replace")) + return self.buffer.write(s.encode(self.encoding, "replace")) - def writelines(self, linelist: List[str]) -> None: - self.buffer.writelines([x.encode(self.encoding, "replace") for x in linelist]) + def writelines(self, lines: Iterable[str]) -> None: + self.buffer.writelines(x.encode(self.encoding, "replace") for x in lines) @property def name(self) -> str: