Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

try getting PyPySide to work #2271

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft

Conversation

pijyoi
Copy link
Contributor

@pijyoi pijyoi commented Apr 24, 2022

With regards to #2270, experiment with swapping inheritance order to get GraphicsItem(s) to work on PyPy.

Except for PySide6 (tested on 6.2.4 and 6.3.0), the patches here cause all other CPython bindings to fail or crash.
Note: The combination of Python 3.10 + PySide6 6.3.0 already fails for other reasons.

The various GraphicItem(s) seem to work.
Dragging of ROI Handles don't work.

The existing codebase does assume the method resolution order of inheriting GraphicsItem before QGraphicsWidget / QGraphicsObject.

@pijyoi
Copy link
Contributor Author

pijyoi commented Apr 24, 2022

So the reason for #2132 is also the reason why CPython bindings were crashing once the inheritance order was swapped.

@j9ac9k
Copy link
Member

j9ac9k commented Apr 24, 2022

The ROI Issue seems limited to the Handle class, and not the parent UIGraphicsItem class, as TargetItem (which also inherits from UIGraphicsItem) behaves as intended.

EDIT: another potential clue, in the ROI example, the handle scales with the zoom of the viewbox, which should not happen, it should remain the same size regardless of zoom level.

@pijyoi
Copy link
Contributor Author

pijyoi commented Apr 24, 2022

As far as I can tell, both Handle and _PolyLineSegment are not getting their hoverEvent called on PyPy.
In the extract of ROIExamples.py below, you can neither drag the existing handles nor add new handles.

import pyqtgraph as pg

pg.mkQApp()
pw = pg.PlotWidget()
pw.show()
    
r2a = pg.PolyLineROI([[0,0], [10,10], [10,30], [30,10]], closed=True)
pw.addItem(r2a)
r2b = pg.PolyLineROI([[0,-20], [10,-10], [10,-30]], closed=False)
pw.addItem(r2b)
r2c = pg.LineSegmentROI([[15, -15], [30, -15]])
pw.addItem(r2c)

pw.disableAutoRange('xy')
pw.autoRange()

pg.exec()

@pijyoi
Copy link
Contributor Author

pijyoi commented Apr 24, 2022

By adding a judicious print to GraphicsScene.sendClickEvent(), it appears that the ROI Handle object got decayed to a QGraphicsItem on PyPy, and thus doesn't get any mouse events delivered to it.

CPython PySide 6.3.0

(py38_pyside6) PS C:\work\repos\pyqtgraph> python C:\work\pyqtgraph\simple_roi.py
<PySide6.QtWidgets.QGraphicsRectItem(0x21742530780, parent=0x21742892a00, pos=0,0, z=1000) at 0x0000021743462880> False
<pyqtgraph.graphicsItems.PlotItem.PlotItem.PlotItem(0x217428919f0, pos=0,0, flags=(ItemUsesExtendedStyleOption|ItemSendsGeometryChanges)) at 0x000002174345EC80> False
<pyqtgraph.graphicsItems.ROI.Handle(0x21742aa7160, parent=0x21742a91c50, pos=15,-15, z=11, flags=(ItemSendsGeometryChanges|ItemSendsScenePositionChanges)) at 0x00000217460E7E80> True
<ViewBox(0x217428929f0, parent=0x21742891a00, pos=23.7344,1, z=-100, flags=(ItemIsFocusable|ItemClipsChildrenToShape|ItemUsesExtendedStyleOption|ItemSendsGeometryChanges)) at 0x000002174345EFC0> True

PyPy PySide6 6.3.0

(pypy) C:\work\repos\pyqtgraph>python \work\pyqtgraph\simple_roi.py
<PySide6.QtWidgets.QGraphicsRectItem(0x2c4d8ab3cd0, parent=0x2c4d7517f90, pos=0,0, z=1000) at 0x000002C4D8AB3C90> False
<pyqtgraph.graphicsItems.PlotItem.PlotItem.PlotItem(0x2c4d7473790, pos=0,0, flags=(ItemUsesExtendedStyleOption|ItemSendsGeometryChanges)) at 0x000002C4D8AB3010> False
<PySide6.QtWidgets.QGraphicsItem(0x2c4d7c6a260, parent=0x2c4d7c65ac0, pos=15,-15, z=11, flags=(ItemSendsGeometryChanges|ItemSendsScenePositionChanges)) at 0x000002C4D8CF8F20> False
<ViewBox(0x2c4d7517f80, parent=0x2c4d74737a0, pos=23.7344,1, z=-100, flags=(ItemIsFocusable|ItemClipsChildrenToShape|ItemUsesExtendedStyleOption|ItemSendsGeometryChanges)) at 0x000002C4D8AB3110> True

