From a95ccf6eda149718b861e890bab4499bff8a1b6c Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 14 Jan 2024 20:39:15 -0300 Subject: [PATCH 1/6] Monkeypatch tabulate._CustomTextWrap._handle_long_word to avoid a wrapping bug. In some cases ANSI escape codes would be broken up in the middle, making output of e.g. report() look broken. This fixes that. --- src/wily/commands/diff.py | 5 +++- src/wily/commands/report.py | 5 +++- src/wily/helper/__init__.py | 59 +++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/wily/commands/diff.py b/src/wily/commands/diff.py index 949e3169..16e2e7d1 100644 --- a/src/wily/commands/diff.py +++ b/src/wily/commands/diff.py @@ -17,7 +17,7 @@ from wily.commands.build import run_operator from wily.config import DEFAULT_PATH from wily.config.types import WilyConfig -from wily.helper import get_maxcolwidth, get_style +from wily.helper import get_maxcolwidth, get_style, handle_long_word from wily.operators import ( BAD_COLORS, GOOD_COLORS, @@ -28,6 +28,9 @@ ) from wily.state import State +# Monkeypatch tabulate to fix wrapping bug (https://github.com/astanin/python-tabulate/issues/307): +tabulate._CustomTextWrap._handle_long_word = handle_long_word # type: ignore + def diff( config: WilyConfig, diff --git a/src/wily/commands/report.py b/src/wily/commands/report.py index 0410bfdb..45fb83e8 100644 --- a/src/wily/commands/report.py +++ b/src/wily/commands/report.py @@ -13,12 +13,15 @@ from wily import MAX_MESSAGE_WIDTH, format_date, format_revision, logger from wily.config.types import WilyConfig -from wily.helper import get_maxcolwidth +from wily.helper import get_maxcolwidth, handle_long_word from wily.helper.custom_enums import ReportFormat from wily.lang import _ from wily.operators import MetricType, resolve_metric_as_tuple from wily.state import State +# Monkeypatch tabulate to fix wrapping bug (https://github.com/astanin/python-tabulate/issues/307): +tabulate._CustomTextWrap._handle_long_word = handle_long_word # type: ignore + ANSI_RED = 31 ANSI_GREEN = 32 ANSI_YELLOW = 33 diff --git a/src/wily/helper/__init__.py b/src/wily/helper/__init__.py index 6dd3d7f0..856ae26c 100644 --- a/src/wily/helper/__init__.py +++ b/src/wily/helper/__init__.py @@ -7,6 +7,8 @@ from functools import lru_cache from typing import Optional, Sized, Union +import tabulate + from wily.defaults import DEFAULT_GRID_STYLE logger = logging.getLogger(__name__) @@ -54,3 +56,60 @@ def generate_cache_path(path: Union[pathlib.Path, str]) -> str: cache_path = str(HOME / ".wily" / sha) logger.debug("Cache path is %s", cache_path) return cache_path + + +strip_ansi = tabulate._strip_ansi # type: ignore +ansi_codes = tabulate._ansi_codes # type: ignore + + +def handle_long_word( + self, reversed_chunks: list[str], cur_line: list[str], cur_len: int, width: int +): + """ + Handle a chunk of text that is too long to fit in any line. + + Fixed version of tabulate._CustomTextWrap._handle_long_word that avoids a + wrapping bug (https://github.com/astanin/python-tabulate/issues/307) where + ANSI escape codes would be broken up in the middle. + """ + # Figure out when indent is larger than the specified width, and make + # sure at least one character is stripped off on every pass + if width < 1: + space_left = 1 + else: + space_left = width - cur_len + + # If we're allowed to break long words, then do so: put as much + # of the next chunk onto the current line as will fit. + if self.break_long_words: + # Tabulate Custom: Build the string up piece-by-piece in order to + # take each character's width into account + chunk = reversed_chunks[-1] + i = 1 + # Only count printable characters, so strip_ansi first, index later. + while len(strip_ansi(chunk)[:i]) <= space_left: + i = i + 1 + # Consider escape codes when breaking words up + total_escape_len = 0 + last_group = 0 + if ansi_codes.search(chunk) is not None: + for group, _, _, _ in ansi_codes.findall(chunk): + escape_len = len(group) + if group in chunk[last_group : i + total_escape_len + escape_len - 1]: + total_escape_len += escape_len + found = ansi_codes.search(chunk[last_group:]) + last_group += found.end() + cur_line.append(chunk[: i + total_escape_len - 1]) + reversed_chunks[-1] = chunk[i + total_escape_len - 1 :] + + # Otherwise, we have to preserve the long word intact. Only add + # it to the current line if there's nothing already there -- + # that minimizes how much we violate the width constraint. + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + # If we're not allowed to break long words, and there's already + # text on the current line, do nothing. Next time through the + # main loop of _wrap_chunks(), we'll wind up here again, but + # cur_len will be zero, so the next line will be entirely + # devoted to the long word that we can't handle right now. From 22d0820a31277b903e3c40125b284bae07d6c5ec Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Mon, 15 Jan 2024 01:23:21 -0300 Subject: [PATCH 2/6] Add a test for util.handle_long_word(). --- test/unit/test_helper.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/unit/test_helper.py b/test/unit/test_helper.py index bb904af7..905820db 100644 --- a/test/unit/test_helper.py +++ b/test/unit/test_helper.py @@ -4,7 +4,7 @@ import tabulate from wily.defaults import DEFAULT_GRID_STYLE -from wily.helper import get_maxcolwidth, get_style +from wily.helper import get_maxcolwidth, get_style, handle_long_word SHORT_DATA = [list("abcdefgh"), list("abcdefgh")] @@ -156,3 +156,21 @@ def test_get_style_none(): with mock.patch("sys.stdout", output): style = get_style() assert style == "fancy_grid" + + +def test_handle_long_word(): + data = "This_is_a_\033[31mtest_string_for_testing_TextWrap\033[0m_with_colors" + expected = [ + "This_is_a_\033[31mte\033[0m", + "\033[31mst_string_fo\033[0m", + "\033[31mr_testing_Te\033[0m", + "\033[31mxtWrap\033[0m_with_", + "colors", + ] + wrapper = tabulate._CustomTextWrap(width=12) + result = wrapper.wrap(data) + assert result != expected + tabulate._CustomTextWrap._handle_long_word = handle_long_word # type: ignore + wrapper = tabulate._CustomTextWrap(width=12) + result = wrapper.wrap(data) + assert result == expected From f83f6d580a7c70cd07c76ac9de7deb23828383aa Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Mon, 15 Jan 2024 01:25:46 -0300 Subject: [PATCH 3/6] Silence pyright errors from accessing tabulate._CustomTextWrap for monkeypatching. --- test/unit/test_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/test_helper.py b/test/unit/test_helper.py index 905820db..efe959a6 100644 --- a/test/unit/test_helper.py +++ b/test/unit/test_helper.py @@ -167,10 +167,10 @@ def test_handle_long_word(): "\033[31mxtWrap\033[0m_with_", "colors", ] - wrapper = tabulate._CustomTextWrap(width=12) + wrapper = tabulate._CustomTextWrap(width=12) # type: ignore result = wrapper.wrap(data) assert result != expected tabulate._CustomTextWrap._handle_long_word = handle_long_word # type: ignore - wrapper = tabulate._CustomTextWrap(width=12) + wrapper = tabulate._CustomTextWrap(width=12) # type: ignore result = wrapper.wrap(data) assert result == expected From f7445abea800d7c4a8a54a90945e9ce90e698e8c Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Mon, 15 Jan 2024 09:21:47 -0300 Subject: [PATCH 4/6] Fix typing for older Python versions by using List[] instead of list[]. --- src/wily/helper/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wily/helper/__init__.py b/src/wily/helper/__init__.py index 856ae26c..85958dcf 100644 --- a/src/wily/helper/__init__.py +++ b/src/wily/helper/__init__.py @@ -5,7 +5,7 @@ import shutil import sys from functools import lru_cache -from typing import Optional, Sized, Union +from typing import List, Optional, Sized, Union import tabulate @@ -63,7 +63,7 @@ def generate_cache_path(path: Union[pathlib.Path, str]) -> str: def handle_long_word( - self, reversed_chunks: list[str], cur_line: list[str], cur_len: int, width: int + self, reversed_chunks: List[str], cur_line: List[str], cur_len: int, width: int ): """ Handle a chunk of text that is too long to fit in any line. From 77378b805e3d89840404b080fd8d73353db49756 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Mon, 15 Jan 2024 09:46:43 -0300 Subject: [PATCH 5/6] Only monkeypatch _handle_long_word once, keep original version for testing. --- src/wily/commands/diff.py | 2 ++ src/wily/commands/report.py | 2 ++ test/unit/test_helper.py | 3 +++ 3 files changed, 7 insertions(+) diff --git a/src/wily/commands/diff.py b/src/wily/commands/diff.py index 16e2e7d1..1f3c2e71 100644 --- a/src/wily/commands/diff.py +++ b/src/wily/commands/diff.py @@ -29,6 +29,8 @@ from wily.state import State # Monkeypatch tabulate to fix wrapping bug (https://github.com/astanin/python-tabulate/issues/307): +if not hasattr(tabulate._CustomTextWrap, "original_handle_long_word"): # type: ignore + tabulate._CustomTextWrap.original_handle_long_word = tabulate._CustomTextWrap._handle_long_word # type: ignore tabulate._CustomTextWrap._handle_long_word = handle_long_word # type: ignore diff --git a/src/wily/commands/report.py b/src/wily/commands/report.py index 45fb83e8..d2049642 100644 --- a/src/wily/commands/report.py +++ b/src/wily/commands/report.py @@ -20,6 +20,8 @@ from wily.state import State # Monkeypatch tabulate to fix wrapping bug (https://github.com/astanin/python-tabulate/issues/307): +if not hasattr(tabulate._CustomTextWrap, "original_handle_long_word"): # type: ignore + tabulate._CustomTextWrap.original_handle_long_word = tabulate._CustomTextWrap._handle_long_word # type: ignore tabulate._CustomTextWrap._handle_long_word = handle_long_word # type: ignore ANSI_RED = 31 diff --git a/test/unit/test_helper.py b/test/unit/test_helper.py index efe959a6..fe8e0394 100644 --- a/test/unit/test_helper.py +++ b/test/unit/test_helper.py @@ -167,6 +167,9 @@ def test_handle_long_word(): "\033[31mxtWrap\033[0m_with_", "colors", ] + if hasattr(tabulate._CustomTextWrap, "original_handle_long_word"): # type: ignore + # We've already monkeypatched _CustomTextWrap, undo it. + tabulate._CustomTextWrap._handle_long_word = tabulate._CustomTextWrap.original_handle_long_word # type: ignore wrapper = tabulate._CustomTextWrap(width=12) # type: ignore result = wrapper.wrap(data) assert result != expected From 987f1b33678f9b55ccd5a21d003db51c11493422 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Mon, 15 Jan 2024 09:54:03 -0300 Subject: [PATCH 6/6] Update the ruff action to avoid errors: upgrade pip, use --output-format instead of --format. --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 679fbb4d..c18e4215 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,8 +54,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - run: python -m pip install --upgrade pip - run: pip install --user ruff - - run: ruff --format=github . + - run: ruff --output-format=github . pyright: runs-on: ubuntu-latest