From 4c53ba37686116d5e00c6203fe968a53bd4c155f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 24 Feb 2024 12:29:58 +0100 Subject: [PATCH] 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 c46ddee4..2679fd24 100644 --- a/src/tablib/core.py +++ b/src/tablib/core.py @@ -74,6 +74,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 @@ -276,27 +279,25 @@ def _package(self, dicts=True, ordered=True): else: dict_pack = dict - # 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_pack(list(zip(self.headers, data_row))) for data_row in _data] + data = [dict_pack(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 9035c1e1..63f6d659 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -565,12 +565,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