diff --git a/.gitignore b/.gitignore index 27405b9..8d0004b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ build. /cov.xml /test.junit.xml flake8.err.log +/.eggs diff --git a/MuPythonLibrary/MuAnsiHandler.py b/MuPythonLibrary/MuAnsiHandler.py index 66f1b63..5011ac7 100644 --- a/MuPythonLibrary/MuAnsiHandler.py +++ b/MuPythonLibrary/MuAnsiHandler.py @@ -29,11 +29,13 @@ import re import os try: + # try to import windows types from winDLL import ctypes from ctypes import LibraryLoader windll = LibraryLoader(ctypes.WinDLL) from ctypes import wintypes except (AttributeError, ImportError): + # if we run into an exception (ie on unix or linux) windll = None # create blank lambda @@ -45,8 +47,77 @@ def winapi_test(): None else: + # if we don't raise an exception when we import windows types + # then execute this but don't catch an exception if raised from ctypes import byref, Structure + # inspired by https://github.com/tartley/colorama/ + class CONSOLE_SCREEN_BUFFER_INFO(Structure): + COORD = wintypes._COORD + """struct in wincon.h.""" + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", wintypes.WORD), + ("srWindow", wintypes.SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] + + def __str__(self): + return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % ( + self.dwSize.Y, self.dwSize.X, + self.dwCursorPosition.Y, self.dwCursorPosition.X, + self.wAttributes, + self.srWindow.Top, self.srWindow.Left, + self.srWindow.Bottom, self.srWindow.Right, + self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X + ) + + # a simple wrapper around the few methods calls to windows + class Win32Console(object): + _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo + _SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute + _SetConsoleTextAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, + ] + _SetConsoleTextAttribute.restype = wintypes.BOOL + _GetStdHandle = windll.kernel32.GetStdHandle + _GetStdHandle.argtypes = [ + wintypes.DWORD, + ] + _GetStdHandle.restype = wintypes.HANDLE + + # from winbase.h + STDOUT = -11 + STDERR = -12 + + @staticmethod + def _winapi_test(handle): + csbi = CONSOLE_SCREEN_BUFFER_INFO() + success = Win32Console._GetConsoleScreenBufferInfo( + handle, byref(csbi)) + return bool(success) + + @staticmethod + def winapi_test(): + return any(Win32Console._winapi_test(h) for h in + (Win32Console._GetStdHandle(Win32Console.STDOUT), + Win32Console._GetStdHandle(Win32Console.STDERR))) + + @staticmethod + def GetConsoleScreenBufferInfo(stream_id=STDOUT): + handle = Win32Console._GetStdHandle(stream_id) + csbi = CONSOLE_SCREEN_BUFFER_INFO() + Win32Console._GetConsoleScreenBufferInfo( + handle, byref(csbi)) + return csbi + + @staticmethod + def SetConsoleTextAttribute(stream_id, attrs): + handle = Win32Console._GetStdHandle(stream_id) + return Win32Console._SetConsoleTextAttribute(handle, attrs) + # from wincon.h class WinColor(object): @@ -166,82 +237,13 @@ def get_ansi_string(color=AnsiColor.RESET): return CSI + str(color) + 'm' -# inspired by https://github.com/tartley/colorama/ -class CONSOLE_SCREEN_BUFFER_INFO(Structure): - COORD = wintypes._COORD - """struct in wincon.h.""" - _fields_ = [ - ("dwSize", COORD), - ("dwCursorPosition", COORD), - ("wAttributes", wintypes.WORD), - ("srWindow", wintypes.SMALL_RECT), - ("dwMaximumWindowSize", COORD), - ] - - def __str__(self): - return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % ( - self.dwSize.Y, self.dwSize.X, - self.dwCursorPosition.Y, self.dwCursorPosition.X, - self.wAttributes, - self.srWindow.Top, self.srWindow.Left, - self.srWindow.Bottom, self.srWindow.Right, - self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X - ) - - -# a simple wrapper around the few methods calls to windows -class Win32Console(object): - _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo - _SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute - _SetConsoleTextAttribute.argtypes = [ - wintypes.HANDLE, - wintypes.WORD, - ] - _SetConsoleTextAttribute.restype = wintypes.BOOL - _GetStdHandle = windll.kernel32.GetStdHandle - _GetStdHandle.argtypes = [ - wintypes.DWORD, - ] - _GetStdHandle.restype = wintypes.HANDLE - - # from winbase.h - STDOUT = -11 - STDERR = -12 - - @staticmethod - def _winapi_test(handle): - csbi = CONSOLE_SCREEN_BUFFER_INFO() - success = Win32Console._GetConsoleScreenBufferInfo( - handle, byref(csbi)) - return bool(success) - - @staticmethod - def winapi_test(): - return any(Win32Console._winapi_test(h) for h in - (Win32Console._GetStdHandle(Win32Console.STDOUT), - Win32Console._GetStdHandle(Win32Console.STDERR))) - - @staticmethod - def GetConsoleScreenBufferInfo(stream_id=STDOUT): - handle = Win32Console._GetStdHandle(stream_id) - csbi = CONSOLE_SCREEN_BUFFER_INFO() - Win32Console._GetConsoleScreenBufferInfo( - handle, byref(csbi)) - return csbi - - @staticmethod - def SetConsoleTextAttribute(stream_id, attrs): - handle = Win32Console._GetStdHandle(stream_id) - return Win32Console._SetConsoleTextAttribute(handle, attrs) - - class ColoredStreamHandler(logging.StreamHandler): # Control Sequence Introducer ANSI_CSI_RE = re.compile('\001?\033\\[((?:\\d|;)*)([a-zA-Z])\002?') def __init__(self, stream=None, strip=None, convert=None): - logging.StreamHandler.__init__(self) + logging.StreamHandler.__init__(self, stream) self.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 @@ -251,23 +253,28 @@ def __init__(self, stream=None, strip=None, convert=None): self.strip = False # should we strip ANSI sequences from our output? if strip is None: - self.strip = self.conversion_supported or ( + strip = self.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 = (self.conversion_supported and not self.stream.closed and self.stream.isatty()) self.convert = convert + self.win32_calls = None - self.win32_calls = self.get_win32_calls() + if stream is not None: + self.stream = stream - self._light = 0 - self._default = Win32Console.GetConsoleScreenBufferInfo( - Win32Console.STDOUT).wAttributes - self.set_attrs(self._default) - self._default_fore = self._fore - self._default_back = self._back - self._default_style = self._style + if self.on_windows: + self.win32_calls = self.get_win32_calls() + self._light = 0 + self._default = Win32Console.GetConsoleScreenBufferInfo( + Win32Console.STDOUT).wAttributes + self.set_attrs(self._default) + self._default_fore = self._fore + self._default_back = self._back + self._default_style = self._style def get_win32_calls(self): if self.convert: @@ -407,8 +414,13 @@ def call_win32(self, command, params): # logging.handler method we are overriding to emit a record def emit(self, record): try: + if record is None: + return msg = self.format(record) - self.write(msg + self.terminator) + if msg is None: + return + self.write(str(msg)) + self.write(self.terminator) self.flush() except Exception: self.handleError(record) diff --git a/MuPythonLibrary/MuAnsiHandler_test.py b/MuPythonLibrary/MuAnsiHandler_test.py new file mode 100644 index 0000000..a8b7874 --- /dev/null +++ b/MuPythonLibrary/MuAnsiHandler_test.py @@ -0,0 +1,77 @@ +import unittest +import logging +from MuPythonLibrary.MuAnsiHandler import ColoredFormatter +from MuPythonLibrary.MuAnsiHandler import ColoredStreamHandler + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + + +class MuAnsiHandlerTest(unittest.TestCase): + + # we are mainly looking for exception to be thrown + + record = logging.makeLogRecord({"name": "", "level": logging.CRITICAL, "levelno": logging.CRITICAL, + "levelname": "CRITICAL", "path": "test_path", "lineno": 0, + "msg": "Test message"}) + record2 = logging.makeLogRecord({"name": "", "level": logging.INFO, "levelno": logging.INFO, + "levelname": "INFO", "path": "test_path", "lineno": 0, + "msg": "Test message"}) + + def test_colored_formatter_init(self): + formatter = ColoredFormatter("%(levelname)s - %(message)s") + # if we didn't throw an exception, then we are good + self.assertNotEqual(formatter, None) + + def test_colored_formatter_to_output_ansi(self): + formatter = ColoredFormatter("%(levelname)s - %(message)s") + + output = formatter.format(MuAnsiHandlerTest.record) + self.assertNotEqual(output, None) + CSI = '\033[' + self.assertGreater(len(output), 0, "We should have some output") + self.assertFalse((CSI not in output), "There was supposed to be a ANSI control code in that %s" % output) + + def test_color_handler_to_strip_ansi(self): + stream = StringIO() + # make sure we set out handler to strip the control sequence + handler = ColoredStreamHandler(stream, strip=True, convert=False) + formatter = ColoredFormatter("%(levelname)s - %(message)s") + handler.formatter = formatter + handler.level = logging.NOTSET + + handler.emit(MuAnsiHandlerTest.record) + handler.flush() + + CSI = '\033[' + + # check for ANSI escape code in stream + stream.seek(0) + lines = stream.readlines() + self.assertGreater(len(lines), 0, "We should have some output %s" % lines) + for line in lines: + if CSI in line: + self.fail("A control sequence was not stripped! %s" % lines) + + def test_color_handler_not_strip_ansi(self): + stream = StringIO() + formatter = ColoredFormatter("%(levelname)s - %(message)s") + handler = ColoredStreamHandler(stream, strip=False, convert=False) + handler.formatter = formatter + handler.level = logging.NOTSET + + handler.emit(MuAnsiHandlerTest.record2) + handler.flush() + + CSI = '\033[' + + found_csi = False + stream.seek(0) + lines = stream.readlines() + self.assertGreater(len(lines), 0, "We should have some output %s" % lines) + for line in lines: + if CSI in line: + found_csi = True + self.assertTrue(found_csi, "We are supposed to to have found an ANSI control character %s" % lines) diff --git a/README.rst b/README.rst index a9414d4..23af0cf 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,12 @@ ================= MU Python Library ================= +.. |build_status_windows| image:: https://dev.azure.com/projectmu/mu%20pip/_apis/build/status/PythonLibrary/Mu%20Pip%20Python%20Library%20-%20PR%20Gate%20(Windows)?branchName=master +.. |build_status_linux| image:: https://dev.azure.com/projectmu/mu%20pip/_apis/build/status/PythonLibrary/Mu%20Pip%20Python%20Library%20-%20PR%20Gate%20(Linux%20-%20Ubuntu%201604)?branchName=master + +|build_status_windows| Current build status for master on Windows + +|build_status_linux| Current build status for master on Linux About ===== @@ -19,4 +25,6 @@ Updated documentation and release process. Transition to Beta. < 0.3.0 ------- -Alpha development \ No newline at end of file +Alpha development + +[![Build Status](https://dev.azure.com/projectmu/mu%20pip/_apis/build/status/PythonLibrary/Mu%20Pip%20Python%20Library%20-%20PR%20Gate%20(Windows)?branchName=master)](https://dev.azure.com/projectmu/mu%20pip/_build/latest?definitionId=13?branchName=master) \ No newline at end of file