Repository: pyqtgraph/pyqtgraph

Issue:
[CI-fail] pyqtgraph/graphicsItems/tests/test_ROI.py::test_PolyLineROI
Could be reproduced locally
```
pyqtgraph/graphicsItems/tests/test_ROI.py::test_PolyLineROI [07:10:13]  Error sending hover exit event:
    |==============================>>
    |  Traceback (most recent call last):
    |    File "/home/travis/mc/envs/test_env/bin/py.test", line 11, in <module>
    |      sys.exit(main())
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/config/__init__.py", line 79, in main
    |      return config.hook.pytest_cmdline_main(config=config)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/hooks.py", line 289, in __call__
    |      return self._hookexec(self, self.get_hookimpls(), kwargs)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/manager.py", line 68, in _hookexec
    |      return self._inner_hookexec(hook, methods, kwargs)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/manager.py", line 62, in <lambda>
    |      firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/callers.py", line 187, in _multicall
    |      res = hook_impl.function(*args)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/main.py", line 242, in pytest_cmdline_main
    |      return wrap_session(config, _main)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/main.py", line 205, in wrap_session
    |      session.exitstatus = doit(config, session) or 0
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/main.py", line 249, in _main
    |      config.hook.pytest_runtestloop(session=session)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/hooks.py", line 289, in __call__
    |      return self._hookexec(self, self.get_hookimpls(), kwargs)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/manager.py", line 68, in _hookexec
    |      return self._inner_hookexec(hook, methods, kwargs)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/manager.py", line 62, in <lambda>
    |      firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/callers.py", line 187, in _multicall
    |      res = hook_impl.function(*args)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/main.py", line 270, in pytest_runtestloop
    |      item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/hooks.py", line 289, in __call__
    |      return self._hookexec(self, self.get_hookimpls(), kwargs)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/manager.py", line 68, in _hookexec
    |      return self._inner_hookexec(hook, methods, kwargs)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/manager.py", line 62, in <lambda>
    |      firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/callers.py", line 187, in _multicall
    |      res = hook_impl.function(*args)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/runner.py", line 77, in pytest_runtest_protocol
    |      runtestprotocol(item, nextitem=nextitem)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/runner.py", line 92, in runtestprotocol
    |      reports.append(call_and_report(item, "call", log))
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/runner.py", line 172, in call_and_report
    |      call = call_runtest_hook(item, when, **kwds)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/runner.py", line 197, in call_runtest_hook
    |      lambda: ihook(item=item, **kwds), when=when, reraise=reraise
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/runner.py", line 225, in from_call
    |      result = func()
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/runner.py", line 197, in <lambda>
    |      lambda: ihook(item=item, **kwds), when=when, reraise=reraise
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/hooks.py", line 289, in __call__
    |      return self._hookexec(self, self.get_hookimpls(), kwargs)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/manager.py", line 68, in _hookexec
    |      return self._inner_hookexec(hook, methods, kwargs)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/manager.py", line 62, in <lambda>
    |      firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/callers.py", line 187, in _multicall
    |      res = hook_impl.function(*args)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/runner.py", line 122, in pytest_runtest_call
    |      item.runtest()
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/python.py", line 1467, in runtest
    |      self.ihook.pytest_pyfunc_call(pyfuncitem=self)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/hooks.py", line 289, in __call__
    |      return self._hookexec(self, self.get_hookimpls(), kwargs)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/manager.py", line 68, in _hookexec
    |      return self._inner_hookexec(hook, methods, kwargs)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/manager.py", line 62, in <lambda>
    |      firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/pluggy/callers.py", line 187, in _multicall
    |      res = hook_impl.function(*args)
    |    File "/home/travis/mc/envs/test_env/lib/python2.7/site-packages/_pytest/python.py", line 179, in pytest_pyfunc_call
    |      testfunction(**testargs)
    |    File "/home/travis/repo-clone/pyqtgraph/graphicsItems/tests/test_ROI.py", line 187, in test_PolyLineROI
    |      mouseMove(plt, center)
    |    File "/home/travis/repo-clone/pyqtgraph/tests/ui_testing.py", line 62, in mouseMove
    |      QtGui.QApplication.sendEvent(widget, event)
    |    File "/home/travis/repo-clone/pyqtgraph/widgets/GraphicsView.py", line 368, in mouseMoveEvent
    |      QtGui.QGraphicsView.mouseMoveEvent(self, ev)
    |    File "/home/travis/repo-clone/pyqtgraph/GraphicsScene/GraphicsScene.py", line 174, in mouseMoveEvent
    |      self.sendHoverEvents(ev)
    |    File "/home/travis/repo-clone/pyqtgraph/GraphicsScene/GraphicsScene.py", line 268, in sendHoverEvents
    |      debug.printExc("Error sending hover exit event:")
    |    --- exception caught here ---
    |    File "/home/travis/repo-clone/pyqtgraph/GraphicsScene/GraphicsScene.py", line 266, in sendHoverEvents
    |      item.hoverEvent(event)
    |    File "/home/travis/repo-clone/pyqtgraph/graphicsItems/ROI.py", line 2189, in hoverEvent
    |      if self.parentItem().translatable:
    |  AttributeError: 'NoneType' object has no attribute 'translatable'
    |==============================<<
```


