diff --git a/changelog/7135.breaking.rst b/changelog/7135.breaking.rst new file mode 100644 index 0000000000..4d5df4e402 --- /dev/null +++ b/changelog/7135.breaking.rst @@ -0,0 +1,15 @@ +Pytest now uses its own ``TerminalWriter`` class instead of using the one from the ``py`` library. +Plugins generally access this class through ``TerminalReporter.writer``, ``TerminalReporter.write()`` +(and similar methods), or ``_pytest.config.create_terminal_writer()``. + +The following breaking changes were made: + +- Output (``write()`` method and others) no longer flush implicitly; the flushing behavior + of the underlying file is respected. To flush explicitly (for example, if you + want output to be shown before an end-of-line is printed), use ``write(flush=True)`` or + ``terminal_writer.flush()``. +- Explicit Windows console support was removed, delegated to the colorama library. +- Support for writing ``bytes`` was removed. +- The ``reline`` method and ``chars_on_current_line`` property were removed. +- The ``stringio`` and ``encoding`` arguments was removed. +- Support for passing a callable instead of a file was removed. diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index 28ddc7b78e..db001e918c 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -1,39 +1,8 @@ -from typing import List -from typing import Sequence +from .terminalwriter import get_terminal_width +from .terminalwriter import TerminalWriter -from py.io import TerminalWriter as BaseTerminalWriter # noqa: F401 - -class TerminalWriter(BaseTerminalWriter): - def _write_source(self, lines: List[str], indents: Sequence[str] = ()) -> None: - """Write lines of source code possibly highlighted. - - Keeping this private for now because the API is clunky. We should discuss how - to evolve the terminal writer so we can have more precise color support, for example - being able to write part of a line in one color and the rest in another, and so on. - """ - if indents and len(indents) != len(lines): - raise ValueError( - "indents size ({}) should have same size as lines ({})".format( - len(indents), len(lines) - ) - ) - if not indents: - indents = [""] * len(lines) - source = "\n".join(lines) - new_lines = self._highlight(source).splitlines() - for indent, new_line in zip(indents, new_lines): - self.line(indent + new_line) - - def _highlight(self, source): - """Highlight the given source code if we have markup support""" - if not self.hasmarkup: - return source - try: - from pygments.formatters.terminal import TerminalFormatter - from pygments.lexers.python import PythonLexer - from pygments import highlight - except ImportError: - return source - else: - return highlight(source, PythonLexer(), TerminalFormatter(bg="dark")) +__all__ = [ + "TerminalWriter", + "get_terminal_width", +] diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py new file mode 100644 index 0000000000..4f22f5a7ad --- /dev/null +++ b/src/_pytest/_io/terminalwriter.py @@ -0,0 +1,206 @@ +"""Helper functions for writing to terminals and files.""" +import os +import shutil +import sys +import unicodedata +from functools import lru_cache +from typing import Optional +from typing import Sequence +from typing import TextIO + + +# This code was initially copied from py 1.8.1, file _io/terminalwriter.py. + + +def get_terminal_width() -> int: + width, _ = shutil.get_terminal_size(fallback=(80, 24)) + + # The Windows get_terminal_size may be bogus, let's sanify a bit. + if width < 40: + width = 80 + + return width + + +@lru_cache(100) +def char_width(c: str) -> int: + # Fullwidth and Wide -> 2, all else (including Ambiguous) -> 1. + return 2 if unicodedata.east_asian_width(c) in ("F", "W") else 1 + + +def get_line_width(text: str) -> int: + text = unicodedata.normalize("NFC", text) + return sum(char_width(c) for c in text) + + +def should_do_markup(file: TextIO) -> bool: + if os.environ.get("PY_COLORS") == "1": + return True + if os.environ.get("PY_COLORS") == "0": + return False + return ( + hasattr(file, "isatty") + and file.isatty() + and os.environ.get("TERM") != "dumb" + and not (sys.platform.startswith("java") and os._name == "nt") + ) + + +class TerminalWriter: + _esctable = dict( + black=30, + red=31, + green=32, + yellow=33, + blue=34, + purple=35, + cyan=36, + white=37, + Black=40, + Red=41, + Green=42, + Yellow=43, + Blue=44, + Purple=45, + Cyan=46, + White=47, + bold=1, + light=2, + blink=5, + invert=7, + ) + + def __init__(self, file: Optional[TextIO] = None) -> None: + if file is None: + file = sys.stdout + if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32": + try: + import colorama + except ImportError: + pass + else: + file = colorama.AnsiToWin32(file).stream + assert file is not None + self._file = file + self.hasmarkup = should_do_markup(file) + self._current_line = "" + self._terminal_width = None # type: Optional[int] + + @property + def fullwidth(self) -> int: + if self._terminal_width is not None: + return self._terminal_width + return get_terminal_width() + + @fullwidth.setter + def fullwidth(self, value: int) -> None: + self._terminal_width = value + + @property + def width_of_current_line(self) -> int: + """Return an estimate of the width so far in the current line.""" + return get_line_width(self._current_line) + + def markup(self, text: str, **markup: bool) -> str: + for name in markup: + if name not in self._esctable: + raise ValueError("unknown markup: {!r}".format(name)) + if self.hasmarkup: + esc = [self._esctable[name] for name, on in markup.items() if on] + if esc: + text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" + return text + + def sep( + self, + sepchar: str, + title: Optional[str] = None, + fullwidth: Optional[int] = None, + **markup: bool + ) -> None: + if fullwidth is None: + fullwidth = self.fullwidth + # the goal is to have the line be as long as possible + # under the condition that len(line) <= fullwidth + if sys.platform == "win32": + # if we print in the last column on windows we are on a + # new line but there is no way to verify/neutralize this + # (we may not know the exact line width) + # so let's be defensive to avoid empty lines in the output + fullwidth -= 1 + if title is not None: + # we want 2 + 2*len(fill) + len(title) <= fullwidth + # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth + # 2*len(sepchar)*N <= fullwidth - len(title) - 2 + # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) + N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1) + fill = sepchar * N + line = "{} {} {}".format(fill, title, fill) + else: + # we want len(sepchar)*N <= fullwidth + # i.e. N <= fullwidth // len(sepchar) + line = sepchar * (fullwidth // len(sepchar)) + # in some situations there is room for an extra sepchar at the right, + # in particular if we consider that with a sepchar like "_ " the + # trailing space is not important at the end of the line + if len(line) + len(sepchar.rstrip()) <= fullwidth: + line += sepchar.rstrip() + + self.line(line, **markup) + + def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None: + if msg: + current_line = msg.rsplit("\n", 1)[-1] + if "\n" in msg: + self._current_line = current_line + else: + self._current_line += current_line + + msg = self.markup(msg, **markup) + + self._file.write(msg) + if flush: + self.flush() + + def line(self, s: str = "", **markup: bool) -> None: + self.write(s, **markup) + self.write("\n") + + def flush(self) -> None: + self._file.flush() + + def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None: + """Write lines of source code possibly highlighted. + + Keeping this private for now because the API is clunky. We should discuss how + to evolve the terminal writer so we can have more precise color support, for example + being able to write part of a line in one color and the rest in another, and so on. + """ + if indents and len(indents) != len(lines): + raise ValueError( + "indents size ({}) should have same size as lines ({})".format( + len(indents), len(lines) + ) + ) + if not indents: + indents = [""] * len(lines) + source = "\n".join(lines) + new_lines = self._highlight(source).splitlines() + for indent, new_line in zip(indents, new_lines): + self.line(indent + new_line) + + def _highlight(self, source: str) -> str: + """Highlight the given source code if we have markup support.""" + if not self.hasmarkup: + return source + try: + from pygments.formatters.terminal import TerminalFormatter + from pygments.lexers.python import PythonLexer + from pygments import highlight + except ImportError: + return source + else: + highlighted = highlight( + source, PythonLexer(), TerminalFormatter(bg="dark") + ) # type: str + return highlighted diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 140e04e972..940eaa6a79 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -15,6 +15,7 @@ import py +import _pytest._io from _pytest.compat import TYPE_CHECKING from _pytest.config.exceptions import UsageError @@ -466,7 +467,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): def __init__(self, *args: Any, **kwargs: Any) -> None: """Use more accurate terminal width via pylib.""" if "width" not in kwargs: - kwargs["width"] = py.io.get_terminal_width() + kwargs["width"] = _pytest._io.get_terminal_width() super().__init__(*args, **kwargs) def _format_action_invocation(self, action: argparse.Action) -> str: diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 3f4a7502d5..cbaa9a9f5f 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -1,5 +1,6 @@ """ submit failure or test session information to a pastebin service. """ import tempfile +from io import StringIO from typing import IO import pytest @@ -99,11 +100,10 @@ def pytest_terminal_summary(terminalreporter): msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc except AttributeError: msg = tr._getfailureheadline(rep) - tw = _pytest.config.create_terminal_writer( - terminalreporter.config, stringio=True - ) + file = StringIO() + tw = _pytest.config.create_terminal_writer(terminalreporter.config, file) rep.toterminal(tw) - s = tw.stringio.getvalue() + s = file.getvalue() assert len(s) pastebinurl = create_new_paste(s) tr.write_line("{} --> {}".format(msg, pastebinurl)) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2b9bf4f5bb..f472354efe 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1424,7 +1424,7 @@ def _showfixtures_main(config, session): def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: for line in doc.split("\n"): - tw.write(indent + line + "\n") + tw.line(indent + line) class Function(PyobjMixin, nodes.Item): diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 8459c1cb9e..178df6004f 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -82,10 +82,11 @@ def longreprtext(self): .. versionadded:: 3.0 """ - tw = TerminalWriter(stringio=True) + file = StringIO() + tw = TerminalWriter(file) tw.hasmarkup = False self.toterminal(tw) - exc = tw.stringio.getvalue() + exc = file.getvalue() return exc.strip() @property diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index f87ccb461e..76785ada7f 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -120,6 +120,7 @@ def show_test_item(item): used_fixtures = sorted(getattr(item, "fixturenames", [])) if used_fixtures: tw.write(" (fixtures used: {})".format(", ".join(used_fixtures))) + tw.flush() def pytest_runtest_setup(item): diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index aa5a95ff92..c9cc589ffe 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -68,6 +68,8 @@ def _show_fixture_action(fixturedef, msg): if hasattr(fixturedef, "cached_param"): tw.write("[{}]".format(fixturedef.cached_param)) + tw.flush() + if capman: capman.resume_global_capture() diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 52c04a49c3..39deaca559 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -343,7 +343,7 @@ def write_fspath_result(self, nodeid, res, **markup): fspath = self.startdir.bestrelpath(fspath) self._tw.line() self._tw.write(fspath + " ") - self._tw.write(res, **markup) + self._tw.write(res, flush=True, **markup) def write_ensure_prefix(self, prefix, extra="", **kwargs): if self.currentfspath != prefix: @@ -359,8 +359,11 @@ def ensure_newline(self): self._tw.line() self.currentfspath = None - def write(self, content, **markup): - self._tw.write(content, **markup) + def write(self, content: str, *, flush: bool = False, **markup: bool) -> None: + self._tw.write(content, flush=flush, **markup) + + def flush(self) -> None: + self._tw.flush() def write_line(self, line, **markup): if not isinstance(line, str): @@ -437,9 +440,11 @@ def pytest_runtest_logstart(self, nodeid, location): if self.showlongtestinfo: line = self._locationline(nodeid, *location) self.write_ensure_prefix(line, "") + self.flush() elif self.showfspath: fsid = nodeid.split("::")[0] self.write_fspath_result(fsid, "") + self.flush() def pytest_runtest_logreport(self, report: TestReport) -> None: self._tests_ran = True @@ -491,6 +496,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self._tw.write(word, **markup) self._tw.write(" " + line) self.currentfspath = -2 + self.flush() @property def _is_last_item(self): @@ -539,24 +545,20 @@ def _write_progress_information_filling_space(self): msg = self._get_progress_information_message() w = self._width_of_current_line fill = self._tw.fullwidth - w - 1 - self.write(msg.rjust(fill), **{color: True}) + self.write(msg.rjust(fill), flush=True, **{color: True}) @property def _width_of_current_line(self): """Return the width of current line, using the superior implementation of py-1.6 when available""" - try: - return self._tw.width_of_current_line - except AttributeError: - # py < 1.6.0 - return self._tw.chars_on_current_line + return self._tw.width_of_current_line def pytest_collection(self) -> None: if self.isatty: if self.config.option.verbose >= 0: - self.write("collecting ... ", bold=True) + self.write("collecting ... ", flush=True, bold=True) self._collect_report_last_write = time.time() elif self.config.option.verbose >= 1: - self.write("collecting ... ", bold=True) + self.write("collecting ... ", flush=True, bold=True) def pytest_collectreport(self, report: CollectReport) -> None: if report.failed: diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 412f11edc0..f0c7146c7e 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,3 +1,4 @@ +import io import operator import os import queue @@ -1037,10 +1038,11 @@ def f(): """ ) excinfo = pytest.raises(ValueError, mod.f) - tw = TerminalWriter(stringio=True) + file = io.StringIO() + tw = TerminalWriter(file=file) repr = excinfo.getrepr(**reproptions) repr.toterminal(tw) - assert tw.stringio.getvalue() + assert file.getvalue() def test_traceback_repr_style(self, importasmod, tw_mock): mod = importasmod( @@ -1255,11 +1257,12 @@ def g(): getattr(excinfo.value, attr).__traceback__ = None r = excinfo.getrepr() - tw = TerminalWriter(stringio=True) + file = io.StringIO() + tw = TerminalWriter(file=file) tw.hasmarkup = False r.toterminal(tw) - matcher = LineMatcher(tw.stringio.getvalue().splitlines()) + matcher = LineMatcher(file.getvalue().splitlines()) matcher.fnmatch_lines( [ "ValueError: invalid value", diff --git a/testing/code/test_terminal_writer.py b/testing/code/test_terminal_writer.py deleted file mode 100644 index 01da3c2350..0000000000 --- a/testing/code/test_terminal_writer.py +++ /dev/null @@ -1,28 +0,0 @@ -import re -from io import StringIO - -import pytest -from _pytest._io import TerminalWriter - - -@pytest.mark.parametrize( - "has_markup, expected", - [ - pytest.param( - True, "{kw}assert{hl-reset} {number}0{hl-reset}\n", id="with markup" - ), - pytest.param(False, "assert 0\n", id="no markup"), - ], -) -def test_code_highlight(has_markup, expected, color_mapping): - f = StringIO() - tw = TerminalWriter(f) - tw.hasmarkup = has_markup - tw._write_source(["assert 0"]) - assert f.getvalue().splitlines(keepends=True) == color_mapping.format([expected]) - - with pytest.raises( - ValueError, - match=re.escape("indents size (2) should have same size as lines (1)"), - ): - tw._write_source(["assert 0"], [" ", " "]) diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py new file mode 100644 index 0000000000..0e9cdb64d0 --- /dev/null +++ b/testing/io/test_terminalwriter.py @@ -0,0 +1,235 @@ +import io +import os +import re +import shutil +import sys +from typing import Generator +from unittest import mock + +import pytest +from _pytest._io import terminalwriter +from _pytest.monkeypatch import MonkeyPatch + + +# These tests were initially copied from py 1.8.1. + + +def test_terminal_width_COLUMNS(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setenv("COLUMNS", "42") + assert terminalwriter.get_terminal_width() == 42 + monkeypatch.delenv("COLUMNS", raising=False) + + +def test_terminalwriter_width_bogus(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr(shutil, "get_terminal_size", mock.Mock(return_value=(10, 10))) + monkeypatch.delenv("COLUMNS", raising=False) + tw = terminalwriter.TerminalWriter() + assert tw.fullwidth == 80 + + +def test_terminalwriter_computes_width(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr(terminalwriter, "get_terminal_width", lambda: 42) + tw = terminalwriter.TerminalWriter() + assert tw.fullwidth == 42 + + +def test_terminalwriter_dumb_term_no_markup(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr(os, "environ", {"TERM": "dumb", "PATH": ""}) + + class MyFile: + closed = False + + def isatty(self): + return True + + with monkeypatch.context() as m: + m.setattr(sys, "stdout", MyFile()) + assert sys.stdout.isatty() + tw = terminalwriter.TerminalWriter() + assert not tw.hasmarkup + + +win32 = int(sys.platform == "win32") + + +class TestTerminalWriter: + @pytest.fixture(params=["path", "stringio"]) + def tw( + self, request, tmpdir + ) -> Generator[terminalwriter.TerminalWriter, None, None]: + if request.param == "path": + p = tmpdir.join("tmpfile") + f = open(str(p), "w+", encoding="utf8") + tw = terminalwriter.TerminalWriter(f) + + def getlines(): + f.flush() + with open(str(p), encoding="utf8") as fp: + return fp.readlines() + + elif request.param == "stringio": + f = io.StringIO() + tw = terminalwriter.TerminalWriter(f) + + def getlines(): + f.seek(0) + return f.readlines() + + tw.getlines = getlines # type: ignore + tw.getvalue = lambda: "".join(getlines()) # type: ignore + + with f: + yield tw + + def test_line(self, tw) -> None: + tw.line("hello") + lines = tw.getlines() + assert len(lines) == 1 + assert lines[0] == "hello\n" + + def test_line_unicode(self, tw) -> None: + msg = "b\u00f6y" + tw.line(msg) + lines = tw.getlines() + assert lines[0] == msg + "\n" + + def test_sep_no_title(self, tw) -> None: + tw.sep("-", fullwidth=60) + lines = tw.getlines() + assert len(lines) == 1 + assert lines[0] == "-" * (60 - win32) + "\n" + + def test_sep_with_title(self, tw) -> None: + tw.sep("-", "hello", fullwidth=60) + lines = tw.getlines() + assert len(lines) == 1 + assert lines[0] == "-" * 26 + " hello " + "-" * (27 - win32) + "\n" + + def test_sep_longer_than_width(self, tw) -> None: + tw.sep("-", "a" * 10, fullwidth=5) + (line,) = tw.getlines() + # even though the string is wider than the line, still have a separator + assert line == "- aaaaaaaaaa -\n" + + @pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi") + @pytest.mark.parametrize("bold", (True, False)) + @pytest.mark.parametrize("color", ("red", "green")) + def test_markup(self, tw, bold: bool, color: str) -> None: + text = tw.markup("hello", **{color: True, "bold": bold}) + assert "hello" in text + + def test_markup_bad(self, tw) -> None: + with pytest.raises(ValueError): + tw.markup("x", wronkw=3) + with pytest.raises(ValueError): + tw.markup("x", wronkw=0) + + def test_line_write_markup(self, tw) -> None: + tw.hasmarkup = True + tw.line("x", bold=True) + tw.write("x\n", red=True) + lines = tw.getlines() + if sys.platform != "win32": + assert len(lines[0]) >= 2, lines + assert len(lines[1]) >= 2, lines + + def test_attr_fullwidth(self, tw) -> None: + tw.sep("-", "hello", fullwidth=70) + tw.fullwidth = 70 + tw.sep("-", "hello") + lines = tw.getlines() + assert len(lines[0]) == len(lines[1]) + + +@pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi") +def test_attr_hasmarkup() -> None: + file = io.StringIO() + tw = terminalwriter.TerminalWriter(file) + assert not tw.hasmarkup + tw.hasmarkup = True + tw.line("hello", bold=True) + s = file.getvalue() + assert len(s) > len("hello\n") + assert "\x1b[1m" in s + assert "\x1b[0m" in s + + +def test_should_do_markup_PY_COLORS_eq_1(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setitem(os.environ, "PY_COLORS", "1") + file = io.StringIO() + tw = terminalwriter.TerminalWriter(file) + assert tw.hasmarkup + tw.line("hello", bold=True) + s = file.getvalue() + assert len(s) > len("hello\n") + assert "\x1b[1m" in s + assert "\x1b[0m" in s + + +def test_should_do_markup_PY_COLORS_eq_0(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setitem(os.environ, "PY_COLORS", "0") + f = io.StringIO() + f.isatty = lambda: True # type: ignore + tw = terminalwriter.TerminalWriter(file=f) + assert not tw.hasmarkup + tw.line("hello", bold=True) + s = f.getvalue() + assert s == "hello\n" + + +class TestTerminalWriterLineWidth: + def test_init(self) -> None: + tw = terminalwriter.TerminalWriter() + assert tw.width_of_current_line == 0 + + def test_update(self) -> None: + tw = terminalwriter.TerminalWriter() + tw.write("hello world") + assert tw.width_of_current_line == 11 + + def test_update_with_newline(self) -> None: + tw = terminalwriter.TerminalWriter() + tw.write("hello\nworld") + assert tw.width_of_current_line == 5 + + def test_update_with_wide_text(self) -> None: + tw = terminalwriter.TerminalWriter() + tw.write("乇乂ㄒ尺卂 ㄒ卄丨匚匚") + assert tw.width_of_current_line == 21 # 5*2 + 1 + 5*2 + + def test_composed(self) -> None: + tw = terminalwriter.TerminalWriter() + text = "café food" + assert len(text) == 9 + tw.write(text) + assert tw.width_of_current_line == 9 + + def test_combining(self) -> None: + tw = terminalwriter.TerminalWriter() + text = "café food" + assert len(text) == 10 + tw.write(text) + assert tw.width_of_current_line == 9 + + +@pytest.mark.parametrize( + "has_markup, expected", + [ + pytest.param( + True, "{kw}assert{hl-reset} {number}0{hl-reset}\n", id="with markup" + ), + pytest.param(False, "assert 0\n", id="no markup"), + ], +) +def test_code_highlight(has_markup, expected, color_mapping): + f = io.StringIO() + tw = terminalwriter.TerminalWriter(f) + tw.hasmarkup = has_markup + tw._write_source(["assert 0"]) + assert f.getvalue().splitlines(keepends=True) == color_mapping.format([expected]) + + with pytest.raises( + ValueError, + match=re.escape("indents size (2) should have same size as lines (1)"), + ): + tw._write_source(["assert 0"], [" ", " "]) diff --git a/testing/test_config.py b/testing/test_config.py index 9035407b76..0c05c4fad7 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1253,7 +1253,7 @@ def test_help_formatter_uses_py_get_terminal_width(monkeypatch): formatter = DropShorterLongHelpFormatter("prog") assert formatter._width == 90 - monkeypatch.setattr("py.io.get_terminal_width", lambda: 160) + monkeypatch.setattr("_pytest._io.get_terminal_width", lambda: 160) formatter = DropShorterLongHelpFormatter("prog") assert formatter._width == 160