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)