Discussion:
Suggested fix from https://github.com/pyqtgraph/pyqtgraph/issues/511 is to check if parent is not none.
Checking if `parentItem()` returns `None` will definitely get rid of the error, but I can't figure out why `parentItem()` would return `None`. Construction of the `_PolyLineSegment` in `PolyLineROI` has the parent explicitly passed (which is the `PolyLineROI` object itself, not `None`), which propagates all the way to the base `QGraphicsObject`. I can't help but wonder if this points to a deeper issue. I'm not all that familiar with the ROI parts of the codebase, but I can investigate a bit more later.
Tagging @campagnola in case he has insight.
@ixjlyons I'm wondering if this could be a case of a signal firing off looking for `parentItem` before `_PolyLineSegment` is instantiated.

Either way we would step through this test carefully with a debugger.
Funny enough on linux/pyside1 configuration, this test generates a segmentation fault (this is the build log for PR #889 ).

https://dev.azure.com/pyqtgraph/pyqtgraph/_build/results?buildId=37&view=logs

```
[None] test_ROI/test_PolyLineROI /home/vsts/work/_temp/eae2fa04-119a-48a6-adb0-48cf34598b3a.sh: line 2:  4031 Segmentation fault      (core dumped) pytest --cov pyqtgraph -sv --test-run-title="Tests for Linux - Python 2.7 - Install Method conda- Bindings pyside" --napoleon-docstrings
```

Wonder if we should put some time/effort in addressing this segfault, and if this issue will resolve itself.

I'm stepping through this test with the following pytest command

```
pytest pyqtgraph/graphicsItems/tests/test_ROI.py::test_PolyLineROI -sv --trace
```

The PolyLineROIs are created at the beginning of the test here:

```
    rois = [
        (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=True, pen=0.3), 'closed'),
        (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=False, pen=0.3), 'open')
    ]
```

I `PolyLineROI` without the parent is the `open` one, and it does not have a parent from the time of instantiation.  I'm still investigating.

Slight update, kids are waking up so going to have to put this away for a while.

both `PolyLineROI`s have a parent item after they are added to a viewbox in `vb.addItem(r)`... so something must be removing the parent item between that line and `mouseMove(plt, center)`

I just stumbled across this issue I think @campagnola reported:

https://bugreports.qt.io/browse/PYSIDE-671

Wonder if he was troubleshooting the same issue here.
TL;DR:
 * Found the initial commit of when catching and printing this error message was added, took me way too long
 * It does not give any insight

---

As it took me too much time, I just wanted to share my history research so nobody else has to do it:

As this error message is catched and printed in GraphicsScene.py, I hoped to find more information on what was thought there via the initial commit. That commit is deeply burried and from a time were the repository was the repository of acq4: https://github.com/pyqtgraph/pyqtgraph/commit/1182727ba505fc4a5b40951ec039ce9b58f28719

Long story short: It does not really give much information. I guess that by that time, the situation was similar to now, and a simple fix was to print the error but catching it, with the hope of understanding it in the future.
> 
> 
> I just stumbled across this issue I think @campagnola reported:
> 
> https://bugreports.qt.io/browse/PYSIDE-671
> 
> Wonder if he was troubleshooting the same issue here.

I'm pretty sure, that this and the related issue https://bugreports.qt.io/browse/PYSIDE-441 have to do with this.
> > I just stumbled across this issue I think @campagnola reported:
> > https://bugreports.qt.io/browse/PYSIDE-671
> > Wonder if he was troubleshooting the same issue here.
> 
> I'm pretty sure, that this and the related issue https://bugreports.qt.io/browse/PYSIDE-441 have to do with this.

https://bugreports.qt.io/browse/PYSIDE-441 is marked as resolved for Qt 5.9, but we're still having this issue on 5.12.

Been spending more time on this issue today, the erroneous segment that's sticking around is one of the segments from the original closed PolyLineROI.  The weird thing is it seems to be a duplicate, so not sure why it wasn't remove/cleaned up earlier...

I'm narrowing in on the fault, ~I think the bug is potentially in the `ViewBox.clear()`~ MouseDragHandler class; if you set the breakpoint on line 177 in `test_ROI.py`, on the line `vb.addItem(r)`; during the second iteration of the for loop, we can still see the 3rd segment of the first ROI.

Querying the debugger for `list(vb.scene().hoverItems.keys())[0]` we get `<pyqtgraph.graphicsItems.ROI._PolyLineSegment(0x7fa515b9ed80, pos=0,0, z=11, flags=(ItemSendsGeometryChanges))  at 0x11d4fcc48>`; querying it's `parentItem()` we get `None`.  This is the guy that we get stumped on later.

As this guy is being held in a `weakref`, it should get dropped from there once it's no longer referred to by anything, so something is referencing it.  To find out what, we can use `gc.get_referrers()`

```python
>>> gc.collect()
>>> referrers = gc.get_referrers(list(vb.scene().hoverItems.keys())[0])
>>> len(referrers)
2
```
This is the contents of the first item:

EDIT: Pretty sure this is a `MouseDragEvent`

```
<class 'dict'>: {'enter': False, 'acceptable': True, 'exit': True, '_HoverEvent__clickItems': <WeakValueDictionary at 0x11d681b70>, '_HoverEvent__dragItems': <WeakValueDictionary at 0x120a77908>, 'currentItem': <pyqtgraph.graphicsItems.ROI._PolyLineSegment(0x7fa515b9ed80, pos=0,0, z=11, flags=(ItemSendsGeometryChanges))  at 0x11d4fcc48>, '_scenePos': PySide2.QtCore.QPointF(113.000000, 100.000000), '_screenPos': PySide2.QtCore.QPoint(1467, 66), '_lastScenePos': PySide2.QtCore.QPointF(123.000000, 90.000000), '_lastScreenPos': PySide2.QtCore.QPoint(1467, 66), '_buttons': <PySide2.QtCore.Qt.MouseButtons object at 0x11d680be8>, '_modifiers': <PySide2.QtCore.Qt.KeyboardModifiers object at 0x11d680b88>}
```

This is the contents of the second item:

EDIT: This second guy is a `MouseDragHandler`

```
<class 'dict'>: {'roi': <pyqtgraph.graphicsItems.ROI._PolyLineSegment(0x7fa515b9ed80, pos=0,0, z=11, flags=(ItemSendsGeometryChanges))  at 0x11d4fcc48>, 'dragMode': None, 'startState': None, 'snapModifier': PySide2.QtCore.Qt.KeyboardModifier.ControlModifier, 'translateModifier': PySide2.QtCore.Qt.KeyboardModifier.NoModifier, 'rotateModifier': PySide2.QtCore.Qt.KeyboardModifier.AltModifier, 'scaleModifier': PySide2.QtCore.Qt.KeyboardModifier.ShiftModifier, 'rotateSpeed': 0.5, 'scaleSpeed': 1.01}
```
Any thoughts on what those two items could be and how to clear out references to that ROI from within it?
Btw. I think the creation of the Qt bug you found was not correlated with this: https://github.com/pyqtgraph/pyqtgraph/blob/3d3d0a24590a59097b6906d34b7a43d54305368d/examples/optics/pyoptic.py#L113
On the uploaded screenshot (from the Travis CI log), it looks like it is the side of one of the drag handles of the poly-line. http://data.pyqtgraph.org/data/roi/polylineroi/4378ca4ce902fefe722cb3306b774055ff440b9a/roi_getarrayregion_inverty shows the image before (left) and after (middle) application of transformations and the difference (right). In the right image, there's a stray diagonal line corresponding to the upper left side of the lower left drag-handle.
More investigating into the issue, looks like a `hoverEvent` is holding onto the `MouseDragEvent` which is in turn holding onto the `_PolyLineSegment`; and thus a garbage collector isn't cleaning it up...Need to figure out how to dismiss that hover event when clearing out all the handles (which in turn clears out the segments...but doesn't fully clear this last segment out, it just sets the `parent` to `None` and removes it from the scene).
Thanks for all the effort digging in to this! If I understand your conclusions, it sounds like GraphicsScene is passing HoverEvents to items that no longer exist in the scene (and possibly have even been deleted), and the reason is that GraphicsScene keeps around weak references to items that previously received events, but despite the weak reference, some removed items are persisting in memory due to some other references buried in the scene event system. Possible solutions for this:

