microsoft / pylance-release

Documentation and issues for Pylance
Creative Commons Attribution 4.0 International
1.72k stars 765 forks source link

Type is not inferred properly in PyQt5 (Type is Unknown) #4772

Closed pgh268400 closed 4 weeks ago

pgh268400 commented 1 year ago

Environment data

Venv

pip 23.1.2 PyQt5 5.15.9 PyQt5-Qt5 5.15.2 PyQt5-sip 12.12.2 setuptools 65.5.0 ////////////////// pip 23.1.2 PySide6 6.5.2
PySide6-Addons 6.5.2
PySide6-Essentials 6.5.2
setuptools 65.5.0 shiboken6 6.5.2

Code Snippet

# main.py
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
import os
from ui.compiled_ui import Ui_MainWindow

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.setupUi(self)
        self.pushButton  # type is Unknown

app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
# ui / compiled_ui.py
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'ui\main.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(400, 265)
        self.centralWidget = QtWidgets.QWidget(MainWindow)
        self.centralWidget.setObjectName("centralWidget")
        self.pushButton = QtWidgets.QPushButton(self.centralWidget)
        self.pushButton.setGeometry(QtCore.QRect(90, 80, 201, 81))
        self.pushButton.setObjectName("pushButton")
        MainWindow.setCentralWidget(self.centralWidget)
        self.statusBar = QtWidgets.QStatusBar(MainWindow)
        self.statusBar.setObjectName("statusBar")
        MainWindow.setStatusBar(self.statusBar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton.setText(_translate("MainWindow", "PushButton"))

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

PyQt5 example (code in problem situation)

import sys
from PySide6.QtWidgets import QApplication, QMainWindow
import os
from ui.compiled_ui import Ui_MainWindow

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.setupUi(self)
        self.pushButton  # type is inferred(QPushButton)

app = QApplication(sys.argv)

window = MainWindow()
window.show()

sys.exit(app.exec())
# -*- coding: utf-8 -*-

################################################################################
## Form generated from reading UI file 'main.ui'
##
## Created by: Qt User Interface Compiler version 6.5.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
    QMetaObject, QObject, QPoint, QRect,
    QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
    QFont, QFontDatabase, QGradient, QIcon,
    QImage, QKeySequence, QLinearGradient, QPainter,
    QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QMainWindow, QPushButton, QSizePolicy,
    QStatusBar, QWidget)

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        if not MainWindow.objectName():
            MainWindow.setObjectName(u"MainWindow")
        MainWindow.resize(400, 265)
        self.centralWidget = QWidget(MainWindow)
        self.centralWidget.setObjectName(u"centralWidget")
        self.pushButton = QPushButton(self.centralWidget)
        self.pushButton.setObjectName(u"pushButton")
        self.pushButton.setGeometry(QRect(90, 80, 201, 81))
        MainWindow.setCentralWidget(self.centralWidget)
        self.statusBar = QStatusBar(MainWindow)
        self.statusBar.setObjectName(u"statusBar")
        MainWindow.setStatusBar(self.statusBar)

        self.retranslateUi(MainWindow)

        QMetaObject.connectSlotsByName(MainWindow)
    # setupUi

    def retranslateUi(self, MainWindow):
        MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
        self.pushButton.setText(QCoreApplication.translate("MainWindow", u"PushButton", None))
    # retranslateUi

Works fine in PySide6 (weird part)

Expected behavior + Actual behavior

Since Pylance has been updated, the part that inherits the PyQt5 UI does not properly infer the type of the self variable.

super().__init__()
self.setupUi(self)
self.pushButton  # type is Unknown

For example, in the code above self.pushButton should be inferred as QPushButton . (At least it was in previous versions) But since Pylance was updated, oddly PyQt5's UI inheritance variable inference doesn't work. More troubling is that type inference works well in PySide6, which has almost the same implementation as PyQT5.

In the previous pylance version, the uic part of PyQt5 could not be inferred properly, but after the update, the uic part was properly inferred, so I think something has changed in the pylance implementation.

Logs

XXX
bschnurr commented 1 year ago

Sorry I couldn't repo. seems to work

are you sure you are importing Ui_MainWindow correctly?

if you right click go to def on Ui_MainWindow below, where does it take you?

