Skip to content

Commit 754e085

Browse files
authored
Merge branch 'master' into qenum
2 parents 5319ed4 + 3ba97e3 commit 754e085

File tree

16 files changed

+394
-151
lines changed

16 files changed

+394
-151
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ on:
1313
- main
1414
- '*.x'
1515

16+
concurrency:
17+
group: test-${{ github.ref }}
18+
cancel-in-progress: true
19+
1620
jobs:
1721
test:
1822
name: Test ${{ matrix.os }} Python ${{ matrix.python-version }} conda=${{ matrix.use-conda }}
@@ -53,7 +57,7 @@ jobs:
5357
pyside6-version: '6.4' # Python 3.11 needs 6.4+
5458
- use-conda: 'Yes'
5559
skip-pyqt6: true # No PyQt6 conda packages yet
56-
pyside6-version: '6.4' # Conda only has 6.4+
60+
pyside6-version: '6.4' # Conda only has 6.4+ for Python <3.8
5761
- use-conda: 'No'
5862
pyqt5-version: '5.15' # Test with latest optional packages
5963
- python-version: '3.7'
@@ -63,6 +67,7 @@ jobs:
6367
- python-version: '3.11'
6468
use-conda: 'No'
6569
skip-pyside2: true # Pyside2 wheels don't support Python 3.11+
70+
pyside6-version: '6.5' # Test upper bound
6671
- os: windows-latest
6772
python-version: '3.7'
6873
use-conda: 'Yes'
@@ -80,7 +85,7 @@ jobs:
8085
- os: macos-latest
8186
python-version: '3.7'
8287
use-conda: 'No'
83-
pyqt6-version: 6.4 # Test upper bound
88+
pyqt6-version: 6.5 # Test upper bound
8489
pyside2-version: 5.15 # Test upper bound
8590
steps:
8691
- name: Checkout branch
@@ -95,6 +100,7 @@ jobs:
95100
run: |
96101
sudo apt update
97102
sudo apt install libpulse-dev libegl1-mesa libopengl0 gstreamer1.0-gl
103+
- uses: tlambert03/setup-qt-libs@v1
98104
- name: Install Conda
99105
uses: conda-incubator/setup-miniconda@v2
100106
with:

.github/workflows/test.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,12 @@ fi
5353

5454
# Build wheel of package
5555
git clean -xdf -e *.coverage
56+
python -m pip install --upgrade pip
5657
python -m pip install --upgrade build
5758
python -bb -X dev -W error -m build
5859

5960
# Install package from built wheel
60-
echo dist/*.whl | xargs -I % python -bb -X dev -W error -W "ignore::DeprecationWarning:pip._internal.locations._distutils" -W "ignore::DeprecationWarning:distutils.command.install" -m pip install --upgrade %
61+
echo dist/*.whl | xargs -I % python -bb -X dev -W error -W "ignore::DeprecationWarning:pip._internal.locations._distutils" -W "ignore::DeprecationWarning:distutils.command.install" -W "ignore::DeprecationWarning:pip._internal.metadata.importlib._envs" -m pip install --upgrade %
6162

6263
# Print environment information
6364
mamba list

qtpy/QtCore.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import TYPE_CHECKING
1111

1212
from . import PYQT6, PYQT5, PYSIDE2, PYSIDE6
13+
from ._utils import possibly_static_exec, possibly_static_exec_
1314

1415
if PYQT5:
1516
from PyQt5.QtCore import *
@@ -49,7 +50,7 @@
4950
pass
5051

5152
# Map missing methods
52-
QCoreApplication.exec_ = QCoreApplication.exec
53+
QCoreApplication.exec_ = lambda *args, **kwargs: possibly_static_exec(QCoreApplication, *args, **kwargs)
5354
QEventLoop.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)
5455
QThread.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)
5556

@@ -83,6 +84,11 @@
8384
# Fails with PySide2 5.12.0
8485
pass
8586

87+
QCoreApplication.exec = lambda *args, **kwargs: possibly_static_exec_(QCoreApplication, *args, **kwargs)
88+
QEventLoop.exec = lambda self, *args, **kwargs: self.exec_(*args, **kwargs)
89+
QThread.exec = lambda self, *args, **kwargs: self.exec_(*args, **kwargs)
90+
QTextStreamManipulator.exec = lambda self, *args, **kwargs: self.exec_(*args, **kwargs)
91+
8692
elif PYSIDE6:
8793
from PySide6.QtCore import *
8894
import PySide6.QtCore
@@ -100,7 +106,7 @@
100106
Qt.MidButton = Qt.MiddleButton
101107

102108
# Map DeprecationWarning methods
103-
QCoreApplication.exec_ = QCoreApplication.exec
109+
QCoreApplication.exec_ = lambda *args, **kwargs: possibly_static_exec(QCoreApplication, *args, **kwargs)
104110
QEventLoop.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)
105111
QThread.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)
106112
QTextStreamManipulator.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)

qtpy/QtGui.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"""Provides QtGui classes and functions."""
1010

1111
from . import PYQT6, PYQT5, PYSIDE2, PYSIDE6, QtModuleNotInstalledError
12-
from .utils import getattr_missing_optional_dep
12+
from ._utils import possibly_static_exec, getattr_missing_optional_dep
1313

1414

1515
_missing_optional_names = {}
@@ -65,7 +65,7 @@ def __getattr__(name):
6565

6666
# Map missing/renamed methods
6767
QDrag.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)
68-
QGuiApplication.exec_ = QGuiApplication.exec
68+
QGuiApplication.exec_ = lambda *args, **kwargs: possibly_static_exec(QGuiApplication, *args, **kwargs)
6969
QTextDocument.print_ = lambda self, *args, **kwargs: self.print(*args, **kwargs)
7070

7171
# Allow unscoped access for enums inside the QtGui module
@@ -105,7 +105,7 @@ def __getattr__(name):
105105

106106
# Map DeprecationWarning methods
107107
QDrag.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)
108-
QGuiApplication.exec_ = QGuiApplication.exec
108+
QGuiApplication.exec_ = lambda *args, **kwargs: possibly_static_exec(QGuiApplication, *args, **kwargs)
109109

110110
if PYSIDE2 or PYSIDE6:
111111
# PySide{2,6} do not accept the `mode` keyword argument in

qtpy/QtWidgets.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,38 @@
77
# -----------------------------------------------------------------------------
88

99
"""Provides widget classes and functions."""
10+
from functools import partialmethod, wraps
1011

11-
from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, QtModuleNotInstalledError
12-
from .utils import getattr_missing_optional_dep
12+
from packaging.version import parse
13+
14+
from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, QT_VERSION as _qt_version
15+
from ._utils import add_action, possibly_static_exec, getattr_missing_optional_dep
1316

1417

1518
_missing_optional_names = {}
1619

20+
1721
def __getattr__(name):
1822
"""Custom getattr to chain and wrap errors due to missing optional deps."""
1923
raise getattr_missing_optional_dep(
2024
name, module_name=__name__, optional_names=_missing_optional_names)
2125

26+
def _dir_to_directory(func):
27+
@wraps(func)
28+
def _dir_to_directory_(*args, **kwargs):
29+
if "dir" in kwargs:
30+
kwargs["directory"] = kwargs.pop("dir")
31+
return func(*args, **kwargs)
32+
return _dir_to_directory_
33+
34+
def _directory_to_dir(func):
35+
@wraps(func)
36+
def _directory_to_dir_(*args, **kwargs):
37+
if "directory" in kwargs:
38+
kwargs["dir"] = kwargs.pop("directory")
39+
return func(*args, **kwargs)
40+
return _directory_to_dir_
41+
2242

2343
if PYQT5:
2444
from PyQt5.QtWidgets import *
@@ -46,9 +66,9 @@ def __getattr__(name):
4666
QPlainTextEdit.setTabStopWidth = lambda self, *args, **kwargs: self.setTabStopDistance(*args, **kwargs)
4767
QPlainTextEdit.tabStopWidth = lambda self, *args, **kwargs: self.tabStopDistance(*args, **kwargs)
4868
QPlainTextEdit.print_ = lambda self, *args, **kwargs: self.print(*args, **kwargs)
49-
QApplication.exec_ = QApplication.exec
69+
QApplication.exec_ = lambda *args, **kwargs: possibly_static_exec(QApplication, *args, **kwargs)
5070
QDialog.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)
51-
QMenu.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)
71+
QMenu.exec_ = lambda *args, **kwargs: possibly_static_exec(QMenu, *args, **kwargs)
5272
QLineEdit.getTextMargins = lambda self: (self.textMargins().left(), self.textMargins().top(), self.textMargins().right(), self.textMargins().bottom())
5373

5474
# Allow unscoped access for enums inside the QtWidgets module
@@ -81,7 +101,23 @@ def __getattr__(name):
81101
QLineEdit.getTextMargins = lambda self: (self.textMargins().left(), self.textMargins().top(), self.textMargins().right(), self.textMargins().bottom())
82102

83103
# Map DeprecationWarning methods
84-
QApplication.exec_ = QApplication.exec
104+
QApplication.exec_ = lambda *args, **kwargs: possibly_static_exec(QApplication, *args, **kwargs)
85105
QDialog.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)
86-
QMenu.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs)
106+
QMenu.exec_ = lambda *args, **kwargs: possibly_static_exec(QMenu, *args, **kwargs)
107+
108+
109+
if PYSIDE2 or PYSIDE6:
110+
QFileDialog.getExistingDirectory = _directory_to_dir(QFileDialog.getExistingDirectory)
111+
QFileDialog.getOpenFileName = _directory_to_dir(QFileDialog.getOpenFileName)
112+
QFileDialog.getOpenFileNames = _directory_to_dir(QFileDialog.getOpenFileNames)
113+
QFileDialog.getSaveFileName = _directory_to_dir(QFileDialog.getSaveFileName)
114+
else:
115+
QFileDialog.getExistingDirectory = _dir_to_directory(QFileDialog.getExistingDirectory)
116+
QFileDialog.getOpenFileName = _dir_to_directory(QFileDialog.getOpenFileName)
117+
QFileDialog.getOpenFileNames = _dir_to_directory(QFileDialog.getOpenFileNames)
118+
QFileDialog.getSaveFileName = _dir_to_directory(QFileDialog.getSaveFileName)
87119

120+
# Make `addAction` compatible with Qt6 >= 6.3
121+
if PYQT5 or PYSIDE2 or parse(_qt_version) < parse('6.3'):
122+
QMenu.addAction = partialmethod(add_action, old_add_action=QMenu.addAction)
123+
QToolBar.addAction = partialmethod(add_action, old_add_action=QToolBar.addAction)

qtpy/_utils.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# -----------------------------------------------------------------------------
2+
# Copyright © 2023- The Spyder Development Team
3+
#
4+
# Released under the terms of the MIT License
5+
# (see LICENSE.txt for details)
6+
# -----------------------------------------------------------------------------
7+
8+
"""Provides utility functions for use by QtPy itself."""
9+
10+
import qtpy
11+
12+
13+
def _wrap_missing_optional_dep_error(
14+
attr_error,
15+
*,
16+
import_error,
17+
wrapper=qtpy.QtModuleNotInstalledError,
18+
**wrapper_kwargs,
19+
):
20+
"""Create a __cause__-chained wrapper error for a missing optional dep."""
21+
qtpy_error = wrapper(**wrapper_kwargs)
22+
import_error.__cause__ = attr_error
23+
qtpy_error.__cause__ = import_error
24+
return qtpy_error
25+
26+
27+
def getattr_missing_optional_dep(name, module_name, optional_names):
28+
"""Wrap AttributeError in a special error if it matches."""
29+
attr_error = AttributeError(f'module {module_name!r} has no attribute {name!r}')
30+
if name in optional_names:
31+
return _wrap_missing_optional_dep_error(attr_error, **optional_names[name])
32+
return attr_error
33+
34+
35+
def possibly_static_exec(cls, *args, **kwargs):
36+
"""Call `self.exec` when `self` is given or a static method otherwise."""
37+
if not args and not kwargs:
38+
# A special case (`cls.exec_()`) to avoid the function resolving error
39+
return cls.exec()
40+
if isinstance(args[0], cls):
41+
if len(args) == 1 and not kwargs:
42+
# A special case (`self.exec_()`) to avoid the function resolving error
43+
return args[0].exec()
44+
return args[0].exec(*args[1:], **kwargs)
45+
else:
46+
return cls.exec(*args, **kwargs)
47+
48+
49+
def possibly_static_exec_(cls, *args, **kwargs):
50+
"""Call `self.exec` when `self` is given or a static method otherwise."""
51+
if not args and not kwargs:
52+
# A special case (`cls.exec()`) to avoid the function resolving error
53+
return cls.exec_()
54+
if isinstance(args[0], cls):
55+
if len(args) == 1 and not kwargs:
56+
# A special case (`self.exec()`) to avoid the function resolving error
57+
return args[0].exec_()
58+
return args[0].exec_(*args[1:], **kwargs)
59+
else:
60+
return cls.exec_(*args, **kwargs)
61+
62+
63+
def add_action(self, *args, old_add_action):
64+
"""Re-order arguments of `addAction` to backport compatibility with Qt>=6.3."""
65+
from qtpy.QtCore import QObject
66+
from qtpy.QtGui import QIcon, QKeySequence
67+
from qtpy.QtWidgets import QAction
68+
69+
action: QAction
70+
icon: QIcon
71+
text: str
72+
shortcut: QKeySequence | QKeySequence.StandardKey | str | int
73+
receiver: QObject
74+
member: bytes
75+
if all(isinstance(arg, t)
76+
for arg, t in zip(args, [str,
77+
(QKeySequence, QKeySequence.StandardKey, str, int),
78+
QObject,
79+
bytes])):
80+
if len(args) == 2:
81+
text, shortcut = args
82+
action = old_add_action(self, text)
83+
action.setShortcut(shortcut)
84+
elif len(args) == 3:
85+
text, shortcut, receiver = args
86+
action = old_add_action(self, text, receiver)
87+
action.setShortcut(shortcut)
88+
elif len(args) == 4:
89+
text, shortcut, receiver, member = args
90+
action = old_add_action(self, text, receiver, member, shortcut)
91+
else:
92+
return old_add_action(self, *args)
93+
return action
94+
elif all(isinstance(arg, t)
95+
for arg, t in zip(args, [QIcon,
96+
str,
97+
(QKeySequence, QKeySequence.StandardKey, str, int),
98+
QObject,
99+
bytes])):
100+
if len(args) == 3:
101+
icon, text, shortcut = args
102+
action = old_add_action(self, icon, text)
103+
action.setShortcut(QKeySequence(shortcut))
104+
elif len(args) == 4:
105+
icon, text, shortcut, receiver = args
106+
action = old_add_action(self, icon, text, receiver)
107+
action.setShortcut(QKeySequence(shortcut))
108+
elif len(args) == 5:
109+
icon, text, shortcut, receiver, member = args
110+
action = old_add_action(self, icon, text, receiver, member, QKeySequence(shortcut))
111+
else:
112+
return old_add_action(self, *args)
113+
return action
114+
return old_add_action(self, *args)

qtpy/tests/optional_deps/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# See https://github.com/spyder-ide/qtpy/pull/387/
55

66

7-
from qtpy.utils import getattr_missing_optional_dep
7+
from qtpy._utils import getattr_missing_optional_dep
88
from .optional_dep import ExampleClass
99

1010

qtpy/tests/test_compat.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77

88
@pytest.mark.skipif(
99
((sys.version_info.major == 3 and sys.version_info.minor == 7)
10-
and sys.platform.startswith('win') and not not_using_conda())
11-
or
12-
(sys.platform.startswith('linux') and not_using_conda()),
13-
reason="sip not included in Python3.7 on Windows, or in non-conda test suite on Linux"
10+
and sys.platform.startswith('win') and not not_using_conda()),
11+
reason="sip not included in Python3.7 on Windows"
1412
)
1513
def test_isalive(qtbot):
1614
"""Test compat.isalive"""

0 commit comments

Comments
 (0)