Skip to content

Commit

Permalink
Fixes #572 - Allow appending rows after a dynamic column was inserted
Browse files Browse the repository at this point in the history
Co-authored-by: Hugo van Kemenade <[email protected]>
  • Loading branch information
claudep and hugovk authored Dec 21, 2023
1 parent 65b6d7b commit 01ac5e6
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 5 deletions.
3 changes: 3 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
- The html format now supports importing from HTML content (#243)
- The ODS format now supports importing from .ods files (#567). The support is
still a bit experimental.
- When adding rows to a dataset with dynamic columns, it's now possible to
provide only static values, and dynamic column values will be automatically
calculated and added to the row (#572).

### Changes

Expand Down
9 changes: 9 additions & 0 deletions docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,15 @@ Adding this function to our dataset as a dynamic column would result in: ::
- {Age: 22, First Name: Kenneth, Gender: Male, Last Name: Reitz}
- {Age: 20, First Name: Bessie, Gender: Female, Last Name: Monke}

When you add new rows to a dataset that contains dynamic columns, you should
either provide all values in the row, or only the non-dynamic values and then
the dynamic values will be automatically generated using the function initially
provided for the column calculation.

..versionchanged:: 3.6.0

In older versions, you could only add new rows with fully-populated rows,
including dynamic columns.

.. _tags:

Expand Down
22 changes: 19 additions & 3 deletions src/tablib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ def __init__(self, *args, **kwargs):
# (column, callback) tuples
self._formatters = []

# {col_index: col_func}
self._dynamic_columns = {}

self.headers = kwargs.get('headers')

self.title = kwargs.get('title')
Expand Down Expand Up @@ -187,6 +190,8 @@ def __delitem__(self, key):

pos = self.headers.index(key)
del self.headers[pos]
if pos in self._dynamic_columns:
del self._dynamic_columns[pos]

for i, row in enumerate(self._data):

Expand Down Expand Up @@ -238,7 +243,13 @@ def _set_in_format(self, fmt_key, in_stream, **kwargs):
def _validate(self, row=None, col=None, safety=False):
"""Assures size of every row in dataset is of proper proportions."""
if row:
is_valid = (len(row) == self.width) if self.width else True
if self.width:
is_valid = (
len(row) == self.width or
len(row) == (self.width - len(self._dynamic_columns))
)
else:
is_valid = True
elif col:
if len(col) < 1:
is_valid = True
Expand Down Expand Up @@ -446,9 +457,13 @@ def insert(self, index, row, tags=()):
The default behaviour is to insert the given row to the :class:`Dataset`
object at the given index.
"""
"""

self._validate(row)
if len(row) < self.width:
for pos, func in self._dynamic_columns.items():
row = list(row)
row.insert(pos, func(row))
self._data.insert(index, Row(row, tags=tags))

def rpush(self, row, tags=()):
Expand Down Expand Up @@ -546,7 +561,8 @@ def insert_col(self, index, col=None, header=None):
col = []

# Callable Columns...
if hasattr(col, '__call__'):
if callable(col):
self._dynamic_columns[self.width] = col
col = list(map(col, self._data))

col = self._clean_col(col)
Expand Down
36 changes: 34 additions & 2 deletions tests/test_tablib.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,42 @@ def test_add_column_with_header_and_data_exists(self):
def test_add_callable_column(self):
"""Verify adding column with values specified as callable."""

def new_col(x):
return x[0]
def new_col(row):
return row[0]

def initials(row):
return f"{row[0][0]}{row[1][0]}"

self.founders.append_col(new_col, header='first_again')
self.founders.append_col(initials, header='initials')

# A new row can still be appended, and the dynamic column value generated.
self.founders.append(('Some', 'One', 71))
# Also acceptable when all dynamic column values are provided.
self.founders.append(('Other', 'Second', 84, 'Other', 'OS'))

self.assertEqual(self.founders[3], ('Some', 'One', 71, 'Some', 'SO'))
self.assertEqual(self.founders[4], ('Other', 'Second', 84, 'Other', 'OS'))
self.assertEqual(
self.founders['first_again'],
['John', 'George', 'Thomas', 'Some', 'Other']
)
self.assertEqual(
self.founders['initials'],
['JA', 'GW', 'TJ', 'SO', 'OS']
)

# However only partial dynamic values provided is not accepted.
with self.assertRaises(tablib.InvalidDimensions):
self.founders.append(('Should', 'Crash', 60, 'Partial'))

# Add a new row after dynamic column deletion
del self.founders['first_again']
self.founders.append(('After', 'Deletion', 75))
self.assertEqual(
self.founders['initials'],
['JA', 'GW', 'TJ', 'SO', 'OS', 'AD']
)

def test_header_slicing(self):
"""Verify slicing by headers."""
Expand Down

0 comments on commit 01ac5e6

Please sign in to comment.