본문으로 바로가기

파일의 IT 블로그

  1. Home
  2. 아카이브/자주쓰는 코드
  3. [Template] PyQT5 UI 컴파일 불러오는 소스 (IDE 자동 완성 가능) - PyQT5 라이브러리 버그 수정 코드 포함

[Template] PyQT5 UI 컴파일 불러오는 소스 (IDE 자동 완성 가능) - PyQT5 라이브러리 버그 수정 코드 포함

· 댓글개 · KRFile

기능

- PyQT5로 UI를 불러와서 실행하는 기본 코드

- VSCode에서 자동 완성 가능

  - 타입 추론을 위해 Pylance 라는 확장프로그램을 반드시 설치해야 합니다. 이 확장이 타입을 추론해서 파이썬 코드 자동완성이 되게끔 도와줍니다.

  - UI에 변화가 생기면 자동으로 인식해 Python코드로 컴파일 함

 

 

 

방법 1 - 다중 상속 이용 (권장하는 방법)

"""
module/ui_loader.py
    UI 파일을 PY로 자동 변환한후 읽어온다
    PY로 변환작업을 거치지 않으면 IDE의 자동 완성 기능이 활성화 되지 않는다
    EX) uic.loadUiType(your_ui)[0] => 자동 완성이 제대로 작동하지 않음!! - 런타임에 불러오기 때문에 정적 분석 불가
    출처 : https://stackoverflow.com/questions/58770646/autocomplete-from-ui
"""

from distutils.dep_util import newer
import os
from PyQt5 import uic


def compile_ui_to_py(ui_dir: str, ui_to_py_dir: str) -> None:
    encoding = 'utf-8'

    # UI 파일이 존재하지 않으면 아무 작업도 수행하지 않는다.
    if not os.path.isfile(ui_dir):
        print("The required file does not exist.")
        return

    # UI 파일이 업데이트 됬는지 확인하고, 업데이트 되었으면 *.py로 변환한다
    if not newer(ui_dir, ui_to_py_dir):
        # print("UI has not changed!")
        pass
    else:
        print("UI changed detected, compiling...")
        # ui 파일이 업데이트 되었다, py파일을 연다.
        fp = open(ui_to_py_dir, "w", encoding=encoding)
        # ui 파일을 py파일로 컴파일한다.
        uic.compileUi(ui_dir, fp,
                      execute=True, indent=4, from_imports=True)
        fp.close()

module 폴더 생성 -> ui_compiler.py 에 작성

 

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>316</width>
    <height>153</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <widget class="QPushButton" name="pushButton">
    <property name="geometry">
     <rect>
      <x>60</x>
      <y>40</y>
      <width>201</width>
      <height>71</height>
     </rect>
    </property>
    <property name="text">
     <string>PushButton</string>
    </property>
   </widget>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

ui 폴더 생성 -> main.ui 에 작성 (혹시나 몰라서 얘기하는건데 UI 파일에 주석달지마세요. 컴파일할때 오류납니다)

 

from module.ui_compiler import compile_ui_to_py
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
import os


# UI -> PY 컴파일
# fmt: off
compile_ui_to_py(os.path.join('ui', 'main.ui'),
                 os.path.join('ui', 'compiled_ui.py'))
from ui.compiled_ui import Ui_MainWindow
# fmt: on


class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self) -> None:
        # 기본 설정 코드
        super().__init__()
        self.setupUi(self)

        # self 변수로 ui요소에 접근 가능
        self.pushButton.setText("Hello World")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

처음부터 윈도우를 만들 때 QMainWindow 와 Ui_MainWindow를 다중 상속 받아서 사용하는 방식입니다.

self 변수로 ui 요소 접근시 타입이 Unknown 으로 추정되는 오류가 있었습니다. (위 코드에서 self.pushButton 의 타입이 QPushButton 이 아닌 Unknown 타입으로 추정되었습니다.) 처음에는 VSCode의 확장프로그램인 Pylance의 타입 추정 버그라고 생각했지만 결론적으로 PyQt5의 버그로 판명났습니다.

 

# 타입 체킹 버그해결을 위해 코드 추가
# 라이브러리 관리자가 제대로 정의해줘야 할듯.
# 관련 이슈 : https://github.com/microsoft/pylance-release/issues/4772

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    class sipsimplewrapper: ...

