Skip to content

Commit

Permalink
Python 3.11 Support (#1210)
Browse files Browse the repository at this point in the history
This supports Python 3.11 with PySide 6.4 and PyQt 5. PySide 2 doesn't
seem to be readily available for Python 3.11, so we're not testing with
it. All of the changes for this still work with the existing set of
tests for Python 3.8 and Python 3.10 with PySide2, PySide6 and PyQt5 on
all platforms, and also with EDM on Python 3.8

I have manually checked that the examples work, modulo #1212 which is an
orthogonal issue to this.

So this is probably as good as it is going to get - we will likely find
additional issues with the new Enums as we start to see real work done
with it.

As a side-effect, fixes #1202
  • Loading branch information
corranwebster authored Mar 10, 2023
1 parent 0fb8373 commit 2b165fa
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 58 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.8', '3.10']
python-version: ['3.8', '3.10', '3.11']
qt-api: ['pyqt5', 'pyside2', 'pyside6']
exclude:
- python-version: '3.11'
qt-api: 'pyside2'
fail-fast: false

env:
Expand Down
54 changes: 39 additions & 15 deletions pyface/ui/qt4/code_editor/code_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@

import sys


from pyface.qt import QtCore, QtGui

from pyface.qt import QtCore, QtGui, is_qt5

from .find_widget import FindWidget
from .gutters import LineNumberWidget, StatusGutterWidget
from .replace_widget import ReplaceWidget
from .pygments_highlighter import PygmentsHighlighter


def _exact_match(user_keys, key_sequence):
"""Utility function for matching key sequences"""
return user_keys.matches(key_sequence) == QtGui.QKeySequence.SequenceMatch.ExactMatch # noqa: E501


class CodeWidget(QtGui.QPlainTextEdit):
""" A widget for viewing and editing code.
"""
Expand Down Expand Up @@ -389,7 +392,10 @@ def keyPressEvent(self, event):
if self.isReadOnly():
return super().keyPressEvent(event)

key_sequence = QtGui.QKeySequence(event.key() + int(event.modifiers()))
if is_qt5:
key_sequence = QtGui.QKeySequence(event.key() + int(event.modifiers()))
else:
key_sequence = QtGui.QKeySequence(event.keyCombination())

self.keyPressEvent_action(event) # FIXME: see above

Expand All @@ -398,35 +404,42 @@ def keyPressEvent(self, event):
# beginning of the document. Likewise, if the cursor is somewhere in the
# last line, the "down" key causes it to go to the end.
cursor = self.textCursor()
if key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.Key.Key_Up)):
if _exact_match(
key_sequence,
QtGui.QKeySequence(QtCore.Qt.Key.Key_Up),
):
cursor.movePosition(QtGui.QTextCursor.MoveOperation.StartOfLine)
if cursor.atStart():
self.setTextCursor(cursor)
event.accept()
elif key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.Key.Key_Down)):
elif _exact_match(
key_sequence,
QtGui.QKeySequence(QtCore.Qt.Key.Key_Down),
):
cursor.movePosition(QtGui.QTextCursor.MoveOperation.EndOfLine)
if cursor.atEnd():
self.setTextCursor(cursor)
event.accept()

elif self.auto_indent and key_sequence.matches(
elif self.auto_indent and _exact_match(
key_sequence,
QtGui.QKeySequence(QtCore.Qt.Key.Key_Return)
):
event.accept()
return self.autoindent_newline()
elif key_sequence.matches(self.indent_key):
elif _exact_match(key_sequence, self.indent_key):
event.accept()
return self.block_indent()
elif key_sequence.matches(self.unindent_key):
elif _exact_match(key_sequence, self.unindent_key):
event.accept()
return self.block_unindent()
elif key_sequence.matches(self.comment_key):
elif _exact_match(key_sequence, self.comment_key):
event.accept()
return self.block_comment()
elif (
self.auto_indent
and self.smart_backspace
and key_sequence.matches(self.backspace_key)
and _exact_match(key_sequence, self.backspace_key)
and self._backspace_should_unindent()
):
event.accept()
Expand Down Expand Up @@ -595,6 +608,13 @@ def __init__(self, parent, font=None, lexer=None):

self.setLayout(layout)

# key bindings
self.find_key = QtGui.QKeySequence(QtGui.QKeySequence.StandardKey.Find)
self.replace_key = QtGui.QKeySequence(QtGui.QKeySequence.StandardKey.Replace) # noqa: E501
# not all platforms have a standard replace key; use Ctrl+Alt+F
if self.replace_key.isEmpty():
self.replace_key = QtGui.QKeySequence("Ctrl+Alt+F")

def _remove_event_listeners(self):
self.code.selectionChanged.disconnect(self._update_replace_enabled)

Expand Down Expand Up @@ -789,13 +809,17 @@ def centerCursor(self):
# ------------------------------------------------------------------------

def keyPressEvent(self, event):
key_sequence = QtGui.QKeySequence(event.key() + int(event.modifiers()))
if key_sequence.matches(QtGui.QKeySequence.StandardKey.Find):
if is_qt5:
key_sequence = QtGui.QKeySequence(event.key() + int(event.modifiers()))
else:
key_sequence = QtGui.QKeySequence(event.keyCombination())

if _exact_match(key_sequence, self.find_key):
self.enable_find()
elif key_sequence.matches(QtGui.QKeySequence.StandardKey.Replace):
elif _exact_match(key_sequence, self.replace_key):
if not self.code.isReadOnly():
self.enable_replace()
elif key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.Key.Key_Escape)):
elif _exact_match(key_sequence, QtGui.QKeySequence(QtCore.Qt.Key.Key_Escape)):
if self.active_find_widget:
self.find.hide()
self.replace.hide()
Expand Down
34 changes: 6 additions & 28 deletions pyface/ui/qt4/code_editor/tests/test_code_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@


import unittest
from unittest import mock


from pyface.qt import QtCore, QtGui
from pyface.qt import QtGui
from pyface.qt.QtTest import QTest


Expand Down Expand Up @@ -62,49 +61,28 @@ def test_readonly_replace_widget(self):
acw.code.setPlainText(text)
acw.show()

# On some platforms, Find/Replace do not have default keybindings
FindKey = QtGui.QKeySequence("Ctrl+F")
ReplaceKey = QtGui.QKeySequence("Ctrl+H")
patcher_find = mock.patch("pyface.qt.QtGui.QKeySequence.StandardKey.Find", FindKey)
patcher_replace = mock.patch(
"pyface.qt.QtGui.QKeySequence.StandardKey.Replace", ReplaceKey
)
patcher_find.start()
patcher_replace.start()
self.addCleanup(patcher_find.stop)
self.addCleanup(patcher_replace.stop)

def click_key_seq(widget, key_seq):
if not isinstance(key_seq, QtGui.QKeySequence):
key_seq = QtGui.QKeySequence(key_seq)
try:
# QKeySequence on python3-pyside does not have `len`
first_key = key_seq[0]
except IndexError:
return False
key = QtCore.Qt.Key(first_key & ~QtCore.Qt.KeyboardModifier.KeyboardModifierMask)
modifier = QtCore.Qt.KeyboardModifier(
first_key & QtCore.Qt.KeyboardModifier.KeyboardModifierMask
)
QTest.keyClick(widget, key, modifier)
QTest.keySequence(widget, key_seq)
return True

acw.code.setReadOnly(True)
if click_key_seq(acw, FindKey):
if click_key_seq(acw, acw.find_key):
self.assertTrue(acw.find.isVisible())
acw.find.hide()

acw.code.setReadOnly(False)
if click_key_seq(acw, FindKey):
if click_key_seq(acw, acw.find_key):
self.assertTrue(acw.find.isVisible())
acw.find.hide()

acw.code.setReadOnly(True)
if click_key_seq(acw, ReplaceKey):
if click_key_seq(acw, acw.replace_key):
self.assertFalse(acw.replace.isVisible())