@j9ac9k
Copy link
Member

j9ac9k commented Apr 24, 2022

I guess that would explain it; probably need to sprinkle in some print(type(self)) statements throughout and see if we can see when the type changes.

@pijyoi
Copy link
Contributor Author

pijyoi commented Apr 25, 2022

Just retrieving the ROI.Handle from the scene will have it be returned as a QGraphicsItem.
A ViewBox.ChildGroup also seems to get the same fate.

from PySide6 import QtWidgets
import pyqtgraph as pg

app = QtWidgets.QApplication([])
scene = QtWidgets.QGraphicsScene(0, 0, 400, 400)
view = QtWidgets.QGraphicsView(scene)

roi = pg.LineSegmentROI([[15, -15], [30, -15]])
scene.addItem(roi)

items = view.scene().items()
for item in items:
    print(item)

@j9ac9k
Copy link
Member

j9ac9k commented Apr 26, 2022

Most confusion ...with a slight modification of your example:

from PySide6 import QtWidgets
import contextlib
import pyqtgraph as pg

app = QtWidgets.QApplication([])
scene = QtWidgets.QGraphicsScene(0, 0, 400, 400)
view = QtWidgets.QGraphicsView(scene)

roi = pg.LineSegmentROI([[15, -15], [30, -15]])
scene.addItem(roi)

items = view.scene().items()
for item in items:
    print(f"{type(item)=}")
print([type(handle['item']) for handle in roi.handles])
print([(handle['item'].scene() is view.scene()) for handle in roi.handles])

This indicates the handles are still of type Handle, not QGraphicsItem. Furthermore, it shows that they are in the same scene, not sure why QGraphicsScene.items() is not showing them there...

@pijyoi
Copy link
Contributor Author

pijyoi commented Apr 26, 2022

If we do a shiboken6.dump(item) on the returned items, on PyPy, we see something like

type(item)=<class 'PySide6.QtWidgets.QGraphicsItem'>
C++ address....... PySide6.QtWidgets.QGraphicsItem/0000016EAA88D530
hasOwnership...... 0
containsCppWrapper 0
validCppObject.... 1
wasCreatedByPython 0

where on CPython, containsCppWrapper and wasCreatedByPython are both 1.

@pijyoi pijyoi force-pushed the tryboot_pypy branch 2 times, most recently from c31e17d to 15b583e Compare May 1, 2022 09:40
@pijyoi
Copy link
Contributor Author

pijyoi commented May 1, 2022

I am only just "discovering" that PyQt but not PySide does the equivalent of a super(), thus automatically calling the __init__ methods of inherited classes coming after any QObjects.
Thus in the snippet below, putting mixin classes after a QObject would have the mixin's __init__ method being called twice on PyQt.
It does happen that a GraphicsItem having its __init__ being called twice has no side-effects.

To avoid M.__init__ being called twice if we really wanted to move the M class before the QObject, we would need to not call M.__init__ manually on PyQt.

from pyqtgraph.Qt import QtCore

class M:
    def __init__(self):
        print("M.__init__")

class C1(M, QtCore.QObject):
    def __init__(self):
        M.__init__(self)
        QtCore.QObject.__init__(self)

class C2(QtCore.QObject, M):
    def __init__(self):
        QtCore.QObject.__init__(self)
        M.__init__(self)

print('C1')
c1 = C1()
print('C2')
c2 = C2()

@j9ac9k
Copy link
Member

j9ac9k commented May 1, 2022

I am only just "discovering" that PyQt but not PySide does the equivalent of a super(), thus automatically calling the __init__ methods of inherited classes coming after any QObjects.
Thus in the snippet below, putting mixin classes after a QObject would have the mixin's __init__ method being called twice on PyQt.

Huh, I had no idea about this. I wonder if this explains some past weird behavior.