따라서 위 코드를 제대로 사용하시려면 추가로 파이썬 폴더\Lib\site-packages 폴더로 이동해 PyQt5 폴더를 찾고 최상단의 __init__.py 파일에 위 코드 3줄을 추가해줘야 합니다.

 

 

# Copyright (c) 2023 Riverbank Computing Limited <info@riverbankcomputing.com>
# 
# This file is part of PyQt5.
# 
# This file may be used under the terms of the GNU General Public License
# version 3.0 as published by the Free Software Foundation and appearing in
# the file LICENSE included in the packaging of this file.  Please review the
# following information to ensure the GNU General Public License version 3.0
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
# 
# If you do not wish to use this file under the terms of the GPL version 3.0
# then you may purchase a commercial license.  For more information contact
# info@riverbankcomputing.com.
# 
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.




# Support PyQt5 sub-packages that have been created by setuptools.
__path__ = __import__('pkgutil').extend_path(__path__, __name__)

# 타입 체킹 버그해결을 위해 코드 추가
# 라이브러리 관리자가 제대로 정의해줘야 할듯.
# 관련 이슈 : https://github.com/microsoft/pylance-release/issues/4772

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    class sipsimplewrapper: ...

def find_qt():
    import os, sys

    qtcore_dll = '\\Qt5Core.dll'

    dll_dir = os.path.dirname(sys.executable)
    if not os.path.isfile(dll_dir + qtcore_dll):
        path = os.environ['PATH']

        dll_dir = os.path.dirname(__file__) + '\\Qt5\\bin'
        if os.path.isfile(dll_dir + qtcore_dll):
            path = dll_dir + ';' + path
            os.environ['PATH'] = path
        else:
            for dll_dir in path.split(';'):
                if os.path.isfile(dll_dir + qtcore_dll):
                    break
            else:
                return

    try:
        os.add_dll_directory(dll_dir)
    except AttributeError:
        pass


find_qt()
del find_qt

__init__.py 폴더에 내용 추가시 대략 위와 같은 모습이 됩니다. 위 3줄을 꼭 추가해주셔야 self.pushButton 과 같이 사용할 때 self 의 UI 변수들의 타입이 제대로 추론됩니다. 그래야 IDE 자동 완성도 작동하니깐 꼭 추가해주세요. PyQt5 라이브러리 관리자한테 메일을 보내놔야 할 거 같습니다.

 

이 이슈를 해결하는 삽질 기록은 여기 바로 아래에 적을태니 관심 있으신 분들은 보세요 :)

PyQT5_Template_Multiple_Inheritance.zip
0.00MB

소스코드 다운로드는 위에서 하시면 됩니다.

다운로드 하시고 Lib\site-packages\PyQt5 가셔서 __init__.py에 위에 제공한 3줄짜리 코드 추가하면 됩니다.

 

Pylance의 버그? - PyQt5 타입 힌트 삽질기

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

# UI -> PY Compile
# fmt: off
compile_ui_to_py(os.path.join('ui', 'main.ui'),
                 os.path.join('ui', 'compiled_ui.py'))
from ui.compiled_ui import Ui_MainWindow
# fmt: on


class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.setupUi(self)
        # self 변수로 ui요소에 접근 가능
        self.pushButton.setText("Hello World")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

해당 코드는 PyQt5 에서 UI 파일을 사용하기 위해 골자로 사용되는 기본 템플릿 코드입니다. 인터넷에 제공된 코드와 제가 작성한 코드가 추가로 작성되어 있습니다. 항상 프로그램 의뢰가 들어오면 파이썬으로 GUI 작업 시 항상 해당 코드를 먼저 불러와서 작업을 시작합니다.

 

해당 코드의 원리를 잠깐 간단하게 살펴봅시다.

 

# UI -> PY Compile
# fmt: off
compile_ui_to_py(os.path.join('ui', 'main.ui'),
                 os.path.join('ui', 'compiled_ui.py'))
# fmt: on

일단 해당 스크립트는 처음에 compile_ui_to_py 라는 module 폴더의 ui_compiler.py 안에 있는 함수를 호출합니다.

이 함수는 qtdesigner 에서 작성한 ui 파일을 파이썬이 이해할 수 있는 py 파일로 변환해주는 함수입니다.

 

