From 7d3d3b9b00366755023a8d5d6eb48844b62d3f04 Mon Sep 17 00:00:00 2001 From: Segev Finer Date: Sun, 30 Jul 2017 21:41:52 +0300 Subject: [PATCH 1/7] Use ENABLE_VIRTUAL_TERMINAL_PROCESSING when available --- colorama/ansitowin32.py | 10 ++++++--- colorama/win32.py | 26 +++++++++++++++++++++++ colorama/winterm.py | 46 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/colorama/ansitowin32.py b/colorama/ansitowin32.py index 6039a054..9941f6d1 100644 --- a/colorama/ansitowin32.py +++ b/colorama/ansitowin32.py @@ -4,7 +4,7 @@ import os from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style, BEL -from .winterm import WinTerm, WinColor, WinStyle +from .winterm import enable_vt_processing, WinTerm, WinColor, WinStyle from .win32 import windll, winapi_test @@ -80,6 +80,8 @@ def __init__(self, wrapped, convert=None, strip=None, autoreset=False): # create the proxy wrapping our output stream self.stream = StreamWrapper(wrapped, self) + vt_processing_supported = enable_vt_processing() + on_windows = os.name == 'nt' # We test if the WinAPI works, because even if we are on Windows # we may be using a terminal that doesn't support the WinAPI @@ -89,12 +91,14 @@ def __init__(self, wrapped, convert=None, strip=None, autoreset=False): # should we strip ANSI sequences from our output? if strip is None: - strip = conversion_supported or (not self.stream.closed and not self.stream.isatty()) + strip = ((not vt_processing_supported and conversion_supported) or + (not self.stream.closed and not self.stream.isatty())) self.strip = strip # should we should convert ANSI sequences into win32 calls? if convert is None: - convert = conversion_supported and not self.stream.closed and self.stream.isatty() + convert = (not vt_processing_supported and conversion_supported and + not self.stream.closed and self.stream.isatty()) self.convert = convert # dict of ansi codes to win32 functions and parameters diff --git a/colorama/win32.py b/colorama/win32.py index c2d83603..1aeb6391 100644 --- a/colorama/win32.py +++ b/colorama/win32.py @@ -16,6 +16,8 @@ else: from ctypes import byref, Structure, c_char, POINTER + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + COORD = wintypes._COORD class CONSOLE_SCREEN_BUFFER_INFO(Structure): @@ -89,6 +91,20 @@ def __str__(self): ] _SetConsoleTitleW.restype = wintypes.BOOL + _GetConsoleMode = windll.kernel32.GetConsoleMode + _GetConsoleMode.argtypes = [ + wintypes.HANDLE, + POINTER(wintypes.DWORD) + ] + _GetConsoleMode.restype = wintypes.BOOL + + _SetConsoleMode = windll.kernel32.SetConsoleMode + _SetConsoleMode.argtypes = [ + wintypes.HANDLE, + wintypes.DWORD + ] + _SetConsoleMode.restype = wintypes.BOOL + def _winapi_test(handle): csbi = CONSOLE_SCREEN_BUFFER_INFO() success = _GetConsoleScreenBufferInfo( @@ -150,3 +166,13 @@ def FillConsoleOutputAttribute(stream_id, attr, length, start): def SetConsoleTitle(title): return _SetConsoleTitleW(title) + + def GetConsoleMode(stream_id): + handle = _GetStdHandle(stream_id) + mode = wintypes.DWORD() + success = _GetConsoleMode(handle, byref(mode)) + return mode.value + + def SetConsoleMode(stream_id, mode): + handle = _GetStdHandle(stream_id) + return _SetConsoleMode(handle, mode) diff --git a/colorama/winterm.py b/colorama/winterm.py index 0fdb4ec4..95b36c95 100644 --- a/colorama/winterm.py +++ b/colorama/winterm.py @@ -1,4 +1,5 @@ # Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import atexit from . import win32 @@ -167,3 +168,48 @@ def erase_line(self, mode=0, on_stderr=False): def set_title(self, title): win32.SetConsoleTitle(title) + + +_enabled_vt_processing = False +_atexit_registered = False + + +def enable_vt_processing(): + global _enabled_vt_processing, _atexit_registered + + if win32.windll is None or not win32.winapi_test(): + return False + + mode = win32.GetConsoleMode(win32.STDOUT) + + if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING: + return True + + if not win32.SetConsoleMode( + win32.STDOUT, + mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING): + return False # Unsupported + + _enabled_vt_processing = True + + if not _atexit_registered: + atexit.register(lambda: restore_vt_processing(True)) + _atexit_registered = True + + return True + + +def restore_vt_processing(atexit=False): + global _enabled_vt_processing, _atexit_registered + + if atexit: + _atexit_registered = False + + if win32.windll is None or not win32.winapi_test(): + return + + if _enabled_vt_processing: + win32.SetConsoleMode( + win32.STDOUT, + win32.GetConsoleMode(win32.STDOUT) & ~win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING) + _enabled_vt_processing = False From 7ec896d86b4720dfd97f31c9152005bffed3e92b Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 25 Aug 2022 15:28:30 -0700 Subject: [PATCH 2/7] Check return value from GetConsoleMode/SetConsoleMode --- colorama/win32.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/colorama/win32.py b/colorama/win32.py index 1aeb6391..6bd96b9f 100644 --- a/colorama/win32.py +++ b/colorama/win32.py @@ -171,8 +171,12 @@ def GetConsoleMode(stream_id): handle = _GetStdHandle(stream_id) mode = wintypes.DWORD() success = _GetConsoleMode(handle, byref(mode)) + if not success: + raise ctypes.WinError() return mode.value def SetConsoleMode(stream_id, mode): handle = _GetStdHandle(stream_id) - return _SetConsoleMode(handle, mode) + success = _SetConsoleMode(handle, mode) + if not success: + raise ctypes.WinError() From 5c8f98ab2ea727a39de76a65b0f3c0e714578e60 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 6 Oct 2022 23:19:19 -0700 Subject: [PATCH 3/7] Simplify enable_vt_processing by removing atexit handling See https://github.com/tartley/colorama/pull/139#issuecomment-1227813147 for rationale. --- colorama/winterm.py | 49 +++++++++++---------------------------------- 1 file changed, 12 insertions(+), 37 deletions(-) diff --git a/colorama/winterm.py b/colorama/winterm.py index 95b36c95..0308cb0a 100644 --- a/colorama/winterm.py +++ b/colorama/winterm.py @@ -1,5 +1,4 @@ # Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -import atexit from . import win32 @@ -170,46 +169,22 @@ def set_title(self, title): win32.SetConsoleTitle(title) -_enabled_vt_processing = False -_atexit_registered = False - - def enable_vt_processing(): - global _enabled_vt_processing, _atexit_registered - if win32.windll is None or not win32.winapi_test(): return False - mode = win32.GetConsoleMode(win32.STDOUT) - - if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING: - return True - - if not win32.SetConsoleMode( - win32.STDOUT, - mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING): - return False # Unsupported - - _enabled_vt_processing = True - - if not _atexit_registered: - atexit.register(lambda: restore_vt_processing(True)) - _atexit_registered = True - - return True + try: + mode = win32.GetConsoleMode(win32.STDOUT) + if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING: + return True - -def restore_vt_processing(atexit=False): - global _enabled_vt_processing, _atexit_registered - - if atexit: - _atexit_registered = False - - if win32.windll is None or not win32.winapi_test(): - return - - if _enabled_vt_processing: win32.SetConsoleMode( win32.STDOUT, - win32.GetConsoleMode(win32.STDOUT) & ~win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING) - _enabled_vt_processing = False + mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING, + ) + + mode = win32.GetConsoleMode(win32.STDOUT) + if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING: + return True + except OSError: + return False From b3fca6f5dd8964321d80ec2425ef9b160a69d093 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 6 Oct 2022 23:20:53 -0700 Subject: [PATCH 4/7] Refactor conditionals to make them more readable --- colorama/ansitowin32.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/colorama/ansitowin32.py b/colorama/ansitowin32.py index 9941f6d1..2b7b2c11 100644 --- a/colorama/ansitowin32.py +++ b/colorama/ansitowin32.py @@ -80,25 +80,24 @@ def __init__(self, wrapped, convert=None, strip=None, autoreset=False): # create the proxy wrapping our output stream self.stream = StreamWrapper(wrapped, self) - vt_processing_supported = enable_vt_processing() - on_windows = os.name == 'nt' # We test if the WinAPI works, because even if we are on Windows # we may be using a terminal that doesn't support the WinAPI # (e.g. Cygwin Terminal). In this case it's up to the terminal # to support the ANSI codes. conversion_supported = on_windows and winapi_test() + native_ansi_supported = not on_windows or enable_vt_processing() + have_tty = not self.stream.closed and self.stream.isatty() + need_conversion = conversion_supported and not native_ansi_supported # should we strip ANSI sequences from our output? if strip is None: - strip = ((not vt_processing_supported and conversion_supported) or - (not self.stream.closed and not self.stream.isatty())) + strip = need_conversion or not have_tty self.strip = strip # should we should convert ANSI sequences into win32 calls? if convert is None: - convert = (not vt_processing_supported and conversion_supported and - not self.stream.closed and self.stream.isatty()) + convert = need_conversion and have_tty self.convert = convert # dict of ansi codes to win32 functions and parameters From db005539a7d1791159024ecd5f9e46e8bed66e75 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Fri, 7 Oct 2022 00:13:47 -0700 Subject: [PATCH 5/7] Fix native ANSI when stderr=console, stdout=not console + basic test --- colorama/ansitowin32.py | 8 +++-- colorama/tests/ansitowin32_test.py | 47 ++++++++++++++++++++++++++++++ colorama/win32.py | 10 +++---- colorama/winterm.py | 20 ++++++++----- 4 files changed, 69 insertions(+), 16 deletions(-) diff --git a/colorama/ansitowin32.py b/colorama/ansitowin32.py index 2b7b2c11..0cf7c397 100644 --- a/colorama/ansitowin32.py +++ b/colorama/ansitowin32.py @@ -86,9 +86,13 @@ def __init__(self, wrapped, convert=None, strip=None, autoreset=False): # (e.g. Cygwin Terminal). In this case it's up to the terminal # to support the ANSI codes. conversion_supported = on_windows and winapi_test() - native_ansi_supported = not on_windows or enable_vt_processing() + try: + fd = wrapped.fileno() + except Exception: + fd = -1 + system_has_native_ansi = not on_windows or enable_vt_processing(fd) have_tty = not self.stream.closed and self.stream.isatty() - need_conversion = conversion_supported and not native_ansi_supported + need_conversion = conversion_supported and not system_has_native_ansi # should we strip ANSI sequences from our output? if strip is None: diff --git a/colorama/tests/ansitowin32_test.py b/colorama/tests/ansitowin32_test.py index bbe99f47..66a22660 100644 --- a/colorama/tests/ansitowin32_test.py +++ b/colorama/tests/ansitowin32_test.py @@ -1,6 +1,7 @@ # Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. from io import StringIO from unittest import TestCase, main +from contextlib import ExitStack try: from unittest.mock import MagicMock, Mock, patch @@ -8,6 +9,7 @@ from mock import MagicMock, Mock, patch from ..ansitowin32 import AnsiToWin32, StreamWrapper +from ..win32 import ENABLE_VIRTUAL_TERMINAL_PROCESSING from .utils import osname @@ -228,5 +230,50 @@ def test_osc_codes(self): stream.write(code) self.assertEqual(winterm.set_title.call_count, 2) + def test_native_windows_ansi(self): + with ExitStack() as stack: + def p(a, b): + stack.enter_context(patch(a, b, create=True)) + # Pretend to be on Windows + p("colorama.ansitowin32.os.name", "nt") + p("colorama.ansitowin32.winapi_test", lambda: True) + p("colorama.win32.winapi_test", lambda: True) + p("colorama.winterm.win32.windll", "non-None") + p("colorama.winterm.get_osfhandle", lambda _: 1234) + + # Pretend that our mock stream has native ANSI support + p( + "colorama.winterm.win32.GetConsoleMode", + lambda _: ENABLE_VIRTUAL_TERMINAL_PROCESSING, + ) + SetConsoleMode = Mock() + p("colorama.winterm.win32.SetConsoleMode", SetConsoleMode) + + stdout = Mock() + stdout.closed = False + stdout.isatty.return_value = True + stdout.fileno.return_value = 1 + + # Our fake console says it has native vt support, so AnsiToWin32 should + # enable that support and do nothing else. + stream = AnsiToWin32(stdout) + SetConsoleMode.assert_called_with(1234, ENABLE_VIRTUAL_TERMINAL_PROCESSING) + self.assertFalse(stream.strip) + self.assertFalse(stream.convert) + self.assertFalse(stream.should_wrap()) + + # Now let's pretend we're on an old Windows console, that doesn't have + # native ANSI support. + p("colorama.winterm.win32.GetConsoleMode", lambda _: 0) + SetConsoleMode = Mock() + p("colorama.winterm.win32.SetConsoleMode", SetConsoleMode) + + stream = AnsiToWin32(stdout) + SetConsoleMode.assert_called_with(1234, ENABLE_VIRTUAL_TERMINAL_PROCESSING) + self.assertTrue(stream.strip) + self.assertTrue(stream.convert) + self.assertTrue(stream.should_wrap()) + + if __name__ == '__main__': main() diff --git a/colorama/win32.py b/colorama/win32.py index 6bd96b9f..841b0e27 100644 --- a/colorama/win32.py +++ b/colorama/win32.py @@ -4,6 +4,8 @@ STDOUT = -11 STDERR = -12 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + try: import ctypes from ctypes import LibraryLoader @@ -16,8 +18,6 @@ else: from ctypes import byref, Structure, c_char, POINTER - ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 - COORD = wintypes._COORD class CONSOLE_SCREEN_BUFFER_INFO(Structure): @@ -167,16 +167,14 @@ def FillConsoleOutputAttribute(stream_id, attr, length, start): def SetConsoleTitle(title): return _SetConsoleTitleW(title) - def GetConsoleMode(stream_id): - handle = _GetStdHandle(stream_id) + def GetConsoleMode(handle): mode = wintypes.DWORD() success = _GetConsoleMode(handle, byref(mode)) if not success: raise ctypes.WinError() return mode.value - def SetConsoleMode(stream_id, mode): - handle = _GetStdHandle(stream_id) + def SetConsoleMode(handle, mode): success = _SetConsoleMode(handle, mode) if not success: raise ctypes.WinError() diff --git a/colorama/winterm.py b/colorama/winterm.py index 0308cb0a..fd7202c0 100644 --- a/colorama/winterm.py +++ b/colorama/winterm.py @@ -1,6 +1,12 @@ # Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -from . import win32 +try: + from msvcrt import get_osfhandle +except ImportError: + def get_osfhandle(_): + raise OSError("This isn't windows!") + +from . import win32 # from wincon.h class WinColor(object): @@ -169,21 +175,19 @@ def set_title(self, title): win32.SetConsoleTitle(title) -def enable_vt_processing(): +def enable_vt_processing(fd): if win32.windll is None or not win32.winapi_test(): return False try: - mode = win32.GetConsoleMode(win32.STDOUT) - if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING: - return True - + handle = get_osfhandle(fd) + mode = win32.GetConsoleMode(handle) win32.SetConsoleMode( - win32.STDOUT, + handle, mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING, ) - mode = win32.GetConsoleMode(win32.STDOUT) + mode = win32.GetConsoleMode(handle) if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING: return True except OSError: From 7b06987f6dbcdaf4ffcfa4aa793a48642180cb0b Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 13 Oct 2022 17:55:44 -0700 Subject: [PATCH 6/7] Use ExitStack backport on python 2 in tests --- colorama/tests/ansitowin32_test.py | 6 +++++- requirements-dev.txt | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/colorama/tests/ansitowin32_test.py b/colorama/tests/ansitowin32_test.py index 66a22660..380a99b7 100644 --- a/colorama/tests/ansitowin32_test.py +++ b/colorama/tests/ansitowin32_test.py @@ -1,7 +1,11 @@ # Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. from io import StringIO from unittest import TestCase, main -from contextlib import ExitStack +try: + from contextlib import ExitStack +except ImportError: + # python 2 + from contextlib2 import ExitStack try: from unittest.mock import MagicMock, Mock, patch diff --git a/requirements-dev.txt b/requirements-dev.txt index dae7e978..296bb90c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ mock>=1.0.1;python_version<"3.3" twine>=3.1.1 +contextlib2;python_version<"3" -e . From e5f74dea6d766b921e526a4bae824725f6ddf9bc Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 13 Oct 2022 18:00:41 -0700 Subject: [PATCH 7/7] Install contextlib2 in tox as well --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 179745ac..0f7e3a3b 100644 --- a/tox.ini +++ b/tox.ini @@ -3,5 +3,7 @@ isolated_build = true envlist = py27, py37, py38, py39, py310, pypy, pypy3 [testenv] -deps = py27,pypy: mock +deps = + py27,pypy: mock + py27,pypy: contextlib2 commands = python -m unittest discover -p *_test.py