diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index f52505c0291..928d72a638b 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -1,10 +1,71 @@ +import os +import sys from typing import List from typing import Sequence import py.io +from _pytest.compat import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import TextIO + + +def use_markup(file: "TextIO") -> bool: + # Backward compatibility with pylib: handle PY_COLORS={0,1} only. + val = os.getenv("PY_COLORS") + if val in ("0", "1"): + return val == "1" + + # TODO + # # PYTEST_FORCE_COLOR: handled as boolean. + # val = os.getenv("PYTEST_FORCE_COLOR") + # if val is not None: + # from _pytest.config import _strtobool + # + # return _strtobool(val) + + # NO_COLOR: disable markup with any value (https://no-color.org/). + if "NO_COLOR" in os.environ: + return False + + # TODO + # if _running_on_ci(): + # return True + + return file.isatty() if hasattr(file, "isatty") else False + class TerminalWriter(py.io.TerminalWriter): # noqa: pygrep-py + def __init__(self, file: "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._lastlen = 0 + self._chars_on_current_line = 0 + self._width_of_current_line = 0 + self.hasmarkup = use_markup(self._file) + + def write(self, msg: str, **markup: bool) -> int: # type: ignore[override] + if not msg: + return 0 + self._update_chars_on_current_line(msg) # type: ignore[attr-defined] + if self.hasmarkup and markup: + markupmsg = self.markup(msg, **markup) + else: + markupmsg = msg + ret = self._file.write(markupmsg) + self._file.flush() + return ret + @property def fullwidth(self): if hasattr(self, "_terminal_width"): @@ -50,3 +111,7 @@ def _highlight(self, source): return source else: return highlight(source, PythonLexer(), TerminalFormatter(bg="dark")) + + +def _running_on_ci(): + return os.environ.get("CI", "").lower() == "true" or "BUILD_NUMBER" in os.environ diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index 9b294bc18dd..b4c9fc37013 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -5,7 +5,6 @@ ~8 terminal lines, unless running in "-vv" mode or running on CI. """ from ..compat import TYPE_CHECKING -from _pytest.assertion.util import _running_on_ci if TYPE_CHECKING: from typing import List @@ -28,7 +27,12 @@ def _should_truncate(config: "Config") -> bool: level = config.getini("assert_truncate_level") # type: str verbose = config.option.verbose # type: int if level == "auto": - return verbose < 2 and not _running_on_ci() + if verbose >= 2: + return False + + from _pytest._io import _running_on_ci + + return not _running_on_ci() return int(level) > verbose diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 756df116268..7bb4c54194d 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -1,7 +1,6 @@ """Utilities for assertion debugging""" import collections.abc import itertools -import os import pprint import re from typing import AbstractSet @@ -534,7 +533,3 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]: "? " + indent + marker, ] return newdiff - - -def _running_on_ci(): - return os.environ.get("CI", "").lower() == "true" or "BUILD_NUMBER" in os.environ diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index b9df21c5ebf..05adcc8a8fd 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 import pytest @@ -93,11 +94,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(tr.config, file=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/reports.py b/src/_pytest/reports.py index 0db5b57b240..e8533dd0605 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -75,17 +75,18 @@ def get_sections(self, prefix): yield prefix, content @property - def longreprtext(self): + def longreprtext(self) -> str: """ Read-only property that returns the full string representation of ``longrepr``. .. 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/terminal.py b/src/_pytest/terminal.py index ad96787be22..3fc00c6c875 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -34,7 +34,6 @@ from _pytest import nodes from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprFileLocation -from _pytest.assertion.util import _running_on_ci from _pytest.compat import order_preserving_dict from _pytest.compat import shell_quote from _pytest.compat import TYPE_CHECKING @@ -1214,10 +1213,15 @@ def short_test_summary(self) -> None: if not self.reportchars: return - if not self.isatty or _running_on_ci(): + if not self.isatty: termwidth = None else: - termwidth = self._tw.fullwidth + from _pytest._io import _running_on_ci + + if _running_on_ci(): + termwidth = None + else: + termwidth = self._tw.fullwidth def show_simple(stat, lines: List[str]) -> None: failed = self.stats.get(stat, []) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 2408ab2fdab..0bdb55ece80 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -3,6 +3,7 @@ import queue import sys import textwrap +from io import StringIO from typing import Union import py.path @@ -1037,10 +1038,11 @@ def f(): """ ) excinfo = pytest.raises(ValueError, mod.f) - tw = TerminalWriter(stringio=True) + file = 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( @@ -1256,11 +1258,12 @@ def g(): getattr(excinfo.value, attr).__traceback__ = None r = excinfo.getrepr() - tw = TerminalWriter(stringio=True) + file = 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",