1. In ROI....hoverEvent make sure parentItem is not None. This would fix the specific error, but would not address the larger class of errors that could be present here.
2. Attempt to track down the source of the leaked references and ensure that these items disappear from memory so they can't possibly receive events later on. This is very hard--Python's memory management is usually good enough that we don't think hard about reference leaks, which leads to code that is full of them. Tracing is a major pain (but see pg.debug.ObjTracker), and furthermore the leaked references may be in the application code, out of our control.
3. Fix GraphicsScene to ensure that it does not send events to items that are not in the scene. This is easy; just add `if item.scene() is not self:` to GraphicsScene before sending any events, and I think it's actually the _correct_ fix. Probably should be applied for all types of events, not just hover.
Hi,

I'm not sure about the order of operations, but basically the `_PolyLineSegment` is not being garbage collected, and it should be during/after `r.clearPoints()` (on line 218 of `test_ROI.py`).  During that call for `r.clearPoints()` each segment has its parent set to `None`, and it's removed from the scene.  As the weakref `vb.scene().hoverItems` is still having reference to the one segment, that meant something was holding onto it.

The two objects that reference the line segment are a `MouseDragEvent` and `MouseDragHandler`.  looking at the `MouseDragEvent` only references the `_PolyLineSegment` and a `HoverEvent`.  

