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

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

Open
pgh268400 opened this issue Aug 29, 2023 · 7 comments
Open

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

pgh268400 opened this issue Aug 29, 2023 · 7 comments
Labels
needs stub waiting for upstream Waiting for upstream to release a fix

Comments

@pgh268400
Copy link

pgh268400 commented Aug 29, 2023

Environment data

  • Language Server version: 2023.8.40
  • OS and version: Windows 10 Version 22H2 64BIT
  • Python version (& distribution if applicable, e.g. Anaconda): Python 3.11.4

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
@github-actions github-actions bot added the needs repro Issue has not been reproduced yet label Aug 29, 2023
@bschnurr
Copy link
Member

bschnurr commented Aug 31, 2023

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

@bschnurr bschnurr added waiting for user response Requires more information from user and removed needs repro Issue has not been reproduced yet labels Aug 31, 2023
@pgh268400
Copy link
Author

pgh268400 commented Aug 31, 2023

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.

@github-actions github-actions bot added user responded Was "waiting for user response" and they responded and removed waiting for user response Requires more information from user labels Aug 31, 2023
@bschnurr
Copy link
Member

@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
Copy link
Contributor

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
Copy link
Author

pgh268400 commented Sep 1, 2023

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
Copy link
Contributor

  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
Copy link
Author

pgh268400 commented Sep 1, 2023

@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?

@bschnurr bschnurr added needs stub waiting for upstream Waiting for upstream to release a fix and removed user responded Was "waiting for user response" and they responded labels Sep 1, 2023
@bschnurr bschnurr removed their assignment Jan 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs stub waiting for upstream Waiting for upstream to release a fix
Projects
None yet
Development

No branches or pull requests

3 participants