enthought / traitsui

TraitsUI: Traits-capable windowing framework
http://docs.enthought.com/traitsui
Other
297 stars 95 forks source link

AttributeError in list editor in Qt #403

Open jonathanrocher opened 6 years ago

jonathanrocher commented 6 years ago

On master, pulled this morning, I just got this traceback when I changed the content of an entry in a list:

Traceback (most recent call last):
  File "/Users/jrocher/.edm/envs/dev_py27/lib/python2.7/site-packages/traits/trait_notifiers.py", line 519, in _dispatch_change_event
    self.dispatch( handler, *args )
  File "/Users/jrocher/.edm/envs/dev_py27/lib/python2.7/site-packages/traits/trait_notifiers.py", line 614, in dispatch
    handler( *args )
  File "/Users/jrocher/Projects/ETS_source/traitsui/traitsui/qt4/list_editor.py", line 251, in update_editor_item
    proxy = control.proxy
AttributeError: 'QPushButton' object has no attribute 'proxy'

I will make a standalone script to show the problem, but thought I would report quickly since this editor was just worked on...

Seen on PyQt 4.11.4-7, Master TraitsUI, and master Pyface, Python 2.7. cc @corranwebster since you were working on the ListEditor this past day.

corranwebster commented 6 years ago

Was this changed via user editing of a value or by a change to the underlying list item value from some other source?

kitchoi commented 4 years ago

We saw this recently with Python 3.6, traitsui 6.1.3, PyQt 5.7.1, both Mac OSX and Windows. The following is a traceback from Mac OSX

Traceback (most recent call last):
  File "/Users/kchoi/.edm/envs/envname/lib/python3.6/site-packages/traits/trait_notifiers.py", line 591, in _dispatch_change_event
    self.dispatch(handler, *args)
  File "/Users/kchoi/.edm/envs/envname/lib/python3.6/site-packages/traits/trait_notifiers.py", line 695, in dispatch
    handler(*args)
  File "/Users/kchoi/.edm/envs/envname/lib/python3.6/site-packages/traitsui/qt4/list_editor.py", line 252, in update_editor_item
    proxy = control.proxy
AttributeError: 'QObject' object has no attribute 'proxy'
kitchoi commented 4 years ago

Here is the smallest example for reproducing the issue:


class Child(HasTraits):
    play_list = List(Float(), [1, 2, 3, 4])

class Model(HasTraits):
    child = Instance(Child, ())

class ListEditorDemo(ModelView):

    # The Trait to be displayed in the editor
    model = Instance(Model)

    def default_traits_view(self):
        return View(
            Item('model.child.play_list', label='Simple'),
            title='ListEditor',
            height=400,
            width=400,
        )

popup = ListEditorDemo(model=Model())

if __name__ == '__main__':
    popup.configure_traits()

It is important that the list is nested as model.child.play_list rather than just model.play_list.

kitchoi commented 4 years ago

Was this changed via user editing of a value or by a change to the underlying list item value from some other source?

Not sure how it happened for @jonathanrocher. In our case, it happened when the user edits a value in the view.

jonathanrocher commented 4 years ago

It's likely to have been the same for me too, though after 2 years, I can't be sure.

corranwebster commented 4 years ago

OK - I have had a quick look into this, and it works fine on PySide and fails on PyQt. ~I also get the error with model.play_list.~

~I think that the issue is around differences in the object model for PyQt/sip and PySide/shiboken. We are tucking information away on the PyQt objects (the proxy attribute) and then dropping the Python reference to the object. Then, when making a change, we go and find the object again, effectively reconstituting it from the C++ level object. It looks like PyQt forgets the attribute, while PySide remembers. It presumably did work at some point 12 years ago.~

Edit: I am wrong we are getting some new QObject instances from somewhere (one per item, I think) under PyQt and these do not have proxies. I can't see where they are coming from, but in any case the solution below is still the right one.

The solution, I think, is ~that to support PyQt we need to hold on to references to the controls that we are attaching proxy information to and look those up, rather than trying to find them again after the fact; or we might just be able to get away with~ keeping track of the proxies and not worry about the actual controls.

A work around is to create simple wrappers in the list, ie. List(Instance(IntClass)) where IntClass is something like:

class IntClass(HasStrictTraits):
    value = Int
corranwebster commented 4 years ago

I think this is because of a bug in Traits:

from traits.api import HasTraits, List, Instance, Int

class Child(HasTraits):
    values = List(Int)

class Parent(HasTraits):
    child = Instance(Child)
    values = List(Int)

p = Parent(
    child=Child(values=[1, 2, 3]),
    values=[4, 5, 6],
)

def printer(object, name, old, new):
    print(object, name, old, new)

p.on_trait_change(printer, 'values')
print("Change list items, listener doesn't fire, as expected")
p.values[1] = 100

p.on_trait_change(printer, 'child.values')
print("Change list items on an instance attr, listener fires child.values_items!")
p.child.values[1] = 100