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

[FIX] Make widgets PyQt6 compatible #929

Merged
merged 3 commits into from
Jan 30, 2023
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
48 changes: 29 additions & 19 deletions orangecontrib/text/widgets/owkeywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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})
Expand Down Expand Up @@ -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,
Expand All @@ -291,22 +295,26 @@ 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(
self.__on_selection_changed
)

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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions orangecontrib/text/widgets/owpreprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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]:
Expand Down
10 changes: 7 additions & 3 deletions orangecontrib/text/widgets/owscoredocuments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
15 changes: 15 additions & 0 deletions orangecontrib/text/widgets/tests/test_owkeywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
21 changes: 21 additions & 0 deletions orangecontrib/text/widgets/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -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