Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support mintty (MSYS/Cygwin terminals) #226

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
48 changes: 47 additions & 1 deletion colorama/ansitowin32.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
from io import UnsupportedOperation
import re
import sys
import os
import ctypes

try:
import msvcrt
except ImportError:
msvcrt = None

from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style, BEL
from .winterm import WinTerm, WinColor, WinStyle
Expand All @@ -13,6 +20,45 @@
winterm = WinTerm()


class FileNameInfo(ctypes.Structure):
"""Struct to get FileNameInfo from the win32api"""
_fields_ = [('FileNameLength', ctypes.c_ulong),
('FileName', ctypes.c_wchar * 40)]


def is_msys_cygwin_tty(stream):
if not hasattr(stream, "fileno"):
return False

if not hasattr(ctypes, "windll") or not hasattr(ctypes.windll.kernel32, "GetFileInformationByHandleEx"):
return False

if msvcrt is None:
return False

try:
fileno = stream.fileno()
except UnsupportedOperation:
# StringIO for example has the fileno attribute but doesn't support calling it
return False

handle = msvcrt.get_osfhandle(fileno)
FILE_NAME_INFO = 2

info = FileNameInfo()
ret = ctypes.windll.kernel32.GetFileInformationByHandleEx(handle,
FILE_NAME_INFO,
ctypes.byref(info),
ctypes.sizeof(info))
if ret == 0:
return False
SSE4 marked this conversation as resolved.
Show resolved Hide resolved

msys_pattern = r"\\msys-[0-9a-f]{16}-pty\d-(to|from)-master"
cygwin_pattern = r"\\cygwin-[0-9a-f]{16}-pty\d-(to|from)-master"

return re.match(msys_pattern, info.FileName) is not None or \
re.match(cygwin_pattern, info.FileName) is not None

class StreamWrapper(object):
'''
Wraps a stream (such as stdout), acting as a transparent proxy for all
Expand Down Expand Up @@ -50,7 +96,7 @@ def isatty(self):
except AttributeError:
return False
else:
return stream_isatty()
return stream_isatty() or is_msys_cygwin_tty(stream)

@property
def closed(self):
Expand Down
75 changes: 72 additions & 3 deletions colorama/tests/isatty_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
import sys
from unittest import TestCase, main
from io import StringIO
from unittest import TestCase, main, skipUnless

from ..ansitowin32 import StreamWrapper, AnsiToWin32
from .utils import pycharm, replace_by, replace_original_by, StreamTTY, StreamNonTTY
try:
from mock import patch, PropertyMock
except ModuleNotFoundError:
from unittest.mock import patch, PropertyMock

from ..ansitowin32 import StreamWrapper, AnsiToWin32, is_msys_cygwin_tty, FileNameInfo
from .utils import pycharm, replace_by, replace_original_by, StreamTTY, StreamNonTTY, StreamNonTTYWithFileNo


def is_a_tty(stream):
Expand Down Expand Up @@ -52,6 +58,69 @@ def test_withPycharmStreamWrapped(self):
self.assertTrue(AnsiToWin32(sys.stdout).stream.isatty())
self.assertTrue(AnsiToWin32(sys.stderr).stream.isatty())

@patch("colorama.ansitowin32.is_msys_cygwin_tty", return_value=False)
def test_isattyCorrectForMintty(self, mock_fn):
self.assertTrue(is_a_tty(StreamTTY()))
self.assertFalse(is_a_tty(StreamNonTTY()))
self.assertEqual(mock_fn.call_count, 1)

@patch("colorama.ansitowin32.is_msys_cygwin_tty", return_value=True)
def test_isattyCorrectForNonMintty(self, mock_fn):
self.assertTrue(is_a_tty(StreamNonTTY()))
self.assertTrue(is_a_tty(StreamTTY()))
self.assertEqual(mock_fn.call_count, 1)

class MinttyTest(TestCase):
"""Tests for the detection of mintty / msys/ cygwin

They're arguably a little brittle to the exact detection implementation, so can be refactored
if the implementation changes.
"""

@patch("colorama.ansitowin32.msvcrt", None)
def test_falseNotOnWindows(self):
self.assertFalse(is_msys_cygwin_tty(StreamNonTTYWithFileNo()))

def test_falseForIoString(self):
self.assertFalse(is_msys_cygwin_tty(StringIO()))

@skipUnless(sys.platform.startswith("win"), "requires Windows")
@patch("ctypes.windll.kernel32", None)
def test_falseIfKernelModuleUnavailable(self):
self.assertFalse(is_msys_cygwin_tty(StreamNonTTYWithFileNo()))

@skipUnless(sys.platform.startswith("win"), "requires Windows")
@patch("ctypes.windll.kernel32.GetFileInformationByHandleEx", return_value=0)
@patch("msvcrt.get_osfhandle", return_value=10)
def test_falseIfWin32CallFails(self, mock_win32_call, mock_handle_call):
self.assertFalse(is_msys_cygwin_tty(StreamNonTTYWithFileNo()))

@skipUnless(sys.platform.startswith("win"), "requires Windows")
@patch("ctypes.windll.kernel32.GetFileInformationByHandleEx", return_value=1)
@patch("msvcrt.get_osfhandle", return_value=1000)
def test_trueForMsys(self, mock_file_call, mock_handle_call):

with patch.object(FileNameInfo, "FileName", new_callable=PropertyMock) as mock_filename_info:
mock_filename_info.return_value = r"\msys-0000000000000000-pty3-to-master"
self.assertTrue(is_msys_cygwin_tty(StreamNonTTYWithFileNo()))

@skipUnless(sys.platform.startswith("win"), "requires Windows")
@patch("ctypes.windll.kernel32.GetFileInformationByHandleEx", return_value=1)
@patch("msvcrt.get_osfhandle", return_value=1000)
def test_trueForCygwin(self, mock_file_call, mock_handle_call):

with patch.object(FileNameInfo, "FileName", new_callable=PropertyMock) as mock_filename_info:
mock_filename_info.return_value = r"\cygwin-0000000000000000-pty3-to-master"
self.assertTrue(is_msys_cygwin_tty(StreamNonTTYWithFileNo()))

@skipUnless(sys.platform.startswith("win"), "requires Windows")
@patch("ctypes.windll.kernel32.GetFileInformationByHandleEx", return_value=1)
@patch("msvcrt.get_osfhandle", return_value=1000)
def test_falseForAnythingElse(self, mock_file_call, mock_handle_call):

with patch.object(FileNameInfo, "FileName", new_callable=PropertyMock) as mock_filename_info:
mock_filename_info.return_value = r"\random-0000000000000000-pty3-to-master"
self.assertFalse(is_msys_cygwin_tty(StreamNonTTYWithFileNo()))

if __name__ == '__main__':
main()
12 changes: 12 additions & 0 deletions colorama/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
import sys
import os

try:
from unittest.mock import Mock
except ImportError:
from mock import Mock


class StreamTTY(StringIO):
def isatty(self):
return True
Expand All @@ -13,6 +18,13 @@ class StreamNonTTY(StringIO):
def isatty(self):
return False

class StreamNonTTYWithFileNo(StringIO):
def isatty(self):
return False

def fileno(self):
return 10

@contextmanager
def osname(name):
orig = os.name
Expand Down