From 1a11ea3449d18549ab492312140bf350933245bb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 10:14:08 +0000 Subject: [PATCH 01/23] Conversion of Segments to legacy Windows API calls --- .gitignore | 1 + mypy.ini | 3 - rich/_win32_console.py | 515 +++++++++++++++++++++++++++++++++ rich/_windows.py | 23 +- rich/_windows_renderer.py | 49 ++++ rich/console.py | 38 +-- rich/segment.py | 2 +- tests/test_windows_renderer.py | 132 +++++++++ 8 files changed, 727 insertions(+), 36 deletions(-) create mode 100644 rich/_win32_console.py create mode 100644 rich/_windows_renderer.py create mode 100644 tests/test_windows_renderer.py diff --git a/.gitignore b/.gitignore index bc0dfdb71..a133d7b14 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .pytype .DS_Store .vscode +.idea/ mypy_report docs/build docs/source/_build diff --git a/mypy.ini b/mypy.ini index c61bcf0ac..acccf0825 100644 --- a/mypy.ini +++ b/mypy.ini @@ -9,8 +9,5 @@ ignore_missing_imports = True [mypy-commonmark.*] ignore_missing_imports = True -[mypy-colorama.*] -ignore_missing_imports = True - [mypy-ipywidgets.*] ignore_missing_imports = True diff --git a/rich/_win32_console.py b/rich/_win32_console.py new file mode 100644 index 000000000..3659db491 --- /dev/null +++ b/rich/_win32_console.py @@ -0,0 +1,515 @@ +"""Light wrapper around the win32 Console API - this module can only be imported on Windows""" +import ctypes +import sys + +if sys.platform == "win32": + windll = ctypes.LibraryLoader(ctypes.WinDLL) +else: + raise ImportError(f"{__name__} can only be imported on Windows") + +import time +from ctypes import Structure, byref, wintypes +from typing import IO, NamedTuple, Type, cast + +from rich.color import ColorSystem +from rich.style import Style +from rich.text import Text + +STDOUT = -11 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + +kernel32 = windll.kernel32 +COORD = wintypes._COORD + + +class WindowsCoordinates(NamedTuple): + """Coordinates in the Windows Console API are (y, x), not (x, y). + This class is intended to prevent that confusion. + Rows and columns are indexed from 0. + This class can be used in place of wintypes._COORD in arguments and argtypes. + """ + + row: int + col: int + + @classmethod + def from_param(cls, value: "WindowsCoordinates") -> COORD: + return COORD(value.col, value.row) + + +class CONSOLE_SCREEN_BUFFER_INFO(Structure): + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", wintypes.WORD), + ("srWindow", wintypes.SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] + + +class CONSOLE_CURSOR_INFO(ctypes.Structure): + _fields_ = [("dwSize", wintypes.DWORD), ("bVisible", wintypes.BOOL)] + + +_GetStdHandle = kernel32.GetStdHandle +_GetStdHandle.argtypes = [ + wintypes.DWORD, +] +_GetStdHandle.restype = wintypes.HANDLE + + +def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE: + return cast(wintypes.HANDLE, _GetStdHandle(handle)) + + +_GetConsoleMode = kernel32.GetConsoleMode +_GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] +_GetConsoleMode.restype = wintypes.BOOL + + +def GetConsoleMode(std_handle: wintypes.HANDLE, console_mode: wintypes.DWORD) -> bool: + return bool(_GetConsoleMode(std_handle, console_mode)) + + +_FillConsoleOutputCharacterW = windll.kernel32.FillConsoleOutputCharacterW +_FillConsoleOutputCharacterW.argtypes = [ + wintypes.HANDLE, + ctypes.c_char, + wintypes.DWORD, + cast(Type[COORD], WindowsCoordinates), + ctypes.POINTER(wintypes.DWORD), +] +_FillConsoleOutputCharacterW.restype = wintypes.BOOL + + +def FillConsoleOutputCharacter( + std_handle: wintypes.HANDLE, + char: str, + length: int, + start: WindowsCoordinates, +) -> int: + """Writes a character to the console screen buffer a specified number of times, beginning at the specified coordinates.""" + assert len(char) == 1 + character = ctypes.c_char(char.encode()) + num_characters = wintypes.DWORD(length) + num_written = wintypes.DWORD(0) + _FillConsoleOutputCharacterW( + std_handle, + character, + num_characters, + start, + byref(num_written), + ) + return num_written.value + + +_FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute +_FillConsoleOutputAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, + wintypes.DWORD, + cast(Type[COORD], WindowsCoordinates), + ctypes.POINTER(wintypes.DWORD), +] +_FillConsoleOutputAttribute.restype = wintypes.BOOL + + +def FillConsoleOutputAttribute( + std_handle: wintypes.HANDLE, + attributes: int, + length: int, + start: WindowsCoordinates, +) -> int: + num_cells = wintypes.DWORD(length) + style_attrs = wintypes.WORD(attributes) + num_written = wintypes.DWORD(0) + _FillConsoleOutputAttribute( + std_handle, style_attrs, num_cells, start, byref(num_written) + ) + return num_written.value + + +_SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute +_SetConsoleTextAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, +] +_SetConsoleTextAttribute.restype = wintypes.BOOL + + +def SetConsoleTextAttribute( + std_handle: wintypes.HANDLE, attributes: wintypes.WORD +) -> bool: + return bool(_SetConsoleTextAttribute(std_handle, attributes)) + + +_GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo +_GetConsoleScreenBufferInfo.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), +] +_GetConsoleScreenBufferInfo.restype = wintypes.BOOL + + +def GetConsoleScreenBufferInfo( + std_handle: wintypes.HANDLE, +) -> CONSOLE_SCREEN_BUFFER_INFO: + console_screen_buffer_info = CONSOLE_SCREEN_BUFFER_INFO() + _GetConsoleScreenBufferInfo(std_handle, byref(console_screen_buffer_info)) + return console_screen_buffer_info + + +_SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition +_SetConsoleCursorPosition.argtypes = [ + wintypes.HANDLE, + cast(Type[COORD], WindowsCoordinates), +] +_SetConsoleCursorPosition.restype = wintypes.BOOL + + +def SetConsoleCursorPosition( + std_handle: wintypes.HANDLE, coords: WindowsCoordinates +) -> bool: + if coords.col < 0 or coords.row < 0: + return False + return bool(_SetConsoleCursorPosition(std_handle, coords)) + + +_SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo +_SetConsoleCursorInfo.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(CONSOLE_CURSOR_INFO), +] +_SetConsoleCursorInfo.restype = wintypes.BOOL + + +def SetConsoleCursorInfo( + std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO +) -> bool: + return bool(_SetConsoleCursorInfo(std_handle, byref(cursor_info))) + + +_SetConsoleTitle = windll.kernel32.SetConsoleTitleW +_SetConsoleTitle.argtypes = [wintypes.LPCWSTR] +_SetConsoleTitle.restype = wintypes.BOOL + + +def SetConsoleTitle(title: str) -> bool: + return bool(_SetConsoleTitle(title)) + + +class LegacyWindowsTerm: + """This class allows interaction with the legacy Windows Console API. It should only be used in the context + of environments where virtual terminal processing is not available. However, if it is used in a Windows environment, + the entire API should work. + + Args: + file (IO[str]): The file which the Windows Console API HANDLE is retrieved from, defaults to sys.stdout. + """ + + # Indices are ANSI color numbers, values are the corresponding Windows Console API color numbers + ANSI_TO_WINDOWS = [ + 0, # black The Windows colours are defined in wincon.h as follows: + 4, # red define FOREGROUND_BLUE 0x0001 -- 0000 0001 + 2, # green define FOREGROUND_GREEN 0x0002 -- 0000 0010 + 6, # yellow define FOREGROUND_RED 0x0004 -- 0000 0100 + 1, # blue define FOREGROUND_INTENSITY 0x0008 -- 0000 1000 + 5, # magenta define BACKGROUND_BLUE 0x0010 -- 0001 0000 + 3, # cyan define BACKGROUND_GREEN 0x0020 -- 0010 0000 + 7, # white define BACKGROUND_RED 0x0040 -- 0100 0000 + 8, # bright black (grey) define BACKGROUND_INTENSITY 0x0080 -- 1000 0000 + 12, # bright red + 10, # bright green + 14, # bright yellow + 9, # bright blue + 13, # bright magenta + 11, # bright cyan + 15, # bright white + ] + + def __init__(self, file: IO[str] = sys.stdout): + self.file = file + handle = GetStdHandle(STDOUT) + self._handle = handle + default_text = GetConsoleScreenBufferInfo(handle).wAttributes + self._default_text = default_text + + self._default_fore = default_text & 7 + self._default_back = (default_text >> 4) & 7 + self._default_attrs = self._default_fore + self._default_back * 16 + + self.write = file.write + self.flush = file.flush + + @property + def cursor_position(self) -> WindowsCoordinates: + """Returns the current position of the cursor (0-based) + + Returns: + WindowsCoordinates: The current cursor position. + """ + coord: COORD = GetConsoleScreenBufferInfo(self._handle).dwCursorPosition + return WindowsCoordinates(row=cast(int, coord.Y), col=cast(int, coord.X)) + + @property + def screen_size(self) -> WindowsCoordinates: + """Returns the current size of the console screen buffer, in character columns and rows + + Returns: + WindowsCoordinates: The width and height of the screen as WindowsCoordinates. + """ + screen_size: COORD = GetConsoleScreenBufferInfo(self._handle).dwSize + return WindowsCoordinates( + row=cast(int, screen_size.Y), col=cast(int, screen_size.X) + ) + + def write_text(self, text: str) -> None: + """Write text directly to the terminal without any modification of styles + + Args: + text (str): The text to write to the console + """ + self.write(text) + self.flush() + + def write_styled(self, text: str, style: Style) -> None: + """Write styled text to the terminal + + Args: + text (str): The text to write + style (Style): The style of the text + """ + # TODO: Check for bold, bright, etc. inside the style + if style.color: + fore = style.color.downgrade(ColorSystem.WINDOWS).number + fore = fore if fore is not None else 7 # Default to ANSI 7: White + fore = self.ANSI_TO_WINDOWS[fore] + else: + fore = self._default_fore + + if style.bgcolor: + back = style.bgcolor.downgrade(ColorSystem.WINDOWS).number + back = back if back is not None else 0 # Default to ANSI 0: Black + back = self.ANSI_TO_WINDOWS[back] + else: + back = self._default_back + + assert fore is not None + assert back is not None + + SetConsoleTextAttribute( + self._handle, attributes=ctypes.c_ushort(fore + back * 16) + ) + self.write_text(text) + SetConsoleTextAttribute(self._handle, attributes=self._default_text) + + def move_cursor_to(self, new_position: WindowsCoordinates) -> None: + """Set the position of the cursor + + Args: + new_position (WindowsCoordinates): The WindowsCoordinates representing the new position of the cursor. + """ + SetConsoleCursorPosition(self._handle, coords=new_position) + + def erase_line(self) -> None: + """Erase all content on the line the cursor is currently located at""" + screen_size = self.screen_size + cursor_position = self.cursor_position + cells_to_erase = screen_size.col + start_coordinates = WindowsCoordinates(row=cursor_position.row, col=0) + FillConsoleOutputCharacter( + self._handle, " ", length=cells_to_erase, start=start_coordinates + ) + FillConsoleOutputAttribute( + self._handle, + self._default_attrs, + length=cells_to_erase, + start=start_coordinates, + ) + + def erase_end_of_line(self) -> None: + """Erase all content from the cursor position to the end of that line""" + cursor_position = self.cursor_position + cells_to_erase = self.screen_size.col - cursor_position.col + FillConsoleOutputCharacter( + self._handle, " ", length=cells_to_erase, start=cursor_position + ) + FillConsoleOutputAttribute( + self._handle, + self._default_attrs, + length=cells_to_erase, + start=cursor_position, + ) + + def erase_start_of_line(self) -> None: + """Erase all content from the cursor position to the start of that line""" + row, col = self.cursor_position + start = WindowsCoordinates(row, 0) + FillConsoleOutputCharacter(self._handle, " ", length=col, start=start) + FillConsoleOutputAttribute( + self._handle, self._default_attrs, length=col, start=start + ) + + def move_cursor_up(self) -> None: + """Move the cursor up a single cell""" + cursor_position = self.cursor_position + SetConsoleCursorPosition( + self._handle, + coords=WindowsCoordinates( + row=cursor_position.row - 1, col=cursor_position.col + ), + ) + + def move_cursor_down(self) -> None: + """Move the cursor down a single cell""" + cursor_position = self.cursor_position + SetConsoleCursorPosition( + self._handle, + coords=WindowsCoordinates( + row=cursor_position.row + 1, + col=cursor_position.col, + ), + ) + + def move_cursor_forward(self) -> None: + """Move the cursor forward a single cell. Wrap to the next line if required.""" + row, col = self.cursor_position + if col == self.screen_size.col - 1: + row += 1 + col = 0 + else: + col += 1 + SetConsoleCursorPosition( + self._handle, coords=WindowsCoordinates(row=row, col=col) + ) + + def move_cursor_to_column(self, column: int) -> None: + """Move cursor to the column specified by the zero-based column index, staying on the same row + + Args: + column (int): The zero-based column index to move the cursor to. + """ + row, _ = self.cursor_position + SetConsoleCursorPosition(self._handle, coords=WindowsCoordinates(row, column)) + + def move_cursor_backward(self) -> None: + """Move the cursor backward a single cell. Wrap to the previous line if required.""" + row, col = self.cursor_position + if col == 0: + row -= 1 + col = self.screen_size.col - 1 + else: + col -= 1 + SetConsoleCursorPosition( + self._handle, coords=WindowsCoordinates(row=row, col=col) + ) + + def hide_cursor(self) -> None: + """Hide the cursor""" + invisible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=0) + SetConsoleCursorInfo(self._handle, cursor_info=invisible_cursor) + + def show_cursor(self) -> None: + """Show the cursor""" + visible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisibile=1) + SetConsoleCursorInfo(self._handle, cursor_info=visible_cursor) + + def set_title(self, title: str) -> None: + """Set the title of the terminal window + + Args: + title (str): The new title of the console window + """ + assert len(title) < 255, "Console title must be less than 255 characters" + SetConsoleTitle(title) + + +if __name__ == "__main__": + handle = GetStdHandle() + console_mode = wintypes.DWORD() + rv = GetConsoleMode(handle, console_mode) + + print(rv) + print(type(rv)) + + from rich.console import Console + + console = Console() + + term = LegacyWindowsTerm(console.file) + term.set_title("Win32 Console Examples") + + style = Style(color="black", bgcolor="red") + + heading = Style.parse("black on green") + + # Check colour output + console.rule("Checking colour output") + # console.print("Checking colour output", style=Style.parse("black on green")) + text = Text("Hello world!", style=style) + console.print(text) + console.print("[bold green]bold green!") + console.print("[italic cyan]italic cyan!") + console.print("[bold white on blue]bold white on blue!") + console.print("[bold black on cyan]bold black on cyan!") + console.print("[black on green]black on green!") + console.print("[blue on green]blue on green!") + console.print("[white on black]white on black!") + console.print("[black on white]black on white!") + console.print("[#1BB152 on #DA812D]#1BB152 on #DA812D!") + + # Check cursor movement + console.rule("Checking cursor movement") + console.print() + term.move_cursor_backward() + term.move_cursor_backward() + term.write_text("went back and wrapped to prev line") + time.sleep(1) + term.move_cursor_up() + term.write_text("we go up") + time.sleep(1) + term.move_cursor_down() + term.write_text("and down") + time.sleep(1) + term.move_cursor_up() + term.move_cursor_backward() + term.move_cursor_backward() + term.write_text("we went up and back 2") + time.sleep(1) + term.move_cursor_down() + term.move_cursor_backward() + term.move_cursor_backward() + term.write_text("we went down and back 2") + time.sleep(1) + + # Check erasing of lines + term.hide_cursor() + console.print() + console.rule("Checking line erasing") + console.print("\n...Deleting to the start of the line...") + term.write_text("The red arrow shows the cursor location, and direction of erase") + time.sleep(1) + term.move_cursor_to_column(16) + term.write_styled("<", Style.parse("black on red")) + term.move_cursor_backward() + time.sleep(1) + term.erase_start_of_line() + time.sleep(1) + + console.print("\n\n...And to the end of the line...") + term.write_text("The red arrow shows the cursor location, and direction of erase") + time.sleep(1) + + term.move_cursor_to_column(16) + term.write_styled(">", Style.parse("black on red")) + time.sleep(1) + term.erase_end_of_line() + time.sleep(1) + + console.print("\n\n...Now the whole line will be erased...") + term.write_styled("I'm going to disappear!", style=Style.parse("black on cyan")) + time.sleep(1) + term.erase_line() + + term.show_cursor() + print("\n") diff --git a/rich/_windows.py b/rich/_windows.py index b1b30b65e..a330cae77 100644 --- a/rich/_windows.py +++ b/rich/_windows.py @@ -21,6 +21,13 @@ class WindowsConsoleFeatures: else: windll = None raise ImportError("Not windows") + + from rich._win32_console import ( + ENABLE_VIRTUAL_TERMINAL_PROCESSING, + GetConsoleMode, + GetStdHandle, + ) + except (AttributeError, ImportError, ValueError): # Fallback if we can't load the Windows DLL @@ -30,27 +37,15 @@ def get_windows_console_features() -> WindowsConsoleFeatures: else: - STDOUT = -11 - ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 - _GetConsoleMode = windll.kernel32.GetConsoleMode - _GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] - _GetConsoleMode.restype = wintypes.BOOL - - _GetStdHandle = windll.kernel32.GetStdHandle - _GetStdHandle.argtypes = [ - wintypes.DWORD, - ] - _GetStdHandle.restype = wintypes.HANDLE - def get_windows_console_features() -> WindowsConsoleFeatures: """Get windows console features. Returns: WindowsConsoleFeatures: An instance of WindowsConsoleFeatures. """ - handle = _GetStdHandle(STDOUT) + handle = GetStdHandle() console_mode = wintypes.DWORD() - result = _GetConsoleMode(handle, console_mode) + result = GetConsoleMode(handle, console_mode) vt = bool(result and console_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) truecolor = False if vt: diff --git a/rich/_windows_renderer.py b/rich/_windows_renderer.py new file mode 100644 index 000000000..7f67202d8 --- /dev/null +++ b/rich/_windows_renderer.py @@ -0,0 +1,49 @@ +from typing import Iterable, Sequence, Tuple, cast + +from rich._win32_console import LegacyWindowsTerm, WindowsCoordinates +from rich.segment import ControlCode, ControlType, Segment + + +def legacy_windows_render(buffer: Iterable[Segment], term: LegacyWindowsTerm) -> None: + for segment in buffer: + text, style, control = segment + + if not control: + if style: + term.write_styled(text, style) + else: + term.write_text(text) + else: + control_codes: Sequence[ControlCode] = control + for control_code in control_codes: + control_type = control_code[0] + if control_type == ControlType.CURSOR_MOVE_TO: + _, x, y = cast(Tuple[ControlType, int, int], control_code) + term.move_cursor_to(WindowsCoordinates(row=y - 1, col=x - 1)) + elif control_type == ControlType.CARRIAGE_RETURN: + term.write_text("\r") + elif control_type == ControlType.HOME: + term.move_cursor_to(WindowsCoordinates(0, 0)) + elif control_type == ControlType.CURSOR_UP: + term.move_cursor_up() + elif control_type == ControlType.CURSOR_DOWN: + term.move_cursor_down() + elif control_type == ControlType.CURSOR_FORWARD: + term.move_cursor_forward() + elif control_type == ControlType.CURSOR_BACKWARD: + term.move_cursor_backward() + elif control_type == ControlType.CURSOR_MOVE_TO_COLUMN: + _, column = cast(Tuple[ControlType, int], control_code) + term.move_cursor_to_column(column - 1) + elif control_type == ControlType.HIDE_CURSOR: + term.hide_cursor() + elif control_type == ControlType.SHOW_CURSOR: + term.show_cursor() + elif control_type == ControlType.ERASE_IN_LINE: + _, mode = cast(Tuple[ControlType, int], control_code) + if mode == 0: + term.erase_end_of_line() + elif mode == 1: + term.erase_start_of_line() + elif mode == 2: + term.erase_line() diff --git a/rich/console.py b/rich/console.py index e2d7a6d52..b29c90aef 100644 --- a/rich/console.py +++ b/rich/console.py @@ -11,7 +11,6 @@ from html import escape from inspect import isclass from itertools import islice -from threading import RLock from time import monotonic from types import FrameType, ModuleType, TracebackType from typing import ( @@ -571,12 +570,6 @@ def detect_legacy_windows() -> bool: return WINDOWS and not get_windows_console_features().vt -if detect_legacy_windows(): # pragma: no cover - from colorama import init - - init(strip=False) - - class Console: """A high level console interface. @@ -1141,7 +1134,7 @@ def show_cursor(self, show: bool = True) -> bool: Args: show (bool, optional): Set visibility of the cursor. """ - if self.is_terminal and not self.legacy_windows: + if self.is_terminal: self.control(Control.show_cursor(show)) return True return False @@ -1916,22 +1909,31 @@ def _check_buffer(self) -> None: display(self._buffer, self._render_buffer(self._buffer[:])) del self._buffer[:] else: - text = self._render_buffer(self._buffer[:]) - del self._buffer[:] - if text: + if WINDOWS: + if self.legacy_windows: + from rich._win32_console import LegacyWindowsTerm + from rich._windows_renderer import legacy_windows_render + + legacy_windows_render( + self._buffer[:], LegacyWindowsTerm(self.file) + ) + else: + text = self._render_buffer(self._buffer[:]) + # https://bugs.python.org/issue37871 + write = self.file.write + for line in text.splitlines(True): + write(line) + else: + text = self._render_buffer(self._buffer[:]) try: - if WINDOWS: # pragma: no cover - # https://bugs.python.org/issue37871 - write = self.file.write - for line in text.splitlines(True): - write(line) - else: - self.file.write(text) + self.file.write(text) self.file.flush() except UnicodeEncodeError as error: error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" raise + del self._buffer[:] + def _render_buffer(self, buffer: Iterable[Segment]) -> str: """Render buffered output, and clear buffer.""" output: List[str] = [] diff --git a/rich/segment.py b/rich/segment.py index 97679cefc..eb44acf11 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -64,7 +64,7 @@ class Segment(NamedTuple): Args: text (str): A piece of text. style (:class:`~rich.style.Style`, optional): An optional style to apply to the text. - control (Tuple[ControlCode..], optional): Optional sequence of control codes. + control (Sequence[ControlCode], optional): Optional sequence of control codes. """ text: str = "" diff --git a/tests/test_windows_renderer.py b/tests/test_windows_renderer.py new file mode 100644 index 000000000..a171456e0 --- /dev/null +++ b/tests/test_windows_renderer.py @@ -0,0 +1,132 @@ +import sys +from unittest.mock import call, create_autospec + +import pytest + +try: + from rich._win32_console import LegacyWindowsTerm, WindowsCoordinates + from rich._windows_renderer import legacy_windows_render +except: + pass +from rich.segment import ControlType, Segment +from rich.style import Style + +pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="windows only") + + +@pytest.fixture +def legacy_term_mock(): + return create_autospec(LegacyWindowsTerm) + + +def test_text_only(legacy_term_mock): + text = "Hello, world!" + buffer = [Segment(text)] + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.write_text.assert_called_once_with(text) + + +def test_text_multiple_segments(legacy_term_mock): + buffer = [Segment("Hello, "), Segment("world!")] + legacy_windows_render(buffer, legacy_term_mock) + + assert legacy_term_mock.write_text.call_args_list == [ + call("Hello, "), + call("world!"), + ] + + +def test_text_with_style(legacy_term_mock): + text = "Hello, world!" + style = Style.parse("black on red") + buffer = [Segment(text, style)] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.write_styled.assert_called_once_with(text, style) + + +def test_control_cursor_move_to(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.CURSOR_MOVE_TO, 20, 30)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.move_cursor_to.assert_called_once_with( + WindowsCoordinates(row=29, col=19) + ) + + +def test_control_carriage_return(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.CARRIAGE_RETURN,)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.write_text.assert_called_once_with("\r") + + +def test_control_home(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.HOME,)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.move_cursor_to.assert_called_once_with(WindowsCoordinates(0, 0)) + + +@pytest.mark.parametrize( + "control_type, method_name", + [ + (ControlType.CURSOR_UP, "move_cursor_up"), + (ControlType.CURSOR_DOWN, "move_cursor_down"), + (ControlType.CURSOR_FORWARD, "move_cursor_forward"), + (ControlType.CURSOR_BACKWARD, "move_cursor_backward"), + ], +) +def test_control_cursor_single_cell_movement( + legacy_term_mock, control_type, method_name +): + buffer = [Segment("", None, [(control_type,)])] + + legacy_windows_render(buffer, legacy_term_mock) + + getattr(legacy_term_mock, method_name).assert_called_once_with() + + +@pytest.mark.parametrize( + "erase_mode, method_name", + [ + (0, "erase_end_of_line"), + (1, "erase_start_of_line"), + (2, "erase_line"), + ], +) +def test_control_erase_line(legacy_term_mock, erase_mode, method_name): + buffer = [Segment("", None, [(ControlType.ERASE_IN_LINE, erase_mode)])] + + legacy_windows_render(buffer, legacy_term_mock) + + getattr(legacy_term_mock, method_name).assert_called_once_with() + + +def test_control_show_cursor(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.SHOW_CURSOR,)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.show_cursor.assert_called_once_with() + + +def test_control_hide_cursor(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.HIDE_CURSOR,)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.hide_cursor.assert_called_once_with() + + +def test_control_cursor_move_to_column(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.CURSOR_MOVE_TO_COLUMN, 3)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.move_cursor_to_column.assert_called_once_with(2) From fe571329a858d608720127726d677a1122ad8ed2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 10:17:30 +0000 Subject: [PATCH 02/23] Add None windll definition for mypy --- rich/_win32_console.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index 3659db491..047aef41f 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -5,6 +5,7 @@ if sys.platform == "win32": windll = ctypes.LibraryLoader(ctypes.WinDLL) else: + windll = None raise ImportError(f"{__name__} can only be imported on Windows") import time From ffc5813e183edd1f6b36ce687d0afa4d5dd705e9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 10:23:29 +0000 Subject: [PATCH 03/23] Attempting to appease mypy on non-Windows platforms --- rich/_win32_console.py | 2 +- tests/test_windows_renderer.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index 047aef41f..84b582d9a 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -2,10 +2,10 @@ import ctypes import sys +windll: Any = None # type: ignore if sys.platform == "win32": windll = ctypes.LibraryLoader(ctypes.WinDLL) else: - windll = None raise ImportError(f"{__name__} can only be imported on Windows") import time diff --git a/tests/test_windows_renderer.py b/tests/test_windows_renderer.py index a171456e0..ff2b273a7 100644 --- a/tests/test_windows_renderer.py +++ b/tests/test_windows_renderer.py @@ -7,6 +7,7 @@ from rich._win32_console import LegacyWindowsTerm, WindowsCoordinates from rich._windows_renderer import legacy_windows_render except: + # These modules can only be imported on Windows pass from rich.segment import ControlType, Segment from rich.style import Style From fb227b8a7f9e6a9478992d5ec6f6b82291497a6e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 10:27:55 +0000 Subject: [PATCH 04/23] Use type of Any for windll --- rich/_win32_console.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index 84b582d9a..0c4c8a4b8 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -1,8 +1,9 @@ """Light wrapper around the win32 Console API - this module can only be imported on Windows""" import ctypes import sys +from typing import IO, Any, NamedTuple, Type, cast -windll: Any = None # type: ignore +windll: Any = None if sys.platform == "win32": windll = ctypes.LibraryLoader(ctypes.WinDLL) else: @@ -10,7 +11,6 @@ import time from ctypes import Structure, byref, wintypes -from typing import IO, NamedTuple, Type, cast from rich.color import ColorSystem from rich.style import Style From e38410619113b03d009ec79b52834f6171b7b7e1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 11:02:06 +0000 Subject: [PATCH 05/23] On legacy Windows, we sometimes still need ANSI output... --- rich/_windows_renderer.py | 6 ++++++ rich/console.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rich/_windows_renderer.py b/rich/_windows_renderer.py index 7f67202d8..e9debe30f 100644 --- a/rich/_windows_renderer.py +++ b/rich/_windows_renderer.py @@ -5,6 +5,12 @@ def legacy_windows_render(buffer: Iterable[Segment], term: LegacyWindowsTerm) -> None: + """Makes appropriate Windows Console API calls based on the segments in the buffer. + + Args: + buffer (Iterable[Segment]): Iterable of Segments to convert to Win32 API calls. + term (LegacyWindowsTerm): Used to call the Windows Console API. + """ for segment in buffer: text, style, control = segment diff --git a/rich/console.py b/rich/console.py index b29c90aef..c3380556c 100644 --- a/rich/console.py +++ b/rich/console.py @@ -1917,7 +1917,7 @@ def _check_buffer(self) -> None: legacy_windows_render( self._buffer[:], LegacyWindowsTerm(self.file) ) - else: + if not self.legacy_windows or self._buffer_index or self.record: text = self._render_buffer(self._buffer[:]) # https://bugs.python.org/issue37871 write = self.file.write From 78fc8d93b03bf763205329ffe8f0f5cf534ff5c2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 11:14:31 +0000 Subject: [PATCH 06/23] Only calling Windows legacy console API if Console.file is stdout --- rich/console.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/rich/console.py b/rich/console.py index c3380556c..62ca1da5c 100644 --- a/rich/console.py +++ b/rich/console.py @@ -1,4 +1,5 @@ import inspect +import io import os import platform import sys @@ -1910,14 +1911,24 @@ def _check_buffer(self) -> None: del self._buffer[:] else: if WINDOWS: - if self.legacy_windows: + try: + file_no = self.file.fileno() + except io.UnsupportedOperation: + file_no = -1 + + legacy_windows_stdout = self.legacy_windows and file_no == 1 + if legacy_windows_stdout: from rich._win32_console import LegacyWindowsTerm from rich._windows_renderer import legacy_windows_render legacy_windows_render( self._buffer[:], LegacyWindowsTerm(self.file) ) - if not self.legacy_windows or self._buffer_index or self.record: + if ( + not legacy_windows_stdout + or self._buffer_index + or self.record + ): text = self._render_buffer(self._buffer[:]) # https://bugs.python.org/issue37871 write = self.file.write From 1d38b45a4e12a3deb022fd0c1cbf7711fbac4fe9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 11:30:20 +0000 Subject: [PATCH 07/23] Handling utf-8 error in check_buffer consistently across Windows & Unix --- rich/console.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/rich/console.py b/rich/console.py index 62ca1da5c..45c132562 100644 --- a/rich/console.py +++ b/rich/console.py @@ -1913,7 +1913,7 @@ def _check_buffer(self) -> None: if WINDOWS: try: file_no = self.file.fileno() - except io.UnsupportedOperation: + except (ValueError, io.UnsupportedOperation): file_no = -1 legacy_windows_stdout = self.legacy_windows and file_no == 1 @@ -1933,16 +1933,20 @@ def _check_buffer(self) -> None: # https://bugs.python.org/issue37871 write = self.file.write for line in text.splitlines(True): - write(line) + try: + write(line) + except UnicodeEncodeError as error: + error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" + raise else: text = self._render_buffer(self._buffer[:]) try: self.file.write(text) - self.file.flush() except UnicodeEncodeError as error: error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" raise + self.file.flush() del self._buffer[:] def _render_buffer(self, buffer: Iterable[Segment]) -> str: From 6779e51de341ef41d5575021d5caf46918ac15dc Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 11:41:00 +0000 Subject: [PATCH 08/23] Don't default to outputting ANSI just because Console.record=True --- rich/console.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rich/console.py b/rich/console.py index 45c132562..15a787208 100644 --- a/rich/console.py +++ b/rich/console.py @@ -1924,11 +1924,9 @@ def _check_buffer(self) -> None: legacy_windows_render( self._buffer[:], LegacyWindowsTerm(self.file) ) - if ( - not legacy_windows_stdout - or self._buffer_index - or self.record - ): + + output_capture_enabled = bool(self._buffer_index) + if not legacy_windows_stdout or output_capture_enabled: text = self._render_buffer(self._buffer[:]) # https://bugs.python.org/issue37871 write = self.file.write From 501dcbdfe8aeae8aabca8d5ad2a0e0b45045aff0 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 16:15:21 +0000 Subject: [PATCH 09/23] Add tests for LegacyWindowsTerm --- rich/_win32_console.py | 21 ++- tests/test_win32_console.py | 341 ++++++++++++++++++++++++++++++++++++ 2 files changed, 351 insertions(+), 11 deletions(-) create mode 100644 tests/test_win32_console.py diff --git a/rich/_win32_console.py b/rich/_win32_console.py index 0c4c8a4b8..b516fd4ad 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -72,7 +72,7 @@ def GetConsoleMode(std_handle: wintypes.HANDLE, console_mode: wintypes.DWORD) -> return bool(_GetConsoleMode(std_handle, console_mode)) -_FillConsoleOutputCharacterW = windll.kernel32.FillConsoleOutputCharacterW +_FillConsoleOutputCharacterW = kernel32.FillConsoleOutputCharacterW _FillConsoleOutputCharacterW.argtypes = [ wintypes.HANDLE, ctypes.c_char, @@ -104,7 +104,7 @@ def FillConsoleOutputCharacter( return num_written.value -_FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute +_FillConsoleOutputAttribute = kernel32.FillConsoleOutputAttribute _FillConsoleOutputAttribute.argtypes = [ wintypes.HANDLE, wintypes.WORD, @@ -130,7 +130,7 @@ def FillConsoleOutputAttribute( return num_written.value -_SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute +_SetConsoleTextAttribute = kernel32.SetConsoleTextAttribute _SetConsoleTextAttribute.argtypes = [ wintypes.HANDLE, wintypes.WORD, @@ -144,7 +144,7 @@ def SetConsoleTextAttribute( return bool(_SetConsoleTextAttribute(std_handle, attributes)) -_GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo +_GetConsoleScreenBufferInfo = kernel32.GetConsoleScreenBufferInfo _GetConsoleScreenBufferInfo.argtypes = [ wintypes.HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), @@ -160,7 +160,7 @@ def GetConsoleScreenBufferInfo( return console_screen_buffer_info -_SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition +_SetConsoleCursorPosition = kernel32.SetConsoleCursorPosition _SetConsoleCursorPosition.argtypes = [ wintypes.HANDLE, cast(Type[COORD], WindowsCoordinates), @@ -171,12 +171,10 @@ def GetConsoleScreenBufferInfo( def SetConsoleCursorPosition( std_handle: wintypes.HANDLE, coords: WindowsCoordinates ) -> bool: - if coords.col < 0 or coords.row < 0: - return False return bool(_SetConsoleCursorPosition(std_handle, coords)) -_SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo +_SetConsoleCursorInfo = kernel32.SetConsoleCursorInfo _SetConsoleCursorInfo.argtypes = [ wintypes.HANDLE, ctypes.POINTER(CONSOLE_CURSOR_INFO), @@ -190,7 +188,7 @@ def SetConsoleCursorInfo( return bool(_SetConsoleCursorInfo(std_handle, byref(cursor_info))) -_SetConsoleTitle = windll.kernel32.SetConsoleTitleW +_SetConsoleTitle = kernel32.SetConsoleTitleW _SetConsoleTitle.argtypes = [wintypes.LPCWSTR] _SetConsoleTitle.restype = wintypes.BOOL @@ -280,7 +278,6 @@ def write_styled(self, text: str, style: Style) -> None: text (str): The text to write style (Style): The style of the text """ - # TODO: Check for bold, bright, etc. inside the style if style.color: fore = style.color.downgrade(ColorSystem.WINDOWS).number fore = fore if fore is not None else 7 # Default to ANSI 7: White @@ -310,6 +307,8 @@ def move_cursor_to(self, new_position: WindowsCoordinates) -> None: Args: new_position (WindowsCoordinates): The WindowsCoordinates representing the new position of the cursor. """ + if new_position.col < 0 or new_position.row < 0: + return SetConsoleCursorPosition(self._handle, coords=new_position) def erase_line(self) -> None: @@ -412,7 +411,7 @@ def hide_cursor(self) -> None: def show_cursor(self) -> None: """Show the cursor""" - visible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisibile=1) + visible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=1) SetConsoleCursorInfo(self._handle, cursor_info=visible_cursor) def set_title(self, title: str) -> None: diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py new file mode 100644 index 000000000..701703d8a --- /dev/null +++ b/tests/test_win32_console.py @@ -0,0 +1,341 @@ +import dataclasses +import sys +from io import StringIO +from unittest import mock +from unittest.mock import call, patch + +import pytest + +import rich +from rich.style import Style + +try: + from rich._win32_console import COORD, LegacyWindowsTerm, WindowsCoordinates +except ImportError: + pass + +pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="windows only") + + +def test_windows_coordinates_to_ctype(): + coord = WindowsCoordinates.from_param(WindowsCoordinates(row=1, col=2)) + assert coord.X == 2 + assert coord.Y == 1 + + +CURSOR_X = 1 +CURSOR_Y = 2 +CURSOR_POSITION = WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X) +SCREEN_WIDTH = 20 +SCREEN_HEIGHT = 30 +DEFAULT_STYLE_ATTRIBUTE = 16 + + +@dataclasses.dataclass +class StubScreenBufferInfo: + dwCursorPosition: COORD = COORD(CURSOR_X, CURSOR_Y) + dwSize: COORD = COORD(SCREEN_WIDTH, SCREEN_HEIGHT) + wAttributes: int = DEFAULT_STYLE_ATTRIBUTE + + +@pytest.fixture +def win32_handle(): + handle = mock.sentinel + with mock.patch.object(rich._win32_console, "GetStdHandle", return_value=handle): + yield handle + + +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_cursor_position(_): + term = LegacyWindowsTerm() + assert term.cursor_position == WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X) + + +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_screen_size(_): + term = LegacyWindowsTerm() + assert term.screen_size == WindowsCoordinates(row=SCREEN_HEIGHT, col=SCREEN_WIDTH) + + +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_write_text(_): + f = StringIO() + text = "Hello, world!" + term = LegacyWindowsTerm(file=f) + + term.write_text(text) + + assert f.getvalue() == text + + +@patch.object(rich._win32_console, "SetConsoleTextAttribute") +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_write_styled(_, SetConsoleTextAttribute, win32_handle): + f = StringIO() + style = Style.parse("black on red") + text = "Hello, world!" + term = LegacyWindowsTerm(file=f) + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + + assert f.getvalue() == text + # Ensure we set the text attributes and then reset them after writing styled text + assert call_args[0].args == (win32_handle,) + assert call_args[0].kwargs["attributes"].value == 64 + assert call_args[1] == call(win32_handle, attributes=DEFAULT_STYLE_ATTRIBUTE) + + +@patch.object(rich._win32_console, "FillConsoleOutputCharacter", return_value=None) +@patch.object(rich._win32_console, "FillConsoleOutputAttribute", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_erase_line( + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle +): + term = LegacyWindowsTerm() + term.erase_line() + start = WindowsCoordinates(row=CURSOR_Y, col=0) + FillConsoleOutputCharacter.assert_called_once_with( + win32_handle, " ", length=SCREEN_WIDTH, start=start + ) + FillConsoleOutputAttribute.assert_called_once_with( + win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=SCREEN_WIDTH, start=start + ) + + +@patch.object(rich._win32_console, "FillConsoleOutputCharacter", return_value=None) +@patch.object(rich._win32_console, "FillConsoleOutputAttribute", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_erase_end_of_line( + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle +): + term = LegacyWindowsTerm() + term.erase_end_of_line() + + FillConsoleOutputCharacter.assert_called_once_with( + win32_handle, " ", length=SCREEN_WIDTH - CURSOR_X, start=CURSOR_POSITION + ) + FillConsoleOutputAttribute.assert_called_once_with( + win32_handle, + DEFAULT_STYLE_ATTRIBUTE, + length=SCREEN_WIDTH - CURSOR_X, + start=CURSOR_POSITION, + ) + + +@patch.object(rich._win32_console, "FillConsoleOutputCharacter", return_value=None) +@patch.object(rich._win32_console, "FillConsoleOutputAttribute", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_erase_start_of_line( + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle +): + term = LegacyWindowsTerm() + term.erase_start_of_line() + + start = WindowsCoordinates(CURSOR_Y, 0) + + FillConsoleOutputCharacter.assert_called_once_with( + win32_handle, " ", length=CURSOR_X, start=start + ) + FillConsoleOutputAttribute.assert_called_once_with( + win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=CURSOR_X, start=start + ) + + +@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_move_cursor_to(_, SetConsoleCursorPosition, win32_handle): + coords = WindowsCoordinates(row=4, col=5) + term = LegacyWindowsTerm() + + term.move_cursor_to(coords) + + SetConsoleCursorPosition.assert_called_once_with(win32_handle, coords=coords) + + +@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_move_cursor_to_out_of_bounds_row(_, SetConsoleCursorPosition, win32_handle): + coords = WindowsCoordinates(row=-1, col=4) + term = LegacyWindowsTerm() + + term.move_cursor_to(coords) + + assert not SetConsoleCursorPosition.called + + +@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_move_cursor_to_out_of_bounds_col(_, SetConsoleCursorPosition, win32_handle): + coords = WindowsCoordinates(row=10, col=-4) + term = LegacyWindowsTerm() + + term.move_cursor_to(coords) + + assert not SetConsoleCursorPosition.called + + +@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_move_cursor_up(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + + term.move_cursor_up() + + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=CURSOR_X) + ) + + +@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_move_cursor_down(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + + term.move_cursor_down() + + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=CURSOR_X) + ) + + +@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_move_cursor_forward(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + + term.move_cursor_forward() + + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X + 1) + ) + + +@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) +def test_move_cursor_forward_newline_wrap(SetConsoleCursorPosition, win32_handle): + cursor_at_end_of_line = StubScreenBufferInfo( + dwCursorPosition=COORD(SCREEN_WIDTH - 1, CURSOR_Y) + ) + with patch.object( + rich._win32_console, + "GetConsoleScreenBufferInfo", + return_value=cursor_at_end_of_line, + ): + term = LegacyWindowsTerm() + term.move_cursor_forward() + + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=0) + ) + + +@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_move_cursor_to_column(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + term.move_cursor_to_column(5) + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(CURSOR_Y, 5) + ) + + +@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_move_cursor_backward(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + term.move_cursor_backward() + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X - 1) + ) + + +@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) +def test_move_cursor_backward_prev_line_wrap(SetConsoleCursorPosition, win32_handle): + cursor_at_start_of_line = StubScreenBufferInfo(dwCursorPosition=COORD(0, CURSOR_Y)) + with patch.object( + rich._win32_console, + "GetConsoleScreenBufferInfo", + return_value=cursor_at_start_of_line, + ): + term = LegacyWindowsTerm() + term.move_cursor_backward() + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=SCREEN_WIDTH - 1) + ) + + +@patch.object(rich._win32_console, "SetConsoleCursorInfo", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_hide_cursor(_, SetConsoleCursorInfo, win32_handle): + term = LegacyWindowsTerm() + term.hide_cursor() + + call_args = SetConsoleCursorInfo.call_args_list + + assert len(call_args) == 1 + assert call_args[0].kwargs["cursor_info"].bVisible == 0 + assert call_args[0].kwargs["cursor_info"].dwSize == 100 + + +@patch.object(rich._win32_console, "SetConsoleCursorInfo", return_value=None) +@patch.object( + rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo +) +def test_show_cursor(_, SetConsoleCursorInfo, win32_handle): + term = LegacyWindowsTerm() + term.show_cursor() + + call_args = SetConsoleCursorInfo.call_args_list + + assert len(call_args) == 1 + assert call_args[0].kwargs["cursor_info"].bVisible == 1 + assert call_args[0].kwargs["cursor_info"].dwSize == 100 + + +@patch.object(rich._win32_console, "SetConsoleTitle", return_value=None) +def test_set_title(SetConsoleTitle): + term = LegacyWindowsTerm() + term.set_title("title") + + SetConsoleTitle.assert_called_once_with("title") + + +@patch.object(rich._win32_console, "SetConsoleTitle", return_value=None) +def test_set_title_too_long(_): + term = LegacyWindowsTerm() + + with pytest.raises(AssertionError): + term.set_title("a" * 255) From 97d05f592a9f0cad9e59768d921d81912e6169ff Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 16:19:18 +0000 Subject: [PATCH 10/23] Fixing test module on non-Windows platforms --- tests/test_win32_console.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py index 701703d8a..3cb26a5b8 100644 --- a/tests/test_win32_console.py +++ b/tests/test_win32_console.py @@ -11,6 +11,14 @@ try: from rich._win32_console import COORD, LegacyWindowsTerm, WindowsCoordinates + + CURSOR_X = 1 + CURSOR_Y = 2 + CURSOR_POSITION = WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X) + SCREEN_WIDTH = 20 + SCREEN_HEIGHT = 30 + DEFAULT_STYLE_ATTRIBUTE = 16 + except ImportError: pass @@ -23,14 +31,6 @@ def test_windows_coordinates_to_ctype(): assert coord.Y == 1 -CURSOR_X = 1 -CURSOR_Y = 2 -CURSOR_POSITION = WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X) -SCREEN_WIDTH = 20 -SCREEN_HEIGHT = 30 -DEFAULT_STYLE_ATTRIBUTE = 16 - - @dataclasses.dataclass class StubScreenBufferInfo: dwCursorPosition: COORD = COORD(CURSOR_X, CURSOR_Y) From 7d08e0a7cdba1d500f748dc5db8dcb9af2392bbb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Mar 2022 16:23:05 +0000 Subject: [PATCH 11/23] Fixing test module on non-Windows platforms --- tests/test_win32_console.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py index 3cb26a5b8..120e4b9e1 100644 --- a/tests/test_win32_console.py +++ b/tests/test_win32_console.py @@ -19,6 +19,12 @@ SCREEN_HEIGHT = 30 DEFAULT_STYLE_ATTRIBUTE = 16 + @dataclasses.dataclass + class StubScreenBufferInfo: + dwCursorPosition: COORD = COORD(CURSOR_X, CURSOR_Y) + dwSize: COORD = COORD(SCREEN_WIDTH, SCREEN_HEIGHT) + wAttributes: int = DEFAULT_STYLE_ATTRIBUTE + except ImportError: pass @@ -31,13 +37,6 @@ def test_windows_coordinates_to_ctype(): assert coord.Y == 1 -@dataclasses.dataclass -class StubScreenBufferInfo: - dwCursorPosition: COORD = COORD(CURSOR_X, CURSOR_Y) - dwSize: COORD = COORD(SCREEN_WIDTH, SCREEN_HEIGHT) - wAttributes: int = DEFAULT_STYLE_ATTRIBUTE - - @pytest.fixture def win32_handle(): handle = mock.sentinel From 151276b2cff08bf5f7a3adea55d2683c083b13a0 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 2 Mar 2022 11:17:31 +0000 Subject: [PATCH 12/23] Run legacy Windows tests on Windows only --- rich/_win32_console.py | 5 +- tests/test_win32_console.py | 478 ++++++++++++++++++------------------ 2 files changed, 241 insertions(+), 242 deletions(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index b516fd4ad..3a4a2fe04 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -1,9 +1,8 @@ -"""Light wrapper around the win32 Console API - this module can only be imported on Windows""" +"""Light wrapper around the win32 Console API - this module should only be imported on Windows""" import ctypes import sys -from typing import IO, Any, NamedTuple, Type, cast +from typing import IO, NamedTuple, Type, cast -windll: Any = None if sys.platform == "win32": windll = ctypes.LibraryLoader(ctypes.WinDLL) else: diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py index 120e4b9e1..fbd727d3b 100644 --- a/tests/test_win32_console.py +++ b/tests/test_win32_console.py @@ -6,11 +6,12 @@ import pytest -import rich from rich.style import Style -try: +if sys.platform == "win32": + from rich._win32_console import COORD, LegacyWindowsTerm, WindowsCoordinates + from rich import _win32_console CURSOR_X = 1 CURSOR_Y = 2 @@ -19,322 +20,321 @@ SCREEN_HEIGHT = 30 DEFAULT_STYLE_ATTRIBUTE = 16 + @dataclasses.dataclass class StubScreenBufferInfo: dwCursorPosition: COORD = COORD(CURSOR_X, CURSOR_Y) dwSize: COORD = COORD(SCREEN_WIDTH, SCREEN_HEIGHT) wAttributes: int = DEFAULT_STYLE_ATTRIBUTE -except ImportError: - pass -pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="windows only") + pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="windows only") -def test_windows_coordinates_to_ctype(): - coord = WindowsCoordinates.from_param(WindowsCoordinates(row=1, col=2)) - assert coord.X == 2 - assert coord.Y == 1 + def test_windows_coordinates_to_ctype(): + coord = WindowsCoordinates.from_param(WindowsCoordinates(row=1, col=2)) + assert coord.X == 2 + assert coord.Y == 1 -@pytest.fixture -def win32_handle(): - handle = mock.sentinel - with mock.patch.object(rich._win32_console, "GetStdHandle", return_value=handle): - yield handle + @pytest.fixture + def win32_handle(): + handle = mock.sentinel + with mock.patch.object(_win32_console, "GetStdHandle", return_value=handle): + yield handle -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_cursor_position(_): - term = LegacyWindowsTerm() - assert term.cursor_position == WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_cursor_position(_): + term = LegacyWindowsTerm() + assert term.cursor_position == WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_screen_size(_): - term = LegacyWindowsTerm() - assert term.screen_size == WindowsCoordinates(row=SCREEN_HEIGHT, col=SCREEN_WIDTH) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_screen_size(_): + term = LegacyWindowsTerm() + assert term.screen_size == WindowsCoordinates(row=SCREEN_HEIGHT, col=SCREEN_WIDTH) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_write_text(_): - f = StringIO() - text = "Hello, world!" - term = LegacyWindowsTerm(file=f) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_text(_): + f = StringIO() + text = "Hello, world!" + term = LegacyWindowsTerm(file=f) - term.write_text(text) + term.write_text(text) - assert f.getvalue() == text + assert f.getvalue() == text -@patch.object(rich._win32_console, "SetConsoleTextAttribute") -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_write_styled(_, SetConsoleTextAttribute, win32_handle): - f = StringIO() - style = Style.parse("black on red") - text = "Hello, world!" - term = LegacyWindowsTerm(file=f) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled(_, SetConsoleTextAttribute, win32_handle): + f = StringIO() + style = Style.parse("black on red") + text = "Hello, world!" + term = LegacyWindowsTerm(file=f) - term.write_styled(text, style) + term.write_styled(text, style) - call_args = SetConsoleTextAttribute.call_args_list + call_args = SetConsoleTextAttribute.call_args_list - assert f.getvalue() == text - # Ensure we set the text attributes and then reset them after writing styled text - assert call_args[0].args == (win32_handle,) - assert call_args[0].kwargs["attributes"].value == 64 - assert call_args[1] == call(win32_handle, attributes=DEFAULT_STYLE_ATTRIBUTE) + assert f.getvalue() == text + # Ensure we set the text attributes and then reset them after writing styled text + assert call_args[0].args == (win32_handle,) + assert call_args[0].kwargs["attributes"].value == 64 + assert call_args[1] == call(win32_handle, attributes=DEFAULT_STYLE_ATTRIBUTE) -@patch.object(rich._win32_console, "FillConsoleOutputCharacter", return_value=None) -@patch.object(rich._win32_console, "FillConsoleOutputAttribute", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_erase_line( - _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle -): - term = LegacyWindowsTerm() - term.erase_line() - start = WindowsCoordinates(row=CURSOR_Y, col=0) - FillConsoleOutputCharacter.assert_called_once_with( - win32_handle, " ", length=SCREEN_WIDTH, start=start - ) - FillConsoleOutputAttribute.assert_called_once_with( - win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=SCREEN_WIDTH, start=start + @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) + @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) - - -@patch.object(rich._win32_console, "FillConsoleOutputCharacter", return_value=None) -@patch.object(rich._win32_console, "FillConsoleOutputAttribute", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_erase_end_of_line( - _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle -): - term = LegacyWindowsTerm() - term.erase_end_of_line() - - FillConsoleOutputCharacter.assert_called_once_with( - win32_handle, " ", length=SCREEN_WIDTH - CURSOR_X, start=CURSOR_POSITION + def test_erase_line( + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + ): + term = LegacyWindowsTerm() + term.erase_line() + start = WindowsCoordinates(row=CURSOR_Y, col=0) + FillConsoleOutputCharacter.assert_called_once_with( + win32_handle, " ", length=SCREEN_WIDTH, start=start + ) + FillConsoleOutputAttribute.assert_called_once_with( + win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=SCREEN_WIDTH, start=start + ) + + + @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) + @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) - FillConsoleOutputAttribute.assert_called_once_with( - win32_handle, - DEFAULT_STYLE_ATTRIBUTE, - length=SCREEN_WIDTH - CURSOR_X, - start=CURSOR_POSITION, + def test_erase_end_of_line( + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + ): + term = LegacyWindowsTerm() + term.erase_end_of_line() + + FillConsoleOutputCharacter.assert_called_once_with( + win32_handle, " ", length=SCREEN_WIDTH - CURSOR_X, start=CURSOR_POSITION + ) + FillConsoleOutputAttribute.assert_called_once_with( + win32_handle, + DEFAULT_STYLE_ATTRIBUTE, + length=SCREEN_WIDTH - CURSOR_X, + start=CURSOR_POSITION, + ) + + + @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) + @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) + def test_erase_start_of_line( + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + ): + term = LegacyWindowsTerm() + term.erase_start_of_line() + start = WindowsCoordinates(CURSOR_Y, 0) -@patch.object(rich._win32_console, "FillConsoleOutputCharacter", return_value=None) -@patch.object(rich._win32_console, "FillConsoleOutputAttribute", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_erase_start_of_line( - _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle -): - term = LegacyWindowsTerm() - term.erase_start_of_line() + FillConsoleOutputCharacter.assert_called_once_with( + win32_handle, " ", length=CURSOR_X, start=start + ) + FillConsoleOutputAttribute.assert_called_once_with( + win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=CURSOR_X, start=start + ) - start = WindowsCoordinates(CURSOR_Y, 0) - FillConsoleOutputCharacter.assert_called_once_with( - win32_handle, " ", length=CURSOR_X, start=start + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) - FillConsoleOutputAttribute.assert_called_once_with( - win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=CURSOR_X, start=start - ) - - -@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_move_cursor_to(_, SetConsoleCursorPosition, win32_handle): - coords = WindowsCoordinates(row=4, col=5) - term = LegacyWindowsTerm() - - term.move_cursor_to(coords) - - SetConsoleCursorPosition.assert_called_once_with(win32_handle, coords=coords) + def test_move_cursor_to(_, SetConsoleCursorPosition, win32_handle): + coords = WindowsCoordinates(row=4, col=5) + term = LegacyWindowsTerm() + term.move_cursor_to(coords) -@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_move_cursor_to_out_of_bounds_row(_, SetConsoleCursorPosition, win32_handle): - coords = WindowsCoordinates(row=-1, col=4) - term = LegacyWindowsTerm() + SetConsoleCursorPosition.assert_called_once_with(win32_handle, coords=coords) - term.move_cursor_to(coords) - assert not SetConsoleCursorPosition.called + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_to_out_of_bounds_row(_, SetConsoleCursorPosition, win32_handle): + coords = WindowsCoordinates(row=-1, col=4) + term = LegacyWindowsTerm() + term.move_cursor_to(coords) -@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_move_cursor_to_out_of_bounds_col(_, SetConsoleCursorPosition, win32_handle): - coords = WindowsCoordinates(row=10, col=-4) - term = LegacyWindowsTerm() + assert not SetConsoleCursorPosition.called - term.move_cursor_to(coords) - assert not SetConsoleCursorPosition.called + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_to_out_of_bounds_col(_, SetConsoleCursorPosition, win32_handle): + coords = WindowsCoordinates(row=10, col=-4) + term = LegacyWindowsTerm() + term.move_cursor_to(coords) -@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_move_cursor_up(_, SetConsoleCursorPosition, win32_handle): - term = LegacyWindowsTerm() + assert not SetConsoleCursorPosition.called - term.move_cursor_up() - SetConsoleCursorPosition.assert_called_once_with( - win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=CURSOR_X) + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) + def test_move_cursor_up(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + term.move_cursor_up() -@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_move_cursor_down(_, SetConsoleCursorPosition, win32_handle): - term = LegacyWindowsTerm() + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=CURSOR_X) + ) - term.move_cursor_down() - SetConsoleCursorPosition.assert_called_once_with( - win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=CURSOR_X) + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) + def test_move_cursor_down(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + term.move_cursor_down() -@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_move_cursor_forward(_, SetConsoleCursorPosition, win32_handle): - term = LegacyWindowsTerm() - - term.move_cursor_forward() - - SetConsoleCursorPosition.assert_called_once_with( - win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X + 1) - ) + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=CURSOR_X) + ) -@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) -def test_move_cursor_forward_newline_wrap(SetConsoleCursorPosition, win32_handle): - cursor_at_end_of_line = StubScreenBufferInfo( - dwCursorPosition=COORD(SCREEN_WIDTH - 1, CURSOR_Y) + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) - with patch.object( - rich._win32_console, - "GetConsoleScreenBufferInfo", - return_value=cursor_at_end_of_line, - ): + def test_move_cursor_forward(_, SetConsoleCursorPosition, win32_handle): term = LegacyWindowsTerm() + term.move_cursor_forward() - SetConsoleCursorPosition.assert_called_once_with( - win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=0) - ) + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X + 1) + ) -@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_move_cursor_to_column(_, SetConsoleCursorPosition, win32_handle): - term = LegacyWindowsTerm() - term.move_cursor_to_column(5) - SetConsoleCursorPosition.assert_called_once_with( - win32_handle, coords=WindowsCoordinates(CURSOR_Y, 5) - ) + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + def test_move_cursor_forward_newline_wrap(SetConsoleCursorPosition, win32_handle): + cursor_at_end_of_line = StubScreenBufferInfo( + dwCursorPosition=COORD(SCREEN_WIDTH - 1, CURSOR_Y) + ) + with patch.object( + _win32_console, + "GetConsoleScreenBufferInfo", + return_value=cursor_at_end_of_line, + ): + term = LegacyWindowsTerm() + term.move_cursor_forward() + + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=0) + ) -@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_move_cursor_backward(_, SetConsoleCursorPosition, win32_handle): - term = LegacyWindowsTerm() - term.move_cursor_backward() - SetConsoleCursorPosition.assert_called_once_with( - win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X - 1) + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) + def test_move_cursor_to_column(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + term.move_cursor_to_column(5) + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(CURSOR_Y, 5) + ) -@patch.object(rich._win32_console, "SetConsoleCursorPosition", return_value=None) -def test_move_cursor_backward_prev_line_wrap(SetConsoleCursorPosition, win32_handle): - cursor_at_start_of_line = StubScreenBufferInfo(dwCursorPosition=COORD(0, CURSOR_Y)) - with patch.object( - rich._win32_console, - "GetConsoleScreenBufferInfo", - return_value=cursor_at_start_of_line, - ): + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_backward(_, SetConsoleCursorPosition, win32_handle): term = LegacyWindowsTerm() term.move_cursor_backward() - SetConsoleCursorPosition.assert_called_once_with( - win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=SCREEN_WIDTH - 1) + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X - 1) + ) + + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + def test_move_cursor_backward_prev_line_wrap(SetConsoleCursorPosition, win32_handle): + cursor_at_start_of_line = StubScreenBufferInfo(dwCursorPosition=COORD(0, CURSOR_Y)) + with patch.object( + _win32_console, + "GetConsoleScreenBufferInfo", + return_value=cursor_at_start_of_line, + ): + term = LegacyWindowsTerm() + term.move_cursor_backward() + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=SCREEN_WIDTH - 1) + ) + + + @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) + def test_hide_cursor(_, SetConsoleCursorInfo, win32_handle): + term = LegacyWindowsTerm() + term.hide_cursor() + call_args = SetConsoleCursorInfo.call_args_list -@patch.object(rich._win32_console, "SetConsoleCursorInfo", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_hide_cursor(_, SetConsoleCursorInfo, win32_handle): - term = LegacyWindowsTerm() - term.hide_cursor() - - call_args = SetConsoleCursorInfo.call_args_list - - assert len(call_args) == 1 - assert call_args[0].kwargs["cursor_info"].bVisible == 0 - assert call_args[0].kwargs["cursor_info"].dwSize == 100 + assert len(call_args) == 1 + assert call_args[0].kwargs["cursor_info"].bVisible == 0 + assert call_args[0].kwargs["cursor_info"].dwSize == 100 -@patch.object(rich._win32_console, "SetConsoleCursorInfo", return_value=None) -@patch.object( - rich._win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo -) -def test_show_cursor(_, SetConsoleCursorInfo, win32_handle): - term = LegacyWindowsTerm() - term.show_cursor() + @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_show_cursor(_, SetConsoleCursorInfo, win32_handle): + term = LegacyWindowsTerm() + term.show_cursor() - call_args = SetConsoleCursorInfo.call_args_list + call_args = SetConsoleCursorInfo.call_args_list - assert len(call_args) == 1 - assert call_args[0].kwargs["cursor_info"].bVisible == 1 - assert call_args[0].kwargs["cursor_info"].dwSize == 100 + assert len(call_args) == 1 + assert call_args[0].kwargs["cursor_info"].bVisible == 1 + assert call_args[0].kwargs["cursor_info"].dwSize == 100 -@patch.object(rich._win32_console, "SetConsoleTitle", return_value=None) -def test_set_title(SetConsoleTitle): - term = LegacyWindowsTerm() - term.set_title("title") + @patch.object(_win32_console, "SetConsoleTitle", return_value=None) + def test_set_title(SetConsoleTitle): + term = LegacyWindowsTerm() + term.set_title("title") - SetConsoleTitle.assert_called_once_with("title") + SetConsoleTitle.assert_called_once_with("title") -@patch.object(rich._win32_console, "SetConsoleTitle", return_value=None) -def test_set_title_too_long(_): - term = LegacyWindowsTerm() + @patch.object(_win32_console, "SetConsoleTitle", return_value=None) + def test_set_title_too_long(_): + term = LegacyWindowsTerm() - with pytest.raises(AssertionError): - term.set_title("a" * 255) + with pytest.raises(AssertionError): + term.set_title("a" * 255) From 862416acf4fa1dadd0e3fc5eab9212890436c4df Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 2 Mar 2022 11:25:23 +0000 Subject: [PATCH 13/23] Fix typing issues --- rich/_win32_console.py | 22 ++++++------ tests/test_win32_console.py | 68 +++++++++++++++---------------------- 2 files changed, 38 insertions(+), 52 deletions(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index 3a4a2fe04..8a6af2433 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -1,8 +1,9 @@ """Light wrapper around the win32 Console API - this module should only be imported on Windows""" import ctypes import sys -from typing import IO, NamedTuple, Type, cast +from typing import IO, Any, NamedTuple, Type, cast +windll: Any = None if sys.platform == "win32": windll = ctypes.LibraryLoader(ctypes.WinDLL) else: @@ -18,7 +19,6 @@ STDOUT = -11 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 -kernel32 = windll.kernel32 COORD = wintypes._COORD @@ -51,7 +51,7 @@ class CONSOLE_CURSOR_INFO(ctypes.Structure): _fields_ = [("dwSize", wintypes.DWORD), ("bVisible", wintypes.BOOL)] -_GetStdHandle = kernel32.GetStdHandle +_GetStdHandle = windll.kernel32.GetStdHandle _GetStdHandle.argtypes = [ wintypes.DWORD, ] @@ -62,7 +62,7 @@ def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE: return cast(wintypes.HANDLE, _GetStdHandle(handle)) -_GetConsoleMode = kernel32.GetConsoleMode +_GetConsoleMode = windll.kernel32.GetConsoleMode _GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] _GetConsoleMode.restype = wintypes.BOOL @@ -71,7 +71,7 @@ def GetConsoleMode(std_handle: wintypes.HANDLE, console_mode: wintypes.DWORD) -> return bool(_GetConsoleMode(std_handle, console_mode)) -_FillConsoleOutputCharacterW = kernel32.FillConsoleOutputCharacterW +_FillConsoleOutputCharacterW = windll.kernel32.FillConsoleOutputCharacterW _FillConsoleOutputCharacterW.argtypes = [ wintypes.HANDLE, ctypes.c_char, @@ -103,7 +103,7 @@ def FillConsoleOutputCharacter( return num_written.value -_FillConsoleOutputAttribute = kernel32.FillConsoleOutputAttribute +_FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute _FillConsoleOutputAttribute.argtypes = [ wintypes.HANDLE, wintypes.WORD, @@ -129,7 +129,7 @@ def FillConsoleOutputAttribute( return num_written.value -_SetConsoleTextAttribute = kernel32.SetConsoleTextAttribute +_SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute _SetConsoleTextAttribute.argtypes = [ wintypes.HANDLE, wintypes.WORD, @@ -143,7 +143,7 @@ def SetConsoleTextAttribute( return bool(_SetConsoleTextAttribute(std_handle, attributes)) -_GetConsoleScreenBufferInfo = kernel32.GetConsoleScreenBufferInfo +_GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo _GetConsoleScreenBufferInfo.argtypes = [ wintypes.HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), @@ -159,7 +159,7 @@ def GetConsoleScreenBufferInfo( return console_screen_buffer_info -_SetConsoleCursorPosition = kernel32.SetConsoleCursorPosition +_SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition _SetConsoleCursorPosition.argtypes = [ wintypes.HANDLE, cast(Type[COORD], WindowsCoordinates), @@ -173,7 +173,7 @@ def SetConsoleCursorPosition( return bool(_SetConsoleCursorPosition(std_handle, coords)) -_SetConsoleCursorInfo = kernel32.SetConsoleCursorInfo +_SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo _SetConsoleCursorInfo.argtypes = [ wintypes.HANDLE, ctypes.POINTER(CONSOLE_CURSOR_INFO), @@ -187,7 +187,7 @@ def SetConsoleCursorInfo( return bool(_SetConsoleCursorInfo(std_handle, byref(cursor_info))) -_SetConsoleTitle = kernel32.SetConsoleTitleW +_SetConsoleTitle = windll.kernel32.SetConsoleTitleW _SetConsoleTitle.argtypes = [wintypes.LPCWSTR] _SetConsoleTitle.restype = wintypes.BOOL diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py index fbd727d3b..0257204ac 100644 --- a/tests/test_win32_console.py +++ b/tests/test_win32_console.py @@ -10,8 +10,8 @@ if sys.platform == "win32": - from rich._win32_console import COORD, LegacyWindowsTerm, WindowsCoordinates from rich import _win32_console + from rich._win32_console import COORD, LegacyWindowsTerm, WindowsCoordinates CURSOR_X = 1 CURSOR_Y = 2 @@ -20,30 +20,25 @@ SCREEN_HEIGHT = 30 DEFAULT_STYLE_ATTRIBUTE = 16 - @dataclasses.dataclass class StubScreenBufferInfo: dwCursorPosition: COORD = COORD(CURSOR_X, CURSOR_Y) dwSize: COORD = COORD(SCREEN_WIDTH, SCREEN_HEIGHT) wAttributes: int = DEFAULT_STYLE_ATTRIBUTE - pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="windows only") - def test_windows_coordinates_to_ctype(): coord = WindowsCoordinates.from_param(WindowsCoordinates(row=1, col=2)) assert coord.X == 2 assert coord.Y == 1 - @pytest.fixture def win32_handle(): handle = mock.sentinel with mock.patch.object(_win32_console, "GetStdHandle", return_value=handle): yield handle - @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) @@ -51,14 +46,14 @@ def test_cursor_position(_): term = LegacyWindowsTerm() assert term.cursor_position == WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X) - @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) def test_screen_size(_): term = LegacyWindowsTerm() - assert term.screen_size == WindowsCoordinates(row=SCREEN_HEIGHT, col=SCREEN_WIDTH) - + assert term.screen_size == WindowsCoordinates( + row=SCREEN_HEIGHT, col=SCREEN_WIDTH + ) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo @@ -72,7 +67,6 @@ def test_write_text(_): assert f.getvalue() == text - @patch.object(_win32_console, "SetConsoleTextAttribute") @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo @@ -93,14 +87,13 @@ def test_write_styled(_, SetConsoleTextAttribute, win32_handle): assert call_args[0].kwargs["attributes"].value == 64 assert call_args[1] == call(win32_handle, attributes=DEFAULT_STYLE_ATTRIBUTE) - @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) def test_erase_line( - _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle ): term = LegacyWindowsTerm() term.erase_line() @@ -112,14 +105,13 @@ def test_erase_line( win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=SCREEN_WIDTH, start=start ) - @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) def test_erase_end_of_line( - _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle ): term = LegacyWindowsTerm() term.erase_end_of_line() @@ -134,14 +126,13 @@ def test_erase_end_of_line( start=CURSOR_POSITION, ) - @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) def test_erase_start_of_line( - _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle ): term = LegacyWindowsTerm() term.erase_start_of_line() @@ -155,7 +146,6 @@ def test_erase_start_of_line( win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=CURSOR_X, start=start ) - @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo @@ -168,12 +158,13 @@ def test_move_cursor_to(_, SetConsoleCursorPosition, win32_handle): SetConsoleCursorPosition.assert_called_once_with(win32_handle, coords=coords) - @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) - def test_move_cursor_to_out_of_bounds_row(_, SetConsoleCursorPosition, win32_handle): + def test_move_cursor_to_out_of_bounds_row( + _, SetConsoleCursorPosition, win32_handle + ): coords = WindowsCoordinates(row=-1, col=4) term = LegacyWindowsTerm() @@ -181,12 +172,13 @@ def test_move_cursor_to_out_of_bounds_row(_, SetConsoleCursorPosition, win32_han assert not SetConsoleCursorPosition.called - @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) - def test_move_cursor_to_out_of_bounds_col(_, SetConsoleCursorPosition, win32_handle): + def test_move_cursor_to_out_of_bounds_col( + _, SetConsoleCursorPosition, win32_handle + ): coords = WindowsCoordinates(row=10, col=-4) term = LegacyWindowsTerm() @@ -194,7 +186,6 @@ def test_move_cursor_to_out_of_bounds_col(_, SetConsoleCursorPosition, win32_han assert not SetConsoleCursorPosition.called - @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo @@ -208,7 +199,6 @@ def test_move_cursor_up(_, SetConsoleCursorPosition, win32_handle): win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=CURSOR_X) ) - @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo @@ -222,7 +212,6 @@ def test_move_cursor_down(_, SetConsoleCursorPosition, win32_handle): win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=CURSOR_X) ) - @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo @@ -236,16 +225,15 @@ def test_move_cursor_forward(_, SetConsoleCursorPosition, win32_handle): win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X + 1) ) - @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) def test_move_cursor_forward_newline_wrap(SetConsoleCursorPosition, win32_handle): cursor_at_end_of_line = StubScreenBufferInfo( dwCursorPosition=COORD(SCREEN_WIDTH - 1, CURSOR_Y) ) with patch.object( - _win32_console, - "GetConsoleScreenBufferInfo", - return_value=cursor_at_end_of_line, + _win32_console, + "GetConsoleScreenBufferInfo", + return_value=cursor_at_end_of_line, ): term = LegacyWindowsTerm() term.move_cursor_forward() @@ -254,7 +242,6 @@ def test_move_cursor_forward_newline_wrap(SetConsoleCursorPosition, win32_handle win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=0) ) - @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo @@ -266,7 +253,6 @@ def test_move_cursor_to_column(_, SetConsoleCursorPosition, win32_handle): win32_handle, coords=WindowsCoordinates(CURSOR_Y, 5) ) - @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo @@ -278,22 +264,25 @@ def test_move_cursor_backward(_, SetConsoleCursorPosition, win32_handle): win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X - 1) ) - @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) - def test_move_cursor_backward_prev_line_wrap(SetConsoleCursorPosition, win32_handle): - cursor_at_start_of_line = StubScreenBufferInfo(dwCursorPosition=COORD(0, CURSOR_Y)) + def test_move_cursor_backward_prev_line_wrap( + SetConsoleCursorPosition, win32_handle + ): + cursor_at_start_of_line = StubScreenBufferInfo( + dwCursorPosition=COORD(0, CURSOR_Y) + ) with patch.object( - _win32_console, - "GetConsoleScreenBufferInfo", - return_value=cursor_at_start_of_line, + _win32_console, + "GetConsoleScreenBufferInfo", + return_value=cursor_at_start_of_line, ): term = LegacyWindowsTerm() term.move_cursor_backward() SetConsoleCursorPosition.assert_called_once_with( - win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=SCREEN_WIDTH - 1) + win32_handle, + coords=WindowsCoordinates(row=CURSOR_Y - 1, col=SCREEN_WIDTH - 1), ) - @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo @@ -308,7 +297,6 @@ def test_hide_cursor(_, SetConsoleCursorInfo, win32_handle): assert call_args[0].kwargs["cursor_info"].bVisible == 0 assert call_args[0].kwargs["cursor_info"].dwSize == 100 - @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo @@ -323,7 +311,6 @@ def test_show_cursor(_, SetConsoleCursorInfo, win32_handle): assert call_args[0].kwargs["cursor_info"].bVisible == 1 assert call_args[0].kwargs["cursor_info"].dwSize == 100 - @patch.object(_win32_console, "SetConsoleTitle", return_value=None) def test_set_title(SetConsoleTitle): term = LegacyWindowsTerm() @@ -331,7 +318,6 @@ def test_set_title(SetConsoleTitle): SetConsoleTitle.assert_called_once_with("title") - @patch.object(_win32_console, "SetConsoleTitle", return_value=None) def test_set_title_too_long(_): term = LegacyWindowsTerm() From 75253775bc0218088da495407c2361ecf8f1a0af Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 2 Mar 2022 11:50:07 +0000 Subject: [PATCH 14/23] Use Python 3.6 & 3.7 compatible means of acceessing mock call args/kwargs in test --- tests/test_win32_console.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py index 0257204ac..d3cc28050 100644 --- a/tests/test_win32_console.py +++ b/tests/test_win32_console.py @@ -83,9 +83,14 @@ def test_write_styled(_, SetConsoleTextAttribute, win32_handle): assert f.getvalue() == text # Ensure we set the text attributes and then reset them after writing styled text - assert call_args[0].args == (win32_handle,) - assert call_args[0].kwargs["attributes"].value == 64 - assert call_args[1] == call(win32_handle, attributes=DEFAULT_STYLE_ATTRIBUTE) + + first_args, first_kwargs = call_args[0] + second_args, second_kwargs = call_args[1] + + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == 64 + assert second_args == (win32_handle,) + assert second_kwargs["attributes"] == DEFAULT_STYLE_ATTRIBUTE @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) @@ -294,8 +299,10 @@ def test_hide_cursor(_, SetConsoleCursorInfo, win32_handle): call_args = SetConsoleCursorInfo.call_args_list assert len(call_args) == 1 - assert call_args[0].kwargs["cursor_info"].bVisible == 0 - assert call_args[0].kwargs["cursor_info"].dwSize == 100 + + args, kwargs = call_args[0] + assert kwargs["cursor_info"].bVisible == 0 + assert kwargs["cursor_info"].dwSize == 100 @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None) @patch.object( @@ -308,8 +315,10 @@ def test_show_cursor(_, SetConsoleCursorInfo, win32_handle): call_args = SetConsoleCursorInfo.call_args_list assert len(call_args) == 1 - assert call_args[0].kwargs["cursor_info"].bVisible == 1 - assert call_args[0].kwargs["cursor_info"].dwSize == 100 + + args, kwargs = call_args[0] + assert kwargs["cursor_info"].bVisible == 1 + assert kwargs["cursor_info"].dwSize == 100 @patch.object(_win32_console, "SetConsoleTitle", return_value=None) def test_set_title(SetConsoleTitle): From 80912a5bae514b8281aa22687f3982e2ae543e28 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 2 Mar 2022 12:41:07 +0000 Subject: [PATCH 15/23] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0e3e518..d62f7ba23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added ProgressColumn `MofNCompleteColumn` to display raw `completed/total` column (similar to DownloadColumn, but displays values as ints, does not convert to floats or add bit/bytes units). https://github.com/Textualize/rich/pull/1941 +- Remove Colorama dependency, call Windows Console API from Rich https://github.com/Textualize/rich/pull/1993 ### Fixed From ceef724ecd1bc24b8af6a2fed119c1b3f90d8744 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 3 Mar 2022 11:36:33 +0000 Subject: [PATCH 16/23] Use Windows Console API to write text --- rich/_win32_console.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index 8a6af2433..7130bd0e0 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -1,4 +1,7 @@ -"""Light wrapper around the win32 Console API - this module should only be imported on Windows""" +"""Light wrapper around the Win32 Console API - this module should only be imported on Windows + +The API that this module wraps is documented at https://docs.microsoft.com/en-us/windows/console/console-functions +""" import ctypes import sys from typing import IO, Any, NamedTuple, Type, cast @@ -196,6 +199,31 @@ def SetConsoleTitle(title: str) -> bool: return bool(_SetConsoleTitle(title)) +_WriteConsole = windll.kernel32.WriteConsoleW +_WriteConsole.argtypes = [ + wintypes.HANDLE, + wintypes.LPWSTR, + wintypes.DWORD, + wintypes.LPDWORD, + wintypes.LPVOID, +] +_WriteConsole.restype = wintypes.BOOL + + +def WriteConsole(std_handle: wintypes.HANDLE, text: str) -> bool: + buffer = wintypes.LPWSTR(text) + num_chars_written = wintypes.LPDWORD() + return bool( + _WriteConsole( + std_handle, + buffer, + wintypes.DWORD(len(text)), + num_chars_written, + wintypes.LPVOID(None), + ) + ) + + class LegacyWindowsTerm: """This class allows interaction with the legacy Windows Console API. It should only be used in the context of environments where virtual terminal processing is not available. However, if it is used in a Windows environment, @@ -267,8 +295,9 @@ def write_text(self, text: str) -> None: Args: text (str): The text to write to the console """ - self.write(text) - self.flush() + WriteConsole(self._handle, text) + # self.write(text) + # self.flush() def write_styled(self, text: str, style: Style) -> None: """Write styled text to the terminal From 1122501d6205742fe2883b140d69ff687b70d43c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 3 Mar 2022 13:54:38 +0000 Subject: [PATCH 17/23] Use WriteConsoleW instead of file.write(...) in LegacyWindowsTerm --- rich/_win32_console.py | 2 -- tests/test_win32_console.py | 24 ++++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index 7130bd0e0..a4a760090 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -296,8 +296,6 @@ def write_text(self, text: str) -> None: text (str): The text to write to the console """ WriteConsole(self._handle, text) - # self.write(text) - # self.flush() def write_styled(self, text: str, style: Style) -> None: """Write styled text to the terminal diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py index d3cc28050..1576561e6 100644 --- a/tests/test_win32_console.py +++ b/tests/test_win32_console.py @@ -55,35 +55,39 @@ def test_screen_size(_): row=SCREEN_HEIGHT, col=SCREEN_WIDTH ) + @patch.object(_win32_console, "WriteConsole", return_value=True) @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) - def test_write_text(_): - f = StringIO() + def test_write_text(_, WriteConsole, win32_handle): text = "Hello, world!" - term = LegacyWindowsTerm(file=f) + term = LegacyWindowsTerm() term.write_text(text) - assert f.getvalue() == text + WriteConsole.assert_called_once_with(win32_handle, text) + @patch.object(_win32_console, "WriteConsole", return_value=True) @patch.object(_win32_console, "SetConsoleTextAttribute") @patch.object( _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo ) - def test_write_styled(_, SetConsoleTextAttribute, win32_handle): - f = StringIO() + def test_write_styled(_, SetConsoleTextAttribute, WriteConsole, win32_handle): style = Style.parse("black on red") text = "Hello, world!" - term = LegacyWindowsTerm(file=f) + term = LegacyWindowsTerm() term.write_styled(text, style) - call_args = SetConsoleTextAttribute.call_args_list + # Check that we've called the Console API to write the text + call_args = WriteConsole.call_args_list + assert len(call_args) == 1 + args, _ = call_args[0] + assert args == (win32_handle, text) - assert f.getvalue() == text # Ensure we set the text attributes and then reset them after writing styled text - + call_args = SetConsoleTextAttribute.call_args_list + assert len(call_args) == 2 first_args, first_kwargs = call_args[0] second_args, second_kwargs = call_args[1] From d87498bf3600dadc294c0b6058683dba54f5fb2c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 3 Mar 2022 14:23:55 +0000 Subject: [PATCH 18/23] Use bitwise operators in LegacyWindowsTerm, fix formatting --- rich/_win32_console.py | 12 ++++-------- rich/console.py | 4 +--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index a4a760090..cf936c66e 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -253,8 +253,7 @@ class LegacyWindowsTerm: 15, # bright white ] - def __init__(self, file: IO[str] = sys.stdout): - self.file = file + def __init__(self) -> None: handle = GetStdHandle(STDOUT) self._handle = handle default_text = GetConsoleScreenBufferInfo(handle).wAttributes @@ -262,10 +261,7 @@ def __init__(self, file: IO[str] = sys.stdout): self._default_fore = default_text & 7 self._default_back = (default_text >> 4) & 7 - self._default_attrs = self._default_fore + self._default_back * 16 - - self.write = file.write - self.flush = file.flush + self._default_attrs = self._default_fore | (self._default_back << 4) @property def cursor_position(self) -> WindowsCoordinates: @@ -322,7 +318,7 @@ def write_styled(self, text: str, style: Style) -> None: assert back is not None SetConsoleTextAttribute( - self._handle, attributes=ctypes.c_ushort(fore + back * 16) + self._handle, attributes=ctypes.c_ushort(fore | (back << 4)) ) self.write_text(text) SetConsoleTextAttribute(self._handle, attributes=self._default_text) @@ -462,7 +458,7 @@ def set_title(self, title: str) -> None: console = Console() - term = LegacyWindowsTerm(console.file) + term = LegacyWindowsTerm() term.set_title("Win32 Console Examples") style = Style(color="black", bgcolor="red") diff --git a/rich/console.py b/rich/console.py index 15a787208..9275a1e7b 100644 --- a/rich/console.py +++ b/rich/console.py @@ -1921,9 +1921,7 @@ def _check_buffer(self) -> None: from rich._win32_console import LegacyWindowsTerm from rich._windows_renderer import legacy_windows_render - legacy_windows_render( - self._buffer[:], LegacyWindowsTerm(self.file) - ) + legacy_windows_render(self._buffer[:], LegacyWindowsTerm()) output_capture_enabled = bool(self._buffer_index) if not legacy_windows_stdout or output_capture_enabled: From 5a051f9f8d4e48da32b610b1fe540bc16e721484 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 3 Mar 2022 14:31:35 +0000 Subject: [PATCH 19/23] Make GetConsoleMode Windows Console wrapper more Pythonic --- rich/_win32_console.py | 8 ++++++-- rich/_windows.py | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index cf936c66e..05f81b1b9 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -70,8 +70,12 @@ def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE: _GetConsoleMode.restype = wintypes.BOOL -def GetConsoleMode(std_handle: wintypes.HANDLE, console_mode: wintypes.DWORD) -> bool: - return bool(_GetConsoleMode(std_handle, console_mode)) +def GetConsoleMode(std_handle: wintypes.HANDLE) -> int: + console_mode = wintypes.DWORD() + success = bool(_GetConsoleMode(std_handle, console_mode)) + if not success: + raise WindowsError("Unable to get legacy Windows Console Mode") + return console_mode.value _FillConsoleOutputCharacterW = windll.kernel32.FillConsoleOutputCharacterW diff --git a/rich/_windows.py b/rich/_windows.py index a330cae77..de50009f7 100644 --- a/rich/_windows.py +++ b/rich/_windows.py @@ -44,9 +44,8 @@ def get_windows_console_features() -> WindowsConsoleFeatures: WindowsConsoleFeatures: An instance of WindowsConsoleFeatures. """ handle = GetStdHandle() - console_mode = wintypes.DWORD() - result = GetConsoleMode(handle, console_mode) - vt = bool(result and console_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) + console_mode = GetConsoleMode(handle) + vt = bool(console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) truecolor = False if vt: win_version = sys.getwindowsversion() From 8f738c7f2af3f2757c47d4c3235f10133b4eb5c3 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 3 Mar 2022 15:13:25 +0000 Subject: [PATCH 20/23] Support reverse, bold (bright), and dim --- rich/_win32_console.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index 05f81b1b9..efaf73271 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -237,6 +237,8 @@ class LegacyWindowsTerm: file (IO[str]): The file which the Windows Console API HANDLE is retrieved from, defaults to sys.stdout. """ + BRIGHT_BIT = 8 + # Indices are ANSI color numbers, values are the corresponding Windows Console API color numbers ANSI_TO_WINDOWS = [ 0, # black The Windows colours are defined in wincon.h as follows: @@ -298,21 +300,30 @@ def write_text(self, text: str) -> None: WriteConsole(self._handle, text) def write_styled(self, text: str, style: Style) -> None: - """Write styled text to the terminal + """Write styled text to the terminal. Args: text (str): The text to write style (Style): The style of the text """ - if style.color: - fore = style.color.downgrade(ColorSystem.WINDOWS).number + color = style.color + bgcolor = style.bgcolor + if style.reverse: + color, bgcolor = bgcolor, color + + if color: + fore = color.downgrade(ColorSystem.WINDOWS).number fore = fore if fore is not None else 7 # Default to ANSI 7: White + if style.bold: + fore = fore | self.BRIGHT_BIT + if style.dim: + fore = fore & ~self.BRIGHT_BIT fore = self.ANSI_TO_WINDOWS[fore] else: fore = self._default_fore - if style.bgcolor: - back = style.bgcolor.downgrade(ColorSystem.WINDOWS).number + if bgcolor: + back = bgcolor.downgrade(ColorSystem.WINDOWS).number back = back if back is not None else 0 # Default to ANSI 0: Black back = self.ANSI_TO_WINDOWS[back] else: @@ -452,11 +463,6 @@ def set_title(self, title: str) -> None: if __name__ == "__main__": handle = GetStdHandle() - console_mode = wintypes.DWORD() - rv = GetConsoleMode(handle, console_mode) - - print(rv) - print(type(rv)) from rich.console import Console @@ -474,9 +480,13 @@ def set_title(self, title: str) -> None: # console.print("Checking colour output", style=Style.parse("black on green")) text = Text("Hello world!", style=style) console.print(text) - console.print("[bold green]bold green!") + console.print("[yellow]yellow!") + console.print("[bold yellow]bold yellow!") + console.print("[bright_yellow]bright_yellow!") + console.print("[dim bright_yellow]dim bright_yellow!") console.print("[italic cyan]italic cyan!") console.print("[bold white on blue]bold white on blue!") + console.print("[reverse bold white on blue]reverse bold white on blue!") console.print("[bold black on cyan]bold black on cyan!") console.print("[black on green]black on green!") console.print("[blue on green]blue on green!") From ca3e966de90b41df8ac215481044df1beea9d3bd Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 3 Mar 2022 16:14:01 +0000 Subject: [PATCH 21/23] Handle legacy windows error --- rich/_win32_console.py | 12 +++-- rich/_windows.py | 10 +++- tests/test_win32_console.py | 105 ++++++++++++++++++++++++++++++++++-- 3 files changed, 117 insertions(+), 10 deletions(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index efaf73271..265b75266 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -17,7 +17,6 @@ from rich.color import ColorSystem from rich.style import Style -from rich.text import Text STDOUT = -11 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 @@ -25,6 +24,10 @@ COORD = wintypes._COORD +class LegacyWindowsError(Exception): + pass + + class WindowsCoordinates(NamedTuple): """Coordinates in the Windows Console API are (y, x), not (x, y). This class is intended to prevent that confusion. @@ -74,7 +77,7 @@ def GetConsoleMode(std_handle: wintypes.HANDLE) -> int: console_mode = wintypes.DWORD() success = bool(_GetConsoleMode(std_handle, console_mode)) if not success: - raise WindowsError("Unable to get legacy Windows Console Mode") + raise LegacyWindowsError("Unable to get legacy Windows Console Mode") return console_mode.value @@ -477,9 +480,8 @@ def set_title(self, title: str) -> None: # Check colour output console.rule("Checking colour output") - # console.print("Checking colour output", style=Style.parse("black on green")) - text = Text("Hello world!", style=style) - console.print(text) + console.print("[on red]on red!") + console.print("[blue]blue!") console.print("[yellow]yellow!") console.print("[bold yellow]bold yellow!") console.print("[bright_yellow]bright_yellow!") diff --git a/rich/_windows.py b/rich/_windows.py index de50009f7..54d834e62 100644 --- a/rich/_windows.py +++ b/rich/_windows.py @@ -26,6 +26,7 @@ class WindowsConsoleFeatures: ENABLE_VIRTUAL_TERMINAL_PROCESSING, GetConsoleMode, GetStdHandle, + LegacyWindowsError, ) except (AttributeError, ImportError, ValueError): @@ -44,8 +45,13 @@ def get_windows_console_features() -> WindowsConsoleFeatures: WindowsConsoleFeatures: An instance of WindowsConsoleFeatures. """ handle = GetStdHandle() - console_mode = GetConsoleMode(handle) - vt = bool(console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) + try: + console_mode = GetConsoleMode(handle) + success = True + except LegacyWindowsError: + console_mode = 0 + success = False + vt = bool(success and console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) truecolor = False if vt: win_version = sys.getwindowsversion() diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py index 1576561e6..4523600d6 100644 --- a/tests/test_win32_console.py +++ b/tests/test_win32_console.py @@ -1,15 +1,13 @@ import dataclasses import sys -from io import StringIO from unittest import mock -from unittest.mock import call, patch +from unittest.mock import patch import pytest from rich.style import Style if sys.platform == "win32": - from rich import _win32_console from rich._win32_console import COORD, LegacyWindowsTerm, WindowsCoordinates @@ -96,6 +94,107 @@ def test_write_styled(_, SetConsoleTextAttribute, WriteConsole, win32_handle): assert second_args == (win32_handle,) assert second_kwargs["attributes"] == DEFAULT_STYLE_ATTRIBUTE + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled_bold(_, SetConsoleTextAttribute, __, win32_handle): + style = Style.parse("bold black on red") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + first_args, first_kwargs = call_args[0] + + expected_attr = 64 + 8 # 64 for red bg, +8 for bright black + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == expected_attr + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled_reverse(_, SetConsoleTextAttribute, __, win32_handle): + style = Style.parse("reverse red on blue") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + first_args, first_kwargs = call_args[0] + + expected_attr = 64 + 1 # 64 for red bg (after reverse), +1 for blue fg + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == expected_attr + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled_reverse(_, SetConsoleTextAttribute, __, win32_handle): + style = Style.parse("dim bright_red on blue") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + first_args, first_kwargs = call_args[0] + + expected_attr = 4 + 16 # 4 for red text (after dim), +16 for blue bg + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == expected_attr + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled_no_foreground_color( + _, SetConsoleTextAttribute, __, win32_handle + ): + style = Style.parse("on blue") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + first_args, first_kwargs = call_args[0] + + expected_attr = 16 | term._default_fore # 16 for blue bg, plus default fg color + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == expected_attr + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled_no_background_color( + _, SetConsoleTextAttribute, __, win32_handle + ): + style = Style.parse("blue") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + first_args, first_kwargs = call_args[0] + + expected_attr = ( + 16 | term._default_back + ) # 16 for blue foreground, plus default bg color + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == expected_attr + @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) @patch.object( From 81c4dc411523f15d61c999262271d3feb3c2b25e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 3 Mar 2022 17:13:45 +0000 Subject: [PATCH 22/23] Add docstrings to Windows console wrapper functions --- rich/_win32_console.py | 109 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/rich/_win32_console.py b/rich/_win32_console.py index 265b75266..c88287564 100644 --- a/rich/_win32_console.py +++ b/rich/_win32_console.py @@ -40,6 +40,15 @@ class WindowsCoordinates(NamedTuple): @classmethod def from_param(cls, value: "WindowsCoordinates") -> COORD: + """Converts a WindowsCoordinates into a wintypes _COORD structure. + This classmethod is internally called by ctypes to perform the conversion. + + Args: + value (WindowsCoordinates): The input coordinates to convert. + + Returns: + wintypes._COORD: The converted coordinates struct. + """ return COORD(value.col, value.row) @@ -65,6 +74,14 @@ class CONSOLE_CURSOR_INFO(ctypes.Structure): def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE: + """Retrieves a handle to the specified standard device (standard input, standard output, or standard error). + + Args: + handle (int): Integer identifier for the handle. Defaults to -11 (stdout). + + Returns: + wintypes.HANDLE: The handle + """ return cast(wintypes.HANDLE, _GetStdHandle(handle)) @@ -74,6 +91,20 @@ def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE: def GetConsoleMode(std_handle: wintypes.HANDLE) -> int: + """Retrieves the current input mode of a console's input buffer + or the current output mode of a console screen buffer. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + + Raises: + LegacyWindowsError: If any error occurs while calling the Windows console API. + + Returns: + int: Value representing the current console mode as documented at + https://docs.microsoft.com/en-us/windows/console/getconsolemode#parameters + """ + console_mode = wintypes.DWORD() success = bool(_GetConsoleMode(std_handle, console_mode)) if not success: @@ -98,8 +129,17 @@ def FillConsoleOutputCharacter( length: int, start: WindowsCoordinates, ) -> int: - """Writes a character to the console screen buffer a specified number of times, beginning at the specified coordinates.""" - assert len(char) == 1 + """Writes a character to the console screen buffer a specified number of times, beginning at the specified coordinates. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + char (str): The character to write. Must be a string of length 1. + length (int): The number of times to write the character. + start (WindowsCoordinates): The coordinates to start writing at. + + Returns: + int: The number of characters written. + """ character = ctypes.c_char(char.encode()) num_characters = wintypes.DWORD(length) num_written = wintypes.DWORD(0) @@ -130,6 +170,18 @@ def FillConsoleOutputAttribute( length: int, start: WindowsCoordinates, ) -> int: + """Sets the character attributes for a specified number of character cells, + beginning at the specified coordinates in a screen buffer. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + attributes (int): Integer value representing the foreground and background colours of the cells. + length (int): The number of cells to set the output attribute of. + start (WindowsCoordinates): The coordinates of the first cell whose attributes are to be set. + + Returns: + int: The number of cells whose attributes were actually set. + """ num_cells = wintypes.DWORD(length) style_attrs = wintypes.WORD(attributes) num_written = wintypes.DWORD(0) @@ -150,6 +202,16 @@ def FillConsoleOutputAttribute( def SetConsoleTextAttribute( std_handle: wintypes.HANDLE, attributes: wintypes.WORD ) -> bool: + """Set the colour attributes for all text written after this function is called. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + attributes (int): Integer value representing the foreground and background colours. + + + Returns: + bool: True if the attribute was set successfully, otherwise False. + """ return bool(_SetConsoleTextAttribute(std_handle, attributes)) @@ -164,6 +226,14 @@ def SetConsoleTextAttribute( def GetConsoleScreenBufferInfo( std_handle: wintypes.HANDLE, ) -> CONSOLE_SCREEN_BUFFER_INFO: + """Retrieves information about the specified console screen buffer. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + + Returns: + CONSOLE_SCREEN_BUFFER_INFO: A CONSOLE_SCREEN_BUFFER_INFO ctype struct contain information about + screen size, cursor position, colour attributes, and more.""" console_screen_buffer_info = CONSOLE_SCREEN_BUFFER_INFO() _GetConsoleScreenBufferInfo(std_handle, byref(console_screen_buffer_info)) return console_screen_buffer_info @@ -180,6 +250,15 @@ def GetConsoleScreenBufferInfo( def SetConsoleCursorPosition( std_handle: wintypes.HANDLE, coords: WindowsCoordinates ) -> bool: + """Set the position of the cursor in the console screen + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + coords (WindowsCoordinates): The coordinates to move the cursor to. + + Returns: + bool: True if the function succeeds, otherwise False. + """ return bool(_SetConsoleCursorPosition(std_handle, coords)) @@ -194,6 +273,15 @@ def SetConsoleCursorPosition( def SetConsoleCursorInfo( std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO ) -> bool: + """Set the cursor info - used for adjusting cursor visibility and width + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct containing the new cursor info. + + Returns: + bool: True if the function succeeds, otherwise False. + """ return bool(_SetConsoleCursorInfo(std_handle, byref(cursor_info))) @@ -203,6 +291,14 @@ def SetConsoleCursorInfo( def SetConsoleTitle(title: str) -> bool: + """Sets the title of the current console window + + Args: + title (str): The new title of the console window. + + Returns: + bool: True if the function succeeds, otherwise False. + """ return bool(_SetConsoleTitle(title)) @@ -218,6 +314,15 @@ def SetConsoleTitle(title: str) -> bool: def WriteConsole(std_handle: wintypes.HANDLE, text: str) -> bool: + """Write a string of text to the console, starting at the current cursor position + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + text (str): The text to write. + + Returns: + bool: True if the function succeeds, otherwise False. + """ buffer = wintypes.LPWSTR(text) num_chars_written = wintypes.LPDWORD() return bool( From 91e01463859a1820e39078c416553fe20f48c999 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 9 Mar 2022 11:48:16 +0000 Subject: [PATCH 23/23] check win32 --- poetry.lock | 65 ++++++++++++++++++++++++++++++++++++++++-------- pyproject.toml | 1 - rich/__main__.py | 5 +--- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/poetry.lock b/poetry.lock index e7275a13e..0489808c6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -846,7 +846,7 @@ python-versions = "*" [[package]] name = "pywinpty" -version = "2.0.2" +version = "2.0.3" description = "Pseudo terminal support for Windows from Python." category = "main" optional = true @@ -986,7 +986,7 @@ python-versions = ">=3.6" [[package]] name = "virtualenv" -version = "20.13.2" +version = "20.13.3" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1049,7 +1049,7 @@ jupyter = ["ipywidgets"] [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "037fa9d7b0d26a13fb1fe08220d8b1afd33508e5d30111335bcfcc2958a04477" +content-hash = "05eba8d679f7fd74e1fb50881b99c09705bea9330a70beeead174c5b8fe6338c" [metadata.files] appnope = [ @@ -1330,12 +1330,28 @@ jupyterlab-widgets = [ {file = "jupyterlab_widgets-1.0.2.tar.gz", hash = "sha256:7885092b2b96bf189c3a705cc3c412a4472ec5e8382d0b47219a66cccae73cfa"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1344,14 +1360,27 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1361,6 +1390,12 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -1533,11 +1568,11 @@ pywin32 = [ {file = "pywin32-303-cp39-cp39-win_amd64.whl", hash = "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34"}, ] pywinpty = [ - {file = "pywinpty-2.0.2-cp310-none-win_amd64.whl", hash = "sha256:4b421379b407bf2f52a64a4c58f61deffe623b5add02d871acb290b771bb6227"}, - {file = "pywinpty-2.0.2-cp37-none-win_amd64.whl", hash = "sha256:238b75fc456a6bc558761a89c9e6b3c8f2f54d79db03ae28997a68313c24b2ca"}, - {file = "pywinpty-2.0.2-cp38-none-win_amd64.whl", hash = "sha256:344858a0b956fdc64a547d5e1980b0257b47f5433ed7cb89bf7b6268cb280c6c"}, - {file = "pywinpty-2.0.2-cp39-none-win_amd64.whl", hash = "sha256:a4a066eaf2e30944d3028d946883ceb7883a499b53c4b89ca2d54bd7a4210550"}, - {file = "pywinpty-2.0.2.tar.gz", hash = "sha256:20ec117183f79642eff555ce0dd1823f942618d65813fb6122d14b6e34b5d05a"}, + {file = "pywinpty-2.0.3-cp310-none-win_amd64.whl", hash = "sha256:7a330ef7a2ce284370b1a1fdd2a80c523585464fa5e5ab934c9f27220fa7feab"}, + {file = "pywinpty-2.0.3-cp37-none-win_amd64.whl", hash = "sha256:6455f1075f978942d318f95616661c605d5e0f991c5b176c0c852d237aafefc0"}, + {file = "pywinpty-2.0.3-cp38-none-win_amd64.whl", hash = "sha256:2e7a288a8121393c526d4e6ec7d65edef75d68c7787ab9560e438df867b75a5d"}, + {file = "pywinpty-2.0.3-cp39-none-win_amd64.whl", hash = "sha256:def51627e6aa659f33ea7a0ea4c6b68365c83af4aad7940600f844746817a0ed"}, + {file = "pywinpty-2.0.3.tar.gz", hash = "sha256:6b29a826e896105370c38d53904c3aaac6c36146a50448fc0ed5082cf9d092bc"}, ] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, @@ -1580,24 +1615,32 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f"}, + {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f907c7359ce8bf7f7e63c82f75ad0223384105f5126f313400b7e8004d9b33c3"}, + {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:902319cfe23366595d3fa769b5b751e6ee6750a0a64c5d9f757d624b2ac3519e"}, {file = "pyzmq-22.3.0-cp310-cp310-win32.whl", hash = "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d"}, {file = "pyzmq-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd"}, {file = "pyzmq-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364"}, + {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:62bcade20813796c426409a3e7423862d50ff0639f5a2a95be4b85b09a618666"}, + {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ea5a79e808baef98c48c884effce05c31a0698c1057de8fc1c688891043c1ce1"}, {file = "pyzmq-22.3.0-cp36-cp36m-win32.whl", hash = "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9"}, {file = "pyzmq-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b"}, {file = "pyzmq-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c"}, + {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08c4e315a76ef26eb833511ebf3fa87d182152adf43dedee8d79f998a2162a0b"}, + {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:badb868fff14cfd0e200eaa845887b1011146a7d26d579aaa7f966c203736b92"}, {file = "pyzmq-22.3.0-cp37-cp37m-win32.whl", hash = "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d"}, {file = "pyzmq-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d"}, {file = "pyzmq-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74"}, + {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:468bd59a588e276961a918a3060948ae68f6ff5a7fa10bb2f9160c18fe341067"}, + {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c88fa7410e9fc471e0858638f403739ee869924dd8e4ae26748496466e27ac59"}, {file = "pyzmq-22.3.0-cp38-cp38-win32.whl", hash = "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7"}, {file = "pyzmq-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45"}, {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356"}, @@ -1605,6 +1648,8 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2"}, + {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:53f4fd13976789ffafedd4d46f954c7bb01146121812b72b4ddca286034df966"}, + {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1b5d457acbadcf8b27561deeaa386b0217f47626b29672fa7bd31deb6e91e1b"}, {file = "pyzmq-22.3.0-cp39-cp39-win32.whl", hash = "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b"}, {file = "pyzmq-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2"}, {file = "pyzmq-22.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f"}, @@ -1719,8 +1764,8 @@ typing-extensions = [ {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] virtualenv = [ - {file = "virtualenv-20.13.2-py2.py3-none-any.whl", hash = "sha256:e7b34c9474e6476ee208c43a4d9ac1510b041c68347eabfe9a9ea0c86aa0a46b"}, - {file = "virtualenv-20.13.2.tar.gz", hash = "sha256:01f5f80744d24a3743ce61858123488e91cb2dd1d3bdf92adaf1bba39ffdedf0"}, + {file = "virtualenv-20.13.3-py2.py3-none-any.whl", hash = "sha256:dd448d1ded9f14d1a4bfa6bfc0c5b96ae3be3f2d6c6c159b23ddcfd701baa021"}, + {file = "virtualenv-20.13.3.tar.gz", hash = "sha256:e9dd1a1359d70137559034c0f5433b34caf504af2dc756367be86a5a32967134"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, diff --git a/pyproject.toml b/pyproject.toml index feee94c8c..f5e1a96e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ typing-extensions = { version = ">=3.7.4, <5.0", python = "<3.8" } dataclasses = { version = ">=0.7,<0.9", python = "<3.7" } pygments = "^2.6.0" commonmark = "^0.9.0" -colorama = "^0.4.0" ipywidgets = { version = "^7.5.1", optional = true } diff --git a/rich/__main__.py b/rich/__main__.py index ea6ccfd9b..7b3ffb4f2 100644 --- a/rich/__main__.py +++ b/rich/__main__.py @@ -226,10 +226,7 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: console.print(test_card) taken = round((process_time() - start) * 1000.0, 1) - text = console.file.getvalue() - # https://bugs.python.org/issue37871 - for line in text.splitlines(True): - print(line, end="") + Console().print(test_card) print(f"rendered in {pre_cache_taken}ms (cold cache)") print(f"rendered in {taken}ms (warm cache)")