From 4b6c67623a1ef8b585daac95cf06b0ea6773c7c9 Mon Sep 17 00:00:00 2001 From: ANSHUL SINGH Date: Tue, 3 Nov 2026 17:36:07 +0000 Subject: [PATCH] Treat CRLF as a single line break in multiline headers _align_header split headers with r'\r|\n|\r\n', so the '\r' alternative consumed the first half of a CRLF and left a blank header row. Put the two-character sequence first. Fixes #488 --- tabulate/__init__.py | 4 ++-- test/test_regression.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 12a2950..3648350 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -754,8 +754,8 @@ def escape_empty(val): # - tsv: TBD # - textile: Replace \n with
(must be well-formed XML) -_multiline_codes = re.compile(r"\r|\n|\r\n") -_multiline_codes_bytes = re.compile(b"\r|\n|\r\n") +_multiline_codes = re.compile(r"\r\n|\r|\n") +_multiline_codes_bytes = re.compile(b"\r\n|\r|\n") # Handle ANSI escape sequences for both control sequence introducer (CSI) and # operating system command (OSC). Both of these begin with 0x1b (or octal 033), diff --git a/test/test_regression.py b/test/test_regression.py index 9555676..8a48c30 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -598,3 +598,21 @@ def test_github_escape_pipe_character(): result = tabulate([["foo|bar"]], headers=("spam|eggs",), tablefmt="github") expected = "| spam\\|eggs |\n|:------------|\n| foo\\|bar |" assert_equal(expected, result) + + +def test_crlf_multiline_header_matches_lf(): + "Regression: CRLF in a header is one line break, same as LF (issue #488)" + expected = "\n".join( + [ + "+-----+", + "| A |", + "| B |", + "+=====+", + "| 1 |", + "+-----+", + ] + ) + lf = tabulate([[1]], headers=["A\nB"], tablefmt="grid") + crlf = tabulate([[1]], headers=["A\r\nB"], tablefmt="grid") + assert_equal(expected, lf) + assert_equal(expected, crlf)