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()