diff --git a/qtpy/QtCore.py b/qtpy/QtCore.py index ac6e78b9..1a66d88e 100644 --- a/qtpy/QtCore.py +++ b/qtpy/QtCore.py @@ -8,18 +8,34 @@ """ Provides QtCore classes and functions. """ - from . import PYQT6, PYQT5, PYSIDE2, PYSIDE6, PythonQtError + if PYQT6: + from PyQt6 import QtCore from PyQt6.QtCore import * from PyQt6.QtCore import pyqtSignal as Signal + from PyQt6.QtCore import pyqtBoundSignal as SignalInstance + from PyQt6.QtCore import pyqtSlot as Slot + from PyQt6.QtCore import pyqtProperty as Property from PyQt6.QtCore import QT_VERSION_STR as __version__ + # For issue #153 + from PyQt6.QtCore import QDateTime + QDateTime.toPython = QDateTime.toPyDateTime + + # Map missing methods QCoreApplication.exec_ = QCoreApplication.exec QEventLoop.exec_ = QEventLoop.exec QThread.exec_ = QThread.exec + # Those are imported from `import *` + del pyqtSignal, pyqtBoundSignal, pyqtSlot, pyqtProperty, QT_VERSION_STR + + # Allow unscoped access for enums inside the QtCore module + from .enums_compat import promote_enums + promote_enums(QtCore) + del QtCore elif PYQT5: from PyQt5.QtCore import * from PyQt5.QtCore import pyqtSignal as Signal diff --git a/qtpy/QtGui.py b/qtpy/QtGui.py index da8369e6..62344572 100644 --- a/qtpy/QtGui.py +++ b/qtpy/QtGui.py @@ -8,16 +8,22 @@ """ Provides QtGui classes and functions. """ -import warnings - from . import PYQT6, PYQT5, PYSIDE2, PYSIDE6, PythonQtError if PYQT6: + from PyQt6 import QtGui from PyQt6.QtGui import * + + # Map missing/renamed methods QDrag.exec_ = QDrag.exec QGuiApplication.exec_ = QGuiApplication.exec QTextDocument.print_ = QTextDocument.print + + # Allow unscoped access for enums inside the QtGui module + from .enums_compat import promote_enums + promote_enums(QtGui) + del QtGui elif PYQT5: from PyQt5.QtGui import * elif PYSIDE2: diff --git a/qtpy/QtWidgets.py b/qtpy/QtWidgets.py index d1f7b730..2734e780 100644 --- a/qtpy/QtWidgets.py +++ b/qtpy/QtWidgets.py @@ -8,29 +8,38 @@ """ Provides widget classes and functions. """ - from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, PythonQtError -from ._patch.qheaderview import introduce_renamed_methods_qheaderview + if PYQT6: + from PyQt6 import QtWidgets from PyQt6.QtWidgets import * from PyQt6.QtGui import QAction, QActionGroup, QShortcut from PyQt6.QtOpenGLWidgets import QOpenGLWidget + + # Map missing/renamed methods QTextEdit.setTabStopWidth = QTextEdit.setTabStopDistance QTextEdit.tabStopWidth = QTextEdit.tabStopDistance + QTextEdit.print_ = QTextEdit.print QPlainTextEdit.setTabStopWidth = QPlainTextEdit.setTabStopDistance QPlainTextEdit.tabStopWidth = QPlainTextEdit.tabStopDistance + QPlainTextEdit.print_ = QPlainTextEdit.print QApplication.exec_ = QApplication.exec QDialog.exec_ = QDialog.exec QMenu.exec_ = QMenu.exec - QTextEdit.print_ = QTextEdit.print - QPlainTextEdit.print_ = QPlainTextEdit.print + + # Allow unscoped access for enums inside the QtWidgets module + from .enums_compat import promote_enums + promote_enums(QtWidgets) + del QtWidgets elif PYQT5: from PyQt5.QtWidgets import * elif PYSIDE6: from PySide6.QtWidgets import * from PySide6.QtGui import QAction, QActionGroup, QShortcut from PySide6.QtOpenGLWidgets import QOpenGLWidget + + # Map missing/renamed methods QTextEdit.setTabStopWidth = QTextEdit.setTabStopDistance QTextEdit.tabStopWidth = QTextEdit.tabStopDistance QPlainTextEdit.setTabStopWidth = QPlainTextEdit.setTabStopDistance diff --git a/qtpy/compat.py b/qtpy/compat.py index 3a51c921..dbac4396 100644 --- a/qtpy/compat.py +++ b/qtpy/compat.py @@ -5,8 +5,6 @@ """ Compatibility functions """ - -from collections.abc import Callable import sys from .QtWidgets import QFileDialog diff --git a/qtpy/enums_compat.py b/qtpy/enums_compat.py new file mode 100644 index 00000000..1340fa36 --- /dev/null +++ b/qtpy/enums_compat.py @@ -0,0 +1,37 @@ +# Copyright © 2009- The Spyder Development Team +# Copyright © 2012- University of North Carolina at Chapel Hill +# Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') +# Ogi Moore ('ognyan.moore@%s.com' % 'gmail') +# KIU Shueng Chuan ('nixchuan@%s.com' % 'gmail') +# Licensed under the terms of the MIT License + +""" +Compatibility functions for scoped and unscoped enum access. +""" + +from . import PYQT6 + +if PYQT6: + import enum + + from . import sip + + def promote_enums(module): + """ + Search enums in the given module and allow unscoped access. + + Taken from: + https://github.com/pyqtgraph/pyqtgraph/blob/pyqtgraph-0.12.1/pyqtgraph/Qt.py#L331-L377 + """ + class_names = [name for name in dir(module) if name.startswith('Q')] + for class_name in class_names: + klass = getattr(module, class_name) + if not isinstance(klass, sip.wrappertype): + continue + attrib_names = [name for name in dir(klass) if name[0].isupper()] + for attrib_name in attrib_names: + attrib = getattr(klass, attrib_name) + if not isinstance(attrib, enum.EnumMeta): + continue + for enum_obj in attrib: + setattr(klass, enum_obj.name, enum_obj) diff --git a/qtpy/sip.py b/qtpy/sip.py new file mode 100644 index 00000000..64e71e66 --- /dev/null +++ b/qtpy/sip.py @@ -0,0 +1,15 @@ +# +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) + +from . import PYQT6, PYQT5, PythonQtError + +if PYQT6: + from PyQt6.sip import * +elif PYQT5: + from PyQt5.sip import * +else: + raise PythonQtError( + 'Currently selected Qt binding does not support this module') diff --git a/qtpy/tests/test_qtcore.py b/qtpy/tests/test_qtcore.py index e9b22ae7..423b2f2b 100644 --- a/qtpy/tests/test_qtcore.py +++ b/qtpy/tests/test_qtcore.py @@ -1,5 +1,6 @@ import pytest -from qtpy import PYQT5, PYQT6, PYSIDE2, QtCore + +from qtpy import PYQT5, PYQT6, PYSIDE2, PYQT_VERSION, QtCore """Test QtCore.""" @@ -9,8 +10,6 @@ def test_qtmsghandler(): assert QtCore.qInstallMessageHandler is not None -@pytest.mark.skipif(not (PYQT5 or PYSIDE2), - reason="Targeted to PyQt5 or PySide2") def test_DateTime_toPython(): """Test QDateTime.toPython""" assert QtCore.QDateTime.toPython is not None @@ -25,3 +24,16 @@ class ClassWithSignal(QtCore.QObject): instance = ClassWithSignal() assert isinstance(instance.signal, QtCore.SignalInstance) + + +@pytest.mark.skipif(PYQT5 and PYQT_VERSION.startswith('5.9'), + reason="A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.*" + "to work with scoped enum access") +def test_enum_access(): + """Test scoped and unscoped enum access for qtpy.QtCore.*.""" + assert QtCore.QAbstractAnimation.Stopped == QtCore.QAbstractAnimation.State.Stopped + assert QtCore.QEvent.ActionAdded == QtCore.QEvent.Type.ActionAdded + assert QtCore.Qt.AlignLeft == QtCore.Qt.AlignmentFlag.AlignLeft + assert QtCore.Qt.Key_Return == QtCore.Qt.Key.Key_Return + assert QtCore.Qt.transparent == QtCore.Qt.GlobalColor.transparent + assert QtCore.Qt.Widget == QtCore.Qt.WindowType.Widget diff --git a/qtpy/tests/test_qtgui.py b/qtpy/tests/test_qtgui.py new file mode 100644 index 00000000..09798955 --- /dev/null +++ b/qtpy/tests/test_qtgui.py @@ -0,0 +1,30 @@ +"""Test QtGui.""" +import pytest + +from qtpy import PYQT5, PYQT_VERSION, QtGui + + +def test_qdrag_functions(): + """Test functions mapping for QtGui.QDrag.""" + assert QtGui.QDrag.exec_ + + +def test_qguiapplication_functions(): + """Test functions mapping for QtGui.QGuiApplication.""" + assert QtGui.QGuiApplication.exec_ + + +def test_qtextdocument_functions(): + """Test functions mapping for QtGui.QTextDocument.""" + assert QtGui.QTextDocument.print_ + + +@pytest.mark.skipif(PYQT5 and PYQT_VERSION.startswith('5.9'), + reason="A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.*" + "to work with scoped enum access") +def test_enum_access(): + """Test scoped and unscoped enum access for qtpy.QtWidgets.*.""" + assert QtGui.QColor.Rgb == QtGui.QColor.Spec.Rgb + assert QtGui.QFont.AllUppercase == QtGui.QFont.Capitalization.AllUppercase + assert QtGui.QIcon.Normal == QtGui.QIcon.Mode.Normal + assert QtGui.QImage.Format_Invalid == QtGui.QImage.Format.Format_Invalid \ No newline at end of file diff --git a/qtpy/tests/test_qtwidgets.py b/qtpy/tests/test_qtwidgets.py new file mode 100644 index 00000000..68e8192c --- /dev/null +++ b/qtpy/tests/test_qtwidgets.py @@ -0,0 +1,43 @@ +"""Test QtWidgets.""" +import pytest + +from qtpy import PYQT5, PYQT_VERSION, QtWidgets + + +def test_qtextedit_functions(): + """Test functions mapping for QtWidgets.QTextEdit.""" + assert QtWidgets.QTextEdit.setTabStopWidth + assert QtWidgets.QTextEdit.tabStopWidth + assert QtWidgets.QTextEdit.print_ + + +def test_qplaintextedit_functions(): + """Test functions mapping for QtWidgets.QPlainTextEdit.""" + assert QtWidgets.QPlainTextEdit.setTabStopWidth + assert QtWidgets.QPlainTextEdit.tabStopWidth + assert QtWidgets.QPlainTextEdit.print_ + + +def test_qapplication_functions(): + """Test functions mapping for QtWidgets.QApplication.""" + assert QtWidgets.QApplication.exec_ + + +def test_qdialog_functions(): + """Test functions mapping for QtWidgets.QDialog.""" + assert QtWidgets.QDialog.exec_ + + +def test_qmenu_functions(): + """Test functions mapping for QtWidgets.QDialog.""" + assert QtWidgets.QMenu.exec_ + + +@pytest.mark.skipif(PYQT5 and PYQT_VERSION.startswith('5.9'), + reason="A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.*" + "to work with scoped enum access") +def test_enum_access(): + """Test scoped and unscoped enum access for qtpy.QtWidgets.*.""" + assert QtWidgets.QFileDialog.AcceptOpen == QtWidgets.QFileDialog.AcceptMode.AcceptOpen + assert QtWidgets.QMessageBox.InvalidRole == QtWidgets.QMessageBox.ButtonRole.InvalidRole + assert QtWidgets.QStyle.State_None == QtWidgets.QStyle.StateFlag.State_None