From 39c70b8c1493dd41b6bfb3e895aa423193d286bd Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 24 Feb 2024 12:29:58 +0100 Subject: [PATCH 1/2] Fixes #578 - Do not mutate data in place when applying formatters --- src/tablib/core.py | 31 ++++++++++++++++--------------- tests/test_tablib.py | 12 +++++++++--- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/tablib/core.py b/src/tablib/core.py index 956458b0..e83e5dac 100644 --- a/src/tablib/core.py +++ b/src/tablib/core.py @@ -73,6 +73,9 @@ def append(self, value): def insert(self, index, value): self._row.insert(index, value) + def copy(self): + return Row(self._row.copy(), self.tags.copy()) + def __contains__(self, item): return item in self._row @@ -270,27 +273,25 @@ def _package(self, dicts=True): _data = list(self._data) - # Execute formatters - if self._formatters: - for row_i, row in enumerate(_data): + def format_row(row): + # Execute formatters + if self._formatters: + row = row.copy() # To not mutate internal data structure for col, callback in self._formatters: - try: - if col is None: - for j, c in enumerate(row): - _data[row_i][j] = callback(c) - else: - _data[row_i][col] = callback(row[col]) - except IndexError: - raise InvalidDatasetIndex + if col is None: + # Apply formatter to all cells + row = [callback(cell) for cell in row] + else: + row[col] = callback(row[col]) + return list(row) if self.headers: if dicts: - data = [dict(list(zip(self.headers, data_row))) for data_row in _data] + data = [dict(list(zip(self.headers, format_row(row)))) for row in _data] else: - data = [list(self.headers)] + list(_data) + data = [list(self.headers)] + [format_row(row) for row in _data] else: - data = [list(row) for row in _data] - + data = [format_row(row) for row in _data] return data def _get_headers(self): diff --git a/tests/test_tablib.py b/tests/test_tablib.py index ee341868..0977cd65 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -564,12 +564,18 @@ def test_formatters(self): """Confirm formatters are being triggered.""" def _formatter(cell_value): - return str(cell_value).upper() + return str(cell_value)[1:] self.founders.add_formatter('last_name', _formatter) - for name in [r['last_name'] for r in self.founders.dict]: - self.assertTrue(name.isupper()) + expected = [ + {'first_name': 'John', 'last_name': 'dams', 'gpa': 90}, + {'first_name': 'George', 'last_name': 'ashington', 'gpa': 67}, + {'first_name': 'Thomas', 'last_name': 'efferson', 'gpa': 50}, + ] + self.assertEqual(self.founders.dict, expected) + # Test once more as the result should be the same + self.assertEqual(self.founders.dict, expected) def test_unicode_renders_markdown_table(self): # add another entry to test right field width for From 99c81721f90547b2ee27be55b6bb4c7f0e5d7263 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 24 Feb 2024 12:44:29 +0100 Subject: [PATCH 2/2] Add test for passing None to add_formatter --- src/tablib/core.py | 5 +++-- tests/test_tablib.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/tablib/core.py b/src/tablib/core.py index e83e5dac..70e26f04 100644 --- a/src/tablib/core.py +++ b/src/tablib/core.py @@ -623,7 +623,8 @@ def get_col(self, index): def add_formatter(self, col, handler): """Adds a formatter to the :class:`Dataset`. - :param col: column to. Accepts index int or header str. + :param col: column to. Accepts index int, header str, or None to apply + the formatter to all columns. :param handler: reference to callback function to execute against each cell value. """ @@ -634,7 +635,7 @@ def add_formatter(self, col, handler): else: raise KeyError - if not col > self.width: + if col is None or col <= self.width: self._formatters.append((col, handler)) else: raise InvalidDatasetIndex diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 0977cd65..ee562b9b 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -577,6 +577,22 @@ def _formatter(cell_value): # Test once more as the result should be the same self.assertEqual(self.founders.dict, expected) + def test_formatters_all_cols(self): + """ + Passing None as first add_formatter param apply formatter to all columns. + """ + + def _formatter(cell_value): + return str(cell_value).upper() + + self.founders.add_formatter(None, _formatter) + + self.assertEqual(self.founders.dict, [ + {'first_name': 'JOHN', 'last_name': 'ADAMS', 'gpa': '90'}, + {'first_name': 'GEORGE', 'last_name': 'WASHINGTON', 'gpa': '67'}, + {'first_name': 'THOMAS', 'last_name': 'JEFFERSON', 'gpa': '50'}, + ]) + def test_unicode_renders_markdown_table(self): # add another entry to test right field width for # integer