acw.code.setReadOnly(False)
if click_key_seq(acw, ReplaceKey):
if click_key_seq(acw, acw.replace_key):
self.assertTrue(acw.replace.isVisible())
acw.replace.hide()
self.assertFalse(acw.replace.isVisible())
2 changes: 1 addition & 1 deletion pyface/ui/qt4/data_view/data_view_item_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
value_type.set_text(self.model, row, column, value)
elif role == Qt.ItemDataRole.CheckStateRole:
if value_type.has_check_state(self.model, row, column):
state = set_check_state_map[value]
state = set_check_state_map[Qt.CheckState(value)]
value_type.set_check_state(self.model, row, column, state)

except DataViewSetError:
Expand Down
12 changes: 6 additions & 6 deletions pyface/ui/qt4/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@

# Map PyQt dialog related constants to the pyface equivalents.
_RESULT_MAP = {
int(QtGui.QDialog.DialogCode.Accepted): OK,
int(QtGui.QDialog.DialogCode.Rejected): CANCEL,
int(QtGui.QMessageBox.StandardButton.Ok): OK,
int(QtGui.QMessageBox.StandardButton.Cancel): CANCEL,
int(QtGui.QMessageBox.StandardButton.Yes): YES,
int(QtGui.QMessageBox.StandardButton.No): NO,
QtGui.QDialog.DialogCode.Accepted: OK,
QtGui.QDialog.DialogCode.Rejected: CANCEL,
QtGui.QMessageBox.StandardButton.Ok: OK,
QtGui.QMessageBox.StandardButton.Cancel: CANCEL,
QtGui.QMessageBox.StandardButton.Yes: YES,
QtGui.QMessageBox.StandardButton.No: NO,
}


Expand Down
2 changes: 1 addition & 1 deletion pyface/ui/qt4/tasks/advanced_editor_area_pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ def __init__(self, editor, parent=None):
style = self.style()
contents_minsize.setHeight(
contents_minsize.height()
+ style.pixelMetric(style.PM_DockWidgetHandleExtent)
+ style.pixelMetric(style.PixelMetric.PM_DockWidgetHandleExtent)
)
self.setMinimumSize(contents_minsize)

Expand Down
8 changes: 4 additions & 4 deletions pyface/ui/qt4/tasks/dock_pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"top": QtCore.Qt.DockWidgetArea.TopDockWidgetArea,
"bottom": QtCore.Qt.DockWidgetArea.BottomDockWidgetArea,
}
INVERSE_AREA_MAP = dict((int(v), k) for k, v in AREA_MAP.items())
INVERSE_AREA_MAP = {v: k for k, v in AREA_MAP.items()}


@provides(IDockPane)
Expand Down Expand Up @@ -85,7 +85,7 @@ def create(self, parent):
style = control.style()
contents_minsize.setHeight(
contents_minsize.height()
+ style.pixelMetric(style.PM_DockWidgetHandleExtent)
+ style.pixelMetric(style.PixelMetric.PM_DockWidgetHandleExtent)
)
control.setMinimumSize(contents_minsize)

Expand Down Expand Up @@ -185,8 +185,8 @@ def _set_visible(self, event):

def _receive_dock_area(self, area):
with self._signal_context():
if int(area) in INVERSE_AREA_MAP:
self.dock_area = INVERSE_AREA_MAP[int(area)]
if area in INVERSE_AREA_MAP:
self.dock_area = INVERSE_AREA_MAP[area]

def _receive_floating(self, floating):
with self._signal_context():
Expand Down
2 changes: 1 addition & 1 deletion pyface/ui/qt4/tasks/task_window_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def get_layout(self):

# Extract the window's corner configuration.
for name, corner in CORNER_MAP.items():
area = INVERSE_AREA_MAP[int(self.control.corner(corner))]
area = INVERSE_AREA_MAP[self.control.corner(corner)]
setattr(layout, name + "_corner", area)

return layout
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pillow = ['pillow']
pyqt5 = ['pyqt5', 'pygments']
pyqt6 = ['pyqt6', 'pygments']
pyside2 = ['pyside2', 'pygments']
pyside6 = ['pyside6<6.4', 'pygments']
pyside6 = ['pyside6', 'pygments']
numpy = ['numpy']
traitsui = ['traitsui']
test = ['packaging']
Expand Down

0 comments on commit 2b165fa

Please sign in to comment.