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

Basic pyside6 support #1057

Merged
merged 7 commits into from
Dec 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test-with-edm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
toolkit: ['wx', 'pyqt5', 'pyside2']
toolkit: ['wx', 'pyqt5', 'pyside2', 'pyside6']
runs-on: ${{ matrix.os }}
env:
# Set root directory, mainly for Windows, so that the EDM Python
Expand All @@ -40,6 +40,7 @@ jobs:
sudo apt-get install libxcb-xinerama0
sudo apt-get install pulseaudio
sudo apt-get install libpulse-mainloop-glib0
sudo apt-get install libgstreamer-gl1.0-0
shell: bash
if: startsWith(matrix.os, 'ubuntu') && matrix.toolkit != 'wx'
- name: Install Wx dependencies for Linux
Expand Down
29 changes: 19 additions & 10 deletions etstool.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@
python etstool.py test-all

Currently supported runtime values include ``3.6``, and currently
supported toolkits are ``null``, ``pyqt5``, ``pyside2`` and ``wx``. Not all
combinations of toolkits and runtimes will work, but the tasks will fail with
a clear error if that is the case.
supported toolkits are ``null``, ``pyqt5``, ``pyqt6``, ``pyside2``, ``pyside6``
and ``wx``. Not all combinations of toolkits and runtimes will work, but the
tasks will fail with a clear error if that is the case.

Tests can still be run via the usual means in other environments if that suits
a developer's purpose.
Expand Down Expand Up @@ -83,7 +83,7 @@
import click

supported_combinations = {
"3.6": {"pyqt5", "pyside2", "wx"},
"3.6": {"pyqt5", "pyside2", "pyside6", "wx"},
}

# Traits version requirement (empty string to mean no specific requirement).
Expand All @@ -98,7 +98,6 @@
"numpy",
"pygments",
"coverage",
"pillow",
"flake8",
"flake8_ets",
}
Expand All @@ -110,10 +109,12 @@

extra_dependencies = {
# XXX once pyside2 is available in EDM, we will want it here
"pyside2": set(),
"pyqt5": {"pyqt5"},
"pyside2": {"pillow"},
# XXX once pyside6 is available in EDM, we will want it here
"pyside6": set(),
"pyqt5": {"pyqt5", "pillow"},
# XXX once wxPython 4 is available in EDM, we will want it here
"wx": set(),
"wx": {"pillow"},
"null": set(),
}

Expand All @@ -134,6 +135,7 @@