So I don't think that hover events are being passed to objects that no longer exist, I think hover events are keeping objects from being swept up by the garbage collector.

I hope that made sense.
I can confirm that adding the if statement and `continue` fixes this issue (in `GraphicsScene.py`)

```python
        for item in prevItems:
            event.currentItem = item
            try:
                if item.scene() is not self:
                    continue
                item.hoverEvent(event)
            except:
                debug.printExc("Error sending hover exit event:")
            finally:
                del self.hoverItems[item]
```

Resolving patch:
diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py
--- a/pyqtgraph/GraphicsScene/GraphicsScene.py
+++ b/pyqtgraph/GraphicsScene/GraphicsScene.py
@@ -263,7 +263,8 @@ def sendHoverEvents(self, ev, exitOnly=False):
         for item in prevItems:
             event.currentItem = item
             try:
-                item.hoverEvent(event)
+                if item.scene() is self:
+                    item.hoverEvent(event)
             except:
                 debug.printExc("Error sending hover exit event:")
             finally:
@@ -288,7 +289,7 @@ def sendDragEvent(self, ev, init=False, final=False):
             else:
                 acceptedItem = None
                 
-            if acceptedItem is not None:
+            if acceptedItem is not None and acceptedItem.scene() is self:
                 #print "Drag -> pre-selected item:", acceptedItem
                 self.dragItem = acceptedItem
                 event.currentItem = self.dragItem
@@ -435,6 +436,8 @@ def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder
         for item in items:
             if hoverable and not hasattr(item, 'hoverEvent'):
                 continue
+            if item.scene() is not self:
+                continue
             shape = item.shape() # Note: default shape() returns boundingRect()
             if shape is None:
                 continue


Test patch: