Skip to content

Commit d33c0ec

Browse files
committed
Add table.__setitem__
1 parent e8335c4 commit d33c0ec

File tree

4 files changed

+127
-7
lines changed

4 files changed

+127
-7
lines changed

c/CHANGELOG.rst

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
----------------------
2-
[0.99.13] - 2021-07-08
2+
[0.99.14] - 2021-0X-XX
33
----------------------
44

5+
**Features**
6+
7+
- Add `tsk_X_table_update_row` methods which allow modifying single rows of tables
8+
(:user:`jeromekelleher`, :issue:`1545`, :pr:`1552`).
9+
10+
----------------------
11+
[0.99.13] - 2021-07-08
12+
----------------------
513
**Fixes**
614

715
- Fix segfault when very large columns overflow

python/CHANGELOG.rst

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
--------------------
2+
[0.3.8] - 2021-0X-XX
3+
--------------------
4+
5+
**Features**
6+
7+
- Add `__setitem__` to all tables allowing single rows to be updated. For example
8+
`tables.nodes[0] = tables.nodes[0].replace(flags=tskit.NODE_IS_SAMPLE)`
9+
(:user:`jeromekelleher`, :user:`benjeffery`, :issue:`1545`, :pr:`1600`).
10+
111
--------------------
212
[0.3.7] - 2021-07-08
313
--------------------

python/tests/test_tables.py

+69-6
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,64 @@ class NotADuck:
448448
with pytest.raises(AttributeError, match="'NotADuck' object has no attribute"):
449449
self.table_class().append(NotADuck())
450450

451+
def test_setitem(self):
452+
table = self.table_class()
453+
for row in self.make_transposed_input_data(10):
454+
table.append(table.row_class(**row))
455+
table2 = self.table_class()
456+
for row in self.make_transposed_input_data(20)[10:]:
457+
table2.append(table.row_class(**row))
458+
assert table != table2
459+
460+
copy = table.copy()
461+
for j in range(10):
462+
table[j] = table[j]
463+
table.assert_equals(copy)
464+
465+
for j in range(10):
466+
table[j] = table2[j]
467+
table.assert_equals(table2)
468+
469+
def test_setitem_duck_type(self):
470+
class Duck:
471+
pass
472+
473+
table = self.table_class()
474+
for row in self.make_transposed_input_data(10):
475+
table.append(table.row_class(**row))
476+
table2 = self.table_class()
477+
for row in self.make_transposed_input_data(20)[10:]:
478+
table2.append(table.row_class(**row))
479+
assert table != table2
480+
481+
for j in range(10):
482+
duck = Duck()
483+
for k, v in dataclasses.asdict(table2[j]).items():
484+
setattr(duck, k, v)
485+
table[j] = duck
486+
table.assert_equals(table2)
487+
488+
def test_setitem_error(self):
489+
class NotADuck:
490+
pass
491+
492+
table = self.table_class()
493+
table.append(table.row_class(**self.make_transposed_input_data(1)[0]))
494+
with pytest.raises(AttributeError, match="'NotADuck' object has no attribute"):
495+
table[0] = NotADuck()
496+
497+
with pytest.raises(IndexError, match="Index out of bounds"):
498+
self.table_class()[0] = table[0]
499+
with pytest.raises(IndexError, match="Index out of bounds"):
500+
self.table_class()[-1] = table[0]
501+
502+
with pytest.raises(TypeError, match="Index must be integer"):
503+
self.table_class()[0.5] = table[0]
504+
with pytest.raises(TypeError, match="Index must be integer"):
505+
self.table_class()[None] = table[0]
506+
with pytest.raises(TypeError, match="Index must be integer"):
507+
self.table_class()[[1]] = table[0]
508+
451509
def test_set_columns_data(self):
452510
for num_rows in [0, 10, 100, 1000]:
453511
input_data = {col.name: col.get_input(num_rows) for col in self.columns}
@@ -1347,12 +1405,6 @@ def test_bad_indexes(self, table):
13471405
with pytest.raises(TypeError, match="not supported between instances"):
13481406
table["foobar"]
13491407

1350-
def test_not_writable(self, table):
1351-
with pytest.raises(TypeError, match="object does not support item assignment"):
1352-
table[5] = 5
1353-
with pytest.raises(TypeError, match="object does not support item assignment"):
1354-
table[[5]] = 5
1355-
13561408

13571409
common_tests = [
13581410
CommonTestsMixin,
@@ -4650,3 +4702,14 @@ def test_with_mutation_times(self):
46504702
tables.compute_mutation_times()
46514703
ts = tables.tree_sequence()
46524704
self.verify_subset_union(ts)
4705+
4706+
4707+
class TestTableSetitemMetadata:
4708+
@pytest.mark.parametrize("table_name", tskit.TABLE_NAMES)
4709+
def test_setitem_metadata(self, ts_fixture, table_name):
4710+
table = getattr(ts_fixture.tables, table_name)
4711+
if hasattr(table, "metadata_schema"):
4712+
assert table.metadata_schema == tskit.MetadataSchema({"codec": "json"})
4713+
assert table[0].metadata != table[1].metadata
4714+
table[0] = table[1]
4715+
assert table[0] == table[1]

python/tskit/tables.py

+39
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,45 @@ def __getitem__(self, index):
374374

375375
return ret
376376

377+
def __setitem__(self, index, new_row):
378+
"""
379+
Replaces a row of this table at the specified index with information from a
380+
row-like object. Metadata, will be validated and encoded according to the table's
381+
:attr:`metadata_schema<tskit.IndividualTable.metadata_schema>`.
382+
383+
:param index: the zero-index of the row to change
384+
:param row-like new_row: An object that has attributes corresponding to the
385+
properties of the new row. Both the objects returned from ``table[i]`` and
386+
e.g. ``ts.individual(i)`` work for this purpose, along with any other
387+
object with the correct attributes.
388+
"""
389+
if isinstance(index, numbers.Integral):
390+
# Single row by integer
391+
if index < 0:
392+
index += len(self)
393+
if index < 0 or index >= len(self):
394+
raise IndexError("Index out of bounds")
395+
else:
396+
raise TypeError("Index must be integer")
397+
398+
row_data = {
399+
column: getattr(new_row, column)
400+
for column in self.column_names
401+
if "_offset" not in column
402+
}
403+
404+
# Encode the meatdata - note that if this becomes a perf bottleneck it is
405+
# possible to use the cached, encoded metadata in the row object, rather than
406+
# decode and reencode
407+
if "metadata" in row_data:
408+
if row_data["metadata"] is None:
409+
row_data["metadata"] = self.metadata_schema.empty_value
410+
row_data["metadata"] = self.metadata_schema.validate_and_encode_row(
411+
row_data["metadata"]
412+
)
413+
414+
self.ll_table.update_row(row_index=index, **row_data)
415+
377416
def append(self, row):
378417
"""
379418
Adds a new row to this table and returns the ID of the new row. Metadata, if

0 commit comments

Comments
 (0)