기본적으로 파이썬에서 Qt 라는 것을 이용해 디자인 작업 시 위 QtDesigner 라는 프로그램을 통해 먼저 디자인 작업을 하며, 여기서 작업해서 나온 UI 파일을 파이썬으로 가져와서 사용하는 형식이기 때문에 위와 같은 변환 과정은 필수적입니다. 

 

# UI -> PY Compile
# fmt: off
compile_ui_to_py(os.path.join('ui', 'main.ui'),
                 os.path.join('ui', 'compiled_ui.py'))
# fmt: on

 

어쨌던 위 함수가 호출되면 ui 폴더의 main.ui 가 파이썬이 이해할 수 있는 compiled_ui.py 가 되고 

 

from ui.compiled_ui import Ui_MainWindow

compiled_ui.py 안에 작성된 Ui_MainWindow 클래스를 import 하게 됩니다.

 

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.setupUi(self)
        # self 변수로 ui요소에 접근 가능
        self.pushButton.setText("Hello World")

이후 화면을 띄우기 위해 기본적으로 Qt 프레임워크에서 사용되는 QMainWindow를 우선 상속하고 그 다음에 UI 데이터를 담고 있는 방금전에 import한 클래스인 Ui_MainWindow 를 두번째로 상속하게 되어서 우리의 MainWindow Class 가 완성되게 됩니다.

 

super().__init__()
self.setupUi(self)

이 MainWindow에서 기본 Setup 을 마치고 (위 코드 역시 프레임워크 사이트에서 제공하는 기본 골자격인 코드입니다. 이해보다는 이렇게 쓰면 화면이 나오나봅다~ 정도로 이해하는게 편합니다)

 

self.pushButton.setText("Hello World")

이후로 self.UI 변수명을 이용해 UI 를 조작하면 됩니다.

하지만 문제는 VSCode가 self.pushButton 을 Unknown 타입으로 제대로 분석하지 못한다는 것이었습니다.

정상적으로 분석된다면 self.pushButton 은 UI 요소에 있던 것으로 다음과 같이 QPushButton 으로 분석되며

다음과 같은 메서드(함수) 자동 완성도 지원해야 합니다.

 

그런데 VSCode는 이를 해내지 못했고 Unknown 타입 (알 수 없는 타입) 으로 지정된다는 것은 곧 QPushButton 에서 지원하는 다양한 함수들의 자동 완성이 지원되지 않는다는 뜻이었습니다.

 

self.setupUi(self)
class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(316, 153)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(60, 40, 201, 71))
        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"))

사실 Ui_MainWindow를 다중 상속하고, self.setupUi(self) 를 호출한 순간부터 자기 스스로가 self 로 넘어감으로써 self.pushButton 은 QPushbutton 으로 분석되어야 하는게 정상입니다.

 

분명 원래는 잘 작동했는데..? 

 

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())

Pylance, PyQt5 모두 업데이트 되었고 PyQt5와 구현이 거의 동일한 라이브러리인 PySide6 에서 확인해본 결과 문제가 없었습니다. PyQt5가 문제인지 Pylance가 문제인지 알 수가 없는 상황이였고.. 저는 이전에 PyQt5 의 타입 추론이 제대로 작동하던 기억을 증거로 VSCode Pylance에 버그가 있어서 타입 추론이 실패했다 라고 단정을 짓게 되었습니다.

 

따라서 관련 Issue(문제) 를 Pylance Github 사이트에 재보하게 되었습니다.

 

https://github.com/microsoft/pylance-release/issues/4772

 

Type is not inferred properly in PyQt5 (Type is Unknown) · Issue #4772 · microsoft/pylance-release

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...

github.com

 

여기서 답변을 들어본 결과 제 생각이 틀렸다는걸 깨닫게 되었습니다.

 

 

 

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: ....
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: ...
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).

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.

 

마이크로소프트의 전 기술 연구원이라는 Eric Traut 라는 분이 답변을 해주셨는데 원문은 위와 같습니다.

음.. 솔직히 읽기 싫죠? 영어를 읽는데 울렁거림이 있으신 분들이 있으니 파파고로 1차 기계번역을 하고 제가 좀 검수를 해보겠습니다.

 

 

 

여기서 문제는 QMainWindow의 클래스 계층 내에 알 수 없는 유형의 클래스가 있다는 것입니다. 이는 해당 MRO(Method Resolution ORder)를 정적으로 제대로 결정할 수 없음을 의미합니다.

QMainWindow는 QWidget으로부터 파생되었으며, QWidget은 다시 QtGui.QPaintDevice에서 파생됩니다. 또 QtGui.QPaintDevice는 PyQt5.sipsimplewrapper 라는 Class에서 파생됩니다.

이 클래스는 파이썬 코드 어디에도 정의되지 않았습니다(".py" 또는 ".py" 파일에 존재하지 않음). 이는 네이티브 라이브러리에서 구현된 것으로 보이며 Pyright 는 이 유형을 알아낼 수 없습니다. (가시성이 없음)

많은 Qt 클래스가 궁극적으로 PyQt5.sipsimplewrapper 에서 파생되기 때문에 적절한 유형 평가를 위해선 이는 큰 혼란을 초래할 수 있습니다. PyQt5의 관리자는 "class sipsimplewrapper: ...." 와 같은 더미 클래스(깡통 클래스) 일지라도 정의를 해서, 라이브러리를 업데이트 해야 합니다.


*Pyright = Pylance 에 포함된 정적 유형 검사기 (타입 체킹기)

 

1. Pylance가 아니라 PyQt5 라이브러리의 문제인가?
- 예, 이것은 PyQt5 라이브러리의 문제입니다. 이 라이브러리의 관리자는 정적 타입이 제대로 작동하려면 문제를 수정해야 합니다. 좋은 소식은 라이브러리에 이미 광범위 한 정적 타입 정보가 있으므로, 라이브러리 관리자가 이 문제를 수정할 수 있습니다. 이것은 아마도 실수로 보입니다. 라이브러리 로컬 복사본에 수동으로 수정을 적용하자면 PyQt5/__init__.py 파일을 열고 파일 끝에 다음을 붙여넣기 합니다.

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    class sipsimplewrapper: ...

2. 왜 예전 Pylance 랑 PyQt 구버전에선 타입 추론이 작동한건가요?
- 알 수 없는 기본 클래스를 처리하는 동작은 Pyright(pylance의 기반이 되는 유형 검사기)에서 수년 동안 변경되지 않았기 때문에, 변경 사항은 1) 라이브러리의 변경 사항 or 2) 라이브러리를 사용하는 방식(기본 클래스를 상속 받는 순서 ex) QMainWindow와 Ui_MainWindow 의 상속 순서를 이야기 함.)의 변경으로 인한 것일 수 있습니다.

3. 지금 문제에서 유저 레벨의 해결책이 있나요?
- Any 유형이 클래스 계층에 도입되면 정적 유형 검사기가 효과적으로 "블라인드"됩니다. MRO에서 해당 부모 클래스 또는 그 너머에 있는 클래스의 클래스에 어떤 속성과 방법이 있는지 알 수 없습니다. 여러 기본 클래스로 자신의 클래스를 정의하는 경우 기본 클래스의 순서를 변경하여 MRO(방법 해상도 순서 지정)에 영향을 미칠 수 있습니다. 이는 MRO 선형화에 영향을 미치고 Any 항목이 있는 위치를 변경할 가능성이 있습니다.

 

 

Eric Traut 의 답변을 다시 정리해보자면 아래와 같습니다.

 

1. 해당 문제는 Pylance가 아닌 PyQt5 라이브러리의 문제다

- Pylance 에서 타입 추론시 사용되는 Pyright라는 도구의 동작 방식이 수년 동안 변경되지 않음

- 문제는 QMainWindow의 최상위 클래스 계층에서 sipsimplewrapper 라는 알 수 없는 Class가 있기 때문임

2. sipsimplewrapper 라는 제대로 정의 되지 않은 부모 클래스 때문에 타입 추론이 망가졌으며, MRO도 제대로 결정할 수 없음.

- 아마도 제 생각엔 sipsimplewrapper 라는 Class는 C++의 Qt와 결합시키기 위해 런타임중에 로딩하는게 아닌가 싶습니다.

 

마지막에 답변한 3번의 내용은 솔직히 무슨뜻인지 제대로 이해하지 못했습니다. 3번 내용은 아마 최상위 부모인 sipsimplewrapper 가 제대로 정의되지 않아서 파이썬의 Class 구조를 Pylance가 제대로 파싱하지 못해서 다른 타입 추론이 망가질 것이며, 이를 "Blind" 됐다고 표현하는 거 같은데 다시 질문을 해도 이제 Eric Traut 형님이 답변을 안하시니 원.. 😅

 

https://tibetsandfox.tistory.com/26

 

파이썬(python) - MRO(Method Resolution Order)

MRO(Method Resolution Order)란? MRO는 파이썬의 상속과 관련있는 개념입니다. 직역하자면 '메소드 결정 순서'정도가 되겠네요. 파이썬은 기본적으로 다중 상속을 지원합니다. 상속받은 부모 클래스가

tibetsandfox.tistory.com

https://twil.weekwith.me/4%EA%B8%B0/%EC%9D%B4%ED%83%9C%ED%98%84/2022-01-09-python-method-resolution-order/

 

파이썬의 MRO(Method Resolution Order) - TWIL

파이썬의 MRO(Method Resolution Order) 도입 MRO(Method Resolution Order)는 단어 의미 그대로 메서드의 결정 순서를 의미하며 이는 곧 상속과 연관되는 개념입니다. 파이썬은 기본적으로 다중 상속을 지원합

twil.weekwith.me

MRO라는 부분도 공부해보니 파이썬에서 Class 다중 상속시 상속받은 부모와 자신을 모두 포함해 어떤 메서드를 먼저 실행할지 실행 순서를 결정하는 방식을 이야기하는 거 같습니다. (Python, C++은 다중 상속을 지원함)

 

일단은 라이브러리 문제고 QMainWindow 최상위 부모의 sipsimplewrapper 라는 것이 올바르게 정의되지 않아서 그렇다고 합니다.

이 정도면 필요한 내용은 어느정도 알아낸듯 싶습니다.

 

실제로 sipsimplewrapper 라는게 PyQt5 라이브러리에 존재하는지 확인해보겠습니다.

 

아까 소스코드로 이동해서 QMainWindow 에 Ctrl + Click 을 해 QMainWindow 의 정의부분으로 이동합니다.

 

 

Eric 형님의 말대로 실제로 QMainWindow가 QWidget 이라는 Class를 상속합니다.

 

QWidget은 다시 QObject 와 QPaintDevice 라는 Class를 다중 상속합니다. 

대충 MRO에 대해 공부해본 바에 따르면 QObject의 메서드가 QPaintDevice 보다 우선 순위를 가지겠네요.

 

 

QObject 에서 부모를 먼저 따라가보겠습니다. 확인해보니 wrapper 라는 Class를 상속하네요

 

wrapper는 다시 simplewrapper 를 상속하고 simplewrapper는 빈 생성자를 가지는 결국 깡통인 Class 입니다. 솔직히 이건 왜 있는지 모르겠네요. 제가 Qt 구조를 자세히 분석해본 경험도 없고 PyQt 로 Qt 인터페이스를 어떻게 구현했는진 당연히 모릅니다.

 

일단 그래도 확인할 수 있었던 점은 QObject의 경우엔 일단 타입이 제대로 명시가 되어있어 문제가 없었다는 점입니다.

그런데 문제는 QPaintDevice 부분입니다.

 

QPaintDevice는 sipsimplewrapper 라는 것을 상속합니다.

 

그런데 PyQt5 라이브러리 어디에서도 이 sipsimplewrapper 의 정체에 대해 코드가 존재하지 않습니다. 보시면 Unknown 타입으로 묘사되죠? 이게 바로 문제였던거라고 하네요.

 

일단 sipsimplewrapper 가 뭔지는 PyQt5 라이브러리 개발자만 알겁니다. 일단은 위에서 wrapper 클래스 들이 빈 깡통인 것으로 보아 아마 sipsimplewrapper 역시 별거 없는 깡통 Class로 추정됩니다.

 

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    class sipsimplewrapper: ...

그래서 급한대로 PyQt5 라이브러리 __init__.py 부분에 sipsimplewrapper class의 정의를 추가해놔서 타입 추론이 다시 제대로 동작하도록 수정해주면 됩니다.

 

그러면 드디어 다시 Pylance의 타입 추론이 돌아와서 pushButton을 QPushButton으로 다시 추론하게 됩니다.

