From c5563f5ad87353fcdbb27a306e8fc9158742fb00 Mon Sep 17 00:00:00 2001 From: PrimozGodec Date: Tue, 17 Jan 2023 15:27:43 +0100 Subject: [PATCH 1/2] Rank - make sorting PyQt6 compatible --- Orange/widgets/data/owrank.py | 17 ++++++++--- Orange/widgets/data/tests/test_owrank.py | 39 ++++++++++++++++++++++-- Orange/widgets/utils/__init__.py | 22 +++++++++++-- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/Orange/widgets/data/owrank.py b/Orange/widgets/data/owrank.py index f326601d813..46b87a49530 100644 --- a/Orange/widgets/data/owrank.py +++ b/Orange/widgets/data/owrank.py @@ -29,6 +29,7 @@ ContextSetting, DomainContextHandler, Setting ) from Orange.widgets.unsupervised.owdistances import InterruptException +from Orange.widgets.utils import enum2int from Orange.widgets.utils.concurrent import ConcurrentWidgetMixin, TaskState from Orange.widgets.utils.sql import check_sql_input from Orange.widgets.utils.itemmodels import VariableListModel @@ -226,7 +227,7 @@ class Outputs: nSelected = ContextSetting(5) auto_apply = Setting(True) - sorting = Setting((0, Qt.DescendingOrder)) + sorting = Setting((0, enum2int(Qt.DescendingOrder))) selected_methods = Setting(set()) settings_version = 3 @@ -496,9 +497,11 @@ def on_done(self, result: Results) -> None: sort_column, sort_order = self.sorting if sort_column < len(labels): # adds 2 to skip the first two columns - self.ranksModel.sort(sort_column + 2, sort_order) + # Qt.SortOrder is Enum in PyQt6 and int-like object in PyQt5 + # in both cases Qt.SortOrder transforms int sort_order to required type + self.ranksModel.sort(sort_column + 2, Qt.SortOrder(sort_order)) self.ranksView.horizontalHeader().setSortIndicator( - sort_column + 2, sort_order + sort_column + 2, Qt.SortOrder(sort_order) ) except ValueError: pass @@ -561,7 +564,7 @@ def headerClick(self, index): self.autoSelection() # Store the header states - sort_order = self.ranksModel.sortOrder() + sort_order = enum2int(self.ranksModel.sortOrder()) sort_column = self.ranksModel.sortColumn() - 2 # -2 for name and '#' columns self.sorting = (sort_column, sort_order) @@ -640,6 +643,12 @@ def migrate_settings(cls, settings, version): column, order = hview.sortIndicatorSection() - 1, hview.sortIndicatorOrder() settings["sorting"] = (column, order) + # before we saved sort order as Qt.SortOrder object, now it is integer + # help users with SortOrder as setting migrate to int setting + if "sorting" in settings: + column, order = settings["sorting"] + settings["sorting"] = (column, enum2int(order)) + @classmethod def migrate_context(cls, context, version): if version is None or version < 3: diff --git a/Orange/widgets/data/tests/test_owrank.py b/Orange/widgets/data/tests/test_owrank.py index 6321e9d4c22..33e75dc75d6 100644 --- a/Orange/widgets/data/tests/test_owrank.py +++ b/Orange/widgets/data/tests/test_owrank.py @@ -2,6 +2,7 @@ import time import warnings import unittest +from enum import Enum from itertools import count from unittest.mock import patch @@ -374,6 +375,41 @@ def test_scores_sorting(self): order2 = self.widget.ranksModel.mapToSourceRows(...).tolist() self.assertNotEqual(order1, order2) + def test_score_sorting_int(self): + """ + Order setting was previously set to Qt.SortOrder which is in PyQt5 + int-like PyQt object. Since in PyQt6 it is Enum (non int) object, and it + is not nice to have objects in settings we changed it to int. This test + cover current case and also case with int-like object before. + """ + self.widget.sorting = (1, 1) # Gini col, descending order + self.send_signal(self.widget.Inputs.data, self.iris) + self.wait_until_finished() + order = self.widget.ranksModel.mapToSourceRows(...).tolist() + self.assertListEqual([2, 3, 0, 1], order) + + self.widget.sorting = (1, 0) # Gini col, descending order + self.send_signal(self.widget.Inputs.data, self.iris) + self.wait_until_finished() + order = self.widget.ranksModel.mapToSourceRows(...).tolist() + self.assertListEqual([1, 0, 3, 2], order) + + # change old setting to int + # since test can run in both pyqt5 or 6 we create SortOrder like object + class SortOrderE(Enum): # pyqt6 like + ASCENDING = 0 + + settings = {"sorting": (1, SortOrderE.ASCENDING), "__version__": 2} + w = self.create_widget(OWRank, stored_settings=settings) + self.assertEqual(0, w.sorting[1]) + + class SortOrderI: # pyqt5 like + ASCENDING = 0 + + settings = {"sorting": (1, SortOrderI.ASCENDING), "__version__": 2} + w = self.create_widget(OWRank, stored_settings=settings) + self.assertEqual(0, w.sorting[1]) + def test_scores_nan_sorting(self): """Check NaNs are sorted last""" data = self.iris.copy() @@ -383,8 +419,7 @@ def test_scores_nan_sorting(self): self.wait_until_finished() # Assert last row is all nan - for order in (Qt.AscendingOrder, - Qt.DescendingOrder): + for order in (Qt.AscendingOrder, Qt.DescendingOrder): self.widget.ranksView.horizontalHeader().setSortIndicator(2, order) last_row = self.widget.ranksModel[self.widget.ranksModel.mapToSourceRows(...)[-1]] np.testing.assert_array_equal(last_row[1:], np.repeat(np.nan, 3)) diff --git a/Orange/widgets/utils/__init__.py b/Orange/widgets/utils/__init__.py index 90bee93ca33..5b6fdb5e180 100644 --- a/Orange/widgets/utils/__init__.py +++ b/Orange/widgets/utils/__init__.py @@ -1,7 +1,7 @@ -import enum import inspect import sys from collections import deque +from enum import Enum, IntEnum from typing import ( TypeVar, Callable, Any, Iterable, Optional, Hashable, Type, Union, Tuple ) @@ -91,7 +91,7 @@ def qname(type_: type) -> str: _T1 = TypeVar("_T1") # pylint: disable=invalid-name -_E = TypeVar("_E", bound=enum.Enum) # pylint: disable=invalid-name +_E = TypeVar("_E", bound=Enum) # pylint: disable=invalid-name _A = TypeVar("_A") # pylint: disable=invalid-name _B = TypeVar("_B") # pylint: disable=invalid-name @@ -176,3 +176,21 @@ def show_part(_point_data, singular, plural, max_shown, _vars): ("Meta", "Metas", 4, domain.metas), ("Feature", "Features", 10, domain.attributes)) return "
".join(show_part(row, *columns) for columns in parts) + + +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 db73b530c23af3888e2fee232b8768df348d3a6c Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Thu, 9 Feb 2023 08:32:06 +0100 Subject: [PATCH 2/2] Rank - bump settings_version to signify selection order as int --- Orange/widgets/data/owrank.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Orange/widgets/data/owrank.py b/Orange/widgets/data/owrank.py index 46b87a49530..becc8db4cb4 100644 --- a/Orange/widgets/data/owrank.py +++ b/Orange/widgets/data/owrank.py @@ -230,7 +230,7 @@ class Outputs: sorting = Setting((0, enum2int(Qt.DescendingOrder))) selected_methods = Setting(set()) - settings_version = 3 + settings_version = 4 settingsHandler = DomainContextHandler() selected_attrs = ContextSetting([], schema_only=True) selectionMethod = ContextSetting(SelectNBest)