diff --git a/colorama/ansitowin32.py b/colorama/ansitowin32.py index 6039a05..11635bc 100644 --- a/colorama/ansitowin32.py +++ b/colorama/ansitowin32.py @@ -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 @@ -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 @@ -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): diff --git a/colorama/tests/isatty_test.py b/colorama/tests/isatty_test.py index 0f84e4b..44fe361 100644 --- a/colorama/tests/isatty_test.py +++ b/colorama/tests/isatty_test.py @@ -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 ImportError: + 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): @@ -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() diff --git a/colorama/tests/utils.py b/colorama/tests/utils.py index 472fafb..3b8cbeb 100644 --- a/colorama/tests/utils.py +++ b/colorama/tests/utils.py @@ -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 @@ -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