일단 정말 힘들었는데.. 일단 PyQt5 라이브러리 버그니깐 이 라이브러리를 관리하고 있는 RiverBank Software? 에 메일을 보내서 라이브러리 수정 요청을 보내놔야 할 거 같습니다.

 

아니면 PySide6 에선 이런 문제가 없으니 GPL 라이선스를 가져서 라이선스 문제가 발생할 수 있는 PyQt 시리즈를 쓰는것보단 GPL보다 느슨한 라이선스를 지닌 LGPL인 PySide 시리즈로 갈아타는걸 권장합니다.

 

PySide6 에서는 sipsimplewrapper 같은 알 수 없는 클래스가 없고 전부 제대로 타입 정의가 되어 있습니다.

 

 

하지만 PySide6 역시 최상위 클래스는 Object라는, 껍데기만 존재하는 Class 인걸로 봐서는.. 결국 PyQt5 에 존재하는 최상단 클래스 sipsimplewrapper도 깡통 클래스 라는 추론이 힘이 실어지네요  ^^;

 

 

방식 2 - 다중 상속 없이 조합을 이용한 사용

"""
module/ui_loader.py
    UI 파일을 PY로 자동 변환한후 읽어온다
    PY로 변환작업을 거치지 않으면 IDE의 자동 완성 기능이 활성화 되지 않는다
    EX) uic.loadUiType(your_ui)[0] => 자동 완성이 제대로 작동하지 않음!! - 런타임에 불러오기 때문에 정적 분석 불가
    출처 : https://stackoverflow.com/questions/58770646/autocomplete-from-ui
"""

from distutils.dep_util import newer
import os
from PyQt5 import uic


def compile_ui_to_py(ui_dir: str, ui_to_py_dir: str) -> None:
    encoding = 'utf-8'

    # UI 파일이 존재하지 않으면 아무 작업도 수행하지 않는다.
    if not os.path.isfile(ui_dir):
        print("The required file does not exist.")
        return

    # UI 파일이 업데이트 됬는지 확인하고, 업데이트 되었으면 *.py로 변환한다
    if not newer(ui_dir, ui_to_py_dir):
        # print("UI has not changed!")
        pass
    else:
        print("UI changed detected, compiling...")
        # ui 파일이 업데이트 되었다, py파일을 연다.
        fp = open(ui_to_py_dir, "w", encoding=encoding)
        # ui 파일을 py파일로 컴파일한다.
        uic.compileUi(ui_dir, fp,
                      execute=True, indent=4, from_imports=True)
        fp.close()

module 폴더 생성 -> ui_compiler.py 에 작성 [방법 1과 동일한 코드]

 

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>398</width>
    <height>235</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget"/>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

ui 폴더 생성 -> main.ui 에 작성 (혹시나 몰라서 얘기하는건데 UI 파일에 주석달지마세요. 컴파일할때 오류납니다)

[방법 1과 동일한 코드]

 

from module.ui_compiler import compile_ui_to_py
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow

import os


# UI -> PY 컴파일
# fmt: off
compile_ui_to_py(os.path.join('ui', 'main.ui'),
                 os.path.join('ui', 'compiled_ui.py'))
from ui.compiled_ui import Ui_MainWindow
# fmt: on


class MainWindow(QMainWindow):
    def __init__(self) -> None:
        # 기본 설정 코드
        super().__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        # 이 아래에 코드 작성
        # 앞으로는 self.ui 를 이용해 UI 요소에 접근하면 됩니다.

        self.setWindowTitle("PyQT5 UI Compile Example")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

main.py 최상단 코드 [방법 1과 비교했을때 다중 상속을 이용하지 않고 self.ui 변수에 UI 윈도우를 생성해 접근한다는 방식이 다릅니다.]

self.ui 로 ui 요소에 접근하면 되며, 자동 완성을 지원함

self.ui.setupUi(self) 아래부터 필요한 코드를 작성해 나가시면 됩니다.

기본 설정코드라고 적혀있는 3줄은 Qt 사용을 위해 꼭 사용하는 코드입니다.

 

PyQT5_Template_Composition.zip
0.00MB

 

폴더 구조 및 소스코드 파일

SNS 공유하기
최근 글
파일의 IT 블로그
추천하는 글
파일의 IT 블로그
💬 댓글 개
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

이모티콘을 클릭하면 댓글창에 입력됩니다.