From 276405a0394e6ade96b23aba493fef0e6fccdbd5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 14:49:32 +0300 Subject: [PATCH 01/26] terminalwriter: vendor TerminalWriter from py Straight copy from py 1.8.1. Doesn't pass linting yet. --- src/_pytest/_io/__init__.py | 2 +- src/_pytest/_io/terminalwriter.py | 421 ++++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 src/_pytest/_io/terminalwriter.py diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index 28ddc7b78..d401cda8e 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -1,7 +1,7 @@ from typing import List from typing import Sequence -from py.io import TerminalWriter as BaseTerminalWriter # noqa: F401 +from .terminalwriter import TerminalWriter as BaseTerminalWriter class TerminalWriter(BaseTerminalWriter): diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py new file mode 100644 index 000000000..be559867c --- /dev/null +++ b/src/_pytest/_io/terminalwriter.py @@ -0,0 +1,421 @@ +""" + +Helper functions for writing to terminals and files. + +""" + + +import sys, os, unicodedata +import py +py3k = sys.version_info[0] >= 3 +py33 = sys.version_info >= (3, 3) +from py.builtin import text, bytes + +win32_and_ctypes = False +colorama = None +if sys.platform == "win32": + try: + import colorama + except ImportError: + try: + import ctypes + win32_and_ctypes = True + except ImportError: + pass + + +def _getdimensions(): + if py33: + import shutil + size = shutil.get_terminal_size() + return size.lines, size.columns + else: + import termios, fcntl, struct + call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) + height, width = struct.unpack("hhhh", call)[:2] + return height, width + + +def get_terminal_width(): + width = 0 + try: + _, width = _getdimensions() + except py.builtin._sysex: + raise + except: + # pass to fallback below + pass + + if width == 0: + # FALLBACK: + # * some exception happened + # * or this is emacs terminal which reports (0,0) + width = int(os.environ.get('COLUMNS', 80)) + + # XXX the windows getdimensions may be bogus, let's sanify a bit + if width < 40: + width = 80 + return width + +terminal_width = get_terminal_width() + +char_width = { + 'A': 1, # "Ambiguous" + 'F': 2, # Fullwidth + 'H': 1, # Halfwidth + 'N': 1, # Neutral + 'Na': 1, # Narrow + 'W': 2, # Wide +} + + +def get_line_width(text): + text = unicodedata.normalize('NFC', text) + return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) + + +# XXX unify with _escaped func below +def ansi_print(text, esc, file=None, newline=True, flush=False): + if file is None: + file = sys.stderr + text = text.rstrip() + if esc and not isinstance(esc, tuple): + esc = (esc,) + if esc and sys.platform != "win32" and file.isatty(): + text = (''.join(['\x1b[%sm' % cod for cod in esc]) + + text + + '\x1b[0m') # ANSI color code "reset" + if newline: + text += '\n' + + if esc and win32_and_ctypes and file.isatty(): + if 1 in esc: + bold = True + esc = tuple([x for x in esc if x != 1]) + else: + bold = False + esctable = {() : FOREGROUND_WHITE, # normal + (31,): FOREGROUND_RED, # red + (32,): FOREGROUND_GREEN, # green + (33,): FOREGROUND_GREEN|FOREGROUND_RED, # yellow + (34,): FOREGROUND_BLUE, # blue + (35,): FOREGROUND_BLUE|FOREGROUND_RED, # purple + (36,): FOREGROUND_BLUE|FOREGROUND_GREEN, # cyan + (37,): FOREGROUND_WHITE, # white + (39,): FOREGROUND_WHITE, # reset + } + attr = esctable.get(esc, FOREGROUND_WHITE) + if bold: + attr |= FOREGROUND_INTENSITY + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + if file is sys.stderr: + handle = GetStdHandle(STD_ERROR_HANDLE) + else: + handle = GetStdHandle(STD_OUTPUT_HANDLE) + oldcolors = GetConsoleInfo(handle).wAttributes + attr |= (oldcolors & 0x0f0) + SetConsoleTextAttribute(handle, attr) + while len(text) > 32768: + file.write(text[:32768]) + text = text[32768:] + if text: + file.write(text) + SetConsoleTextAttribute(handle, oldcolors) + else: + file.write(text) + + if flush: + file.flush() + +def should_do_markup(file): + 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(object): + _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) + + # XXX deprecate stringio argument + def __init__(self, file=None, stringio=False, encoding=None): + if file is None: + if stringio: + self.stringio = file = py.io.TextIO() + else: + from sys import stdout as file + elif py.builtin.callable(file) and not ( + hasattr(file, "write") and hasattr(file, "flush")): + file = WriteFile(file, encoding=encoding) + if hasattr(file, "isatty") and file.isatty() and colorama: + file = colorama.AnsiToWin32(file).stream + self.encoding = encoding or getattr(file, 'encoding', "utf-8") + self._file = file + self.hasmarkup = should_do_markup(file) + self._lastlen = 0 + self._chars_on_current_line = 0 + self._width_of_current_line = 0 + + @property + def fullwidth(self): + if hasattr(self, '_terminal_width'): + return self._terminal_width + return get_terminal_width() + + @fullwidth.setter + def fullwidth(self, value): + self._terminal_width = value + + @property + def chars_on_current_line(self): + """Return the number of characters written so far in the current line. + + Please note that this count does not produce correct results after a reline() call, + see #164. + + .. versionadded:: 1.5.0 + + :rtype: int + """ + return self._chars_on_current_line + + @property + def width_of_current_line(self): + """Return an estimate of the width so far in the current line. + + .. versionadded:: 1.6.0 + + :rtype: int + """ + return self._width_of_current_line + + def _escaped(self, text, esc): + if esc and self.hasmarkup: + text = (''.join(['\x1b[%sm' % cod for cod in esc]) + + text +'\x1b[0m') + return text + + def markup(self, text, **kw): + esc = [] + for name in kw: + if name not in self._esctable: + raise ValueError("unknown markup: %r" %(name,)) + if kw[name]: + esc.append(self._esctable[name]) + return self._escaped(text, tuple(esc)) + + def sep(self, sepchar, title=None, fullwidth=None, **kw): + 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 = "%s %s %s" % (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, **kw) + + def write(self, msg, **kw): + if msg: + if not isinstance(msg, (bytes, text)): + msg = text(msg) + + self._update_chars_on_current_line(msg) + + if self.hasmarkup and kw: + markupmsg = self.markup(msg, **kw) + else: + markupmsg = msg + write_out(self._file, markupmsg) + + def _update_chars_on_current_line(self, text_or_bytes): + newline = b'\n' if isinstance(text_or_bytes, bytes) else '\n' + current_line = text_or_bytes.rsplit(newline, 1)[-1] + if isinstance(current_line, bytes): + current_line = current_line.decode('utf-8', errors='replace') + if newline in text_or_bytes: + self._chars_on_current_line = len(current_line) + self._width_of_current_line = get_line_width(current_line) + else: + self._chars_on_current_line += len(current_line) + self._width_of_current_line += get_line_width(current_line) + + def line(self, s='', **kw): + self.write(s, **kw) + self._checkfill(s) + self.write('\n') + + def reline(self, line, **kw): + if not self.hasmarkup: + raise ValueError("cannot use rewrite-line without terminal") + self.write(line, **kw) + self._checkfill(line) + self.write('\r') + self._lastlen = len(line) + + def _checkfill(self, line): + diff2last = self._lastlen - len(line) + if diff2last > 0: + self.write(" " * diff2last) + +class Win32ConsoleWriter(TerminalWriter): + def write(self, msg, **kw): + if msg: + if not isinstance(msg, (bytes, text)): + msg = text(msg) + + self._update_chars_on_current_line(msg) + + oldcolors = None + if self.hasmarkup and kw: + handle = GetStdHandle(STD_OUTPUT_HANDLE) + oldcolors = GetConsoleInfo(handle).wAttributes + default_bg = oldcolors & 0x00F0 + attr = default_bg + if kw.pop('bold', False): + attr |= FOREGROUND_INTENSITY + + if kw.pop('red', False): + attr |= FOREGROUND_RED + elif kw.pop('blue', False): + attr |= FOREGROUND_BLUE + elif kw.pop('green', False): + attr |= FOREGROUND_GREEN + elif kw.pop('yellow', False): + attr |= FOREGROUND_GREEN|FOREGROUND_RED + else: + attr |= oldcolors & 0x0007 + + SetConsoleTextAttribute(handle, attr) + write_out(self._file, msg) + if oldcolors: + SetConsoleTextAttribute(handle, oldcolors) + +class WriteFile(object): + def __init__(self, writemethod, encoding=None): + self.encoding = encoding + self._writemethod = writemethod + + def write(self, data): + if self.encoding: + data = data.encode(self.encoding, "replace") + self._writemethod(data) + + def flush(self): + return + + +if win32_and_ctypes: + TerminalWriter = Win32ConsoleWriter + import ctypes + from ctypes import wintypes + + # ctypes access to the Windows console + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + FOREGROUND_BLACK = 0x0000 # black text + FOREGROUND_BLUE = 0x0001 # text color contains blue. + FOREGROUND_GREEN = 0x0002 # text color contains green. + FOREGROUND_RED = 0x0004 # text color contains red. + FOREGROUND_WHITE = 0x0007 + FOREGROUND_INTENSITY = 0x0008 # text color is intensified. + BACKGROUND_BLACK = 0x0000 # background color black + BACKGROUND_BLUE = 0x0010 # background color contains blue. + BACKGROUND_GREEN = 0x0020 # background color contains green. + BACKGROUND_RED = 0x0040 # background color contains red. + BACKGROUND_WHITE = 0x0070 + BACKGROUND_INTENSITY = 0x0080 # background color is intensified. + + SHORT = ctypes.c_short + class COORD(ctypes.Structure): + _fields_ = [('X', SHORT), + ('Y', SHORT)] + class SMALL_RECT(ctypes.Structure): + _fields_ = [('Left', SHORT), + ('Top', SHORT), + ('Right', SHORT), + ('Bottom', SHORT)] + class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): + _fields_ = [('dwSize', COORD), + ('dwCursorPosition', COORD), + ('wAttributes', wintypes.WORD), + ('srWindow', SMALL_RECT), + ('dwMaximumWindowSize', COORD)] + + _GetStdHandle = ctypes.windll.kernel32.GetStdHandle + _GetStdHandle.argtypes = [wintypes.DWORD] + _GetStdHandle.restype = wintypes.HANDLE + def GetStdHandle(kind): + return _GetStdHandle(kind) + + SetConsoleTextAttribute = ctypes.windll.kernel32.SetConsoleTextAttribute + SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD] + SetConsoleTextAttribute.restype = wintypes.BOOL + + _GetConsoleScreenBufferInfo = \ + ctypes.windll.kernel32.GetConsoleScreenBufferInfo + _GetConsoleScreenBufferInfo.argtypes = [wintypes.HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] + _GetConsoleScreenBufferInfo.restype = wintypes.BOOL + def GetConsoleInfo(handle): + info = CONSOLE_SCREEN_BUFFER_INFO() + _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) + return info + + def _getdimensions(): + handle = GetStdHandle(STD_OUTPUT_HANDLE) + info = GetConsoleInfo(handle) + # Substract one from the width, otherwise the cursor wraps + # and the ending \n causes an empty line to display. + return info.dwSize.Y, info.dwSize.X - 1 + +def write_out(fil, msg): + # XXX sometimes "msg" is of type bytes, sometimes text which + # complicates the situation. Should we try to enforce unicode? + try: + # on py27 and above writing out to sys.stdout with an encoding + # should usually work for unicode messages (if the encoding is + # capable of it) + fil.write(msg) + except UnicodeEncodeError: + # on py26 it might not work because stdout expects bytes + if fil.encoding: + try: + fil.write(msg.encode(fil.encoding)) + except UnicodeEncodeError: + # it might still fail if the encoding is not capable + pass + else: + fil.flush() + return + # fallback: escape all unicode characters + msg = msg.encode("unicode-escape").decode("ascii") + fil.write(msg) + fil.flush() From 3014d9a3f75f988c4925176a133efbaf75637ce1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:00:58 +0300 Subject: [PATCH 02/26] terminalwriter: auto-format --- src/_pytest/_io/terminalwriter.py | 218 ++++++++++++++++++------------ 1 file changed, 130 insertions(+), 88 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index be559867c..aeb5c110d 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -1,15 +1,16 @@ -""" +"""Helper functions for writing to terminals and files.""" +import sys +import os +import unicodedata -Helper functions for writing to terminals and files. - -""" - - -import sys, os, unicodedata import py +from py.builtin import text, bytes + +# This code was initially copied from py 1.8.1, file _io/terminalwriter.py. + py3k = sys.version_info[0] >= 3 py33 = sys.version_info >= (3, 3) -from py.builtin import text, bytes + win32_and_ctypes = False colorama = None @@ -19,6 +20,7 @@ if sys.platform == "win32": except ImportError: try: import ctypes + win32_and_ctypes = True except ImportError: pass @@ -27,10 +29,14 @@ if sys.platform == "win32": def _getdimensions(): if py33: import shutil + size = shutil.get_terminal_size() return size.lines, size.columns else: - import termios, fcntl, struct + import termios + import fcntl + import struct + call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) height, width = struct.unpack("hhhh", call)[:2] return height, width @@ -50,27 +56,28 @@ def get_terminal_width(): # FALLBACK: # * some exception happened # * or this is emacs terminal which reports (0,0) - width = int(os.environ.get('COLUMNS', 80)) + width = int(os.environ.get("COLUMNS", 80)) # XXX the windows getdimensions may be bogus, let's sanify a bit if width < 40: width = 80 return width + terminal_width = get_terminal_width() char_width = { - 'A': 1, # "Ambiguous" - 'F': 2, # Fullwidth - 'H': 1, # Halfwidth - 'N': 1, # Neutral - 'Na': 1, # Narrow - 'W': 2, # Wide + "A": 1, # "Ambiguous" + "F": 2, # Fullwidth + "H": 1, # Halfwidth + "N": 1, # Neutral + "Na": 1, # Narrow + "W": 2, # Wide } def get_line_width(text): - text = unicodedata.normalize('NFC', text) + text = unicodedata.normalize("NFC", text) return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) @@ -82,11 +89,11 @@ def ansi_print(text, esc, file=None, newline=True, flush=False): if esc and not isinstance(esc, tuple): esc = (esc,) if esc and sys.platform != "win32" and file.isatty(): - text = (''.join(['\x1b[%sm' % cod for cod in esc]) + - text + - '\x1b[0m') # ANSI color code "reset" + text = ( + "".join(["\x1b[%sm" % cod for cod in esc]) + text + "\x1b[0m" + ) # ANSI color code "reset" if newline: - text += '\n' + text += "\n" if esc and win32_and_ctypes and file.isatty(): if 1 in esc: @@ -94,16 +101,17 @@ def ansi_print(text, esc, file=None, newline=True, flush=False): esc = tuple([x for x in esc if x != 1]) else: bold = False - esctable = {() : FOREGROUND_WHITE, # normal - (31,): FOREGROUND_RED, # red - (32,): FOREGROUND_GREEN, # green - (33,): FOREGROUND_GREEN|FOREGROUND_RED, # yellow - (34,): FOREGROUND_BLUE, # blue - (35,): FOREGROUND_BLUE|FOREGROUND_RED, # purple - (36,): FOREGROUND_BLUE|FOREGROUND_GREEN, # cyan - (37,): FOREGROUND_WHITE, # white - (39,): FOREGROUND_WHITE, # reset - } + esctable = { + (): FOREGROUND_WHITE, # normal + (31,): FOREGROUND_RED, # red + (32,): FOREGROUND_GREEN, # green + (33,): FOREGROUND_GREEN | FOREGROUND_RED, # yellow + (34,): FOREGROUND_BLUE, # blue + (35,): FOREGROUND_BLUE | FOREGROUND_RED, # purple + (36,): FOREGROUND_BLUE | FOREGROUND_GREEN, # cyan + (37,): FOREGROUND_WHITE, # white + (39,): FOREGROUND_WHITE, # reset + } attr = esctable.get(esc, FOREGROUND_WHITE) if bold: attr |= FOREGROUND_INTENSITY @@ -114,7 +122,7 @@ def ansi_print(text, esc, file=None, newline=True, flush=False): else: handle = GetStdHandle(STD_OUTPUT_HANDLE) oldcolors = GetConsoleInfo(handle).wAttributes - attr |= (oldcolors & 0x0f0) + attr |= oldcolors & 0x0F0 SetConsoleTextAttribute(handle, attr) while len(text) > 32768: file.write(text[:32768]) @@ -128,21 +136,43 @@ def ansi_print(text, esc, file=None, newline=True, flush=False): if flush: file.flush() + def should_do_markup(file): - if os.environ.get('PY_COLORS') == '1': + if os.environ.get("PY_COLORS") == "1": return True - if os.environ.get('PY_COLORS') == '0': + 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') + 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(object): - _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) + _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, + ) # XXX deprecate stringio argument def __init__(self, file=None, stringio=False, encoding=None): @@ -152,11 +182,12 @@ class TerminalWriter(object): else: from sys import stdout as file elif py.builtin.callable(file) and not ( - hasattr(file, "write") and hasattr(file, "flush")): + hasattr(file, "write") and hasattr(file, "flush") + ): file = WriteFile(file, encoding=encoding) if hasattr(file, "isatty") and file.isatty() and colorama: file = colorama.AnsiToWin32(file).stream - self.encoding = encoding or getattr(file, 'encoding', "utf-8") + self.encoding = encoding or getattr(file, "encoding", "utf-8") self._file = file self.hasmarkup = should_do_markup(file) self._lastlen = 0 @@ -165,7 +196,7 @@ class TerminalWriter(object): @property def fullwidth(self): - if hasattr(self, '_terminal_width'): + if hasattr(self, "_terminal_width"): return self._terminal_width return get_terminal_width() @@ -198,15 +229,14 @@ class TerminalWriter(object): def _escaped(self, text, esc): if esc and self.hasmarkup: - text = (''.join(['\x1b[%sm' % cod for cod in esc]) + - text +'\x1b[0m') + text = "".join(["\x1b[%sm" % cod for cod in esc]) + text + "\x1b[0m" return text def markup(self, text, **kw): esc = [] for name in kw: if name not in self._esctable: - raise ValueError("unknown markup: %r" %(name,)) + raise ValueError("unknown markup: %r" % (name,)) if kw[name]: esc.append(self._esctable[name]) return self._escaped(text, tuple(esc)) @@ -227,7 +257,7 @@ class TerminalWriter(object): # 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) + N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1) fill = sepchar * N line = "%s %s %s" % (fill, title, fill) else: @@ -256,10 +286,10 @@ class TerminalWriter(object): write_out(self._file, markupmsg) def _update_chars_on_current_line(self, text_or_bytes): - newline = b'\n' if isinstance(text_or_bytes, bytes) else '\n' + newline = b"\n" if isinstance(text_or_bytes, bytes) else "\n" current_line = text_or_bytes.rsplit(newline, 1)[-1] if isinstance(current_line, bytes): - current_line = current_line.decode('utf-8', errors='replace') + current_line = current_line.decode("utf-8", errors="replace") if newline in text_or_bytes: self._chars_on_current_line = len(current_line) self._width_of_current_line = get_line_width(current_line) @@ -267,17 +297,17 @@ class TerminalWriter(object): self._chars_on_current_line += len(current_line) self._width_of_current_line += get_line_width(current_line) - def line(self, s='', **kw): + def line(self, s="", **kw): self.write(s, **kw) self._checkfill(s) - self.write('\n') + self.write("\n") def reline(self, line, **kw): if not self.hasmarkup: raise ValueError("cannot use rewrite-line without terminal") self.write(line, **kw) self._checkfill(line) - self.write('\r') + self.write("\r") self._lastlen = len(line) def _checkfill(self, line): @@ -285,6 +315,7 @@ class TerminalWriter(object): if diff2last > 0: self.write(" " * diff2last) + class Win32ConsoleWriter(TerminalWriter): def write(self, msg, **kw): if msg: @@ -299,17 +330,17 @@ class Win32ConsoleWriter(TerminalWriter): oldcolors = GetConsoleInfo(handle).wAttributes default_bg = oldcolors & 0x00F0 attr = default_bg - if kw.pop('bold', False): + if kw.pop("bold", False): attr |= FOREGROUND_INTENSITY - if kw.pop('red', False): + if kw.pop("red", False): attr |= FOREGROUND_RED - elif kw.pop('blue', False): + elif kw.pop("blue", False): attr |= FOREGROUND_BLUE - elif kw.pop('green', False): + elif kw.pop("green", False): attr |= FOREGROUND_GREEN - elif kw.pop('yellow', False): - attr |= FOREGROUND_GREEN|FOREGROUND_RED + elif kw.pop("yellow", False): + attr |= FOREGROUND_GREEN | FOREGROUND_RED else: attr |= oldcolors & 0x0007 @@ -318,6 +349,7 @@ class Win32ConsoleWriter(TerminalWriter): if oldcolors: SetConsoleTextAttribute(handle, oldcolors) + class WriteFile(object): def __init__(self, writemethod, encoding=None): self.encoding = encoding @@ -339,39 +371,46 @@ if win32_and_ctypes: # ctypes access to the Windows console STD_OUTPUT_HANDLE = -11 - STD_ERROR_HANDLE = -12 - FOREGROUND_BLACK = 0x0000 # black text - FOREGROUND_BLUE = 0x0001 # text color contains blue. - FOREGROUND_GREEN = 0x0002 # text color contains green. - FOREGROUND_RED = 0x0004 # text color contains red. - FOREGROUND_WHITE = 0x0007 - FOREGROUND_INTENSITY = 0x0008 # text color is intensified. - BACKGROUND_BLACK = 0x0000 # background color black - BACKGROUND_BLUE = 0x0010 # background color contains blue. - BACKGROUND_GREEN = 0x0020 # background color contains green. - BACKGROUND_RED = 0x0040 # background color contains red. - BACKGROUND_WHITE = 0x0070 - BACKGROUND_INTENSITY = 0x0080 # background color is intensified. + STD_ERROR_HANDLE = -12 + FOREGROUND_BLACK = 0x0000 # black text + FOREGROUND_BLUE = 0x0001 # text color contains blue. + FOREGROUND_GREEN = 0x0002 # text color contains green. + FOREGROUND_RED = 0x0004 # text color contains red. + FOREGROUND_WHITE = 0x0007 + FOREGROUND_INTENSITY = 0x0008 # text color is intensified. + BACKGROUND_BLACK = 0x0000 # background color black + BACKGROUND_BLUE = 0x0010 # background color contains blue. + BACKGROUND_GREEN = 0x0020 # background color contains green. + BACKGROUND_RED = 0x0040 # background color contains red. + BACKGROUND_WHITE = 0x0070 + BACKGROUND_INTENSITY = 0x0080 # background color is intensified. SHORT = ctypes.c_short + class COORD(ctypes.Structure): - _fields_ = [('X', SHORT), - ('Y', SHORT)] + _fields_ = [("X", SHORT), ("Y", SHORT)] + class SMALL_RECT(ctypes.Structure): - _fields_ = [('Left', SHORT), - ('Top', SHORT), - ('Right', SHORT), - ('Bottom', SHORT)] + _fields_ = [ + ("Left", SHORT), + ("Top", SHORT), + ("Right", SHORT), + ("Bottom", SHORT), + ] + class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): - _fields_ = [('dwSize', COORD), - ('dwCursorPosition', COORD), - ('wAttributes', wintypes.WORD), - ('srWindow', SMALL_RECT), - ('dwMaximumWindowSize', COORD)] + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", wintypes.WORD), + ("srWindow", SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] _GetStdHandle = ctypes.windll.kernel32.GetStdHandle _GetStdHandle.argtypes = [wintypes.DWORD] _GetStdHandle.restype = wintypes.HANDLE + def GetStdHandle(kind): return _GetStdHandle(kind) @@ -379,11 +418,13 @@ if win32_and_ctypes: SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD] SetConsoleTextAttribute.restype = wintypes.BOOL - _GetConsoleScreenBufferInfo = \ - ctypes.windll.kernel32.GetConsoleScreenBufferInfo - _GetConsoleScreenBufferInfo.argtypes = [wintypes.HANDLE, - ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] + _GetConsoleScreenBufferInfo = ctypes.windll.kernel32.GetConsoleScreenBufferInfo + _GetConsoleScreenBufferInfo.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), + ] _GetConsoleScreenBufferInfo.restype = wintypes.BOOL + def GetConsoleInfo(handle): info = CONSOLE_SCREEN_BUFFER_INFO() _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) @@ -396,6 +437,7 @@ if win32_and_ctypes: # and the ending \n causes an empty line to display. return info.dwSize.Y, info.dwSize.X - 1 + def write_out(fil, msg): # XXX sometimes "msg" is of type bytes, sometimes text which # complicates the situation. Should we try to enforce unicode? From 5e2d820308a0a78212efb6dadfdc7cbe02b7cbec Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:54:58 +0300 Subject: [PATCH 03/26] terminalwriter: fix lints --- src/_pytest/_io/terminalwriter.py | 68 ++++++++++++------------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index aeb5c110d..609d4418c 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -1,16 +1,13 @@ """Helper functions for writing to terminals and files.""" -import sys import os +import shutil +import sys import unicodedata +from io import StringIO -import py -from py.builtin import text, bytes # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. -py3k = sys.version_info[0] >= 3 -py33 = sys.version_info >= (3, 3) - win32_and_ctypes = False colorama = None @@ -20,35 +17,24 @@ if sys.platform == "win32": except ImportError: try: import ctypes - - win32_and_ctypes = True except ImportError: pass + else: + win32_and_ctypes = True def _getdimensions(): - if py33: - import shutil - - size = shutil.get_terminal_size() - return size.lines, size.columns - else: - import termios - import fcntl - import struct - - call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) - height, width = struct.unpack("hhhh", call)[:2] - return height, width + size = shutil.get_terminal_size() + return size.lines, size.columns def get_terminal_width(): width = 0 try: _, width = _getdimensions() - except py.builtin._sysex: + except (KeyboardInterrupt, SystemExit, MemoryError, GeneratorExit): raise - except: + except BaseException: # pass to fallback below pass @@ -150,7 +136,7 @@ def should_do_markup(file): ) -class TerminalWriter(object): +class TerminalWriter: _esctable = dict( black=30, red=31, @@ -178,12 +164,10 @@ class TerminalWriter(object): def __init__(self, file=None, stringio=False, encoding=None): if file is None: if stringio: - self.stringio = file = py.io.TextIO() + self.stringio = file = StringIO() else: from sys import stdout as file - elif py.builtin.callable(file) and not ( - hasattr(file, "write") and hasattr(file, "flush") - ): + elif callable(file) and not (hasattr(file, "write") and hasattr(file, "flush")): file = WriteFile(file, encoding=encoding) if hasattr(file, "isatty") and file.isatty() and colorama: file = colorama.AnsiToWin32(file).stream @@ -236,7 +220,7 @@ class TerminalWriter(object): esc = [] for name in kw: if name not in self._esctable: - raise ValueError("unknown markup: %r" % (name,)) + raise ValueError("unknown markup: {!r}".format(name)) if kw[name]: esc.append(self._esctable[name]) return self._escaped(text, tuple(esc)) @@ -259,7 +243,7 @@ class TerminalWriter(object): # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1) fill = sepchar * N - line = "%s %s %s" % (fill, title, fill) + line = "{} {} {}".format(fill, title, fill) else: # we want len(sepchar)*N <= fullwidth # i.e. N <= fullwidth // len(sepchar) @@ -274,8 +258,8 @@ class TerminalWriter(object): def write(self, msg, **kw): if msg: - if not isinstance(msg, (bytes, text)): - msg = text(msg) + if not isinstance(msg, (bytes, str)): + msg = str(msg) self._update_chars_on_current_line(msg) @@ -319,8 +303,8 @@ class TerminalWriter(object): class Win32ConsoleWriter(TerminalWriter): def write(self, msg, **kw): if msg: - if not isinstance(msg, (bytes, text)): - msg = text(msg) + if not isinstance(msg, (bytes, str)): + msg = str(msg) self._update_chars_on_current_line(msg) @@ -350,7 +334,7 @@ class Win32ConsoleWriter(TerminalWriter): SetConsoleTextAttribute(handle, oldcolors) -class WriteFile(object): +class WriteFile: def __init__(self, writemethod, encoding=None): self.encoding = encoding self._writemethod = writemethod @@ -365,9 +349,11 @@ class WriteFile(object): if win32_and_ctypes: - TerminalWriter = Win32ConsoleWriter - import ctypes + import ctypes # noqa: F811 from ctypes import wintypes + from ctypes import windll # type: ignore[attr-defined] # noqa: F821 + + TerminalWriter = Win32ConsoleWriter # type: ignore[misc] # noqa: F821 # ctypes access to the Windows console STD_OUTPUT_HANDLE = -11 @@ -407,18 +393,18 @@ if win32_and_ctypes: ("dwMaximumWindowSize", COORD), ] - _GetStdHandle = ctypes.windll.kernel32.GetStdHandle + _GetStdHandle = windll.kernel32.GetStdHandle _GetStdHandle.argtypes = [wintypes.DWORD] _GetStdHandle.restype = wintypes.HANDLE def GetStdHandle(kind): return _GetStdHandle(kind) - SetConsoleTextAttribute = ctypes.windll.kernel32.SetConsoleTextAttribute + SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD] SetConsoleTextAttribute.restype = wintypes.BOOL - _GetConsoleScreenBufferInfo = ctypes.windll.kernel32.GetConsoleScreenBufferInfo + _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo _GetConsoleScreenBufferInfo.argtypes = [ wintypes.HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), @@ -430,7 +416,7 @@ if win32_and_ctypes: _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) return info - def _getdimensions(): + def _getdimensions(): # noqa: F811 handle = GetStdHandle(STD_OUTPUT_HANDLE) info = GetConsoleInfo(handle) # Substract one from the width, otherwise the cursor wraps From 1d596b27a707414145793b16ad1b1f2ffbc02799 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:17:44 +0300 Subject: [PATCH 04/26] terminalwriter: move Win32ConsoleWriter definition under win32 conditional This way non-Windows platforms skip it. It also uses things defined inside the `if`. --- src/_pytest/_io/terminalwriter.py | 67 +++++++++++++++---------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 609d4418c..17740e83e 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -300,40 +300,6 @@ class TerminalWriter: self.write(" " * diff2last) -class Win32ConsoleWriter(TerminalWriter): - def write(self, msg, **kw): - if msg: - if not isinstance(msg, (bytes, str)): - msg = str(msg) - - self._update_chars_on_current_line(msg) - - oldcolors = None - if self.hasmarkup and kw: - handle = GetStdHandle(STD_OUTPUT_HANDLE) - oldcolors = GetConsoleInfo(handle).wAttributes - default_bg = oldcolors & 0x00F0 - attr = default_bg - if kw.pop("bold", False): - attr |= FOREGROUND_INTENSITY - - if kw.pop("red", False): - attr |= FOREGROUND_RED - elif kw.pop("blue", False): - attr |= FOREGROUND_BLUE - elif kw.pop("green", False): - attr |= FOREGROUND_GREEN - elif kw.pop("yellow", False): - attr |= FOREGROUND_GREEN | FOREGROUND_RED - else: - attr |= oldcolors & 0x0007 - - SetConsoleTextAttribute(handle, attr) - write_out(self._file, msg) - if oldcolors: - SetConsoleTextAttribute(handle, oldcolors) - - class WriteFile: def __init__(self, writemethod, encoding=None): self.encoding = encoding @@ -353,6 +319,39 @@ if win32_and_ctypes: from ctypes import wintypes from ctypes import windll # type: ignore[attr-defined] # noqa: F821 + class Win32ConsoleWriter(TerminalWriter): + def write(self, msg, **kw): + if msg: + if not isinstance(msg, (bytes, str)): + msg = str(msg) + + self._update_chars_on_current_line(msg) + + oldcolors = None + if self.hasmarkup and kw: + handle = GetStdHandle(STD_OUTPUT_HANDLE) + oldcolors = GetConsoleInfo(handle).wAttributes + default_bg = oldcolors & 0x00F0 + attr = default_bg + if kw.pop("bold", False): + attr |= FOREGROUND_INTENSITY + + if kw.pop("red", False): + attr |= FOREGROUND_RED + elif kw.pop("blue", False): + attr |= FOREGROUND_BLUE + elif kw.pop("green", False): + attr |= FOREGROUND_GREEN + elif kw.pop("yellow", False): + attr |= FOREGROUND_GREEN | FOREGROUND_RED + else: + attr |= oldcolors & 0x0007 + + SetConsoleTextAttribute(handle, attr) + write_out(self._file, msg) + if oldcolors: + SetConsoleTextAttribute(handle, oldcolors) + TerminalWriter = Win32ConsoleWriter # type: ignore[misc] # noqa: F821 # ctypes access to the Windows console From c749e44efc37c2792b0d1257930a24c74b4a7251 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:37:38 +0300 Subject: [PATCH 05/26] terminalwriter: remove custom win32 screen width code Python 3 does this on its own so we can use the shared code: https://github.com/python/cpython/commit/bcf2b59fb5f18c09a26da3e9b60a37367f2a28ba --- src/_pytest/_io/terminalwriter.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 17740e83e..6ff39111a 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -415,13 +415,6 @@ if win32_and_ctypes: _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) return info - def _getdimensions(): # noqa: F811 - handle = GetStdHandle(STD_OUTPUT_HANDLE) - info = GetConsoleInfo(handle) - # Substract one from the width, otherwise the cursor wraps - # and the ending \n causes an empty line to display. - return info.dwSize.Y, info.dwSize.X - 1 - def write_out(fil, msg): # XXX sometimes "msg" is of type bytes, sometimes text which From 6c1b6a09b878ea74796618d02e7a8222464e1b9a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:12:32 +0300 Subject: [PATCH 06/26] terminalwriter: simplify get_terminal_width() The shutil.get_terminal_size() handles everything this did already. --- src/_pytest/_io/terminalwriter.py | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 6ff39111a..5533aa914 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -23,35 +23,16 @@ if sys.platform == "win32": win32_and_ctypes = True -def _getdimensions(): - size = shutil.get_terminal_size() - return size.lines, size.columns +def get_terminal_width() -> int: + width, _ = shutil.get_terminal_size(fallback=(80, 24)) - -def get_terminal_width(): - width = 0 - try: - _, width = _getdimensions() - except (KeyboardInterrupt, SystemExit, MemoryError, GeneratorExit): - raise - except BaseException: - # pass to fallback below - pass - - if width == 0: - # FALLBACK: - # * some exception happened - # * or this is emacs terminal which reports (0,0) - width = int(os.environ.get("COLUMNS", 80)) - - # XXX the windows getdimensions may be bogus, let's sanify a bit + # The Windows get_terminal_size may be bogus, let's sanify a bit. if width < 40: width = 80 + return width -terminal_width = get_terminal_width() - char_width = { "A": 1, # "Ambiguous" "F": 2, # Fullwidth From 9a59970cad80114aeb24ffcd869defc127cb6173 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:28:26 +0300 Subject: [PATCH 07/26] terminalwriter: optimize get_line_width() a bit This function is called a lot when printing a lot of text, and is very slow -- this speeds it up a bit. --- src/_pytest/_io/terminalwriter.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 5533aa914..17a67bddb 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -3,6 +3,7 @@ import os import shutil import sys import unicodedata +from functools import lru_cache from io import StringIO @@ -33,19 +34,15 @@ def get_terminal_width() -> int: return width -char_width = { - "A": 1, # "Ambiguous" - "F": 2, # Fullwidth - "H": 1, # Halfwidth - "N": 1, # Neutral - "Na": 1, # Narrow - "W": 2, # Wide -} +@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): text = unicodedata.normalize("NFC", text) - return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) + return sum(char_width(c) for c in text) # XXX unify with _escaped func below From b6cc90e0afe90c84d84c5b15a2db75d87a2681d7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:45:10 +0300 Subject: [PATCH 08/26] terminalwriter: remove support for writing bytes directly It is not used and slows things down. --- src/_pytest/_io/terminalwriter.py | 56 +++++++------------------------ 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 17a67bddb..0e7f0ccff 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -234,37 +234,32 @@ class TerminalWriter: self.line(line, **kw) - def write(self, msg, **kw): + def write(self, msg: str, **kw) -> None: if msg: - if not isinstance(msg, (bytes, str)): - msg = str(msg) - self._update_chars_on_current_line(msg) if self.hasmarkup and kw: markupmsg = self.markup(msg, **kw) else: markupmsg = msg - write_out(self._file, markupmsg) + self._file.write(markupmsg) + self._file.flush() - def _update_chars_on_current_line(self, text_or_bytes): - newline = b"\n" if isinstance(text_or_bytes, bytes) else "\n" - current_line = text_or_bytes.rsplit(newline, 1)[-1] - if isinstance(current_line, bytes): - current_line = current_line.decode("utf-8", errors="replace") - if newline in text_or_bytes: + def _update_chars_on_current_line(self, text: str) -> None: + current_line = text.rsplit("\n", 1)[-1] + if "\n" in text: self._chars_on_current_line = len(current_line) self._width_of_current_line = get_line_width(current_line) else: self._chars_on_current_line += len(current_line) self._width_of_current_line += get_line_width(current_line) - def line(self, s="", **kw): + def line(self, s: str = "", **kw): self.write(s, **kw) self._checkfill(s) self.write("\n") - def reline(self, line, **kw): + def reline(self, line: str, **kw): if not self.hasmarkup: raise ValueError("cannot use rewrite-line without terminal") self.write(line, **kw) @@ -272,7 +267,7 @@ class TerminalWriter: self.write("\r") self._lastlen = len(line) - def _checkfill(self, line): + def _checkfill(self, line: str) -> None: diff2last = self._lastlen - len(line) if diff2last > 0: self.write(" " * diff2last) @@ -298,11 +293,8 @@ if win32_and_ctypes: from ctypes import windll # type: ignore[attr-defined] # noqa: F821 class Win32ConsoleWriter(TerminalWriter): - def write(self, msg, **kw): + def write(self, msg: str, **kw): if msg: - if not isinstance(msg, (bytes, str)): - msg = str(msg) - self._update_chars_on_current_line(msg) oldcolors = None @@ -326,7 +318,8 @@ if win32_and_ctypes: attr |= oldcolors & 0x0007 SetConsoleTextAttribute(handle, attr) - write_out(self._file, msg) + self._file.write(msg) + self._file.flush() if oldcolors: SetConsoleTextAttribute(handle, oldcolors) @@ -392,28 +385,3 @@ if win32_and_ctypes: info = CONSOLE_SCREEN_BUFFER_INFO() _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) return info - - -def write_out(fil, msg): - # XXX sometimes "msg" is of type bytes, sometimes text which - # complicates the situation. Should we try to enforce unicode? - try: - # on py27 and above writing out to sys.stdout with an encoding - # should usually work for unicode messages (if the encoding is - # capable of it) - fil.write(msg) - except UnicodeEncodeError: - # on py26 it might not work because stdout expects bytes - if fil.encoding: - try: - fil.write(msg.encode(fil.encoding)) - except UnicodeEncodeError: - # it might still fail if the encoding is not capable - pass - else: - fil.flush() - return - # fallback: escape all unicode characters - msg = msg.encode("unicode-escape").decode("ascii") - fil.write(msg) - fil.flush() From a6819726cdaf05a479fcc5f74d2f091f85b672c7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:51:14 +0300 Subject: [PATCH 09/26] terminalwriter: remove unused function ansi_print --- src/_pytest/_io/terminalwriter.py | 56 ------------------------------- 1 file changed, 56 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 0e7f0ccff..6e77f2ebf 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -45,62 +45,6 @@ def get_line_width(text): return sum(char_width(c) for c in text) -# XXX unify with _escaped func below -def ansi_print(text, esc, file=None, newline=True, flush=False): - if file is None: - file = sys.stderr - text = text.rstrip() - if esc and not isinstance(esc, tuple): - esc = (esc,) - if esc and sys.platform != "win32" and file.isatty(): - text = ( - "".join(["\x1b[%sm" % cod for cod in esc]) + text + "\x1b[0m" - ) # ANSI color code "reset" - if newline: - text += "\n" - - if esc and win32_and_ctypes and file.isatty(): - if 1 in esc: - bold = True - esc = tuple([x for x in esc if x != 1]) - else: - bold = False - esctable = { - (): FOREGROUND_WHITE, # normal - (31,): FOREGROUND_RED, # red - (32,): FOREGROUND_GREEN, # green - (33,): FOREGROUND_GREEN | FOREGROUND_RED, # yellow - (34,): FOREGROUND_BLUE, # blue - (35,): FOREGROUND_BLUE | FOREGROUND_RED, # purple - (36,): FOREGROUND_BLUE | FOREGROUND_GREEN, # cyan - (37,): FOREGROUND_WHITE, # white - (39,): FOREGROUND_WHITE, # reset - } - attr = esctable.get(esc, FOREGROUND_WHITE) - if bold: - attr |= FOREGROUND_INTENSITY - STD_OUTPUT_HANDLE = -11 - STD_ERROR_HANDLE = -12 - if file is sys.stderr: - handle = GetStdHandle(STD_ERROR_HANDLE) - else: - handle = GetStdHandle(STD_OUTPUT_HANDLE) - oldcolors = GetConsoleInfo(handle).wAttributes - attr |= oldcolors & 0x0F0 - SetConsoleTextAttribute(handle, attr) - while len(text) > 32768: - file.write(text[:32768]) - text = text[32768:] - if text: - file.write(text) - SetConsoleTextAttribute(handle, oldcolors) - else: - file.write(text) - - if flush: - file.flush() - - def should_do_markup(file): if os.environ.get("PY_COLORS") == "1": return True From 0528307ebfc08aa8b826d2905c827fcdd6a86419 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:15:23 +0300 Subject: [PATCH 10/26] terminalwriter: remove unused function TerminalWriter.reline --- src/_pytest/_io/terminalwriter.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 6e77f2ebf..c11eb9aba 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -96,7 +96,6 @@ class TerminalWriter: self.encoding = encoding or getattr(file, "encoding", "utf-8") self._file = file self.hasmarkup = should_do_markup(file) - self._lastlen = 0 self._chars_on_current_line = 0 self._width_of_current_line = 0 @@ -114,11 +113,6 @@ class TerminalWriter: def chars_on_current_line(self): """Return the number of characters written so far in the current line. - Please note that this count does not produce correct results after a reline() call, - see #164. - - .. versionadded:: 1.5.0 - :rtype: int """ return self._chars_on_current_line @@ -127,8 +121,6 @@ class TerminalWriter: def width_of_current_line(self): """Return an estimate of the width so far in the current line. - .. versionadded:: 1.6.0 - :rtype: int """ return self._width_of_current_line @@ -200,22 +192,8 @@ class TerminalWriter: def line(self, s: str = "", **kw): self.write(s, **kw) - self._checkfill(s) self.write("\n") - def reline(self, line: str, **kw): - if not self.hasmarkup: - raise ValueError("cannot use rewrite-line without terminal") - self.write(line, **kw) - self._checkfill(line) - self.write("\r") - self._lastlen = len(line) - - def _checkfill(self, line: str) -> None: - diff2last = self._lastlen - len(line) - if diff2last > 0: - self.write(" " * diff2last) - class WriteFile: def __init__(self, writemethod, encoding=None): From dac05ccd9a290ac232d933dfb2c4b0f651468867 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:57:21 +0300 Subject: [PATCH 11/26] terminalwriter: remove support for passing callable as file in TerminalWriter Not used. --- src/_pytest/_io/terminalwriter.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index c11eb9aba..437c96794 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -83,17 +83,14 @@ class TerminalWriter: ) # XXX deprecate stringio argument - def __init__(self, file=None, stringio=False, encoding=None): + def __init__(self, file=None, stringio=False): if file is None: if stringio: self.stringio = file = StringIO() else: from sys import stdout as file - elif callable(file) and not (hasattr(file, "write") and hasattr(file, "flush")): - file = WriteFile(file, encoding=encoding) if hasattr(file, "isatty") and file.isatty() and colorama: file = colorama.AnsiToWin32(file).stream - self.encoding = encoding or getattr(file, "encoding", "utf-8") self._file = file self.hasmarkup = should_do_markup(file) self._chars_on_current_line = 0 @@ -195,20 +192,6 @@ class TerminalWriter: self.write("\n") -class WriteFile: - def __init__(self, writemethod, encoding=None): - self.encoding = encoding - self._writemethod = writemethod - - def write(self, data): - if self.encoding: - data = data.encode(self.encoding, "replace") - self._writemethod(data) - - def flush(self): - return - - if win32_and_ctypes: import ctypes # noqa: F811 from ctypes import wintypes From 94a57d235322588ccde609bd9c2c384f0e81a337 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:04:57 +0300 Subject: [PATCH 12/26] io: combine _io.TerminalWriter and _io.terminalwriter.TerminalWriter Previously it extended an external type but now it come move to the type itself. --- src/_pytest/_io/__init__.py | 41 +++---------------------------- src/_pytest/_io/terminalwriter.py | 34 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index d401cda8e..880c3c87a 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -1,39 +1,6 @@ -from typing import List -from typing import Sequence - -from .terminalwriter import TerminalWriter as BaseTerminalWriter +from .terminalwriter import TerminalWriter -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", +] diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 437c96794..e4e5db228 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -5,6 +5,7 @@ import sys import unicodedata from functools import lru_cache from io import StringIO +from typing import Sequence # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. @@ -191,6 +192,39 @@ class TerminalWriter: self.write(s, **kw) self.write("\n") + 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): + """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")) + if win32_and_ctypes: import ctypes # noqa: F811 From 66ee7556494dea45c70b8cd2dfac523653b83b4f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:45:47 +0300 Subject: [PATCH 13/26] terminalwriter: remove TerminalWriter's stringio argument Had a mark indicating it should be removed, and I agree, it's better to just use the `file` argument. --- src/_pytest/_io/terminalwriter.py | 9 ++------- src/_pytest/pastebin.py | 8 ++++---- src/_pytest/reports.py | 5 +++-- testing/code/test_excinfo.py | 11 +++++++---- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index e4e5db228..907b90543 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -4,7 +4,6 @@ import shutil import sys import unicodedata from functools import lru_cache -from io import StringIO from typing import Sequence @@ -83,13 +82,9 @@ class TerminalWriter: invert=7, ) - # XXX deprecate stringio argument - def __init__(self, file=None, stringio=False): + def __init__(self, file=None): if file is None: - if stringio: - self.stringio = file = StringIO() - else: - from sys import stdout as file + file = sys.stdout if hasattr(file, "isatty") and file.isatty() and colorama: file = colorama.AnsiToWin32(file).stream self._file = file diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 3f4a7502d..cbaa9a9f5 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/reports.py b/src/_pytest/reports.py index 8459c1cb9..178df6004 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -82,10 +82,11 @@ class BaseReport: .. 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/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 412f11edc..f0c7146c7 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 @@ raise ValueError() """ ) 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 @@ raise ValueError() 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", From 8d2d1c40f8030b337dedeabcb4a1e54fb5316176 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:50:14 +0300 Subject: [PATCH 14/26] terminalwriter: inline function _escaped Doesn't add much. --- src/_pytest/_io/terminalwriter.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 907b90543..d86b1aef7 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -118,11 +118,6 @@ class TerminalWriter: """ return self._width_of_current_line - def _escaped(self, text, esc): - if esc and self.hasmarkup: - text = "".join(["\x1b[%sm" % cod for cod in esc]) + text + "\x1b[0m" - return text - def markup(self, text, **kw): esc = [] for name in kw: @@ -130,7 +125,9 @@ class TerminalWriter: raise ValueError("unknown markup: {!r}".format(name)) if kw[name]: esc.append(self._esctable[name]) - return self._escaped(text, tuple(esc)) + if esc and self.hasmarkup: + text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" + return text def sep(self, sepchar, title=None, fullwidth=None, **kw): if fullwidth is None: From f6564a548a6587722be6d4122d96d9136ff0f4ee Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:55:08 +0300 Subject: [PATCH 15/26] terminalwriter: remove win32 specific code in favor of relying on colorama On Windows we already depend on colorama, which takes care of all of this custom code on its own. --- src/_pytest/_io/terminalwriter.py | 123 ++---------------------------- 1 file changed, 7 insertions(+), 116 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index d86b1aef7..2e213e93a 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -10,20 +10,6 @@ from typing import Sequence # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. -win32_and_ctypes = False -colorama = None -if sys.platform == "win32": - try: - import colorama - except ImportError: - try: - import ctypes - except ImportError: - pass - else: - win32_and_ctypes = True - - def get_terminal_width() -> int: width, _ = shutil.get_terminal_size(fallback=(80, 24)) @@ -85,8 +71,13 @@ class TerminalWriter: def __init__(self, file=None): if file is None: file = sys.stdout - if hasattr(file, "isatty") and file.isatty() and colorama: - file = colorama.AnsiToWin32(file).stream + if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32": + try: + import colorama + except ImportError: + pass + else: + file = colorama.AnsiToWin32(file).stream self._file = file self.hasmarkup = should_do_markup(file) self._chars_on_current_line = 0 @@ -216,103 +207,3 @@ class TerminalWriter: return source else: return highlight(source, PythonLexer(), TerminalFormatter(bg="dark")) - - -if win32_and_ctypes: - import ctypes # noqa: F811 - from ctypes import wintypes - from ctypes import windll # type: ignore[attr-defined] # noqa: F821 - - class Win32ConsoleWriter(TerminalWriter): - def write(self, msg: str, **kw): - if msg: - self._update_chars_on_current_line(msg) - - oldcolors = None - if self.hasmarkup and kw: - handle = GetStdHandle(STD_OUTPUT_HANDLE) - oldcolors = GetConsoleInfo(handle).wAttributes - default_bg = oldcolors & 0x00F0 - attr = default_bg - if kw.pop("bold", False): - attr |= FOREGROUND_INTENSITY - - if kw.pop("red", False): - attr |= FOREGROUND_RED - elif kw.pop("blue", False): - attr |= FOREGROUND_BLUE - elif kw.pop("green", False): - attr |= FOREGROUND_GREEN - elif kw.pop("yellow", False): - attr |= FOREGROUND_GREEN | FOREGROUND_RED - else: - attr |= oldcolors & 0x0007 - - SetConsoleTextAttribute(handle, attr) - self._file.write(msg) - self._file.flush() - if oldcolors: - SetConsoleTextAttribute(handle, oldcolors) - - TerminalWriter = Win32ConsoleWriter # type: ignore[misc] # noqa: F821 - - # ctypes access to the Windows console - STD_OUTPUT_HANDLE = -11 - STD_ERROR_HANDLE = -12 - FOREGROUND_BLACK = 0x0000 # black text - FOREGROUND_BLUE = 0x0001 # text color contains blue. - FOREGROUND_GREEN = 0x0002 # text color contains green. - FOREGROUND_RED = 0x0004 # text color contains red. - FOREGROUND_WHITE = 0x0007 - FOREGROUND_INTENSITY = 0x0008 # text color is intensified. - BACKGROUND_BLACK = 0x0000 # background color black - BACKGROUND_BLUE = 0x0010 # background color contains blue. - BACKGROUND_GREEN = 0x0020 # background color contains green. - BACKGROUND_RED = 0x0040 # background color contains red. - BACKGROUND_WHITE = 0x0070 - BACKGROUND_INTENSITY = 0x0080 # background color is intensified. - - SHORT = ctypes.c_short - - class COORD(ctypes.Structure): - _fields_ = [("X", SHORT), ("Y", SHORT)] - - class SMALL_RECT(ctypes.Structure): - _fields_ = [ - ("Left", SHORT), - ("Top", SHORT), - ("Right", SHORT), - ("Bottom", SHORT), - ] - - class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): - _fields_ = [ - ("dwSize", COORD), - ("dwCursorPosition", COORD), - ("wAttributes", wintypes.WORD), - ("srWindow", SMALL_RECT), - ("dwMaximumWindowSize", COORD), - ] - - _GetStdHandle = windll.kernel32.GetStdHandle - _GetStdHandle.argtypes = [wintypes.DWORD] - _GetStdHandle.restype = wintypes.HANDLE - - def GetStdHandle(kind): - return _GetStdHandle(kind) - - SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute - SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD] - SetConsoleTextAttribute.restype = wintypes.BOOL - - _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo - _GetConsoleScreenBufferInfo.argtypes = [ - wintypes.HANDLE, - ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), - ] - _GetConsoleScreenBufferInfo.restype = wintypes.BOOL - - def GetConsoleInfo(handle): - info = CONSOLE_SCREEN_BUFFER_INFO() - _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) - return info From e8fc5f99fa1a0b258da3713674a1a99948441eeb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 17:08:18 +0300 Subject: [PATCH 16/26] terminalwriter: add type annotations --- src/_pytest/_io/terminalwriter.py | 50 +++++++++++++++++-------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 2e213e93a..0ab6a31da 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -4,7 +4,9 @@ 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. @@ -26,12 +28,12 @@ def char_width(c: str) -> int: return 2 if unicodedata.east_asian_width(c) in ("F", "W") else 1 -def get_line_width(text): +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): +def should_do_markup(file: TextIO) -> bool: if os.environ.get("PY_COLORS") == "1": return True if os.environ.get("PY_COLORS") == "0": @@ -68,7 +70,7 @@ class TerminalWriter: invert=7, ) - def __init__(self, file=None): + 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": @@ -78,38 +80,33 @@ class TerminalWriter: pass else: file = colorama.AnsiToWin32(file).stream + assert file is not None self._file = file self.hasmarkup = should_do_markup(file) self._chars_on_current_line = 0 self._width_of_current_line = 0 @property - def fullwidth(self): + def fullwidth(self) -> int: if hasattr(self, "_terminal_width"): return self._terminal_width return get_terminal_width() @fullwidth.setter - def fullwidth(self, value): + def fullwidth(self, value: int) -> None: self._terminal_width = value @property - def chars_on_current_line(self): - """Return the number of characters written so far in the current line. - - :rtype: int - """ + def chars_on_current_line(self) -> int: + """Return the number of characters written so far in the current line.""" return self._chars_on_current_line @property - def width_of_current_line(self): - """Return an estimate of the width so far in the current line. - - :rtype: int - """ + def width_of_current_line(self) -> int: + """Return an estimate of the width so far in the current line.""" return self._width_of_current_line - def markup(self, text, **kw): + def markup(self, text: str, **kw: bool) -> str: esc = [] for name in kw: if name not in self._esctable: @@ -120,7 +117,13 @@ class TerminalWriter: text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" return text - def sep(self, sepchar, title=None, fullwidth=None, **kw): + def sep( + self, + sepchar: str, + title: Optional[str] = None, + fullwidth: Optional[int] = None, + **kw: bool + ) -> None: if fullwidth is None: fullwidth = self.fullwidth # the goal is to have the line be as long as possible @@ -151,7 +154,7 @@ class TerminalWriter: self.line(line, **kw) - def write(self, msg: str, **kw) -> None: + def write(self, msg: str, **kw: bool) -> None: if msg: self._update_chars_on_current_line(msg) @@ -171,7 +174,7 @@ class TerminalWriter: self._chars_on_current_line += len(current_line) self._width_of_current_line += get_line_width(current_line) - def line(self, s: str = "", **kw): + def line(self, s: str = "", **kw: bool) -> None: self.write(s, **kw) self.write("\n") @@ -195,8 +198,8 @@ class TerminalWriter: 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""" + def _highlight(self, source: str) -> str: + """Highlight the given source code if we have markup support.""" if not self.hasmarkup: return source try: @@ -206,4 +209,7 @@ class TerminalWriter: except ImportError: return source else: - return highlight(source, PythonLexer(), TerminalFormatter(bg="dark")) + highlighted = highlight( + source, PythonLexer(), TerminalFormatter(bg="dark") + ) # type: str + return highlighted From 8e04d35a3347a5e5b1152047d52e10032cfb509f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 17:09:13 +0300 Subject: [PATCH 17/26] terminalwriter: remove unneeded hasattr use --- src/_pytest/_io/terminalwriter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 0ab6a31da..a4989a7f0 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -85,10 +85,11 @@ class TerminalWriter: self.hasmarkup = should_do_markup(file) self._chars_on_current_line = 0 self._width_of_current_line = 0 + self._terminal_width = None # type: Optional[int] @property def fullwidth(self) -> int: - if hasattr(self, "_terminal_width"): + if self._terminal_width is not None: return self._terminal_width return get_terminal_width() From d9b43647b792ed71d92946edee17ded5d86eb6dc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 17:38:16 +0300 Subject: [PATCH 18/26] terminalwriter: inline function _update_chars_on_current_line --- src/_pytest/_io/terminalwriter.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index a4989a7f0..5db4dc8b6 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -157,7 +157,13 @@ class TerminalWriter: def write(self, msg: str, **kw: bool) -> None: if msg: - self._update_chars_on_current_line(msg) + current_line = msg.rsplit("\n", 1)[-1] + if "\n" in msg: + self._chars_on_current_line = len(current_line) + self._width_of_current_line = get_line_width(current_line) + else: + self._chars_on_current_line += len(current_line) + self._width_of_current_line += get_line_width(current_line) if self.hasmarkup and kw: markupmsg = self.markup(msg, **kw) @@ -166,15 +172,6 @@ class TerminalWriter: self._file.write(markupmsg) self._file.flush() - def _update_chars_on_current_line(self, text: str) -> None: - current_line = text.rsplit("\n", 1)[-1] - if "\n" in text: - self._chars_on_current_line = len(current_line) - self._width_of_current_line = get_line_width(current_line) - else: - self._chars_on_current_line += len(current_line) - self._width_of_current_line += get_line_width(current_line) - def line(self, s: str = "", **kw: bool) -> None: self.write(s, **kw) self.write("\n") From 1bc4170e63ca430a4f5e8532a53a71a060d115fa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 17:58:33 +0300 Subject: [PATCH 19/26] terminalwriter: don't flush implicitly; add explicit flushes Flushing on every write is somewhat expensive. Rely on line buffering instead (if line buffering for stdout is disabled, there must be some reason...), and add explicit flushes when not outputting lines. This is how regular `print()` e.g. work so should be familiar. --- src/_pytest/_io/terminalwriter.py | 8 ++++++-- src/_pytest/python.py | 2 +- src/_pytest/runner.py | 1 + src/_pytest/setuponly.py | 2 ++ src/_pytest/terminal.py | 18 ++++++++++++------ 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 5db4dc8b6..7bd8507c2 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -155,7 +155,7 @@ class TerminalWriter: self.line(line, **kw) - def write(self, msg: str, **kw: bool) -> None: + def write(self, msg: str, *, flush: bool = False, **kw: bool) -> None: if msg: current_line = msg.rsplit("\n", 1)[-1] if "\n" in msg: @@ -170,12 +170,16 @@ class TerminalWriter: else: markupmsg = msg self._file.write(markupmsg) - self._file.flush() + if flush: + self.flush() def line(self, s: str = "", **kw: bool) -> None: self.write(s, **kw) 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. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2b9bf4f5b..f472354ef 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/runner.py b/src/_pytest/runner.py index f87ccb461..76785ada7 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 aa5a95ff9..c9cc589ff 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 52c04a49c..d46456733 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -343,7 +343,7 @@ class TerminalReporter: 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 @@ class TerminalReporter: 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 @@ class TerminalReporter: 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 @@ class TerminalReporter: self._tw.write(word, **markup) self._tw.write(" " + line) self.currentfspath = -2 + self.flush() @property def _is_last_item(self): @@ -539,7 +545,7 @@ class TerminalReporter: 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): @@ -553,10 +559,10 @@ class TerminalReporter: 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: From dd32c72ff0a9b875e3efbd31e28d63feaac8f32d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 18:10:54 +0300 Subject: [PATCH 20/26] terminalwriter: remove unused property chars_on_current_line --- src/_pytest/_io/terminalwriter.py | 8 -------- src/_pytest/terminal.py | 6 +----- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 7bd8507c2..124ffe795 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -83,7 +83,6 @@ class TerminalWriter: assert file is not None self._file = file self.hasmarkup = should_do_markup(file) - self._chars_on_current_line = 0 self._width_of_current_line = 0 self._terminal_width = None # type: Optional[int] @@ -97,11 +96,6 @@ class TerminalWriter: def fullwidth(self, value: int) -> None: self._terminal_width = value - @property - def chars_on_current_line(self) -> int: - """Return the number of characters written so far in the current line.""" - return self._chars_on_current_line - @property def width_of_current_line(self) -> int: """Return an estimate of the width so far in the current line.""" @@ -159,10 +153,8 @@ class TerminalWriter: if msg: current_line = msg.rsplit("\n", 1)[-1] if "\n" in msg: - self._chars_on_current_line = len(current_line) self._width_of_current_line = get_line_width(current_line) else: - self._chars_on_current_line += len(current_line) self._width_of_current_line += get_line_width(current_line) if self.hasmarkup and kw: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index d46456733..39deaca55 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -550,11 +550,7 @@ class TerminalReporter: @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: From d5584c7207e094aa833358285f749e01f907aa00 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 18:14:57 +0300 Subject: [PATCH 21/26] terminalwriter: compute width_of_current_line lazily Currently this property is computed eagerly, which means get_line_width() is computed on everything written, but that is a slow function. Compute it lazily, so that get_line_width() only runs when needed. --- src/_pytest/_io/terminalwriter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 124ffe795..204222c88 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -83,7 +83,7 @@ class TerminalWriter: assert file is not None self._file = file self.hasmarkup = should_do_markup(file) - self._width_of_current_line = 0 + self._current_line = "" self._terminal_width = None # type: Optional[int] @property @@ -99,7 +99,7 @@ class TerminalWriter: @property def width_of_current_line(self) -> int: """Return an estimate of the width so far in the current line.""" - return self._width_of_current_line + return get_line_width(self._current_line) def markup(self, text: str, **kw: bool) -> str: esc = [] @@ -153,9 +153,9 @@ class TerminalWriter: if msg: current_line = msg.rsplit("\n", 1)[-1] if "\n" in msg: - self._width_of_current_line = get_line_width(current_line) + self._current_line = current_line else: - self._width_of_current_line += get_line_width(current_line) + self._current_line += current_line if self.hasmarkup and kw: markupmsg = self.markup(msg, **kw) From 0e36596268e33f55ef9e491e1f84301536c7e41a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 19:10:45 +0300 Subject: [PATCH 22/26] testing/io: port TerminalWriter tests from py --- testing/io/test_terminalwriter.py | 209 ++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 testing/io/test_terminalwriter.py diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py new file mode 100644 index 000000000..1e4f04ac0 --- /dev/null +++ b/testing/io/test_terminalwriter.py @@ -0,0 +1,209 @@ +import io +import os +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") + def test_markup(self, tw) -> None: + for bold in (True, False): + for color in ("red", "green"): + text2 = tw.markup("hello", **{color: True, "bold": bold}) + assert text2.find("hello") != -1 + 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 From bafc9bd58b0cd244ba23da51cea01a792c1ea641 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 14:47:34 +0300 Subject: [PATCH 23/26] testing: merge code/test_terminal_writer.py into io/test_terminalwriter.py --- testing/code/test_terminal_writer.py | 28 ---------------------------- testing/io/test_terminalwriter.py | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 28 deletions(-) delete mode 100644 testing/code/test_terminal_writer.py diff --git a/testing/code/test_terminal_writer.py b/testing/code/test_terminal_writer.py deleted file mode 100644 index 01da3c235..000000000 --- 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 index 1e4f04ac0..b3bd9cfae 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -1,5 +1,6 @@ import io import os +import re import shutil import sys from typing import Generator @@ -207,3 +208,26 @@ class TestTerminalWriterLineWidth: 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"], [" ", " "]) From 414a87a53f1b424c5e5073583e6cd978857a1d9b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 14:20:48 +0300 Subject: [PATCH 24/26] config/argparsing: use our own get_terminal_width() --- src/_pytest/_io/__init__.py | 2 ++ src/_pytest/config/argparsing.py | 3 ++- testing/test_config.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index 880c3c87a..db001e918 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -1,6 +1,8 @@ +from .terminalwriter import get_terminal_width from .terminalwriter import TerminalWriter __all__ = [ "TerminalWriter", + "get_terminal_width", ] diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 140e04e97..940eaa6a7 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -15,6 +15,7 @@ from typing import Union 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/testing/test_config.py b/testing/test_config.py index 9035407b7..0c05c4fad 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 From d8558e87c596b9ff7bbac829689f149a2030e33f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 14:40:59 +0300 Subject: [PATCH 25/26] terminalwriter: clean up markup function a bit --- src/_pytest/_io/terminalwriter.py | 31 ++++++++++++++----------------- testing/io/test_terminalwriter.py | 12 +++++++----- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 204222c88..4f22f5a7a 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -101,15 +101,14 @@ class TerminalWriter: """Return an estimate of the width so far in the current line.""" return get_line_width(self._current_line) - def markup(self, text: str, **kw: bool) -> str: - esc = [] - for name in kw: + 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 kw[name]: - esc.append(self._esctable[name]) - if esc and self.hasmarkup: - text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" + 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( @@ -117,7 +116,7 @@ class TerminalWriter: sepchar: str, title: Optional[str] = None, fullwidth: Optional[int] = None, - **kw: bool + **markup: bool ) -> None: if fullwidth is None: fullwidth = self.fullwidth @@ -147,9 +146,9 @@ class TerminalWriter: if len(line) + len(sepchar.rstrip()) <= fullwidth: line += sepchar.rstrip() - self.line(line, **kw) + self.line(line, **markup) - def write(self, msg: str, *, flush: bool = False, **kw: bool) -> None: + def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None: if msg: current_line = msg.rsplit("\n", 1)[-1] if "\n" in msg: @@ -157,16 +156,14 @@ class TerminalWriter: else: self._current_line += current_line - if self.hasmarkup and kw: - markupmsg = self.markup(msg, **kw) - else: - markupmsg = msg - self._file.write(markupmsg) + msg = self.markup(msg, **markup) + + self._file.write(msg) if flush: self.flush() - def line(self, s: str = "", **kw: bool) -> None: - self.write(s, **kw) + def line(self, s: str = "", **markup: bool) -> None: + self.write(s, **markup) self.write("\n") def flush(self) -> None: diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index b3bd9cfae..0e9cdb64d 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -112,11 +112,13 @@ class TestTerminalWriter: assert line == "- aaaaaaaaaa -\n" @pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi") - def test_markup(self, tw) -> None: - for bold in (True, False): - for color in ("red", "green"): - text2 = tw.markup("hello", **{color: True, "bold": bold}) - assert text2.find("hello") != -1 + @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): From e40bf1d1da6771f951bd4b6126fc3cb107a7c9e7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 16:40:03 +0300 Subject: [PATCH 26/26] Add a changelog for TerminalWriter changes --- changelog/7135.breaking.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 changelog/7135.breaking.rst diff --git a/changelog/7135.breaking.rst b/changelog/7135.breaking.rst new file mode 100644 index 000000000..4d5df4e40 --- /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.