From 2a138ad5ba973c12b7392a351a0b32fa5576942c Mon Sep 17 00:00:00 2001 From: thejedi433 Date: Thu, 8 Oct 2026 09:07:37 +0300 Subject: [PATCH] Fix TypeError when rowalign is passed as tuple The _expand_iterable helper function attempted to concatenate a tuple with a list using the + operator, which raises TypeError in Python. This caused tabulate() to fail when users passed alignment parameters (rowalign, colalign, numalign, stralign) as tuples instead of lists. Fix by converting the iterable to a list before concatenation. Also exclude bytes from iterable check for consistency. Fixes #440 --- tabulate/__init__.py | 4 +- test/test_rowalign_tuple.py | 75 +++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 test/test_rowalign_tuple.py diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 12a2950..b8d5fe3 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2487,8 +2487,8 @@ def _expand_iterable(original, num_desired, default): If `original` is not a list to begin with (i.e. scalar value) a list of length `num_desired` completely populated with `default will be returned """ - if isinstance(original, Iterable) and not isinstance(original, str): - return original + [default] * (num_desired - len(original)) + if isinstance(original, Iterable) and not isinstance(original, (str, bytes)): + return list(original) + [default] * (num_desired - len(original)) else: return [default] * num_desired diff --git a/test/test_rowalign_tuple.py b/test/test_rowalign_tuple.py new file mode 100644 index 0000000..f2a701c --- /dev/null +++ b/test/test_rowalign_tuple.py @@ -0,0 +1,75 @@ +"""Test for rowalign tuple fix (GitHub issue #440)""" + +from tabulate import tabulate + + +def test_rowalign_tuple_basic(): + """Test that rowalign accepts a tuple without raising TypeError""" + table = [['Alice', 24], ['Bob', 19]] + headers = ['Name', 'Age'] + # This used to raise: TypeError: can only concatenate tuple (not "list") to tuple + result = tabulate(table, headers=headers, rowalign=('center', 'bottom'), tablefmt='grid') + assert isinstance(result, str) + assert 'Alice' in result + assert 'Bob' in result + + +def test_rowalign_tuple_vs_list_equivalence(): + """Test that tuple and list produce identical output""" + table = [['Alice', 24], ['Bob', 19], ['Charlie', 30]] + headers = ['Name', 'Age'] + alignments = ('center', 'bottom', 'top') + + result_tuple = tabulate(table, headers=headers, rowalign=alignments, tablefmt='grid') + result_list = tabulate(table, headers=headers, rowalign=list(alignments), tablefmt='grid') + + assert result_tuple == result_list + + +def test_rowalign_tuple_partial(): + """Test tuple with fewer elements than rows (should pad with default)""" + table = [['Alice', 24], ['Bob', 19], ['Charlie', 30]] + headers = ['Name', 'Age'] + # Tuple with only 1 element for 3 rows + result = tabulate(table, headers=headers, rowalign=('center',), tablefmt='grid') + assert isinstance(result, str) + + +def test_rowalign_list_still_works(): + """Ensure list input still works (regression test)""" + table = [['Alice', 24], ['Bob', 19]] + headers = ['Name', 'Age'] + result = tabulate(table, headers=headers, rowalign=['center', 'bottom'], tablefmt='grid') + assert isinstance(result, str) + + +def test_rowalign_none_still_works(): + """Ensure None input still works (regression test)""" + table = [['Alice', 24], ['Bob', 19]] + headers = ['Name', 'Age'] + result = tabulate(table, headers=headers, rowalign=None, tablefmt='grid') + assert isinstance(result, str) + + +def test_colalign_tuple(): + """Test that colalign also accepts tuple (uses same _expand_iterable)""" + table = [['Alice', 24], ['Bob', 19]] + headers = ['Name', 'Age'] + result = tabulate(table, headers=headers, colalign=('left', 'right'), tablefmt='grid') + assert isinstance(result, str) + + +def test_numalign_tuple(): + """Test that numalign also accepts tuple (uses same _expand_iterable)""" + table = [['Alice', 24], ['Bob', 19]] + headers = ['Name', 'Age'] + result = tabulate(table, headers=headers, numalign=('right', 'left'), tablefmt='grid') + assert isinstance(result, str) + + +def test_stralign_tuple(): + """Test that stralign also accepts tuple (uses same _expand_iterable)""" + table = [['Alice', 24], ['Bob', 19]] + headers = ['Name', 'Age'] + result = tabulate(table, headers=headers, stralign=('left', 'right'), tablefmt='grid') + assert isinstance(result, str)