It does happen that a GraphicsItem having its __init__ being called twice has no side-effects.

I'm wondering if this should be explicitly stated somewhere, like a comment in the GraphicsItem docstring or something.

@pijyoi
Copy link
Contributor Author

pijyoi commented May 2, 2022

I'm wondering if this should be explicitly stated somewhere, like a comment in the GraphicsItem docstring or something.

The current code in master has GraphicsItem as the first class to be inherited, so its __init__ will only get called once.
It's only in this PR that GraphicsItem is put as the second class; this results in __init__ getting called twice on PyQt.

I think maybe for DockDrop, having its __init__ being called twice (on PyQt) might have a side effect; as it instantiates a DockAreaOverlay. The second call to __init__ replaces the first instance. Note that this is already the case on master.

@pijyoi
Copy link
Contributor Author

pijyoi commented May 5, 2022

There's something wrong on PyPySide6 when running the dockarea.py example.
To reproduce: Drag dock6 to somewhere else other than dock4. Then drag dock6 to dock4.

Traceback (most recent call last):
  File "c:\work\repos\pyqtgraph\pyqtgraph\widgets\GraphicsView.py", line 137, in paintEvent
    return super().paintEvent(ev)
TypeError: 'PySide6.QtWidgets.QGraphicsView.paintEvent' called with wrong argument types:
  PySide6.QtWidgets.QGraphicsView.paintEvent(QPainter)
Supported signatures:
  PySide6.QtWidgets.QGraphicsView.paintEvent(PySide6.QtGui.QPaintEvent)

UPDATE: this error seems to have gone away with PyPySide 6.3.1

@pijyoi
Copy link
Contributor Author

pijyoi commented Jun 17, 2022

TypeError: 'QByteArray' does not support the buffer interface

This affects arrayToQPath with connect="pairs" (ErrorBarItem) or connect="array".
A quick workaround here is to enable the experimental mode of #2324.
Alternatively, we could configure segmentedLineMode="on" (added in #2185).

@j9ac9k
Copy link
Member

j9ac9k commented Jun 18, 2022

Probably should open a bug-report with the pyside folks on this. @stonebig it seem you have a vested interest in pypy support, would you mind creating a bug report on the pyside ticket tracker for the issue @pijyoi detailed here

TypeError: 'QByteArray' does not support the buffer interface

?

@pijyoi
Copy link
Contributor Author

pijyoi commented Jun 18, 2022

The QByteArray is a small issue.

The big issue is the crashing when the QWidget is not listed as the left-most class in multiple inheritance. This breaks the recommended code pattern of placing Mix-In classes on the left-hand-side.

@j9ac9k
Copy link
Member

j9ac9k commented Jun 18, 2022

The QByteArray is a small issue.

The big issue is the crashing when the QWidget is not listed as the left-most class in multiple inheritance. This breaks the recommended code pattern of placing Mix-In classes on the left-hand-side.

I got it in my head that that may have been patched in 6.3.1; probably should file a bug-report for that too; I alerted the pyside maintainer that has been working on this on the gitter chat, but it may have been lost in the noise.

@pijyoi pijyoi changed the title try getting GraphicsItem(s) to work on PyPy try getting PyPySide to work Jun 19, 2022
@pijyoi pijyoi force-pushed the tryboot_pypy branch 7 times, most recently from d57384b to 2bfb519 Compare June 24, 2022 13:39
@pijyoi
Copy link
Contributor Author

pijyoi commented Jun 26, 2022

A minimal Parameter Tree test-case that passes on CPython but fails on PyPy.
This is actually caught in the existing tests (test_parametertypes.py::test_types), but the tests were modified in this PR to bypass it for PyPy.

The issue can be described as follows:
For the "str" Parameter type, setting a brush or a pen will end up with an empty string when the value is retrieved.
Setting a color works though.
In addition, the issue only occurs when the "str" Parameter is part of a tree, not when it is standalone.

The baffling thing is that all objects are converted to str type when it gets set to the Parameter anyway, so it shouldn't have mattered at all what the original object was.
Tagging @ntjess.

Of course, it probably doesn't make practical sense to be setting a brush or a pen to a "str" Parameter to begin with.

import pyqtgraph as pg
import pyqtgraph.parametertree as pt

app = pg.mkQApp()

def check(param):
    objs = [pg.mkColor('k'), pg.mkBrush('k'), pg.mkPen('k')]
    results = []
    for obj in objs:
        param.setValue(obj)
        results.append(str(obj) == param.value())
    return results

def test_param():
    param = pt.Parameter.create(name='params', type='str')
    results = check(param)
    assert all(results), results

def test_tree():
    root = pt.Parameter.create(name='params', type='group', children=[
        dict(name='str', type='str')
    ])
    tree = pt.ParameterTree()
    tree.setParameters(root)

    param = root.child('str')
    results = check(param)
    assert all(results), results

@pijyoi pijyoi force-pushed the tryboot_pypy branch 2 times, most recently from d691411 to 9b43579 Compare July 12, 2022 11:24
@pijyoi
Copy link
Contributor Author

pijyoi commented Jul 12, 2022

I have a more minimal test-case to demonstrate the ROI-Handle decay issue in tests_pypy/test_roi_handle_decay.py.
In essence, it boils down to: "a GraphicsObject that has a parent gets decayed to QGraphicsItem".
In the pyqtgraph library, it appears that the only GraphicsObject that has a parent is a ROI.Handle.

@pijyoi
Copy link
Contributor Author

pijyoi commented Sep 13, 2022

Triggered a CI-run with PyPySide 6.3.2. No change in failing test cases.
Note: one of the failing test-cases is related to https://bugreports.qt.io/browse/PYSIDE-1991

@j9ac9k
Copy link
Member

j9ac9k commented Sep 17, 2022

I profiled this branch with 1000 iterations of PlotSpeedTest.py to try and better see where the discrepancy in performance is.

Results are a bit puzzling, the call tree in CPython looks like what I would expect, but in PyPy it's a decent amount different. Also I'm not seeing entries for methods in the PyPy table that correspond to arrayToQPath and such

EDIT: tried running the pypy version w/ vmprof, but that resulted in a segfault so ....

EDIT2: In PyPy, looks like boundingRect is one of the sources of a major penalty as is PlotCurveItem.updateData

CPython

cpython-plotspeedtest

Top 10 entries sorted by "own time"

image

PyPy

pypy-plotspeedtest

Top 10 entries sorted by "own time"

image

@pijyoi
Copy link
Contributor Author

pijyoi commented Oct 14, 2022

Triggered a CI run with PyPySide 6.4.0 using the PyPy 3.9 wheels available on pypi.org.
Numpy wheels on pypi.org are for PyPy 3.8, so this causes a build from source of numpy.

No change in failing test cases.

@pijyoi
Copy link
Contributor Author

pijyoi commented Jan 9, 2023

The pypi.org provided wheels of PyPySide6 (6.4.1, 6.4.2) segfault immediately upon

from PySide6 import QtCore

when used with the latest versions of PyPy 3.9 (7.3.10, 7.3.11).
Last working version was 7.3.9.
Tested combinations:

  • Win / {6.4.1, 6.4.2} / {7.3.10, 7.3.11}
  • Linux / 6.4.1 / 7.3.10

Closing this PR as there's nothing that pyqtgraph can do here.

@pijyoi pijyoi closed this Jan 9, 2023
@pijyoi
Copy link
Contributor Author

pijyoi commented Mar 24, 2023

A fix for the crash has been merged (https://bugreports.qt.io/browse/PYSIDE-2264) but will only arrive in PySide6 6.5

pijyoi added 12 commits May 20, 2023 10:52
there are 2 methods of Container that are meant to take precedence over
QWidget.
- childEvent()
  - this was renamed to childEvent_() in
    6dbda78
    to workaround a bug in early versions of PyQt6.
- close()
  - this is renamed to close_() in this commit
ROI handles currently don't work
- mixin lhs inheritance crashes on PyPy
- after changing the mixin to rhs, we have the following issues:
  - GLViewMixin.__init__ calls a QOpenGLWidget method. This fails
    on PyPySide 6.5.0's implementation of cooperative inheritance.
    It complains that QOpenGLWidget has not been initialized yet.
  - GLViewMixin overrides some of QOpenGLWidget's methods, and this
    doesn't work if the mixin is on the rhs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants