Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b783693

Browse files
authored
feat: support column_width in xlsx format
Co-authored-by: Andrew Graham-Yooll <[email protected]> Co-authored-by: Hugo van Kemenade <[email protected]>
1 parent 15e1130 commit b783693

5 files changed

Lines changed: 100 additions & 2 deletions

File tree

AUTHORS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ by the Jazzband GitHub team.
44
Here is a list of past and present much-appreciated contributors:
55

66
Alex Gaynor
7+
Andrew Graham-Yooll
78
Andrii Soldatenko
89
Benjamin Wohlwend
910
Bruno Soares
1011
Claude Paroz
1112
Daniel Santos
13+
Egor Osokin
1214
Erik Youngren
1315
Hugo van Kemenade
1416
Iuri de Silvio

HISTORY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# History
22

3+
## 3.8.0 (Unreleased)
4+
5+
### Improvements
6+
7+
- Add support for exporting XLSX with column width (#516)
8+
39
## 3.7.0 (2024-10-08)
410

511
### Improvements

docs/formats.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,18 @@ The ``import_set()`` method also supports a ``skip_lines`` parameter that you
250250
can set to a number of lines that should be skipped before starting to read
251251
data.
252252

253+
The ``export_set()`` method supports a ``column_width`` parameter. Depending
254+
on the value passed, the column width will be set accordingly. It can be
255+
either ``None``, an integer, or default "adaptive". If "adaptive" is passed,
256+
the column width will be unique and will be calculated based on values' length.
257+
For example::
258+
259+
data = tablib.Dataset()
260+
data.export('xlsx', column_width='adaptive')
261+
262+
.. versionchanged:: 3.8.0
263+
The ``column_width`` parameter for ``export_set()`` was added.
264+
253265
.. versionchanged:: 3.1.0
254266

255267
The ``skip_lines`` parameter for ``import_set()`` was added.

src/tablib/formats/_xlsx.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
""" Tablib - XLSX Support.
22
"""
3-
43
import re
54
from io import BytesIO
65

@@ -35,7 +34,8 @@ def detect(cls, stream):
3534
return False
3635

3736
@classmethod
38-
def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=False):
37+
def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-",
38+
escape=False, column_width="adaptive"):
3939
"""Returns XLSX representation of Dataset.
4040
4141
If ``freeze_panes`` is True, Export will freeze panes only after first line.
@@ -48,6 +48,12 @@ def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=F
4848
If ``escape`` is True, formulae will have the leading '=' character removed.
4949
This is a security measure to prevent formulae from executing by default
5050
in exported XLSX files.
51+
52+
If ``column_width`` is set to "adaptive", the column width will be set to the maximum
53+
width of the content in each column. If it is set to an integer, the column width will be
54+
set to that integer value. If it is set to None, the column width will be set as the
55+
default openpyxl.Worksheet width value.
56+
5157
"""
5258
wb = Workbook()
5359
ws = wb.worksheets[0]
@@ -59,6 +65,8 @@ def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=F
5965

6066
cls.dset_sheet(dataset, ws, freeze_panes=freeze_panes, escape=escape)
6167

68+
cls._adapt_column_width(ws, column_width)
69+
6270
stream = BytesIO()
6371
wb.save(stream)
6472
return stream.getvalue()
@@ -166,3 +174,31 @@ def dset_sheet(cls, dataset, ws, freeze_panes=True, escape=False):
166174

167175
if escape and cell.data_type == 'f' and cell.value.startswith('='):
168176
cell.value = cell.value.replace("=", "")
177+
178+
@classmethod
179+
def _adapt_column_width(cls, worksheet, width):
180+
if isinstance(width, str) and width != "adaptive":
181+
msg = (
182+
f"Invalid value for column_width: {width}. "
183+
"Must be 'adaptive' or an integer."
184+
)
185+
raise ValueError(msg)
186+
187+
if width is None:
188+
return
189+
190+
column_widths = []
191+
if width == "adaptive":
192+
for row in worksheet.values:
193+
for i, cell in enumerate(row):
194+
cell_width = len(str(cell))
195+
if len(column_widths) > i:
196+
if cell_width > column_widths[i]:
197+
column_widths[i] = cell_width
198+
else:
199+
column_widths.append(cell_width)
200+
else:
201+
column_widths = [width] * worksheet.max_column
202+
203+
for i, column_width in enumerate(column_widths, 1): # start at 1
204+
worksheet.column_dimensions[get_column_letter(i)].width = column_width

tests/test_tablib.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,25 @@ def get_format_str(cell):
13391339

13401340

13411341
class XLSXTests(BaseTestCase):
1342+
def _helper_export_column_width(self, column_width):
1343+
"""check that column width adapts to value length"""
1344+
def _get_width(data, input_arg):
1345+
xlsx_content = data.export('xlsx', column_width=input_arg)
1346+
wb = load_workbook(filename=BytesIO(xlsx_content))
1347+
ws = wb.active
1348+
return ws.column_dimensions['A'].width
1349+
1350+
xls_source = Path(__file__).parent / 'files' / 'xlsx_cell_values.xlsx'
1351+
with xls_source.open('rb') as fh:
1352+
data = tablib.Dataset().load(fh)
1353+
width_before = _get_width(data, column_width)
1354+
data.append([
1355+
'verylongvalue-verylongvalue-verylongvalue-verylongvalue-'
1356+
'verylongvalue-verylongvalue-verylongvalue-verylongvalue',
1357+
])
1358+
width_after = _get_width(data, width_before)
1359+
return width_before, width_after
1360+
13421361
def test_xlsx_format_detect(self):
13431362
"""Test the XLSX format detection."""
13441363
in_stream = self.founders.xlsx
@@ -1483,6 +1502,29 @@ def test_xlsx_raise_ValueError_on_cell_write_during_export(self):
14831502
wb = load_workbook(filename=BytesIO(_xlsx))
14841503
self.assertEqual('[1]', wb.active['A1'].value)
14851504

1505+
def test_xlsx_column_width_adaptive(self):
1506+
""" Test that column width adapts to value length"""
1507+
width_before, width_after = self._helper_export_column_width("adaptive")
1508+
self.assertEqual(width_before, 11)
1509+
self.assertEqual(width_after, 11)
1510+
1511+
def test_xlsx_column_width_integer(self):
1512+
"""Test that column width changes to integer length"""
1513+
width_before, width_after = self._helper_export_column_width(10)
1514+
self.assertEqual(width_before, 10)
1515+
self.assertEqual(width_after, 10)
1516+
1517+
def test_xlsx_column_width_none(self):
1518+
"""Test that column width does not change"""
1519+
width_before, width_after = self._helper_export_column_width(None)
1520+
self.assertEqual(width_before, 13)
1521+
self.assertEqual(width_after, 13)
1522+
1523+
def test_xlsx_column_width_value_error(self):
1524+
"""Raise ValueError if column_width is not a valid input"""
1525+
with self.assertRaises(ValueError):
1526+
self._helper_export_column_width("invalid input")
1527+
14861528

14871529
class JSONTests(BaseTestCase):
14881530
def test_json_format_detect(self):

0 commit comments

Comments
 (0)