class MainWindow(QMainWindow, Ui_MainWindow):

I had to add from ui.compiled_ui import Ui_MainWindow

pgh268400 commented 1 year ago

Yes you are right. I think I made a mistake while managing several pieces of code. Sorry for the confusion.

I updated the code again.

Ui_MainWindow leads to a class in compiled_ui.py in the ui folder.

image image Ctrl + Click on Ui_MainWindow PYQT5 problem (Unknown)

Could it be a problem with my computer environment that you are unable to reproduce?

image I don't know the nuances of making an analysis successful in PySide.

bschnurr commented 1 year ago

@EricTraut

any ideas on why swapping multiple heritance order is affecting member variables

repo: pip install PyQt5


from PyQt5.QtWidgets import QMainWindow

class Foo():
    pushButton = "button"

class Bar(QMainWindow, Foo):
    def __init__(self) -> None:
        self.pushButton    # type is Unknown

class Bar2(Foo, QMainWindow):
    def __init__(self) -> None:
        self.pushButton    # type is str
erictraut commented 1 year ago

The problem here is that QMainWindow has a class within its class hierarchy with an unknown type. That means its MRO (method resolution order) cannot be properly determined statically.

QMainWindow derives from QWidget which derives from QtGui.QPaintDevice which derives from a class called PyQt5.sipsimplewrapper. This class is not defined anywhere in Python code (in either a ".py" or ".pyi" file). It appears to be implemented in a native library, so pyright has no visibility into its type.

It looks like many Qt classes ultimately derive from PyQt5.sipsimplewrapper, so this is probably wreaking havoc for proper type evaluations. The maintainers of PyQt5 should update the library to define this class, even if it's a dummy class definition like class sipsimplewrapper: ....

pgh268400 commented 1 year ago

I actually checked that there is no static type declaration for PyQt5.sipsimplewrapper. (I think it's loading during runtime to glue C++ and Python together.)

  1. So in conclusion, is it a problem with the pyqt5 library?

  2. By the way, why was the type inference for pyqt5 done properly in the old version of pylance?

At least it wasn't a problem when I used it before.

  1. Also, is there a solution so that type inference can be done properly when using PyQt5 at the user level? (By using multiple inheritance rather than using member variables and combinations.)
erictraut commented 1 year ago
  1. Yes, this is a problem in the PyQt5 library. The maintainers of this library will need to provide a fix the problem if you want this to work with static typing. The good news is that the library already has extensive static typing information, so the maintainers are likely open to fixing this issue. This was probably just an oversight.

If you want to apply a manual fix to your local copy of the library, you can open the PyQt5/__init__.py file and paste the following to the end of the file:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    class sipsimplewrapper: ...
  1. The behavior for handling unknown base classes hasn't changed for years in pyright (the type checker upon which pylance is based), so any change you've seen is probably due to either 1) changes in the library, or 2) changes in how you're using the library (e.g. the order in which you've specified the base classes, as in the example above).

  2. When an Any type is introduced into the class hierarchy, a static type checker is effectively "blinded". It can't know what attributes and methods will be present on the class from that parent class or any class that comes beyond it in the MRO. If you are defining your own class with multiple base classes, you can impact the MRO (method resolution ordering) by changing the order of your base classes. This will affect the MRO linearization and potentially change where the Any entry is.

pgh268400 commented 1 year ago

@erictraut

Wow. I didn't know I could get such a high quality answer.

from typing import TYPE_CHECKING

if TYPE_CHECKING:
class sipsimplewrapper: ...

The problem was solved by manually adding the code you actually provided to the library file. After reviewing the Pyside6 library code, considering that there is no static type issue like this (sipsimplewrapper does not exist in the first place in Pyside6), I think the library issue is correct.

I think all I have to do now is ask the PyQt5 administrator to statically define sipsimplewrapper. (Is it correct?) However, since PyQt5 itself is a GPL license, if I continue to develop it, there may be licensing problems, and since this library is not publicly receiving Pull Requests from Github, I think I should consider migrating PySide6 from LGPL with relatively loose licenses.

And I didn't quite understand what you explained in number 3, does the reason why the type is presumed to be Unknown so far means that all type reasoning is broken because the highest class, sipsimplewrapper, was an undefined Any type?