diff --git a/py/code/excinfo.py b/py/code/excinfo.py index 4f73b092c..543e0a5b0 100644 --- a/py/code/excinfo.py +++ b/py/code/excinfo.py @@ -275,7 +275,7 @@ class ReprEntry(Repr): self.reprfuncargs.toterminal(tw) for line in self.lines: red = line.startswith("E ") - tw.line(tw.markup(bold=True, red=red, text=line)) + tw.line(line, bold=True, red=red) if self.reprlocals: #tw.sep(self.localssep, "Locals") tw.line("") diff --git a/py/io/terminalwriter.py b/py/io/terminalwriter.py index 536e7fa92..f21dc5e4a 100644 --- a/py/io/terminalwriter.py +++ b/py/io/terminalwriter.py @@ -14,6 +14,57 @@ def _getdimensions(): height,width = struct.unpack( "hhhh", call ) [:2] return height, width +if sys.platform == 'win32': + # ctypes access to the Windows console + + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + 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_BLUE = 0x0010 # background color contains blue. + BACKGROUND_GREEN = 0x0020 # background color contains green. + BACKGROUND_RED = 0x0040 # background color contains red. + BACKGROUND_WHITE = 0x0007 + BACKGROUND_INTENSITY = 0x0080 # background color is intensified. + + def GetStdHandle(kind): + import ctypes + return ctypes.windll.kernel32.GetStdHandle(kind) + + def SetConsoleTextAttribute(handle, attr): + import ctypes + ctypes.windll.kernel32.SetConsoleTextAttribute( + handle, attr) + + def _getdimensions(): + import ctypes + from ctypes import wintypes + + 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)] + STD_OUTPUT_HANDLE = -11 + handle = GetStdHandle(STD_OUTPUT_HANDLE) + info = CONSOLE_SCREEN_BUFFER_INFO() + ctypes.windll.kernel32.GetConsoleScreenBufferInfo( + handle, ctypes.byref(info)) + return info.dwSize.Y, info.dwSize.X + def get_terminal_width(): try: height, width = _getdimensions() @@ -31,15 +82,46 @@ 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(): - if not isinstance(esc, tuple): - esc = (esc,) text = (''.join(['\x1b[%sm' % cod for cod in esc]) + text + '\x1b[0m') # ANSI color code "reset" if newline: text += '\n' - file.write(text) + + if esc and sys.platform == "win32" 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) + SetConsoleTextAttribute(handle, attr) + file.write(text) + SetConsoleTextAttribute(handle, FOREGROUND_WHITE) + else: + file.write(text) + if flush: file.flush() @@ -60,8 +142,7 @@ class TerminalWriter(object): file = WriteFile(file) self._file = file self.fullwidth = get_terminal_width() - self.hasmarkup = sys.platform != "win32" and \ - hasattr(file, 'isatty') and file.isatty() + self.hasmarkup = hasattr(file, 'isatty') and file.isatty() def _escaped(self, text, esc): if esc and self.hasmarkup: @@ -119,6 +200,81 @@ class TerminalWriter(object): self._file.write('\n') self._file.flush() + +class Win32ConsoleWriter(object): + + def __init__(self, file=None, stringio=False): + if file is None: + if stringio: + self.stringio = file = py.std.cStringIO.StringIO() + else: + file = py.std.sys.stdout + elif callable(file): + file = WriteFile(file) + self._file = file + self.fullwidth = get_terminal_width() + self.hasmarkup = hasattr(file, 'isatty') and file.isatty() + + def sep(self, sepchar, title=None, fullwidth=None, **kw): + if fullwidth is None: + fullwidth = self.fullwidth + # On a Windows console, writing in the last column + # causes a line feed. + fullwidth -= 1 + # the goal is to have the line be as long as possible + # under the condition that len(line) <= fullwidth + 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 = (fullwidth - len(title) - 2) // (2*len(sepchar)) + 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, s, **kw): + if s: + s = str(s) + if self.hasmarkup: + handle = GetStdHandle(STD_OUTPUT_HANDLE) + + if self.hasmarkup and kw: + attr = 0 + 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 + else: + attr |= FOREGROUND_WHITE + + SetConsoleTextAttribute(handle, attr) + self._file.write(s) + self._file.flush() + if self.hasmarkup: + SetConsoleTextAttribute(handle, FOREGROUND_WHITE) + + def line(self, s='', **kw): + self.write(s + '\n', **kw) + +if sys.platform == 'win32': + TerminalWriter = Win32ConsoleWriter + class WriteFile(object): def __init__(self, writemethod): self.write = writemethod diff --git a/py/io/testing/test_terminalwriter.py b/py/io/testing/test_terminalwriter.py index 0577189c6..3fcffbc2d 100644 --- a/py/io/testing/test_terminalwriter.py +++ b/py/io/testing/test_terminalwriter.py @@ -1,7 +1,11 @@ import py -import os +import os, sys from py.__.io import terminalwriter +def skip_win32(): + if sys.platform == 'win32': + py.test.skip('Not relevant on win32') + def test_terminalwriter_computes_width(): py.magic.patch(terminalwriter, 'get_terminal_width', lambda: 42) try: @@ -36,6 +40,7 @@ class BaseTests: tw.sep("-", fullwidth=60) l = self.getlines() assert len(l) == 1 + skip_win32() assert l[0] == "-" * 60 + "\n" def test_sep_with_title(self): @@ -43,14 +48,17 @@ class BaseTests: tw.sep("-", "hello", fullwidth=60) l = self.getlines() assert len(l) == 1 + skip_win32() assert l[0] == "-" * 26 + " hello " + "-" * 27 + "\n" def test__escaped(self): + skip_win32() tw = self.getwriter() text2 = tw._escaped("hello", (31)) assert text2.find("hello") != -1 def test_markup(self): + skip_win32() tw = self.getwriter() for bold in (True, False): for color in ("red", "green"): @@ -65,6 +73,7 @@ class BaseTests: tw.line("x", bold=True) tw.write("x\n", red=True) l = self.getlines() + skip_win32() assert len(l[0]) > 2, l assert len(l[1]) > 2, l diff --git a/py/test/plugin/pytest_terminal.py b/py/test/plugin/pytest_terminal.py index d842bb389..5cd5e7373 100644 --- a/py/test/plugin/pytest_terminal.py +++ b/py/test/plugin/pytest_terminal.py @@ -40,13 +40,13 @@ class TerminalReporter: self.currentfspath = fspath self._tw.write(res) - def write_ensure_prefix(self, prefix, extra=""): + def write_ensure_prefix(self, prefix, extra="", **kwargs): if self.currentfspath != prefix: self._tw.line() self.currentfspath = prefix self._tw.write(prefix) if extra: - self._tw.write(extra) + self._tw.write(extra, **kwargs) self.currentfspath = -2 def ensure_newline(self): @@ -77,13 +77,13 @@ class TerminalReporter: def getoutcomeword(self, event): if event.passed: - return self._tw.markup("PASS", green=True) + return "PASS", dict(green=True) elif event.failed: - return self._tw.markup("FAIL", red=True) + return "FAIL", dict(red=True) elif event.skipped: return "SKIP" else: - return self._tw.markup("???", red=True) + return "???", dict(red=True) def pyevent_internalerror(self, event): for line in str(event.repr).split("\n"): @@ -139,13 +139,17 @@ class TerminalReporter: def pyevent_itemtestreport(self, event): fspath = event.colitem.fspath cat, letter, word = self.getcategoryletterword(event) + if isinstance(word, tuple): + word, markup = word + else: + markup = {} self.stats.setdefault(cat, []).append(event) if not self.config.option.verbose: self.write_fspath_result(fspath, letter) else: info = event.colitem.repr_metainfo() line = info.verboseline(basedir=self.curdir) + " " - self.write_ensure_prefix(line, word) + self.write_ensure_prefix(line, word, **markup) def pyevent_collectionreport(self, event): if not event.passed: