Repository: pyqtgraph/pyqtgraph

Issue:
pyqtgraph.Qt.loadUiType broken for PySide2 >= 5.14
https://dev.azure.com/pyqtgraph/pyqtgraph/_build/results?buildId=557&view=logs&j=1d023af0-a188-58ef-16b4-2e987dd32dd1&t=addebc46-66b7-583c-f559-d6f453020f3e&l=308

There's a note here, though I'm not sure what it means precisely: https://wiki.qt.io/Qt_for_Python_Development_Notes#24._October_2019

Location of the issue for reference: https://github.com/pyqtgraph/pyqtgraph/blob/6e9a48558c010803883f0f83a7170581098b978a/pyqtgraph/Qt.py#L91


Discussion:
I'll take a look later this evening to see what the qtpy library does.
Here is the relevant bit on qtpy's repo:

https://github.com/spyder-ide/qtpy/blob/d53db43e11200ee9324f399504c8a97d2432658c/qtpy/uic.py

I've been wondering if we should consider migrating qtpy as a dependency (and migrate some of the specific Qt.py content to pyqtgraph into the the qtpy library.
After talking w/ @campagnola the two suggested ways to fix this is 

* precompile the exampler loader .ui file for pyside2
* raise a useful error message when attemping to use loadUiType on pyside2 5.14+
Looks like this was "fixed" upstream: https://bugreports.qt.io/browse/PYSIDE-1223

Versions of PySide2 5.14.2.2 and 5.15.0 have restored this functionality and our example app is able to run w/o issue now.  Huge thank you to @cmaureir for getting this feature re-implemented in the PySide2 library!
My pleasure! let me know if we can provide other functions to help pyqtgraph :)
@cmaureir only other upstream issue we've identified with certainty is #1095 reported as QTBUG-80937 (I know this isn't a PySide specific issue) ...We'll be sure to reach out if we identify anything else!

Resolving patch:
diff --git a/examples/PlotWidget.py b/examples/PlotWidget.py
--- a/examples/PlotWidget.py
+++ b/examples/PlotWidget.py
@@ -13,7 +13,7 @@
 import pyqtgraph as pg
 
 #QtGui.QApplication.setGraphicsSystem('raster')
-app = QtGui.QApplication([])
+app = pg.mkQApp()
 mw = QtGui.QMainWindow()
 mw.setWindowTitle('pyqtgraph example: PlotWidget')
 mw.resize(800,800)
diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py
--- a/pyqtgraph/Qt.py
+++ b/pyqtgraph/Qt.py
@@ -10,7 +10,7 @@
 
 """
 
-import os, sys, re, time
+import os, sys, re, time, subprocess, warnings
 
 from .python2_3 import asUnicode
 
@@ -105,24 +105,38 @@ def _loadUiType(uiFile):
     if QT_LIB == "PYSIDE":
         import pysideuic
     else:
-        import pyside2uic as pysideuic
-    import xml.etree.ElementTree as xml
+        try:
+            import pyside2uic as pysideuic
+        except ImportError:
+            # later vserions of pyside2 have dropped pysideuic; use the uic binary instead.
+            pysideuic = None
 
+    # get class names from ui file
+    import xml.etree.ElementTree as xml
     parsed = xml.parse(uiFile)
     widget_class = parsed.find('widget').get('class')
     form_class = parsed.find('class').text
-    
-    with open(uiFile, 'r') as f:
-        o = _StringIO()
-        frame = {}
-
-        pysideuic.compileUi(f, o, indent=0)
-        pyc = compile(o.getvalue(), '<string>', 'exec')
-        exec(pyc, frame)
 
-        #Fetch the base_class and form class based on their type in the xml from designer
-        form_class = frame['Ui_%s'%form_class]
-        base_class = eval('QtGui.%s'%widget_class)
+    # convert ui file to python code
+    if pysideuic is None:
+        pyside2version = tuple(map(int, PySide2.__version__.split(".")))
+        if pyside2version >= (5, 14) and pyside2version < (5, 14, 2, 2):
+            warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15')
+        uipy = subprocess.check_output(['pyside2-uic', uiFile])
+    else:
+        o = _StringIO()
+        with open(uiFile, 'r') as f:
+            pysideuic.compileUi(f, o, indent=0)
+        uipy = o.getvalue()
+
+    # exceute python code
+    pyc = compile(uipy, '<string>', 'exec')
+    frame = {}
+    exec(pyc, frame)
+
+    # fetch the base_class and form class based on their type in the xml from designer
+    form_class = frame['Ui_%s'%form_class]
+    base_class = eval('QtGui.%s'%widget_class)
 
     return form_class, base_class
 


Test patch:
diff --git a/examples/test_examples.py b/examples/test_examples.py
--- a/examples/test_examples.py
+++ b/examples/test_examples.py
@@ -150,7 +150,12 @@ def flaky_print(*args):
     )
 }
 
-@pytest.mark.skipif(Qt.QT_LIB == "PySide2" and "Qt.QtVersion.startswith('5.14')", reason="new PySide2 doesn't have loadUi functionality")
+@pytest.mark.skipif(
+    Qt.QT_LIB == "PySide2"
+    and tuple(map(int, Qt.PySide2.__version__.split("."))) >= (5, 14) 
+    and tuple(map(int, Qt.PySide2.__version__.split("."))) < (5, 14, 2, 2), 
+    reason="new PySide2 doesn't have loadUi functionality"
+)
 @pytest.mark.parametrize(
     "frontend, f",
     [
diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py
--- a/pyqtgraph/tests/test_qt.py
+++ b/pyqtgraph/tests/test_qt.py
@@ -15,7 +15,12 @@ def test_isQObjectAlive():
 
 @pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be '
                                                      'packaged with conda')
-@pytest.mark.skipif(pg.Qt.QT_LIB == "PySide2" and "pg.Qt.QtVersion.startswith('5.14')", reason="new PySide2 doesn't have loadUi functionality")
+@pytest.mark.skipif(
+    pg.Qt.QT_LIB == "PySide2"
+    and tuple(map(int, pg.Qt.PySide2.__version__.split("."))) >= (5, 14) 
+    and tuple(map(int, pg.Qt.PySide2.__version__.split("."))) < (5, 14, 2, 2), 
+    reason="new PySide2 doesn't have loadUi functionality"
+)
 def test_loadUiType():
     path = os.path.dirname(__file__)
     formClass, baseClass = pg.Qt.loadUiType(os.path.join(path, 'uictest.ui'))