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] Rank - make sorting setting PyQt6 compatible #6301

Merged
merged 2 commits into from
Feb 9, 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
19 changes: 14 additions & 5 deletions Orange/widgets/data/owrank.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -226,10 +227,10 @@ 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
settings_version = 4
settingsHandler = DomainContextHandler()
selected_attrs = ContextSetting([], schema_only=True)
selectionMethod = ContextSetting(SelectNBest)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down
39 changes: 37 additions & 2 deletions Orange/widgets/data/tests/test_owrank.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import time
import warnings
import unittest
from enum import Enum
from itertools import count
from unittest.mock import patch

Expand Down Expand Up @@ -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()
Expand All @@ -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))
Expand Down
22 changes: 20 additions & 2 deletions Orange/widgets/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -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
)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 "<br/>".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