environment_vars = {
"pyside2": {"ETS_TOOLKIT": "qt4", "QT_API": "pyside2"},
"pyside6": {"ETS_TOOLKIT": "qt4", "QT_API": "pyside6"},
"pyqt5": {"ETS_TOOLKIT": "qt4", "QT_API": "pyqt5"},
"wx": {"ETS_TOOLKIT": "wx"},
"null": {"ETS_TOOLKIT": "null"},
Expand Down Expand Up @@ -208,6 +210,13 @@ def install(edm, runtime, toolkit, environment, editable, source):
"{edm} run -e {environment} -- pip install pyside2",
]
)
elif toolkit == "pyside6":
commands.extend(
[
"{edm} run -e {environment} -- pip install pyside6",
"{edm} run -e {environment} -- pip install pillow",
]
)
elif toolkit == "wx":
if sys.platform == "darwin":
commands.append(
Expand Down Expand Up @@ -281,7 +290,7 @@ def test(edm, runtime, toolkit, environment, no_environment_vars=False):
parameters = get_parameters(edm, runtime, toolkit, environment)
if toolkit == "wx":
parameters["exclude"] = "qt"
elif toolkit in {"pyqt5", "pyside2"}:
elif toolkit in {"pyqt5", "pyside2", "pyside6"}:
parameters["exclude"] = "wx"
else:
parameters["exclude"] = "(wx|qt)"
Expand All @@ -294,7 +303,7 @@ def test(edm, runtime, toolkit, environment, no_environment_vars=False):

if toolkit == "wx":
environ["EXCLUDE_TESTS"] = "qt"
elif toolkit in {"pyqt5", "pyside2"}:
elif toolkit in {"pyqt5", "pyside2", "pyside6"}:
environ["EXCLUDE_TESTS"] = "wx"
else:
environ["EXCLUDE_TESTS"] = "(wx|qt)"
Expand Down
2 changes: 1 addition & 1 deletion pyface/layout_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@

from .toolkit import toolkit_object

layout_widget = toolkit_object("layout_widget:LayoutWidget")
LayoutWidget = toolkit_object("layout_widget:LayoutWidget")
6 changes: 5 additions & 1 deletion pyface/ui/qt4/console/call_tip_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,11 @@ def show_tip(self, tip):
# the current line.
padding = 3 # Distance in pixels between cursor bounds and tip box.
cursor_rect = text_edit.cursorRect(cursor)
screen_rect = QtGui.QApplication.desktop().screenGeometry(text_edit)
if QtCore.__version_info__ >= (5, 10):
screen = text_edit.window().windowHandle().screen()
screen_rect = screen.availableGeometry()
else:
screen_rect = QtGui.QApplication.desktop().screenGeometry(text_edit)
point = text_edit.mapToGlobal(cursor_rect.bottomRight())
point.setY(point.y() + padding)
tip_height = self.size().height()
Expand Down
6 changes: 3 additions & 3 deletions pyface/ui/qt4/console/console_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def eventFilter(self, obj, event):
# Make middle-click paste safe.
elif (
etype == QtCore.QEvent.MouseButtonRelease
and event.button() == QtCore.Qt.MidButton
and event.button() == QtCore.Qt.MiddleButton
and obj == self._control.viewport()
):
cursor = self._control.cursorForPosition(event.pos())
Expand Down Expand Up @@ -561,7 +561,7 @@ def _set_font(self, font):
else:
width = font_metrics.width(" ")

self._control.setTabStopWidth(self.tab_width * width)
self._control.setTabStopDistance(self.tab_width * width)

self._control.document().setDefaultFont(font)
if self._page_control:
Expand Down Expand Up @@ -871,7 +871,7 @@ def _set_tab_width(self, tab_width):
else:
width = font_metrics.width(" ")

self._control.setTabStopWidth(tab_width * width)
self._control.setTabStopDistance(tab_width * width)

self._tab_width = tab_width

Expand Down
4 changes: 2 additions & 2 deletions pyface/ui/qt4/data_view/data_view_item_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import logging

from pyface.qt import is_qt5
from pyface.qt import is_qt4
from pyface.qt.QtCore import QAbstractItemModel, QMimeData, QModelIndex, Qt
from pyface.qt.QtGui import QColor
from pyface.data_view.abstract_data_model import AbstractDataModel
Expand Down Expand Up @@ -145,7 +145,7 @@ def flags(self, index):
return Qt.ItemIsEnabled

flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
if is_qt5 and not self.model.can_have_children(row):
if not is_qt4 and not self.model.can_have_children(row):
flags |= Qt.ItemNeverHasChildren

try:
Expand Down
2 changes: 1 addition & 1 deletion pyface/ui/qt4/directory_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def _create_control(self, parent):
dlg = QtGui.QFileDialog(parent, self.title, self.default_path)

dlg.setViewMode(QtGui.QFileDialog.Detail)
dlg.setFileMode(QtGui.QFileDialog.DirectoryOnly)
dlg.setFileMode(QtGui.QFileDialog.Directory)

if not self.new_directory:
dlg.setOptions(QtGui.QFileDialog.ReadOnly)
Expand Down
6 changes: 3 additions & 3 deletions pyface/ui/qt4/system_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply


from pyface.qt import QtGui, is_qt5
from pyface.qt import QtGui, is_qt4


from traits.api import HasTraits, Int, Property, provides, Tuple
Expand Down Expand Up @@ -43,7 +43,7 @@ def _get_screen_width(self):
# QDesktopWidget.screenGeometry() is deprecated and Qt docs
# suggest using screens() instead, but screens in not available in qt4
# see issue: enthought/pyface#721
if is_qt5:
if not is_qt4:
return QtGui.QApplication.instance().screens()[0].availableGeometry().width()
else:
return QtGui.QApplication.instance().desktop().availableGeometry().width()
Expand All @@ -52,7 +52,7 @@ def _get_screen_height(self):
# QDesktopWidget.screenGeometry(int screen) is deprecated and Qt docs
# suggest using screens() instead, but screens in not available in qt4
# see issue: enthought/pyface#721
if is_qt5:
if not is_qt4:
return (
QtGui.QApplication.instance().screens()[0].availableGeometry().height()
)
Expand Down
17 changes: 12 additions & 5 deletions pyface/ui/qt4/tasks/advanced_editor_area_pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import sys


from pyface.qt import QtCore, QtGui
from pyface.qt import QtCore, QtGui, is_pyside, is_qt6


from traits.api import (
Expand Down Expand Up @@ -80,10 +80,17 @@ def create(self, parent):
# Add shortcuts for switching to a specific tab.
mod = "Ctrl+" if sys.platform == "darwin" else "Alt+"
mapper = QtCore.QSignalMapper(self.control)
mapper.mapped.connect(self._activate_tab)
self._connections_to_remove.append(
(mapper.mapped, self._activate_tab)
)
if is_pyside and is_qt6:
mapper.mappedInt.connect(self._activate_tab)
self._connections_to_remove.append(
(mapper.mappedInt, self._activate_tab)
)
else:
mapper.mapped.connect(self._activate_tab)
self._connections_to_remove.append(
(mapper.mapped, self._activate_tab)
)

for i in range(1, 10):
sequence = QtGui.QKeySequence(mod + str(i))
shortcut = QtGui.QShortcut(sequence, self.control)
Expand Down
16 changes: 11 additions & 5 deletions pyface/ui/qt4/tasks/editor_area_pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from traits.api import Any, Callable, List, observe, provides, Tuple


from pyface.qt import QtCore, QtGui
from pyface.qt import QtCore, QtGui, is_qt6, is_pyside


from .task_pane import TaskPane
Expand Down Expand Up @@ -84,10 +84,16 @@ def create(self, parent):
# Add shortcuts for switching to a specific tab.
mod = "Ctrl+" if sys.platform == "darwin" else "Alt+"
mapper = QtCore.QSignalMapper(self.control)
mapper.mapped.connect(self.control.setCurrentIndex)
self._connections_to_remove.append(
(mapper.mapped, self.control.setCurrentIndex)
)
if is_pyside and is_qt6:
mapper.mappedInt.connect(self.control.setCurrentIndex)
self._connections_to_remove.append(
(mapper.mappedInt, self.control.setCurrentIndex)
)
else:
mapper.mapped.connect(self.control.setCurrentIndex)
self._connections_to_remove.append(
(mapper.mapped, self.control.setCurrentIndex)
)
for i in range(1, 10):
sequence = QtGui.QKeySequence(mod + str(i))
shortcut = QtGui.QShortcut(sequence, self.control)
Expand Down
16 changes: 11 additions & 5 deletions pyface/ui/qt4/tasks/split_editor_area_pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
Str,
Tuple,
)
from pyface.qt import is_qt4, QtCore, QtGui
from pyface.qt import is_qt4, QtCore, QtGui, is_qt6, is_pyside
from pyface.action.api import Action, Group, MenuManager
from pyface.tasks.task_layout import PaneItem, Tabbed, Splitter
from pyface.mimedata import PyMimeData
Expand Down Expand Up @@ -331,10 +331,16 @@ def set_key_bindings(self):
# Add shortcuts for switching to a specific tab.
mod = "Ctrl+" if sys.platform == "darwin" else "Alt+"
mapper = QtCore.QSignalMapper(self.control)
mapper.mapped.connect(self._activate_tab)
self._connections_to_remove.append(
(mapper.mapped, self._activate_tab)
)
if is_pyside and is_qt6:
mapper.mappedInt.connect(self._activate_tab)
self._connections_to_remove.append(
(mapper.mappedInt, self._activate_tab)
)
else:
mapper.mapped.connect(self._activate_tab)
self._connections_to_remove.append(
(mapper.mapped, self._activate_tab)
)
for i in range(1, 10):
sequence = QtGui.QKeySequence(mod + str(i))
shortcut = QtGui.QShortcut(sequence, self.control)
Expand Down
1 change: 0 additions & 1 deletion pyface/ui/qt4/tests/test_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,5 @@ def exit_app(event):
# Attempt to leave the QApplication in a reasonably clean
# state in case of failure.
qt_app.sendPostedEvents()
qt_app.flush()

self.assertTrue(application_running[0])
1 change: 0 additions & 1 deletion pyface/ui/qt4/util/gui_test_assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ def tearDown(self):
with self.event_loop_with_timeout(repeat=5):
self.gui.invoke_later(self.qt_app.closeAllWindows)

self.qt_app.flush()
self.pyface_raise_patch.stop()
if self.traitsui_raise_patch is not None:
self.traitsui_raise_patch.stop()
Expand Down
4 changes: 3 additions & 1 deletion pyface/ui/qt4/util/tests/test_modal_dialog_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import unittest
from io import StringIO

from pyface.qt import QtGui
from pyface.qt import QtGui, is_qt6
from pyface.api import Dialog, MessageDialog, OK, CANCEL
from traits.api import HasStrictTraits

Expand Down Expand Up @@ -97,6 +97,7 @@ def test_dialog_was_not_opened_on_traitsui_dialog(self):
# but no dialog is opened
self.assertFalse(tester.dialog_was_opened)

@unittest.skipIf(is_qt6, "TEMPORARY: getting tests to run on pyside6")
def test_capture_errors_on_failure(self):
dialog = MessageDialog()
tester = ModalDialogTester(dialog.open)
Expand All @@ -116,6 +117,7 @@ def failure(tester):
tester.open_and_run(when_opened=failure)
self.assertIn("raise self.failureException(msg)", alt_stderr)

@unittest.skipIf(is_qt6, "TEMPORARY: getting tests to run on pyside6")
def test_capture_errors_on_error(self):
dialog = MessageDialog()
tester = ModalDialogTester(dialog.open)
Expand Down
2 changes: 1 addition & 1 deletion pyface/ui/qt4/workbench/split_tab_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,7 @@ def mouseReleaseEvent(self, e):
QtGui.QTabBar.mouseReleaseEvent(self, e)

if e.button() != QtCore.Qt.LeftButton:
if e.button() == QtCore.Qt.MidButton:
if e.button() == QtCore.Qt.MidddleButton:
self.tabCloseRequested.emit(self.tabAt(e.pos()))
return

Expand Down
7 changes: 6 additions & 1 deletion pyface/util/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,16 @@ def filter_tests(test_suite, exclusion_pattern):


def has_traitsui():
""" Is traitsui installed? """
""" Is traitsui installed and sufficiently recent? """
try:
import traitsui # noqa: F401
except ImportError:
return False
from pyface.toolkit import toolkit
if toolkit.toolkit.startswith("qt"):
from pyface.qt import is_qt6
if is_qt6:
return Version(traitsui.__version__) >= Version("7.4")
return True


Expand Down
2 changes: 1 addition & 1 deletion pyface/viewer/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
""" Abstract base class for all viewers. """


from pyface.widget.layout_widget import LayoutWidget
from pyface.layout_widget import LayoutWidget


class Viewer(LayoutWidget):
Expand Down