From 80ab47307baea8ca7940a41bb845a411fa088a0f Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 17 Feb 2022 13:18:54 +0300 Subject: [PATCH 01/21] feat: support column_width in xlxs format --- AUTHORS | 1 + docs/formats.rst | 9 +++++++++ src/tablib/formats/_xlsx.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/AUTHORS b/AUTHORS index ed4eec6d..908036e6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -34,3 +34,4 @@ Here is a list of past and present much-appreciated contributors: Tsuyoshi Hombashi Tushar Makkar Yunis Yilmaz + Egor Osokin diff --git a/docs/formats.rst b/docs/formats.rst index 178edbda..542238a4 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -250,6 +250,15 @@ The ``import_set()`` method also supports a ``skip_lines`` parameter that you can set to a number of lines that should be skipped before starting to read data. +The ``export_set()`` method supports a ``column_width`` parameter. Depending on the +value you pass, the column width will be set accordingly. It can be either None, int, or "adapt". +If "adapt" is passed, the column width will be unique for every column and will be +calculated based on values' length + + +.. versionchanged:: 3.2.0 + The ``column_width`` parameter for ``export_set()`` was added. + .. versionchanged:: 3.1.0 The ``skip_lines`` parameter for ``import_set()`` was added. diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index 966ba5c8..71b828e5 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -3,11 +3,13 @@ import re from io import BytesIO +from typing import Optional, Union from openpyxl.reader.excel import ExcelReader, load_workbook from openpyxl.styles import Alignment, Font from openpyxl.utils import get_column_letter from openpyxl.workbook import Workbook +from openpyxl.writer.excel import ExcelWriter import tablib @@ -58,6 +60,11 @@ def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=F ) cls.dset_sheet(dataset, ws, freeze_panes=freeze_panes, escape=escape) + if isinstance(column_width, str) and column_width != "adaptive": + raise ValueError(f"Unsupported value `{column_width}` passed to `column_width` " + f"parameter. It supports 'adaptive' or integer values") + + cls._adapt_column_width(ws, column_width) stream = BytesIO() wb.save(stream) @@ -166,3 +173,25 @@ def dset_sheet(cls, dataset, ws, freeze_panes=True, escape=False): if escape and cell.data_type == 'f' and cell.value.startswith('='): cell.value = cell.value.replace("=", "") + + @classmethod + def _adapt_column_width(cls, worksheet, + width: Optional[Union[str, int]]) -> None: + if width is None: + return + + column_widths = [] + if isinstance(width, str) and width == "adaptive": + for row in worksheet.values: + for i, cell in enumerate(row): + cell = str(cell) + if len(column_widths) > i: + if len(cell) > column_widths[i]: + column_widths[i] = len(cell) + else: + column_widths += [len(cell)] + else: + column_widths = [width] * len(worksheet.values) + + for i, column_width in enumerate(column_widths, 1): # start at 1 + worksheet.column_dimensions[get_column_letter(i)].width = column_width From 7a903b6125f5fe1c0a08381b1ab8604864c35c84 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 1 Apr 2022 13:12:25 +0300 Subject: [PATCH 02/21] Update docs/formats.rst Co-authored-by: Hugo van Kemenade --- docs/formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/formats.rst b/docs/formats.rst index 542238a4..0fc2a9d9 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -256,7 +256,7 @@ If "adapt" is passed, the column width will be unique for every column and will calculated based on values' length -.. versionchanged:: 3.2.0 +.. versionchanged:: 3.3.0 The ``column_width`` parameter for ``export_set()`` was added. .. versionchanged:: 3.1.0 From 7fbbd9d971fcae7f31934fadd053eb207dd9cc1d Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 1 Apr 2022 14:56:57 +0300 Subject: [PATCH 03/21] chore: add tests for adaptive column width in xlsx format --- docs/formats.rst | 11 ++++++++--- tests/test_tablib.py | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/formats.rst b/docs/formats.rst index 0fc2a9d9..1c100264 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -251,9 +251,14 @@ can set to a number of lines that should be skipped before starting to read data. The ``export_set()`` method supports a ``column_width`` parameter. Depending on the -value you pass, the column width will be set accordingly. It can be either None, int, or "adapt". -If "adapt" is passed, the column width will be unique for every column and will be -calculated based on values' length +value you pass, the column width will be set accordingly. It can be either None, int, or "adaptive". +If "adaptive" is passed, the column width will be unique for every column and will be +calculated based on values' length. Example of usage + +```python3 +data = tablib.Dataset() +data.export('xlsx', column_width='adaptive') +``` .. versionchanged:: 3.3.0 diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 0df8e5d0..570898c0 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -11,11 +11,13 @@ from decimal import Decimal from io import BytesIO, StringIO from pathlib import Path +from tempfile import TemporaryFile from uuid import uuid4 import xlrd from odf import opendocument, table from openpyxl.reader.excel import load_workbook +from MarkupPy import markup import tablib from tablib.core import Row, detect_format @@ -1483,7 +1485,25 @@ def test_xlsx_raise_ValueError_on_cell_write_during_export(self): wb = load_workbook(filename=BytesIO(_xlsx)) self.assertEqual('[1]', wb.active['A1'].value) + def test_xlsx_column_width(self): + """check that column width adapts to value length""" + def _get_width(data): + xlsx_content = data.export('xlsx') + wb = load_workbook(filename=BytesIO(xlsx_content)) + ws = wb.active + return ws.column_dimensions['A'].width + xls_source = Path(__file__).parent / 'files' / 'xlsx_cell_values.xlsx' + with xls_source.open('rb') as fh: + data = tablib.Dataset().load(fh) + width_before = _get_width(data) + data.append([ + 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue', + ]) + width_after = _get_width(data) + + assert width_before != width_after + class JSONTests(BaseTestCase): def test_json_format_detect(self): """Test JSON format detection.""" From c1196d67bf9c4f14464c568b26869dcda772c381 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 1 Apr 2022 15:02:06 +0300 Subject: [PATCH 04/21] chore: change assert style --- tests/test_tablib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 570898c0..459a435a 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -1501,8 +1501,8 @@ def _get_width(data): 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue', ]) width_after = _get_width(data) - - assert width_before != width_after + + self.assertNotEqual(width_before, width_after) class JSONTests(BaseTestCase): def test_json_format_detect(self): From 68112014b7a3c59e503de2f5e0990f842db0dd9a Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 1 Apr 2022 15:44:31 +0300 Subject: [PATCH 05/21] Update src/tablib/formats/_xlsx.py Co-authored-by: Hugo van Kemenade --- src/tablib/formats/_xlsx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index 71b828e5..7ce212f7 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -37,7 +37,7 @@ def detect(cls, stream): return False @classmethod - def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=False): + def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=False, column_width="adaptive"): """Returns XLSX representation of Dataset. If ``freeze_panes`` is True, Export will freeze panes only after first line. @@ -173,7 +173,7 @@ def dset_sheet(cls, dataset, ws, freeze_panes=True, escape=False): if escape and cell.data_type == 'f' and cell.value.startswith('='): cell.value = cell.value.replace("=", "") - + @classmethod def _adapt_column_width(cls, worksheet, width: Optional[Union[str, int]]) -> None: From 19c0c586e8c47502f7a53b7bb246064023d2db03 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 1 Apr 2022 15:44:38 +0300 Subject: [PATCH 06/21] Update src/tablib/formats/_xlsx.py Co-authored-by: Hugo van Kemenade --- src/tablib/formats/_xlsx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index 7ce212f7..239541b6 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -62,7 +62,7 @@ def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=F cls.dset_sheet(dataset, ws, freeze_panes=freeze_panes, escape=escape) if isinstance(column_width, str) and column_width != "adaptive": raise ValueError(f"Unsupported value `{column_width}` passed to `column_width` " - f"parameter. It supports 'adaptive' or integer values") + "parameter. It supports 'adaptive' or integer values") cls._adapt_column_width(ws, column_width) From f6a9664e11e7e602d5017e1ee128330400a5c25c Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 1 Apr 2022 15:24:26 +0300 Subject: [PATCH 07/21] chore: alphabetical order in AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 908036e6..61497bfe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,6 +9,7 @@ Here is a list of past and present much-appreciated contributors: Bruno Soares Claude Paroz Daniel Santos + Egor Osokin Erik Youngren Hugo van Kemenade Iuri de Silvio From d42f313790d6f4c25048b75c1f29abf19fee62e8 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 1 Apr 2022 15:44:45 +0300 Subject: [PATCH 08/21] chore: docs --- docs/formats.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/formats.rst b/docs/formats.rst index 1c100264..62f1f72f 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -251,14 +251,13 @@ can set to a number of lines that should be skipped before starting to read data. The ``export_set()`` method supports a ``column_width`` parameter. Depending on the -value you pass, the column width will be set accordingly. It can be either None, int, or "adaptive". +value you pass, the column width will be set accordingly. It can be either ``None``, an integer, or "adaptive". If "adaptive" is passed, the column width will be unique for every column and will be -calculated based on values' length. Example of usage +calculated based on values' length. For example:: + + data = tablib.Dataset() + data.export('xlsx', column_width='adaptive') -```python3 -data = tablib.Dataset() -data.export('xlsx', column_width='adaptive') -``` .. versionchanged:: 3.3.0 From 618930ad0863fe5552e0b215ae220857eea657b0 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 1 Apr 2022 15:50:41 +0300 Subject: [PATCH 09/21] chore: add tests and fix bug --- src/tablib/formats/_xlsx.py | 2 +- tests/test_tablib.py | 47 +++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index 239541b6..ab686c44 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -191,7 +191,7 @@ def _adapt_column_width(cls, worksheet, else: column_widths += [len(cell)] else: - column_widths = [width] * len(worksheet.values) + column_widths = [width] * worksheet.max_column for i, column_width in enumerate(column_widths, 1): # start at 1 worksheet.column_dimensions[get_column_letter(i)].width = column_width diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 459a435a..d354f4a5 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -1485,10 +1485,30 @@ def test_xlsx_raise_ValueError_on_cell_write_during_export(self): wb = load_workbook(filename=BytesIO(_xlsx)) self.assertEqual('[1]', wb.active['A1'].value) - def test_xlsx_column_width(self): + def test_xlsx_column_width_none(self): + """check that column width adapts to value length""" + + def _get_width(data): + xlsx_content = data.export('xlsx', column_width=None) + wb = load_workbook(filename=BytesIO(xlsx_content)) + ws = wb.active + return ws.column_dimensions['A'].width + + xls_source = Path(__file__).parent / 'files' / 'xlsx_cell_values.xlsx' + with xls_source.open('rb') as fh: + data = tablib.Dataset().load(fh) + width_before = _get_width(data) + data.append([ + 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue', + ]) + width_after = _get_width(data) + + self.assertEqual(width_before, width_after) + + def test_xlsx_column_width_adaptive(self): """check that column width adapts to value length""" def _get_width(data): - xlsx_content = data.export('xlsx') + xlsx_content = data.export('xlsx', column_width='adaptive') wb = load_workbook(filename=BytesIO(xlsx_content)) ws = wb.active return ws.column_dimensions['A'].width @@ -1503,7 +1523,30 @@ def _get_width(data): width_after = _get_width(data) self.assertNotEqual(width_before, width_after) + + def test_xlsx_column_width_integer(self): + """check that column width adapts to value length""" + _some_integer = 10 + def _get_width(data): + xlsx_content = data.export('xlsx', column_width=_some_integer) + wb = load_workbook(filename=BytesIO(xlsx_content)) + ws = wb.active + return ws.column_dimensions['A'].width + + xls_source = Path(__file__).parent / 'files' / 'xlsx_cell_values.xlsx' + with xls_source.open('rb') as fh: + data = tablib.Dataset().load(fh) + width_before = _get_width(data) + data.append([ + 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue', + ]) + width_after = _get_width(data) + + self.assertEqual(_some_integer, width_before) + self.assertEqual(_some_integer, width_after) + + class JSONTests(BaseTestCase): def test_json_format_detect(self): """Test JSON format detection.""" From 7bfafaaafd9203895368539cd20df36320543c0d Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 8 Jun 2022 14:25:59 +0300 Subject: [PATCH 10/21] refactor: helper function for tests and new test case for error Co-authored-by: Hugo van Kemenade --- tests/test_tablib.py | 72 ++++++++++++++++---------------------------- 1 file changed, 26 insertions(+), 46 deletions(-) diff --git a/tests/test_tablib.py b/tests/test_tablib.py index d354f4a5..c9184ffa 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -1484,31 +1484,11 @@ def test_xlsx_raise_ValueError_on_cell_write_during_export(self): _xlsx = data.export('xlsx') wb = load_workbook(filename=BytesIO(_xlsx)) self.assertEqual('[1]', wb.active['A1'].value) - - def test_xlsx_column_width_none(self): - """check that column width adapts to value length""" - - def _get_width(data): - xlsx_content = data.export('xlsx', column_width=None) - wb = load_workbook(filename=BytesIO(xlsx_content)) - ws = wb.active - return ws.column_dimensions['A'].width - - xls_source = Path(__file__).parent / 'files' / 'xlsx_cell_values.xlsx' - with xls_source.open('rb') as fh: - data = tablib.Dataset().load(fh) - width_before = _get_width(data) - data.append([ - 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue', - ]) - width_after = _get_width(data) - - self.assertEqual(width_before, width_after) - def test_xlsx_column_width_adaptive(self): + def _helper_export_column_width(self, input_arg): """check that column width adapts to value length""" - def _get_width(data): - xlsx_content = data.export('xlsx', column_width='adaptive') + def _get_width(data, input_arg): + xlsx_content = data.export('xlsx', column_width=input_arg) wb = load_workbook(filename=BytesIO(xlsx_content)) ws = wb.active return ws.column_dimensions['A'].width @@ -1516,35 +1496,35 @@ def _get_width(data): xls_source = Path(__file__).parent / 'files' / 'xlsx_cell_values.xlsx' with xls_source.open('rb') as fh: data = tablib.Dataset().load(fh) - width_before = _get_width(data) + width_before = _get_width(data, input_arg) data.append([ 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue', ]) - width_after = _get_width(data) - - self.assertNotEqual(width_before, width_after) + width_after = _get_width(data, width_before) + return width_before, width_after - def test_xlsx_column_width_integer(self): - """check that column width adapts to value length""" - _some_integer = 10 - - def _get_width(data): - xlsx_content = data.export('xlsx', column_width=_some_integer) - wb = load_workbook(filename=BytesIO(xlsx_content)) - ws = wb.active - return ws.column_dimensions['A'].width + def test_xlsx_column_width_none(self): + """check column width with None""" + width_before, width_after = self._helper_export_column_width(None) + self.assertEqual(width_before, 13) + self.assertEqual(width_after, 13) - xls_source = Path(__file__).parent / 'files' / 'xlsx_cell_values.xlsx' - with xls_source.open('rb') as fh: - data = tablib.Dataset().load(fh) - width_before = _get_width(data) - data.append([ - 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue', - ]) - width_after = _get_width(data) + def test_xlsx_column_width_adaptive(self): + """check column width with 'adaptive'""" + width_before, width_after = self._helper_export_column_width("adaptive") + self.assertEqual(width_before, 11) + self.assertEqual(width_after, 11) - self.assertEqual(_some_integer, width_before) - self.assertEqual(_some_integer, width_after) + def test_xlsx_column_width_integer(self): + """check column width with an integer""" + width_before, width_after = self._helper_export_column_width(10) + self.assertEqual(width_before, 10) + self.assertEqual(width_after, 10) + + def test_xlsx_column_width_value_error(self): + """check column width with invalid input""" + with self.assertRaises(ValueError): + self._helper_export_column_width("invalid input") class JSONTests(BaseTestCase): From eeca7e5298b9e5b63714285038e85ff4e92c3b8f Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 21 Dec 2022 13:34:48 +0300 Subject: [PATCH 11/21] chore: use from __future__ import annotations --- src/tablib/formats/_xlsx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index ab686c44..1a496af6 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -1,9 +1,9 @@ """ Tablib - XLSX Support. """ +from __future__ import annotations import re from io import BytesIO -from typing import Optional, Union from openpyxl.reader.excel import ExcelReader, load_workbook from openpyxl.styles import Alignment, Font @@ -176,7 +176,7 @@ def dset_sheet(cls, dataset, ws, freeze_panes=True, escape=False): @classmethod def _adapt_column_width(cls, worksheet, - width: Optional[Union[str, int]]) -> None: + width: str | int | None) -> None: if width is None: return From be9db5e836f66416b6dc6f898f9f2677af203bc4 Mon Sep 17 00:00:00 2001 From: Andrew Graham-Yooll Date: Sat, 18 Jan 2025 15:13:05 +0800 Subject: [PATCH 12/21] Update tests and code for easier reading --- src/tablib/formats/_xlsx.py | 20 +++++++------ tests/test_tablib.py | 56 ++++++++++++++++++------------------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index 1a496af6..96c72876 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -9,7 +9,6 @@ from openpyxl.styles import Alignment, Font from openpyxl.utils import get_column_letter from openpyxl.workbook import Workbook -from openpyxl.writer.excel import ExcelWriter import tablib @@ -50,6 +49,11 @@ def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=F If ``escape`` is True, formulae will have the leading '=' character removed. This is a security measure to prevent formulae from executing by default in exported XLSX files. + + If ``column_width`` is set to "adaptive", the column width will be set to the maximum + width of the content in each column. If it is set to an integer, the column width will be + set to that integer value. If it is set to None, the column width will be set as the default openpyxl.Worksheet width value. + """ wb = Workbook() ws = wb.worksheets[0] @@ -60,9 +64,6 @@ def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=F ) cls.dset_sheet(dataset, ws, freeze_panes=freeze_panes, escape=escape) - if isinstance(column_width, str) and column_width != "adaptive": - raise ValueError(f"Unsupported value `{column_width}` passed to `column_width` " - "parameter. It supports 'adaptive' or integer values") cls._adapt_column_width(ws, column_width) @@ -175,13 +176,16 @@ def dset_sheet(cls, dataset, ws, freeze_panes=True, escape=False): cell.value = cell.value.replace("=", "") @classmethod - def _adapt_column_width(cls, worksheet, - width: str | int | None) -> None: + def _adapt_column_width(cls, worksheet, width): + if isinstance(width, str) and width != "adaptive": + raise ValueError(f"Unsupported value `{width}` passed to `column_width` " + "parameter. It supports 'adaptive' or integer values") + if width is None: return column_widths = [] - if isinstance(width, str) and width == "adaptive": + if width == "adaptive": for row in worksheet.values: for i, cell in enumerate(row): cell = str(cell) @@ -189,7 +193,7 @@ def _adapt_column_width(cls, worksheet, if len(cell) > column_widths[i]: column_widths[i] = len(cell) else: - column_widths += [len(cell)] + column_widths.append(len(cell)) else: column_widths = [width] * worksheet.max_column diff --git a/tests/test_tablib.py b/tests/test_tablib.py index c9184ffa..0e5a92ef 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -1341,6 +1341,24 @@ def get_format_str(cell): class XLSXTests(BaseTestCase): + def _helper_export_column_width(self, column_width): + """check that column width adapts to value length""" + def _get_width(data, input_arg): + xlsx_content = data.export('xlsx', column_width=input_arg) + wb = load_workbook(filename=BytesIO(xlsx_content)) + ws = wb.active + return ws.column_dimensions['A'].width + + xls_source = Path(__file__).parent / 'files' / 'xlsx_cell_values.xlsx' + with xls_source.open('rb') as fh: + data = tablib.Dataset().load(fh) + width_before = _get_width(data, column_width) + data.append([ + 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue', + ]) + width_after = _get_width(data, width_before) + return width_before, width_after + def test_xlsx_format_detect(self): """Test the XLSX format detection.""" in_stream = self.founders.xlsx @@ -1484,45 +1502,27 @@ def test_xlsx_raise_ValueError_on_cell_write_during_export(self): _xlsx = data.export('xlsx') wb = load_workbook(filename=BytesIO(_xlsx)) self.assertEqual('[1]', wb.active['A1'].value) - - def _helper_export_column_width(self, input_arg): - """check that column width adapts to value length""" - def _get_width(data, input_arg): - xlsx_content = data.export('xlsx', column_width=input_arg) - wb = load_workbook(filename=BytesIO(xlsx_content)) - ws = wb.active - return ws.column_dimensions['A'].width - - xls_source = Path(__file__).parent / 'files' / 'xlsx_cell_values.xlsx' - with xls_source.open('rb') as fh: - data = tablib.Dataset().load(fh) - width_before = _get_width(data, input_arg) - data.append([ - 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue', - ]) - width_after = _get_width(data, width_before) - return width_before, width_after - - def test_xlsx_column_width_none(self): - """check column width with None""" - width_before, width_after = self._helper_export_column_width(None) - self.assertEqual(width_before, 13) - self.assertEqual(width_after, 13) - + def test_xlsx_column_width_adaptive(self): - """check column width with 'adaptive'""" + """ Test that column width adapts to value length""" width_before, width_after = self._helper_export_column_width("adaptive") self.assertEqual(width_before, 11) self.assertEqual(width_after, 11) def test_xlsx_column_width_integer(self): - """check column width with an integer""" + """Test that column width changes to integer length""" width_before, width_after = self._helper_export_column_width(10) self.assertEqual(width_before, 10) self.assertEqual(width_after, 10) + + def test_xlsx_column_width_none(self): + """Test that column width does not change""" + width_before, width_after = self._helper_export_column_width(None) + self.assertEqual(width_before, 13) + self.assertEqual(width_after, 13) def test_xlsx_column_width_value_error(self): - """check column width with invalid input""" + """Raise ValueError if column_width is not a valid input""" with self.assertRaises(ValueError): self._helper_export_column_width("invalid input") From af35169ba59ad6f42a125eb958379b4cbed2e8e2 Mon Sep 17 00:00:00 2001 From: Andrew Graham-Yooll Date: Sat, 18 Jan 2025 15:16:53 +0800 Subject: [PATCH 13/21] Update format doc for clarity --- docs/formats.rst | 6 +++--- src/tablib/formats/_xlsx.py | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/formats.rst b/docs/formats.rst index 62f1f72f..728da248 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -251,8 +251,8 @@ can set to a number of lines that should be skipped before starting to read data. The ``export_set()`` method supports a ``column_width`` parameter. Depending on the -value you pass, the column width will be set accordingly. It can be either ``None``, an integer, or "adaptive". -If "adaptive" is passed, the column width will be unique for every column and will be +value passed, the column width will be set accordingly. It can be either ``None``, an integer, or default "adaptive". +If "adaptive" is passed, the column width will be unique and will be calculated based on values' length. For example:: data = tablib.Dataset() @@ -260,7 +260,7 @@ calculated based on values' length. For example:: -.. versionchanged:: 3.3.0 +.. versionchanged:: 3.8.0 The ``column_width`` parameter for ``export_set()`` was added. .. versionchanged:: 3.1.0 diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index 96c72876..da6ad9ec 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -1,7 +1,5 @@ """ Tablib - XLSX Support. """ -from __future__ import annotations - import re from io import BytesIO From c3fcf8df77d8e19048b8b83d051f6059d0b22b09 Mon Sep 17 00:00:00 2001 From: Andrew Graham-Yooll Date: Sat, 18 Jan 2025 15:19:03 +0800 Subject: [PATCH 14/21] Update message format for exception raised --- src/tablib/formats/_xlsx.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index da6ad9ec..21080b76 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -176,8 +176,11 @@ def dset_sheet(cls, dataset, ws, freeze_panes=True, escape=False): @classmethod def _adapt_column_width(cls, worksheet, width): if isinstance(width, str) and width != "adaptive": - raise ValueError(f"Unsupported value `{width}` passed to `column_width` " - "parameter. It supports 'adaptive' or integer values") + msg = ( + f"Invalid value for column_width: {width}. " + f"Must be 'adaptive' or an integer." + ) + raise ValueError(msg) if width is None: return From c29c9ddf82619bc33c8658a63456eb5a76f1c678 Mon Sep 17 00:00:00 2001 From: Andrew Graham-Yooll Date: Sat, 18 Jan 2025 15:28:14 +0800 Subject: [PATCH 15/21] Linting and formatting --- AUTHORS | 1 - src/tablib/formats/_xlsx.py | 12 +++++++----- tests/test_tablib.py | 11 +++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/AUTHORS b/AUTHORS index 61497bfe..670faf12 100644 --- a/AUTHORS +++ b/AUTHORS @@ -35,4 +35,3 @@ Here is a list of past and present much-appreciated contributors: Tsuyoshi Hombashi Tushar Makkar Yunis Yilmaz - Egor Osokin diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index 21080b76..30ec6d15 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -34,7 +34,8 @@ def detect(cls, stream): return False @classmethod - def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=False, column_width="adaptive"): + def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", + escape=False, column_width="adaptive"): """Returns XLSX representation of Dataset. If ``freeze_panes`` is True, Export will freeze panes only after first line. @@ -50,7 +51,8 @@ def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-", escape=F If ``column_width`` is set to "adaptive", the column width will be set to the maximum width of the content in each column. If it is set to an integer, the column width will be - set to that integer value. If it is set to None, the column width will be set as the default openpyxl.Worksheet width value. + set to that integer value. If it is set to None, the column width will be set as the + default openpyxl.Worksheet width value. """ wb = Workbook() @@ -181,7 +183,7 @@ def _adapt_column_width(cls, worksheet, width): f"Must be 'adaptive' or an integer." ) raise ValueError(msg) - + if width is None: return @@ -197,6 +199,6 @@ def _adapt_column_width(cls, worksheet, width): column_widths.append(len(cell)) else: column_widths = [width] * worksheet.max_column - - for i, column_width in enumerate(column_widths, 1): # start at 1 + + for i, column_width in enumerate(column_widths, 1): # start at 1 worksheet.column_dimensions[get_column_letter(i)].width = column_width diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 0e5a92ef..32119918 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -11,13 +11,11 @@ from decimal import Decimal from io import BytesIO, StringIO from pathlib import Path -from tempfile import TemporaryFile from uuid import uuid4 import xlrd from odf import opendocument, table from openpyxl.reader.excel import load_workbook -from MarkupPy import markup import tablib from tablib.core import Row, detect_format @@ -1354,11 +1352,12 @@ def _get_width(data, input_arg): data = tablib.Dataset().load(fh) width_before = _get_width(data, column_width) data.append([ - 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue-verylongvalue', + 'verylongvalue-verylongvalue-verylongvalue-verylongvalue-' + 'verylongvalue-verylongvalue-verylongvalue-verylongvalue', ]) width_after = _get_width(data, width_before) return width_before, width_after - + def test_xlsx_format_detect(self): """Test the XLSX format detection.""" in_stream = self.founders.xlsx @@ -1502,7 +1501,7 @@ def test_xlsx_raise_ValueError_on_cell_write_during_export(self): _xlsx = data.export('xlsx') wb = load_workbook(filename=BytesIO(_xlsx)) self.assertEqual('[1]', wb.active['A1'].value) - + def test_xlsx_column_width_adaptive(self): """ Test that column width adapts to value length""" width_before, width_after = self._helper_export_column_width("adaptive") @@ -1514,7 +1513,7 @@ def test_xlsx_column_width_integer(self): width_before, width_after = self._helper_export_column_width(10) self.assertEqual(width_before, 10) self.assertEqual(width_after, 10) - + def test_xlsx_column_width_none(self): """Test that column width does not change""" width_before, width_after = self._helper_export_column_width(None) From fd73a9657d5a951e9378aecbed5123f9ee05a145 Mon Sep 17 00:00:00 2001 From: Andrew Graham-Yooll Date: Sun, 19 Jan 2025 14:19:16 +0800 Subject: [PATCH 16/21] Linting and formatting --- AUTHORS | 1 + docs/formats.rst | 11 +++++------ src/tablib/formats/_xlsx.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/AUTHORS b/AUTHORS index 670faf12..c8fc9803 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,6 +4,7 @@ by the Jazzband GitHub team. Here is a list of past and present much-appreciated contributors: Alex Gaynor + Andrew Graham-Yooll Andrii Soldatenko Benjamin Wohlwend Bruno Soares diff --git a/docs/formats.rst b/docs/formats.rst index 728da248..792dfbd5 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -250,16 +250,15 @@ The ``import_set()`` method also supports a ``skip_lines`` parameter that you can set to a number of lines that should be skipped before starting to read data. -The ``export_set()`` method supports a ``column_width`` parameter. Depending on the -value passed, the column width will be set accordingly. It can be either ``None``, an integer, or default "adaptive". -If "adaptive" is passed, the column width will be unique and will be -calculated based on values' length. For example:: +The ``export_set()`` method supports a ``column_width`` parameter. Depending +on the value passed, the column width will be set accordingly. It can be +either ``None``, an integer, or default "adaptive". If "adaptive" is passed, +the column width will be unique and will be calculated based on values' length. +For example:: data = tablib.Dataset() data.export('xlsx', column_width='adaptive') - - .. versionchanged:: 3.8.0 The ``column_width`` parameter for ``export_set()`` was added. diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index 30ec6d15..5bb821cb 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -180,7 +180,7 @@ def _adapt_column_width(cls, worksheet, width): if isinstance(width, str) and width != "adaptive": msg = ( f"Invalid value for column_width: {width}. " - f"Must be 'adaptive' or an integer." + "Must be 'adaptive' or an integer." ) raise ValueError(msg) From 6d1a7fa5f5a588ae5f5418df58839a83e8b4902e Mon Sep 17 00:00:00 2001 From: Andrew Graham-Yooll Date: Sun, 19 Jan 2025 14:22:53 +0800 Subject: [PATCH 17/21] Add history entry --- HISTORY.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 077b3170..a5bb445e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # History +## Unreleased + +### Improvements + +- Add support for exporting XLSX with column width (#516) + ## 3.7.0 (2024-10-08) ### Improvements From 3bed224d5b08c5a27686b5982e6ff9848f102931 Mon Sep 17 00:00:00 2001 From: Andrew Graham-Yooll Date: Sun, 19 Jan 2025 14:24:48 +0800 Subject: [PATCH 18/21] Fix history versioning --- HISTORY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index a5bb445e..cf3b7020 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,6 @@ # History -## Unreleased +## 3.8.0 (Unreleased) ### Improvements From dc68dfa956d2a6106831706832e4fb274d835200 Mon Sep 17 00:00:00 2001 From: Andrew Graham-Yooll Date: Mon, 20 Jan 2025 08:03:19 +0100 Subject: [PATCH 19/21] Update docs/formats.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/formats.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/formats.rst b/docs/formats.rst index 792dfbd5..a0bb2b7b 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -250,11 +250,11 @@ The ``import_set()`` method also supports a ``skip_lines`` parameter that you can set to a number of lines that should be skipped before starting to read data. -The ``export_set()`` method supports a ``column_width`` parameter. Depending -on the value passed, the column width will be set accordingly. It can be -either ``None``, an integer, or default "adaptive". If "adaptive" is passed, -the column width will be unique and will be calculated based on values' length. -For example:: +The ``export_set()`` method supports a ``column_width`` parameter. Depending +on the value passed, the column width will be set accordingly. It can be +either ``None``, an integer, or default "adaptive". If "adaptive" is passed, +the column width will be unique and will be calculated based on values' length. +For example:: data = tablib.Dataset() data.export('xlsx', column_width='adaptive') From f4277f300e1cba2742d7ec276a94b5d79c2c4658 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 07:03:27 +0000 Subject: [PATCH 20/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/formats.rst b/docs/formats.rst index a0bb2b7b..ce546dda 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -261,7 +261,7 @@ For example:: .. versionchanged:: 3.8.0 The ``column_width`` parameter for ``export_set()`` was added. - + .. versionchanged:: 3.1.0 The ``skip_lines`` parameter for ``import_set()`` was added. From 2999501db7771927ce08cc781545702180eedfdc Mon Sep 17 00:00:00 2001 From: Andrew Graham-Yooll Date: Mon, 20 Jan 2025 08:03:51 +0100 Subject: [PATCH 21/21] Update src/tablib/formats/_xlsx.py Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/tablib/formats/_xlsx.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index 5bb821cb..1a75c20f 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -191,12 +191,12 @@ def _adapt_column_width(cls, worksheet, width): if width == "adaptive": for row in worksheet.values: for i, cell in enumerate(row): - cell = str(cell) + cell_width = len(str(cell)) if len(column_widths) > i: - if len(cell) > column_widths[i]: - column_widths[i] = len(cell) + if cell_width > column_widths[i]: + column_widths[i] = cell_width else: - column_widths.append(len(cell)) + column_widths.append(cell_width) else: column_widths = [width] * worksheet.max_column