From 46fa2fdf9af7c8b2341ae3e9108c826e28000032 Mon Sep 17 00:00:00 2001
From: PrimozGodec
Date: Tue, 17 Jan 2023 13:20:30 +0100
Subject: [PATCH 1/3] Keywords - pyqt6 compatibility
---
orangecontrib/text/widgets/owkeywords.py | 48 +++++++++++--------
.../text/widgets/tests/test_owkeywords.py | 15 ++++++
orangecontrib/text/widgets/utils/__init__.py | 21 ++++++++
3 files changed, 65 insertions(+), 19 deletions(-)
diff --git a/orangecontrib/text/widgets/owkeywords.py b/orangecontrib/text/widgets/owkeywords.py
index cfbf081db..ead98451d 100644
--- a/orangecontrib/text/widgets/owkeywords.py
+++ b/orangecontrib/text/widgets/owkeywords.py
@@ -23,6 +23,7 @@
from orangecontrib.text.keywords import ScoringMethods, AggregationMethods, \
YAKE_LANGUAGE_MAPPING, RAKE_LANGUAGES, EMBEDDING_LANGUAGE_MAPPING
from orangecontrib.text.preprocess import BaseNormalizer
+from orangecontrib.text.widgets.utils import enum2int
from orangecontrib.text.widgets.utils.words import create_words_table, \
WORDS_COLUMN_NAME
@@ -143,7 +144,7 @@ class KeywordsTableModel(PyTableModel):
def data(self, index, role=Qt.DisplayRole):
if role in (gui.BarRatioRole, Qt.DisplayRole):
return super().data(index, Qt.EditRole)
- if role == Qt.BackgroundColorRole and index.column() == 0:
+ if role == Qt.BackgroundRole and index.column() == 0:
return TableModel.ColorForRole[TableModel.Meta]
return super().data(index, role)
@@ -166,10 +167,13 @@ def __nan_less_than(self, left_ind: QModelIndex, right_ind: QModelIndex,
right = self.sourceModel().data(right_ind, role=Qt.EditRole)
if isinstance(right, float) and isinstance(left, float):
# NaNs always at the end
+ # PyQt5's SortOrder is IntEnum (inherit from int) in PyQt6 it is Enum
+ # this solution is made that way that it works in both cases
+ is_descending = order == Qt.DescendingOrder
if np.isnan(right):
- right = 1 - order
+ right = 1 - is_descending
if np.isnan(left):
- left = 1 - order
+ left = 1 - is_descending
return left < right
return super().lessThan(left_ind, right_ind)
@@ -183,7 +187,9 @@ class OWKeywords(OWWidget, ConcurrentWidgetMixin):
buttons_area_orientation = Qt.Vertical
- DEFAULT_SORTING = (1, Qt.DescendingOrder)
+ # Qt.DescendingOrder is IntEnum in PyQt5 and Enum in PyQt6 (both have value attr)
+ # in setting we want to save integer and not Enum object (in case of PyQt6)
+ DEFAULT_SORTING = (1, enum2int(Qt.DescendingOrder))
settingsHandler = DomainContextHandler()
selected_scoring_methods: Set[str] = Setting({ScoringMethods.TF_IDF})
@@ -265,9 +271,7 @@ def _setup_gui(self):
button.setChecked(method == self.sel_method)
grid.addWidget(button, method, 0)
self.__sel_method_buttons.addButton(button, method)
- self.__sel_method_buttons.buttonClicked[int].connect(
- self._set_selection_method
- )
+ self.__sel_method_buttons.buttonClicked.connect(self._set_selection_method)
spin = gui.spin(
box, self, "n_selected", 1, 999, addToLayout=False,
@@ -291,14 +295,16 @@ def select_manual():
self.view = KeywordsTableView()
self.view.pressedAny.connect(select_manual)
- self.view.horizontalHeader().setSortIndicator(*self.DEFAULT_SORTING)
+ self.view.horizontalHeader().setSortIndicator(
+ self.DEFAULT_SORTING[0], Qt.SortOrder(self.DEFAULT_SORTING[1])
+ )
self.view.horizontalHeader().sectionClicked.connect(
self.__on_horizontal_header_clicked)
self.mainArea.layout().addWidget(self.view)
proxy = SortFilterProxyModel()
proxy.setFilterKeyColumn(0)
- proxy.setFilterCaseSensitivity(False)
+ proxy.setFilterCaseSensitivity(Qt.CaseInsensitive)
self.view.setModel(proxy)
self.view.model().setSourceModel(self.model)
self.view.selectionModel().selectionChanged.connect(
@@ -306,7 +312,9 @@ def select_manual():
)
def __on_scoring_method_state_changed(self, state: int, method_name: str):
- if state == Qt.Checked:
+ # state is int but Qt.Checked is IntEnum in PyQt5 and Enum in PyQt6
+ # if value is not transformed to CheckState comparison is False in PyQt6
+ if Qt.CheckState(state) == Qt.Checked:
self.selected_scoring_methods.add(method_name)
elif method_name in self.selected_scoring_methods:
self.selected_scoring_methods.remove(method_name)
@@ -337,7 +345,7 @@ def __on_filter_changed(self):
def __on_horizontal_header_clicked(self, index: int):
header = self.view.horizontalHeader()
- self.sort_column_order = (index, header.sortIndicatorOrder())
+ self.sort_column_order = (index, enum2int(header.sortIndicatorOrder()))
self._select_rows()
# explicitly call commit, because __on_selection_changed will not be
# invoked, since selection is actually the same, only order is not
@@ -398,9 +406,9 @@ def update_scores(self):
self.start(run, self.corpus, self.words, self.__cached_keywords,
self.selected_scoring_methods, kwargs, self.agg_method)
- def _set_selection_method(self, method: int):
- self.sel_method = method
- self.__sel_method_buttons.button(method).setChecked(True)
+ def _set_selection_method(self):
+ self.sel_method = self.__sel_method_buttons.checkedId()
+ self.__sel_method_buttons.button(self.sel_method).setChecked(True)
self._select_rows()
def _select_rows(self):
@@ -456,10 +464,12 @@ def _apply_sorting(self):
self.sort_column_order = self.DEFAULT_SORTING
header = self.view.horizontalHeader()
- current_sorting = (header.sortIndicatorSection(),
- header.sortIndicatorOrder())
- if current_sorting != self.sort_column_order:
- header.setSortIndicator(*self.sort_column_order)
+ # PyQt6's SortOrder is Enum (and not IntEnum as in PyQt5),
+ # transform sort_column_order[1], which is int, in Qt.SortOrder Enum
+ sco = (self.sort_column_order[0], Qt.SortOrder(self.sort_column_order[1]))
+ current_sorting = (header.sortIndicatorSection(), header.sortIndicatorOrder())
+ if current_sorting != sco:
+ header.setSortIndicator(*sco)
def onDeleteWidget(self):
self.shutdown()
@@ -474,7 +484,7 @@ def commit(self):
attrs = [ContinuousVariable(model.headerData(i, Qt.Horizontal))
for i in range(1, model.columnCount())]
- data = sorted(model, key=lambda a: a[sort_column], reverse=reverse)
+ data = sorted(model, key=lambda a: a[sort_column], reverse=bool(reverse))
words_data = [s[0] for s in data if s[0] in self.selected_words]
words = create_words_table(words_data)
diff --git a/orangecontrib/text/widgets/tests/test_owkeywords.py b/orangecontrib/text/widgets/tests/test_owkeywords.py
index 60489ecbd..171db94b8 100644
--- a/orangecontrib/text/widgets/tests/test_owkeywords.py
+++ b/orangecontrib/text/widgets/tests/test_owkeywords.py
@@ -4,6 +4,7 @@
from unittest.mock import Mock, patch
import numpy as np
+from AnyQt.QtWidgets import QCheckBox
from Orange.data import Table
from Orange.widgets.tests.base import WidgetTest, simulate
@@ -206,6 +207,20 @@ def dummy_embedding(tokens, language, progress_callback=None):
self.assertEqual(m[2][1].call_args[1]["language"], "Finnish")
self.assertEqual(m[3][1].call_args[1]["language"], "Kazakh")
+ def test_method_change(self):
+ """Test method change by clicking"""
+ self.send_signal(self.widget.Inputs.corpus, self.corpus)
+ out = self.get_output(self.widget.Outputs.words)
+ self.assertEqual({"TF-IDF"}, {a.name for a in out.domain.attributes})
+
+ self.widget.controlArea.findChildren(QCheckBox)[1].click() # yake cb
+ out = self.get_output(self.widget.Outputs.words)
+ self.assertEqual({"TF-IDF", "YAKE!"}, {a.name for a in out.domain.attributes})
+
+ self.widget.controlArea.findChildren(QCheckBox)[1].click()
+ out = self.get_output(self.widget.Outputs.words)
+ self.assertEqual({"TF-IDF"}, {a.name for a in out.domain.attributes})
+
def test_send_report(self):
self.send_signal(self.widget.Inputs.corpus, self.corpus)
self.wait_until_finished()
diff --git a/orangecontrib/text/widgets/utils/__init__.py b/orangecontrib/text/widgets/utils/__init__.py
index c7b2a6b05..eba92aea3 100644
--- a/orangecontrib/text/widgets/utils/__init__.py
+++ b/orangecontrib/text/widgets/utils/__init__.py
@@ -1,3 +1,24 @@
+from enum import IntEnum, Enum
+from typing import Union
+
from .decorators import *
from .widgets import *
from .concurrent import asynchronous
+
+
+def enum2int(enum: Union[Enum, IntEnum]) -> int:
+ """
+ PyQt5 uses IntEnum like object for settings, for example SortOrder while
+ PyQt6 uses Enum. PyQt5's IntEnum also does not support value attribute.
+ This function transform both settings objects to int.
+
+ Parameters
+ ----------
+ enum
+ IntEnum like object or Enum object with Qt's settings
+
+ Returns
+ -------
+ Settings transformed to int
+ """
+ return int(enum) if isinstance(enum, int) else enum.value
From 7b7b4baf1199db1e06a70c8cf07314fec39bf53e Mon Sep 17 00:00:00 2001
From: PrimozGodec
Date: Tue, 17 Jan 2023 13:20:53 +0100
Subject: [PATCH 2/3] Preprocess Text - PyQt6 compatibility
---
orangecontrib/text/widgets/owpreprocess.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/orangecontrib/text/widgets/owpreprocess.py b/orangecontrib/text/widgets/owpreprocess.py
index 5dbe66348..6b6efde82 100644
--- a/orangecontrib/text/widgets/owpreprocess.py
+++ b/orangecontrib/text/widgets/owpreprocess.py
@@ -9,7 +9,7 @@
QRadioButton, QGridLayout, QLineEdit, QSpinBox, QFormLayout, QHBoxLayout, \
QDoubleSpinBox, QFileDialog, QAbstractSpinBox
from AnyQt.QtWidgets import QWidget, QPushButton, QSizePolicy, QStyle
-from AnyQt.QtGui import QBrush
+from AnyQt.QtGui import QBrush, QValidator
from Orange.util import wrap_callback
from orangewidget.utils.filedialogs import RecentPath
@@ -137,8 +137,10 @@ class RangeSpecialValueSpins(RangeSpins):
class SpinBox(QSpinBox):
def validate(self, *args):
# accept empty input
+ st = QValidator.State
valid, text, pos = super().validate(*args)
- return 2 if valid else 0, text, pos
+ new_state = st.Acceptable if valid != st.Invalid else st.Invalid
+ return new_state, text, pos
def valueFromText(self, text: str) -> int:
return max(int(text) if text else self.minimum(), self.minimum())
@@ -242,8 +244,7 @@ def set_file_list(self):
self.file_combo.addItem(recent.basename)
self.file_combo.model().item(i).setToolTip(recent.abspath)
if not os.path.exists(recent.abspath):
- self.file_combo.setItemData(i, QBrush(Qt.red),
- Qt.TextColorRole)
+ self.file_combo.setItemData(i, QBrush(Qt.red), Qt.ForegroundRole)
self.file_combo.addItem(_DEFAULT_NONE)
def last_path(self) -> Optional[str]:
From 484fe584f817339787d3d9d56631821425b8d509 Mon Sep 17 00:00:00 2001
From: PrimozGodec
Date: Tue, 17 Jan 2023 14:37:04 +0100
Subject: [PATCH 3/3] Score Documents - PyQt6 compatibility
---
orangecontrib/text/widgets/owscoredocuments.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/orangecontrib/text/widgets/owscoredocuments.py b/orangecontrib/text/widgets/owscoredocuments.py
index f9446422a..2cee3178a 100644
--- a/orangecontrib/text/widgets/owscoredocuments.py
+++ b/orangecontrib/text/widgets/owscoredocuments.py
@@ -38,6 +38,7 @@
LANGS_TO_ISO,
DocumentEmbedder,
)
+from orangecontrib.text.widgets.utils import enum2int
from orangecontrib.text.widgets.utils.words import create_words_table
def _word_frequency(corpus: Corpus, words: List[str], callback: Callable) -> np.ndarray:
@@ -317,7 +318,7 @@ class OWScoreDocuments(OWWidget, ConcurrentWidgetMixin):
buttons_area_orientation = Qt.Vertical
# default order - table sorted in input order
- DEFAULT_SORTING = (-1, Qt.AscendingOrder)
+ DEFAULT_SORTING = (-1, enum2int(Qt.AscendingOrder))
settingsHandler = PerfectDomainContextHandler()
auto_commit: bool = Setting(True)
@@ -451,7 +452,7 @@ def __on_filter_changed(self) -> None:
def __on_horizontal_header_clicked(self, index: int):
header = self.view.horizontalHeader()
- self.sort_column_order = (index, header.sortIndicatorOrder())
+ self.sort_column_order = (index, enum2int(header.sortIndicatorOrder()))
self._select_rows()
# when sorting change output table must consider the new order
# call explicitly since selection in table is not changed
@@ -595,7 +596,10 @@ def _fill_table(self) -> None:
# if not enough columns do not apply sorting from settings since
# sorting can besaved for score column while scores are still computing
# tables is filled before scores are computed with document names
- self.view.horizontalHeader().setSortIndicator(*self.sort_column_order)
+ # PyQt6's SortOrder is Enum (and not IntEnum as in PyQt5),
+ # transform sort_column_order[1], which is int, in Qt.SortOrder Enum
+ sco = (self.sort_column_order[0], Qt.SortOrder(self.sort_column_order[1]))
+ self.view.horizontalHeader().setSortIndicator(*sco)
self._select_rows()