Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

Commit

Permalink
Attempted to fix MuAnsiHandler on non-windows platforms (#2)
Browse files Browse the repository at this point in the history
* Attempted to fix mu_ansi_handler on non-windows platforms
* Fleshed out the ansihandler test
  • Loading branch information
matthewfcarlson authored and corthon committed Dec 14, 2018
1 parent 659538b commit f000afa
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 81 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ build.
/cov.xml
/test.junit.xml
flake8.err.log
/.eggs
172 changes: 92 additions & 80 deletions MuPythonLibrary/MuAnsiHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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)
77 changes: 77 additions & 0 deletions MuPythonLibrary/MuAnsiHandler_test.py
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 9 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
@@ -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
=====
Expand All @@ -19,4 +25,6 @@ Updated documentation and release process. Transition to Beta.
< 0.3.0
-------

Alpha development
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)

0 comments on commit f000afa

Please sign in to comment.