diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3e4ced7..811621ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24915d9a..ab324e67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: seed-isort-config - repo: https://github.com/timothycrosley/isort - rev: 5.5.1 + rev: 5.5.3 hooks: - id: isort files: "\\.(py)$" @@ -35,7 +35,7 @@ repos: args: [--no-strict-optional, --ignore-missing-imports] - repo: https://github.com/prettier/prettier - rev: 2.1.1 + rev: 2.1.2 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4d5215c4..55adc249 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## [Unreleased] - ././2020 + +- [💪 Support Windows OS coloring and encoding by @hadialqattan](https://github.com/hakancelik96/unimport/pull/116) + ## [0.3.0] - 22/September/2020 - [🐞💊 Fix, improve: Names, Imports and star suggestion by @hakancelik96](https://github.com/hakancelik96/unimport/pull/112) diff --git a/tests/test_color.py b/tests/test_color.py index c57637d3..d01bf470 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -1,7 +1,9 @@ import textwrap import unittest -from unimport.color import COLORS, Color # unimport: skip +from unimport.color import Color # unimport: skip +from unimport.color import terminal_supports_color # unimport: skip +from unimport.color import COLORS class TestColor(unittest.TestCase): @@ -12,9 +14,12 @@ def setUp(self): test_template = textwrap.dedent( f""" def test_{color}(self): - action_test = COLORS["{color}"] + self.test_content + Color.reset + if terminal_supports_color: + action_test = COLORS["{color}"] + self.test_content + Color.reset + else: + action_test = self.test_content expected_text = Color(self.test_content).{color} - self.assertEqual(action_test, expected_text) + self.assertEqual(expected_text, action_test) """ ) exec(test_template) diff --git a/tests/test_scanner.py b/tests/test_scanner.py index 78ccb607..1ed2d4cc 100644 --- a/tests/test_scanner.py +++ b/tests/test_scanner.py @@ -1,4 +1,3 @@ -import tempfile import unittest from unimport.constants import PY38_PLUS diff --git a/tests/test_session.py b/tests/test_session.py index bd463b3e..3a3b0718 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,10 +1,34 @@ +import os import tempfile import unittest +from contextlib import contextmanager from pathlib import Path +from typing import Iterator from unimport.session import Session +@contextmanager +def reopenable_temp_file(content: str) -> Iterator[Path]: + """Reopenable tempfile to support writing/reading to/from the opened + tempfile (requiered for Windows OS). + + For more information: https://bit.ly/3cr0Qkl + + :param content: string content to write. + :yields: tempfile path. + """ + try: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", encoding="utf-8", delete=False + ) as tmp: + tmp_path = Path(tmp.name) + tmp.write(content) + yield tmp_path + finally: + os.unlink(tmp_path) + + class TestSession(unittest.TestCase): maxDiff = None include_star_import = True @@ -18,12 +42,8 @@ def test_list_paths_and_read(self): self.assertTrue(str(p).endswith(".py")) def temp_refactor(self, source: str, expected: str, apply: bool = False): - with tempfile.NamedTemporaryFile(mode="w", suffix=".py") as tmp: - tmp.write(source) - tmp.seek(0) - result, _ = self.session.refactor_file( - path=Path(tmp.name), apply=apply - ) + with reopenable_temp_file(source) as tmp_path: + result, _ = self.session.refactor_file(path=tmp_path, apply=apply) self.assertEqual(result, expected) def test_bad_encoding(self): @@ -43,12 +63,10 @@ def test_diff(self): self.assertEqual(diff, self.session.diff("import os")) def test_diff_file(self): - with tempfile.NamedTemporaryFile(mode="w", suffix=".py") as tmp: - tmp.write("import os") - tmp.seek(0) - diff_file = self.session.diff_file(path=Path(tmp.name)) + with reopenable_temp_file("import os") as tmp_path: + diff_file = self.session.diff_file(path=tmp_path) diff = ( - f"--- {tmp.name}\n", + f"--- {tmp_path.as_posix()}\n", "+++ \n", "@@ -1 +0,0 @@\n", "-import os", @@ -57,9 +75,5 @@ def test_diff_file(self): def test_read(self): source = "b�se" - with tempfile.NamedTemporaryFile(mode="w", suffix=".py") as tmp: - tmp.write(source) - tmp.seek(0) - self.assertEqual( - (source, "utf-8"), self.session.read(Path(tmp.name)) - ) + with reopenable_temp_file(source) as tmp_path: + self.assertEqual((source, "utf-8"), self.session.read(tmp_path)) diff --git a/unimport/color.py b/unimport/color.py index bb576285..53898557 100644 --- a/unimport/color.py +++ b/unimport/color.py @@ -1,3 +1,5 @@ +import sys + COLORS = { "black": "\033[30m", "red": "\033[31m", @@ -11,6 +13,56 @@ } +if sys.platform == "win32": # pragma: no cover (windows) + + def _enable() -> None: + from ctypes import POINTER, WINFUNCTYPE, WinError, windll + from ctypes.wintypes import BOOL, DWORD, HANDLE + + STD_ERROR_HANDLE = -12 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + + def bool_errcheck(result, func, args): + if not result: + raise WinError() + return args + + GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( + ("GetStdHandle", windll.kernel32), + ((1, "nStdHandle"),), + ) + + GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( + ("GetConsoleMode", windll.kernel32), + ((1, "hConsoleHandle"), (2, "lpMode")), + ) + GetConsoleMode.errcheck = bool_errcheck + + SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( + ("SetConsoleMode", windll.kernel32), + ((1, "hConsoleHandle"), (1, "dwMode")), + ) + SetConsoleMode.errcheck = bool_errcheck + + # As of Windows 10, the Windows console supports (some) ANSI escape + # sequences, but it needs to be enabled using `SetConsoleMode` first. + # + # More info on the escape sequences supported: + # https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx + stderr = GetStdHandle(STD_ERROR_HANDLE) + flags = GetConsoleMode(stderr) + SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + + try: + _enable() + except OSError: + terminal_supports_color = False + else: + terminal_supports_color = True +else: # pragma: win32 no cover + terminal_supports_color = True + + class Color: reset = "\033[0m" @@ -18,7 +70,10 @@ def __init__(self, content: str) -> None: self.content = content def template(self, color: str) -> str: - return COLORS[color] + self.content + self.reset + if terminal_supports_color: + return COLORS[color] + self.content + self.reset + else: + return self.content def __getattribute__(self, name: str) -> str: if name in COLORS: diff --git a/unimport/session.py b/unimport/session.py index ad7233bf..dcdb6e50 100644 --- a/unimport/session.py +++ b/unimport/session.py @@ -96,6 +96,8 @@ def diff_file(self, path: Path) -> Tuple[str, ...]: result, _ = self.refactor_file(path, apply=False) return tuple( difflib.unified_diff( - source.splitlines(), result.splitlines(), fromfile=str(path) + source.splitlines(), + result.splitlines(), + fromfile=path.as_posix(), ) )