enthought / traits

Observable typed attributes for Python classes
Other
432 stars 85 forks source link

Fix missing observer registration when inserting into nested list #1761

Closed rhaschke closed 10 months ago

rhaschke commented 10 months ago

When using nested list: ta.List(trait=ta.List(trait=ta.Int, items=False), items=False), inserting an inner list failed to register observers on that list. Appending and extending works as expected. The culprit was that the item validation was performed twice, thus probably registering the observers on the wrong object.

Sample code illustrating the behavior:

import traits.api as ta

class Provider(ta.HasTraits):
    l = ta.List(trait=ta.List(trait=ta.Int, items=False), items=False)

    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        self.observe(self.assigned, "l")
        self.observe(self.outer_changed, "l:items")
        self.observe(self.inner_changed, "l:items:items")

    def assigned(self, event):
        print("assigned", event)

    def outer_changed(self, event):
        print("outer changed", event)

    def inner_changed(self, event):
        print("inner changed", event)

p = Provider()
p.l = [[1, 2], [3], [4]]  # assign list

print()
p.l[0] = [0]  # assign outer item
del p.l[1]  # delete outer item
p.l.append([7, 8])  # append outer item
p.l.insert(-1, [5, 6])  # insert outer item (next-to-last)

# Operating on appended outer item invokes the (inner) observer callbacks:
print()
p.l[-1].append(9)
p.l[-1].extend([10, 11])
p.l[-1][-2] = 20

print()
print(p.l)
print("\nOperating on inserted outer item doesn't invoke the observer callbacks:")
p.l[-2].append(7)
p.l[-2].extend([8, 9, 10])
p.l[-2][-1] = 30
print(p.l)

Expected vs. actual output (without this PR, the green part is missing):

 assigned TraitChangeEvent(object=<__main__.Provider object at 0x7f8f5988e160>, name='l', old=[], new=[[1, 2], [3], [4]])

 outer changed ListChangeEvent(object=[[0], [3], [4]], index=0, removed=[[1, 2]], added=[[0]])
 outer changed ListChangeEvent(object=[[0], [4]], index=1, removed=[[3]], added=[])
 outer changed ListChangeEvent(object=[[0], [4], [7, 8]], index=2, removed=[], added=[[7, 8]])
 outer changed ListChangeEvent(object=[[0], [4], [5, 6], [7, 8]], index=2, removed=[], added=[[5, 6]])

 inner changed ListChangeEvent(object=[7, 8, 9], index=2, removed=[], added=[9])
 inner changed ListChangeEvent(object=[7, 8, 9, 10, 11], index=3, removed=[], added=[10, 11])
 inner changed ListChangeEvent(object=[7, 8, 9, 20, 11], index=3, removed=[10], added=[20])

 [[0], [4], [5, 6], [7, 8, 9, 20, 11]]

 Operating on inserted outer item doesn't invoke the observer callbacks:
+inner changed ListChangeEvent(object=[5, 6, 7], index=2, removed=[], added=[7])
+inner changed ListChangeEvent(object=[5, 6, 7, 8, 9, 10], index=3, removed=[], added=[8, 9, 10])
+inner changed ListChangeEvent(object=[5, 6, 7, 8, 9, 30], index=5, removed=[10], added=[30])
 [[0], [4], [5, 6, 7, 8, 9, 30], [7, 8, 9, 20, 11]]
mdickinson commented 10 months ago

Nice catch! Thank you for the report, analysis and fix.

I've pushed a regression test to this branch in commit 6c6afbf81abe9b97271e2cb88795b64bffd4e56e.