Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

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