From 843eaeedbdbe631f973b76e66230443851e51c8f Mon Sep 17 00:00:00 2001 From: Rupayon Haldar <80724680+rupayon123@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:28:40 -0400 Subject: [PATCH] Format signed integers inside ANSI color sequences --- tabulate/__init__.py | 6 ++++-- test/test_output.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 12a2950..76f87f8 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1321,12 +1321,14 @@ def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True): elif valtype is int: if isinstance(val, str): val_striped = val.encode("unicode_escape").decode("utf-8") - colored = re.search(r"(\\[xX]+[0-9a-fA-F]+\[\d+[mM]+)([0-9.]+)(\\.*)$", val_striped) + colored = re.search( + r"(\\[xX]+[0-9a-fA-F]+\[\d+[mM]+)([-+]?[0-9.]+)(\\.*)$", val_striped + ) if colored: total_groups = len(colored.groups()) if total_groups == 3: digits = colored.group(2) - if digits.isdigit(): + if digits.isdigit() or (intfmt and digits.lstrip("+-").isdigit()): val_new = colored.group(1) + format(int(digits), intfmt) + colored.group(3) val = val_new.encode("utf-8").decode("unicode_escape") intfmt = "" diff --git a/test/test_output.py b/test/test_output.py index ea3da87..76a4ea6 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -2871,6 +2871,38 @@ def test_intfmt_with_colors(): assert_equal(expected, formatted) +@mark.parametrize( + ("value", "intfmt", "expected"), + [ + ("-1234", ",", "-1,234"), + ("+1234", "+,", "+1,234"), + ("-1234", "#x", "-0x4d2"), + ("+1234", "#x", "0x4d2"), + ("1234", ",", "1,234"), + ("0", ",", "0"), + ], +) +def test_intfmt_with_signed_colors(value, intfmt, expected): + "Integer formatting preserves ANSI colors around signed values." + colored = f"\x1b[31m{value}\x1b[0m" + result = tabulate([[colored]], intfmt=intfmt, tablefmt="plain") + assert_equal(f"\x1b[31m{expected}\x1b[0m", result) + + +@mark.parametrize("value", ["+1234", "-001234", "-0"]) +def test_signed_colors_without_intfmt(value): + "An omitted format retains signed colored text as supplied." + colored = f"\x1b[31m{value}\x1b[0m" + assert_equal(colored, tabulate([[colored]], tablefmt="plain")) + + +def test_signed_colors_without_column_intfmt(): + "A column without a requested format retains its signed colored text." + colored = "\x1b[31m+1234\x1b[0m" + result = tabulate([[1234, colored]], intfmt=[","], tablefmt="plain") + assert_equal(f"1,234 {colored}", result) + + def test_empty_data_with_headers(): "Output: table with empty data and headers as firstrow" expected = ""