diff --git a/Orange/widgets/data/owcolor.py b/Orange/widgets/data/owcolor.py index 3028cca3ce2..a385a238b4d 100644 --- a/Orange/widgets/data/owcolor.py +++ b/Orange/widgets/data/owcolor.py @@ -1,6 +1,7 @@ import os from itertools import chain import json +from typing import List import numpy as np @@ -10,7 +11,7 @@ from AnyQt.QtWidgets import QHeaderView, QColorDialog, QTableView, QComboBox, \ QFileDialog, QMessageBox -from orangewidget.settings import IncompatibleContext +from orangewidget.settings import IncompatibleContext, TypeSupport import Orange from Orange.preprocess.transformation import Identity @@ -232,6 +233,48 @@ def from_dict(cls, var, data): return obj, warnings +class AttrDescTypeSupport(TypeSupport): + @classmethod + def pack_value(cls, value, _=None): + packed = {k: list(tuple(x) for x in v) if k == "new_colors" else v + for k, v in value.__dict__.items() + if k.startswith("new_") and v is not None} + packed["var"] = value.var.name + return packed + + @classmethod + def unpack_value(cls, value, _, domain, *_a): + var = domain[value["var"]] + desc = cls.supported_types[0](var) + if "new_name" in value: + desc.name = value["new_name"] + return desc + + +class DiscAttrDescTypeSupport(AttrDescTypeSupport): + supported_types = (DiscAttrDesc, ) + + @classmethod + def unpack_value(cls, value, tp, domain, *_): + desc = super().unpack_value(value, tp, domain) + for i, color in enumerate(value.get("new_colors", ())): + desc.set_color(i, color) + for i, color in enumerate(value.get("new_values", ())): + desc.set_value(i, color) + return desc + + +class ContAttrDescTypeSupport(AttrDescTypeSupport): + supported_types = (ContAttrDesc, ) + + @classmethod + def unpack_value(cls, value, tp, domain, *_): + desc = super().unpack_value(value, tp, domain) + if "new_palette_name" in value: + desc.palette_name = value["new_palette_name"] + return desc + + class ColorTableModel(QAbstractTableModel): """ Base color model for discrete and continuous variables. The model handles: @@ -547,12 +590,12 @@ class Outputs: settingsHandler = settings.PerfectDomainContextHandler( match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL) - disc_descs = settings.ContextSetting([]) - cont_descs = settings.ContextSetting([]) + disc_descs: List[DiscAttrDesc] = settings.ContextSetting([]) + cont_descs: List[ContAttrDesc] = settings.ContextSetting([]) selected_schema_index = settings.Setting(0) auto_apply = settings.Setting(True) - settings_version = 2 + settings_version = 3 want_main_area = False @@ -801,9 +844,17 @@ def was(n, o): self.report_raw(f"{table}
") @classmethod - def migrate_context(cls, _, version): + def migrate_context(cls, context, version): if not version or version < 2: raise IncompatibleContext + if version < 3: + values = context.values + values["disc_descs"] = [ + DiscAttrDescTypeSupport.pack_value(desc) + for desc in values["disc_descs"]] + values["cont_descs"] = [ + ContAttrDescTypeSupport.pack_value(desc) + for desc in values["cont_descs"]] if __name__ == "__main__": # pragma: no cover diff --git a/Orange/widgets/data/owcorrelations.py b/Orange/widgets/data/owcorrelations.py index 427d6545e83..9d9a4cde897 100644 --- a/Orange/widgets/data/owcorrelations.py +++ b/Orange/widgets/data/owcorrelations.py @@ -5,6 +5,7 @@ from operator import attrgetter from types import SimpleNamespace from itertools import combinations, groupby, chain +from typing import List import numpy as np from scipy.stats import spearmanr, pearsonr @@ -254,8 +255,8 @@ class Outputs: settings_version = 3 settingsHandler = DomainContextHandler() - selection = ContextSetting([]) - feature = ContextSetting(None) + selection: List[ContinuousVariable] = ContextSetting([]) + feature: ContinuousVariable = ContextSetting(None) correlation_type = Setting(0) class Information(OWWidget.Information): diff --git a/Orange/widgets/data/owcreateclass.py b/Orange/widgets/data/owcreateclass.py index 5d704e0739a..52d976b5e77 100644 --- a/Orange/widgets/data/owcreateclass.py +++ b/Orange/widgets/data/owcreateclass.py @@ -1,13 +1,14 @@ """Widget for creating classes from non-numeric attribute by substrings""" import re from itertools import count +from typing import List, Dict import numpy as np from AnyQt.QtWidgets import QGridLayout, QLabel, QLineEdit, QSizePolicy, QWidget from AnyQt.QtCore import QSize, Qt -from Orange.data import StringVariable, DiscreteVariable, Domain +from Orange.data import StringVariable, DiscreteVariable, Domain, Variable from Orange.data.table import Table from Orange.statistics.util import bincount from Orange.preprocess.transformation import Transformation, Lookup @@ -176,9 +177,9 @@ class Outputs: buttons_area_orientation = Qt.Vertical settingsHandler = DomainContextHandler() - attribute = ContextSetting(None) + attribute: Variable = ContextSetting(None) class_name = ContextSetting("class") - rules = ContextSetting({}) + rules: Dict[str, List[List[str]]] = ContextSetting({}) match_beginning = ContextSetting(False) case_sensitive = ContextSetting(False) diff --git a/Orange/widgets/data/owdatasets.py b/Orange/widgets/data/owdatasets.py index caf3cafb5cd..aac0200851c 100644 --- a/Orange/widgets/data/owdatasets.py +++ b/Orange/widgets/data/owdatasets.py @@ -180,7 +180,7 @@ class Outputs: data = Output("Data", Orange.data.Table) #: Selected dataset id - selected_id = settings.Setting(None) # type: Optional[str] + selected_id: Optional[str] = settings.Setting(None) #: main area splitter state splitter_state = settings.Setting(b'') # type: bytes diff --git a/Orange/widgets/data/owfeaturestatistics.py b/Orange/widgets/data/owfeaturestatistics.py index 91ab9a3f3ad..53542cdf543 100644 --- a/Orange/widgets/data/owfeaturestatistics.py +++ b/Orange/widgets/data/owfeaturestatistics.py @@ -716,11 +716,11 @@ class Outputs: settings_version = 2 auto_commit = Setting(True) - color_var = ContextSetting(None) # type: Optional[Variable] + color_var: Optional[Variable] = ContextSetting(None) # filter_string = ContextSetting('') - sorting = Setting((0, Qt.DescendingOrder)) - selected_vars = ContextSetting([], schema_only=True) + sorting: Tuple[int, int] = Setting((0, Qt.DescendingOrder)) + selected_vars: List[Variable] = ContextSetting([], schema_only=True) def __init__(self): super().__init__() @@ -859,8 +859,9 @@ def commit(self): # Send a table with only selected columns to output variables = self.selected_vars - self.info.set_output_summary(len(self.data[:, variables]), - format_summary_details(self.data[:, variables])) + reduced_data = self.data[:, variables] + self.info.set_output_summary(len(reduced_data), + format_summary_details(reduced_data)) self.Outputs.reduced_data.send(self.data[:, variables]) # Send the statistics of the selected variables to ouput @@ -895,7 +896,7 @@ def migrate_context(cls, context, version): # is no suitable conversion function, and StringVariable (3) # was the only hidden var when settings_version < 2, so: if tpe != 3] - selected_vars = [all_vars[i] for i in selected_rows] + selected_vars = [all_vars[i] for i in selected_rows[0]] context.values["selected_vars"] = selected_vars, -3 diff --git a/Orange/widgets/data/owfile.py b/Orange/widgets/data/owfile.py index 31f6ee147eb..fa4ec02b7dc 100644 --- a/Orange/widgets/data/owfile.py +++ b/Orange/widgets/data/owfile.py @@ -2,7 +2,7 @@ import logging from itertools import chain from urllib.parse import urlparse -from typing import List +from typing import List, Dict import numpy as np from AnyQt.QtWidgets import \ @@ -14,8 +14,9 @@ from Orange.data.io import FileFormat, UrlReader, class_from_qualified_name from Orange.util import log_warnings from Orange.widgets import widget, gui -from Orange.widgets.settings import Setting, ContextSetting, \ - PerfectDomainContextHandler, SettingProvider +from Orange.widgets.settings import \ + Setting, PerfectDomainContextHandler, SettingProvider +from Orange.widgets.utils import vartype from Orange.widgets.utils.domaineditor import DomainEditor from Orange.widgets.utils.itemmodels import PyListModel from Orange.widgets.utils.filedialogs import RecentPathsWComboMixin, \ @@ -23,13 +24,8 @@ from Orange.widgets.utils.widgetpreview import WidgetPreview from Orange.widgets.utils.state_summary import format_summary_details from Orange.widgets.widget import Output, Msg - -# Backward compatibility: class RecentPath used to be defined in this module, -# and it is used in saved (pickled) settings. It must be imported into the -# module's namespace so that old saved settings still work from Orange.widgets.utils.filedialogs import RecentPath - log = logging.getLogger(__name__) @@ -95,10 +91,11 @@ class Outputs: LOCAL_FILE, URL = range(2) settingsHandler = PerfectDomainContextHandler( - match_values=PerfectDomainContextHandler.MATCH_VALUES_ALL - ) + match_values=PerfectDomainContextHandler.MATCH_VALUES_ALL) # pylint seems to want declarations separated from definitions + # This must be in the widget, not the mixin - otherwise all widgets will + # share the same paths recent_paths: List[RecentPath] recent_urls: List[str] variables: list @@ -114,11 +111,9 @@ class Outputs: ]) recent_urls = Setting([]) source = Setting(LOCAL_FILE) - sheet_names = Setting({}) + sheet_names: Dict[str, str] = Setting({}) url = Setting("") - variables = ContextSetting([]) - domain_editor = SettingProvider(DomainEditor) class Warning(widget.OWWidget.Warning): @@ -471,13 +466,6 @@ def missing_prop(prop): f"Last entry: {table[-1, 'Timestamp']}

" return text - def storeSpecificSettings(self): - self.current_context.modified_variables = self.variables[:] - - def retrieveSpecificSettings(self): - if hasattr(self.current_context, "modified_variables"): - self.variables[:] = self.current_context.modified_variables - def reset_domain_edit(self): self.domain_editor.reset_domain() self.apply_domain_edit() @@ -578,6 +566,13 @@ def workflowEnvChanged(self, key, value, oldvalue): """ self.update_file_list(key, value, oldvalue) + @classmethod + def migrate_context(cls, context, _): + if hasattr(context, "modified_variables"): + delattr(context, "modified_variables") + de_vars = context.values["domain_editor"]["variables"] + de_vars[:] = [(x[0], vartype(x[1]), *x[2:]) for x in de_vars] + if __name__ == "__main__": # pragma: no cover WidgetPreview(OWFile).run() diff --git a/Orange/widgets/data/owmergedata.py b/Orange/widgets/data/owmergedata.py index 7080dea450b..2204ed9e005 100644 --- a/Orange/widgets/data/owmergedata.py +++ b/Orange/widgets/data/owmergedata.py @@ -1,5 +1,6 @@ from collections import namedtuple from itertools import chain, product +from typing import List, Tuple, Dict import numpy as np @@ -7,6 +8,8 @@ from AnyQt.QtWidgets import ( QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout ) + +from orangewidget.settings import Context from orangewidget.utils.combobox import ComboBoxSearch import Orange @@ -173,12 +176,19 @@ def data(self, index, role=Qt.DisplayRole): return super().data(index, role) +class MergeDataContext(Context): + variables1: Dict[str, int] + variables2: Dict[str, int] + + class MergeDataContextHandler(ContextHandler): # `widget` is used as an argument in most methods # pylint: disable=redefined-outer-name # context handlers override methods using different signatures # pylint: disable=arguments-differ + ContextType = MergeDataContext + def new_context(self, variables1, variables2): context = super().new_context() context.variables1 = variables1 @@ -286,7 +296,8 @@ class Outputs: "merging_types")] settingsHandler = MergeDataContextHandler() - attr_pairs = ContextSetting(None, schema_only=True) + attr_pairs: List[Tuple[Variable, Variable]] \ + = ContextSetting([], schema_only=True) merging = Setting(LeftJoin) auto_apply = Setting(True) settings_version = 2 diff --git a/Orange/widgets/data/owpaintdata.py b/Orange/widgets/data/owpaintdata.py index 5fcdf7fd36c..ae8014040d1 100644 --- a/Orange/widgets/data/owpaintdata.py +++ b/Orange/widgets/data/owpaintdata.py @@ -4,6 +4,7 @@ import itertools from functools import partial, singledispatch from collections import namedtuple +from typing import List import numpy as np @@ -761,8 +762,8 @@ class Outputs: symbol_size = Setting(10) #: current data array (shape=(N, 3)) as presented on the output - data = Setting(None, schema_only=True) - labels = Setting(["C1", "C2"], schema_only=True) + data: List[List[float]] = Setting(None, schema_only=True) + labels: List[str] = Setting(["C1", "C2"], schema_only=True) buttons_area_orientation = Qt.Vertical graph_name = "plot" diff --git a/Orange/widgets/data/owpivot.py b/Orange/widgets/data/owpivot.py index 9b04718d53c..d1ee8bd166a 100644 --- a/Orange/widgets/data/owpivot.py +++ b/Orange/widgets/data/owpivot.py @@ -1,5 +1,6 @@ # pylint: disable=missing-docstring -from typing import Iterable, Set +from enum import IntEnum +from typing import Iterable, Set, Tuple from collections import defaultdict from itertools import product, chain @@ -20,7 +21,6 @@ from Orange.data.filter import FilterContinuous, FilterDiscrete, Values from Orange.statistics.util import (nanmin, nanmax, nanunique, nansum, nanvar, nanmean, nanmedian, nanmode, bincount) -from Orange.util import Enum from Orange.widgets import gui from Orange.widgets.settings import (Setting, ContextSetting, DomainContextHandler) @@ -36,7 +36,7 @@ BorderColorRole = next(gui.OrangeUserRole) -class AggregationFunctionsEnum(Enum): +class AggregationFunctionsEnum(IntEnum): (Count, Count_defined, Sum, Mean, Min, Max, Mode, Median, Var, Majority) = range(10) @@ -45,7 +45,7 @@ def __init__(self, *_, **__): self.func = None @property - def value(self): + def value(self): # pylint: disable=invalid-overridden-method return self._value_ def __call__(self, *args): @@ -748,11 +748,11 @@ class Warning(OWWidget.Warning): too_many_values = Msg("Selected variable has too many values.") settingsHandler = DomainContextHandler() - row_feature = ContextSetting(None) - col_feature = ContextSetting(None) - val_feature = ContextSetting(None) - sel_agg_functions = Setting(set([Pivot.Count])) - selection = Setting(set(), schema_only=True) + row_feature: Variable = ContextSetting(None) + col_feature: DiscreteVariable = ContextSetting(None) + val_feature: Variable = ContextSetting(None) + sel_agg_functions: Set[AggregationFunctionsEnum] = Setting({Pivot.Count}) + selection: Set[Tuple[int, int]] = Setting(set(), schema_only=True) auto_commit = Setting(True) AGGREGATIONS = (Pivot.Count, diff --git a/Orange/widgets/data/owrank.py b/Orange/widgets/data/owrank.py index 640da176c06..1cfd7d411c2 100644 --- a/Orange/widgets/data/owrank.py +++ b/Orange/widgets/data/owrank.py @@ -4,7 +4,7 @@ from functools import partial from itertools import chain from types import SimpleNamespace -from typing import Any, Callable, List, Tuple +from typing import Any, Callable, List, Tuple, Set import numpy as np from AnyQt.QtCore import ( @@ -20,8 +20,8 @@ from scipy.sparse import issparse from Orange.data import ( - ContinuousVariable, DiscreteVariable, Domain, StringVariable, Table -) + Table, Domain, + ContinuousVariable, DiscreteVariable, StringVariable, Variable) from Orange.data.util import get_unique_names_duplicates from Orange.preprocess import score from Orange.widgets import gui, report @@ -269,12 +269,12 @@ class Outputs: nSelected = ContextSetting(5) auto_apply = Setting(True) - sorting = Setting((0, Qt.DescendingOrder)) - selected_methods = Setting(set()) + sorting: Tuple[int, int] = Setting((0, Qt.DescendingOrder)) + selected_methods: Set[str] = Setting(set()) settings_version = 3 settingsHandler = DomainContextHandler() - selected_attrs = ContextSetting([], schema_only=True) + selected_attrs: List[Variable] = ContextSetting([], schema_only=True) selectionMethod = ContextSetting(SelectNBest) class Information(OWWidget.Information): diff --git a/Orange/widgets/data/owselectcolumns.py b/Orange/widgets/data/owselectcolumns.py index 3b1b1f1d108..aea3e611585 100644 --- a/Orange/widgets/data/owselectcolumns.py +++ b/Orange/widgets/data/owselectcolumns.py @@ -166,7 +166,7 @@ class Outputs: want_control_area = True settingsHandler = SelectAttributesDomainContextHandler(first_match=False) - domain_role_hints = ContextSetting({}) + domain_role_hints: Dict[Tuple[str, int], Tuple[str, int]] = ContextSetting({}) use_input_features = Setting(False) ignore_new_features = Setting(False) auto_commit = Setting(True) diff --git a/Orange/widgets/data/owtable.py b/Orange/widgets/data/owtable.py index 7ebaa6da5c3..e7d91745ad3 100644 --- a/Orange/widgets/data/owtable.py +++ b/Orange/widgets/data/owtable.py @@ -6,6 +6,7 @@ from collections import OrderedDict, namedtuple from math import isnan +from typing import List import numpy from scipy.sparse import issparse @@ -177,6 +178,9 @@ class OWDataTable(OWWidget): priority = 50 keywords = [] + buttons_area_orientation = Qt.Vertical + dist_color_RGB = (220, 220, 220, 255) + class Inputs: data = Input("Data", Table, multiple=True) @@ -184,17 +188,14 @@ class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) - buttons_area_orientation = Qt.Vertical - show_distributions = Setting(False) - dist_color_RGB = Setting((220, 220, 220, 255)) show_attribute_labels = Setting(True) select_rows = Setting(True) auto_commit = Setting(True) color_by_class = Setting(True) - selected_rows = Setting([], schema_only=True) - selected_cols = Setting([], schema_only=True) + selected_rows: List[int] = Setting([], schema_only=True) + selected_cols: List[int] = Setting([], schema_only=True) settings_version = 2 diff --git a/Orange/widgets/data/owtranspose.py b/Orange/widgets/data/owtranspose.py index cbbb84bd552..81eb7c891d9 100644 --- a/Orange/widgets/data/owtranspose.py +++ b/Orange/widgets/data/owtranspose.py @@ -1,4 +1,4 @@ -from Orange.data import Table, ContinuousVariable, StringVariable +from Orange.data import Table, ContinuousVariable, StringVariable, Variable from Orange.widgets.settings import (Setting, ContextSetting, DomainContextHandler) from Orange.widgets.utils.itemmodels import DomainModel @@ -32,7 +32,7 @@ class Outputs: settingsHandler = DomainContextHandler() feature_type = ContextSetting(GENERIC) feature_name = ContextSetting("") - feature_names_column = ContextSetting(None) + feature_names_column: Variable = ContextSetting(None) auto_apply = Setting(True) class Warning(OWWidget.Warning): diff --git a/Orange/widgets/data/tests/test_owfeaturestatistics.py b/Orange/widgets/data/tests/test_owfeaturestatistics.py index 269a6e543c2..42f3c808d10 100644 --- a/Orange/widgets/data/tests/test_owfeaturestatistics.py +++ b/Orange/widgets/data/tests/test_owfeaturestatistics.py @@ -476,7 +476,7 @@ def test_restores_previous_selection(self): def test_settings_migration_to_ver21(self): settings = { - 'controlAreaVisible': True, 'savedWidgetGeometry': '', + 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1, 'context_settings': [ Context( diff --git a/Orange/widgets/evaluate/owpredictions.py b/Orange/widgets/evaluate/owpredictions.py index 483961e7ee7..19394ee3a70 100644 --- a/Orange/widgets/evaluate/owpredictions.py +++ b/Orange/widgets/evaluate/owpredictions.py @@ -73,8 +73,8 @@ class Error(OWWidget.Error): score_table = settings.SettingProvider(ScoreTable) #: List of selected class value indices in the `class_values` list - selected_classes = settings.ContextSetting([]) - selection = settings.Setting([], schema_only=True) + selected_classes: List[int] = settings.ContextSetting([]) + selection: List[int] = settings.Setting([], schema_only=True) def __init__(self): super().__init__() diff --git a/Orange/widgets/evaluate/utils.py b/Orange/widgets/evaluate/utils.py index b22dbfdea23..98372700cf1 100644 --- a/Orange/widgets/evaluate/utils.py +++ b/Orange/widgets/evaluate/utils.py @@ -1,6 +1,7 @@ import warnings from functools import partial from itertools import chain +from typing import Set import numpy as np @@ -131,7 +132,7 @@ def is_bad(x): class ScoreTable(OWComponent, QObject): - shown_scores = \ + shown_scores: Set[str] = \ Setting(set(chain(*BUILTIN_SCORERS_ORDER.values()))) shownScoresChanged = Signal() diff --git a/Orange/widgets/settings.py b/Orange/widgets/settings.py index 3bbe31eb2c0..71fdff99896 100644 --- a/Orange/widgets/settings.py +++ b/Orange/widgets/settings.py @@ -36,13 +36,13 @@ import itertools import logging import warnings +from typing import Tuple, Dict, Union, List, Optional from orangewidget.settings import ( Setting, SettingProvider, SettingsHandler, ContextSetting, ContextHandler, Context, IncompatibleContext, SettingsPrinter, - rename_setting, widget_settings_dir -) -from orangewidget.settings import _apply_setting + rename_setting, widget_settings_dir, + TypeSupport) from Orange.data import Domain, Variable from Orange.util import OrangeDeprecationWarning @@ -62,11 +62,58 @@ ] +class VariableTypeSupport(TypeSupport): + supported_types = (Variable, ) + handle_derived_types = True + + @classmethod + def pack_value(cls, value, tp=None): + if isinstance(value, Variable): + return value.name, 100 + vartype(value) + return value + + @classmethod + # pylint: disable=keyword-arg-before-vararg + def unpack_value(cls, value, tp, domain=None, *args): + # This essentially decodes Tuple[str, int] to Union[Variable, Any]: + # if vartype is 100 (or 0), it leaves value as it is. Not nice, but + # needed for backward compatiblity -- and also practical in cases where + # a combo gives a few fixed options + variables. See Merge Data. + if isinstance(tp, type) and issubclass(tp, Variable): + if value is None: + return None + if domain is None: + warnings.warn("unpacking variables requires having a domain") + return None + # Compatibility with ancient settings that stored Variable as + # string, without any type-checking + if isinstance(value, str): + if value in domain: + return domain[value] + else: + saved_type = value[1] % 100 + if saved_type == 0: + return value[0] + var = domain[value[0]] + if vartype(var) != saved_type: + warnings.warn( + f"'{var.name}' is of wrong type ({type(var).__name__})") + return var + return super().unpack_value(value, tp, domain, *args) + + +class DomainContext(Context): + attributes: Dict[str, int] + metas: Dict[str, int] + + class DomainContextHandler(ContextHandler): """Context handler for widgets with settings that depend on the input dataset. Suitable settings are selected based on the data domain.""" + ContextType = DomainContext + MATCH_VALUES_NONE, MATCH_VALUES_CLASS, MATCH_VALUES_ALL = range(3) def __init__(self, *, match_values=0, first_match=True, **kwargs): @@ -80,6 +127,45 @@ def __init__(self, *, match_values=0, first_match=True, **kwargs): .format(name), OrangeDeprecationWarning ) + def analyze_settings(self, provider, prefix, class_name): + super().analyze_settings(provider, prefix, class_name) + if type(self) is DomainContextHandler and \ + not any(isinstance(setting, ContextSetting) + for setting, *_ in self.provider.traverse_settings()): + warnings.warn(f"{class_name} uses DomainContextHandler without " + "having any context settings") + + def _migrate_contexts(self, contexts: List[Context]) -> None: + super()._migrate_contexts(contexts) + self._migrate_annotated(contexts) + + def _migrate_annotated(self, contexts: List[Context]): + for setting, *_ in self.provider.traverse_settings(): + if not isinstance(setting, ContextSetting) \ + or setting.type is None \ + or not self.is_allowed_type(setting.type): + continue + name = setting.name + for context in contexts: + if name not in context.values: + continue + value = context.values[name] + if isinstance(value, tuple) and len(value) == 2 and ( + value[1] == -2 # encoded any + or ( # encoded list or dict + (value[1] == -3 and isinstance(value[0], list) + or value[1] == -4 and isinstance(value[0], dict)) + and all( # check that all elements/keys encode vars + x is None or + isinstance(x, tuple) + and len(x) == 2 + and isinstance(x[0], str) + and isinstance(x[1], int) + and 1 <= x[1] % 100 <= 4 + for x in value[0]) + )): + context.values[name] = value[0] + def encode_domain(self, domain): """ domain: Orange.data.domain to encode @@ -140,7 +226,7 @@ def filter_value(self, setting, data, domain, attrs, metas): @staticmethod def encode_variable(var): - return var.name, 100 + vartype(var) + return VariableTypeSupport.pack_value(var) @classmethod def encode_setting(cls, context, setting, value): @@ -181,9 +267,9 @@ def get_var(name): for name_type in data] if dtype == -4: return {get_var(name): val for (name, _), val in data.items()} - if dtype >= 100: + if dtype > 0: return get_var(data) - return value[0] + return data else: return value @@ -251,7 +337,7 @@ def match_list(self, setting, value, context, attrs, metas): def match_value(self, setting, value, attrs, metas): """Match a single value """ - if value[1] < 0: + if not isinstance(value, tuple) or value[1] < 0: return 0, 0 if self._var_exists(setting, value, attrs, metas): @@ -278,22 +364,30 @@ def is_encoded_var(value): and isinstance(value[0], str) and isinstance(value[1], int) \ and value[1] >= 0 + +class ClassValueContext(Context): + classes: Optional[Tuple[str, ...]] + + class ClassValuesContextHandler(ContextHandler): """Context handler used for widgets that work with a single discrete variable""" + ContextType = ClassValueContext + def open_context(self, widget, classes): if isinstance(classes, Variable): if classes.is_discrete: classes = classes.values else: classes = None - + if classes is not None: + classes = tuple(classes) super().open_context(widget, classes) def new_context(self, classes): context = super().new_context() - context.classes = classes + context.classes = None if classes is None else tuple(classes) return context def match(self, context, classes): @@ -309,6 +403,13 @@ def match(self, context, classes): return self.NO_MATCH +class PerfectDomainContext(Context): + VarDesc = Tuple[str, Union[List[str], int]] + attributes: Tuple[VarDesc, ...] + class_vars: Tuple[VarDesc, ...] + metas: Tuple[VarDesc, ...] + + class PerfectDomainContextHandler(DomainContextHandler): """Context handler that matches a context only when the same domain is available. @@ -316,6 +417,8 @@ class PerfectDomainContextHandler(DomainContextHandler): It uses a different encoding than the DomainContextHandler. """ + ContextType = PerfectDomainContext + def new_context(self, domain, attributes, class_vars, metas): """Same as DomainContextHandler, but also store class_vars""" context = super().new_context(domain, attributes, metas) diff --git a/Orange/widgets/unsupervised/owdistancematrix.py b/Orange/widgets/unsupervised/owdistancematrix.py index 65ce2050f2e..4bfb1c3596e 100644 --- a/Orange/widgets/unsupervised/owdistancematrix.py +++ b/Orange/widgets/unsupervised/owdistancematrix.py @@ -1,5 +1,6 @@ import itertools import math +from typing import List import numpy as np @@ -8,15 +9,18 @@ from AnyQt.QtGui import QColor, QPen, QBrush from AnyQt.QtCore import Qt, QAbstractTableModel, QSize +from orangewidget.settings import Context + from Orange.data import Table, Variable, StringVariable from Orange.misc import DistMatrix -from Orange.widgets import widget, gui + +from Orange.widgets import gui from Orange.widgets.gui import OrangeUserRole from Orange.widgets.settings import Setting, ContextSetting, ContextHandler from Orange.widgets.utils.itemmodels import VariableListModel from Orange.widgets.utils.itemselectionmodel import SymmetricSelectionModel from Orange.widgets.utils.widgetpreview import WidgetPreview -from Orange.widgets.widget import Input, Output +from Orange.widgets.widget import OWWidget, AttributeList, Input, Output class DistanceMatrixModel(QAbstractTableModel): @@ -147,7 +151,16 @@ def sizeHintForColumn(self, column: int) -> int: return hint + 1 if self.showGrid() else hint +class DistanceMatrixContext(Context): + dim: int + annotations: List[str] + annotation: str + selection: List[int] + + class DistanceMatrixContextHandler(ContextHandler): + ContextType = DistanceMatrixContext + @staticmethod def _var_names(annotations): return [a.name if isinstance(a, Variable) else a for a in annotations] @@ -180,7 +193,7 @@ def settings_to_widget(self, widget, *args): widget.tableview.selectionModel().setSelectedItems(context.selection) -class OWDistanceMatrix(widget.OWWidget): +class OWDistanceMatrix(OWWidget): name = "Distance Matrix" description = "View distance matrix." icon = "icons/DistanceMatrix.svg" @@ -197,7 +210,7 @@ class Outputs: settingsHandler = DistanceMatrixContextHandler() auto_commit = Setting(True) annotation_idx = ContextSetting(1) - selection = ContextSetting([]) + selection: List[int] = ContextSetting([]) want_control_area = True want_main_area = False @@ -288,7 +301,7 @@ def _update_labels(self): attr = self.distances.row_items.domain.attributes labels = [str(attr[i]) for i in range(self.distances.shape[0])] elif self.annotation_idx == 2 and \ - isinstance(self.items, widget.AttributeList): + isinstance(self.items, AttributeList): labels = [v.name for v in self.items] elif isinstance(self.items, Table): var = self.annot_combo.model()[self.annotation_idx] diff --git a/Orange/widgets/utils/__init__.py b/Orange/widgets/utils/__init__.py index e974de1cc2f..0a39c8e10e5 100644 --- a/Orange/widgets/utils/__init__.py +++ b/Orange/widgets/utils/__init__.py @@ -9,11 +9,20 @@ from AnyQt.QtCore import QObject -from Orange.data.variable import TimeVariable +from Orange.data.variable import \ + Variable, DiscreteVariable, ContinuousVariable, StringVariable, TimeVariable from Orange.util import deepgetattr +_vartype2int = {DiscreteVariable: 1, + ContinuousVariable: 2, + StringVariable: 3, + TimeVariable: 4} + def vartype(var): + if isinstance(var, type) and issubclass(var, Variable): + return _vartype2int[var] + if var.is_discrete: return 1 elif var.is_continuous: diff --git a/Orange/widgets/utils/domaineditor.py b/Orange/widgets/utils/domaineditor.py index d9f9b016540..9247639d687 100644 --- a/Orange/widgets/utils/domaineditor.py +++ b/Orange/widgets/utils/domaineditor.py @@ -1,5 +1,6 @@ from itertools import chain from copy import deepcopy +from typing import List, Tuple import numpy as np import scipy.sparse as sp @@ -16,6 +17,7 @@ from Orange.widgets import gui from Orange.widgets.gui import HorizontalGridDelegate from Orange.widgets.settings import ContextSetting +from Orange.widgets.utils import vartype from Orange.widgets.utils.itemmodels import TableModel @@ -40,8 +42,9 @@ class VarTableModel(QAbstractTableModel): places = "feature", "target", "meta", "skip" typenames = "categorical", "numeric", "text", "datetime" vartypes = DiscreteVariable, ContinuousVariable, StringVariable, TimeVariable - name2type = dict(zip(typenames, vartypes)) - type2name = dict(zip(vartypes, typenames)) + vtype2name = {vartype(vt): name for vt, name in zip(vartypes, typenames)} + name2vtype = {v: k for k, v in vtype2name.items()} + vtype2type = {vartype(vt): vt for vt in vartypes} def __init__(self, variables, *args): super().__init__(*args) @@ -72,14 +75,14 @@ def data(self, index, role): val = self.variables[row][col] if role in (Qt.DisplayRole, Qt.EditRole): if col == Column.tpe: - return self.type2name[val] + return self.vtype2name[val] if col == Column.place: return self.places[val] else: return val if role == Qt.DecorationRole: if col == Column.tpe: - return gui.attributeIconDict[self.vartypes.index(val) + 1] + return gui.attributeIconDict[val] if role == Qt.ForegroundRole: if self.variables[row][Column.place] == Place.skip \ and col != Column.place: @@ -96,19 +99,22 @@ def data(self, index, role): return None def setData(self, index, value, role=Qt.EditRole): + def set_at(c, value): + row_data = self.variables[row] + self.variables[row] = row_data[:c] + (value, ) + row_data[c + 1:] + row, col = index.row(), index.column() - row_data = self.variables[row] if role == Qt.EditRole: if col == Column.name and not (value.isspace() or value == ""): - row_data[col] = value + set_at(Column.name, value) elif col == Column.tpe: - vartype = self.name2type[value] - row_data[col] = vartype - if not vartype.is_primitive() and \ - row_data[Column.place] < Place.meta: - row_data[Column.place] = Place.meta + vtype = self.name2vtype[value] + set_at(Column.tpe, vtype) + if not self.vtype2type[vtype].is_primitive() and \ + self.variables[row][Column.place] < Place.meta: + set_at(Column.place, Place.meta) elif col == Column.place: - row_data[col] = self.places.index(value) + set_at(Column.place, self.places.index(value)) else: return False # Settings may change background colors @@ -185,8 +191,9 @@ def setEditorData(self, combo, index): class PlaceDelegate(ComboDelegate): def setEditorData(self, combo, index): combo.clear() - to_meta = not self.view.model().variables[ - index.row()][Column.tpe].is_primitive() + model = self.view.model() + vtype = model.variables[index.row()][Column.tpe] + to_meta = not model.vtype2type[vtype].is_primitive() combo.addItems(self.items[2 * to_meta:]) combo.setCurrentIndex(self.items.index(index.data()) - 2 * to_meta) combo.showPopup() @@ -200,7 +207,7 @@ class DomainEditor(QTableView): widget : parent widget """ - variables = ContextSetting([]) + variables: List[Tuple[str, int, int, str, bool]] = ContextSetting([]) def __init__(self, widget): super().__init__() @@ -285,14 +292,14 @@ def get_domain(self, domain, data, deduplicate=False): cols = [[], [], []] # Xcols, Ycols, Mcols def numbers_are_round(var, col_data): - if type(var) == ContinuousVariable: + if type(var) is ContinuousVariable: data = np.asarray(col_data.data) # Works for dense and sparse data = data[~np.isnan(data)] return (data == data.astype(int)).all() return False # Exit early with original domain if the user didn't actually change anything - if all((name == orig_var.name and tpe == type(orig_var) and place == orig_plc) + if all((name == orig_var.name and tpe == vartype(orig_var) and place == orig_plc) for (name, tpe, place, _, _), (orig_var, orig_plc) in zip(variables, chain(((at, Place.feature) for at in domain.attributes), @@ -309,7 +316,7 @@ def numbers_are_round(var, col_data): else: renamed_iter = iter(relevant_names) renamed = [] - for (name, tpe, place, _, may_be_numeric), (orig_var, orig_plc) in \ + for (name, vartpe, place, _, may_be_numeric), (orig_var, orig_plc) in \ zip(variables, chain([(at, Place.feature) for at in domain.attributes], [(cl, Place.class_var) for cl in domain.class_vars], @@ -317,6 +324,7 @@ def numbers_are_round(var, col_data): if place == Place.skip: continue + tpe = VarTableModel.vtype2type[vartpe] new_name = next(renamed_iter) if new_name != name and name not in renamed: renamed.append(name) @@ -324,11 +332,11 @@ def numbers_are_round(var, col_data): col_data = self._get_column(data, orig_var, orig_plc) is_sparse = sp.issparse(col_data) - if new_name == orig_var.name and tpe == type(orig_var): + if new_name == orig_var.name and tpe is type(orig_var): var = orig_var - elif tpe == type(orig_var): + elif tpe is type(orig_var): var = orig_var.copy(name=new_name) - elif tpe == DiscreteVariable: + elif tpe is DiscreteVariable: values = natural_sorted( list(str(i) for i in unique(col_data) if not self._is_missing(i)) @@ -340,7 +348,7 @@ def numbers_are_round(var, col_data): values = [str(int(float(v))) for v in values] var = tpe(new_name, values) col_data = self._to_column(col_data, is_sparse) - elif tpe == StringVariable: + elif tpe is StringVariable: var = tpe.make(new_name) if type(orig_var) in [DiscreteVariable, TimeVariable]: col_data = [orig_var.repr_val(x) if not np.isnan(x) else "" @@ -354,7 +362,7 @@ def numbers_are_round(var, col_data): # don't obey sparsity for StringVariable since they are # in metas which are transformed to dense below col_data = self._to_column(col_data, False, dtype=object) - elif tpe == ContinuousVariable and type(orig_var) == DiscreteVariable: + elif tpe is ContinuousVariable and type(orig_var) is DiscreteVariable: var = tpe.make(new_name) if may_be_numeric: col_data = [np.nan if self._is_missing(x) else float(orig_var.values[int(x)]) @@ -439,9 +447,9 @@ def discrete_value_display(value_list): return result return [ - [var.name, type(var), place, + (var.name, vartype(var), place, discrete_value_display(var.values) if var.is_discrete else "", - may_be_numeric(var)] + may_be_numeric(var)) for place, vars in enumerate( (domain.attributes, domain.class_vars, domain.metas)) for var in vars diff --git a/Orange/widgets/utils/tests/test_domaineditor.py b/Orange/widgets/utils/tests/test_domaineditor.py index 44e7bb42e31..b0d3bd1c829 100644 --- a/Orange/widgets/utils/tests/test_domaineditor.py +++ b/Orange/widgets/utils/tests/test_domaineditor.py @@ -23,13 +23,15 @@ def setUp(self): self.widget = MockWidget() self.editor = self.widget.domain_editor + discrete, continuous, string, time = range(1, 5) + self.orig_variables = [ - ["d1", DiscreteVariable, 0, "x, y, z, ...", False], - ["d2", DiscreteVariable, 0, "1, 2, 3, ...", True], - ["c1", ContinuousVariable, 0, "", True], - ["d3", DiscreteVariable, 1, "4, 3, 6, ...", True], - ["s", StringVariable, 2, "", False], - ["t", TimeVariable, 2, "", True] + ("d1", discrete, 0, "x, y, z, ...", False), + ("d2", discrete, 0, "1, 2, 3, ...", True), + ("c1", continuous, 0, "", True), + ("d3", discrete, 1, "4, 3, 6, ...", True), + ("s", string, 2, "", False), + ("t", time, 2, "", True) ] self.domain = Domain( [DiscreteVariable("d1", values=list("xyzw")), @@ -51,7 +53,7 @@ def test_deduplication(self): # No duplicates - domain, _ = \ + domain, *_ = \ editor.get_domain(self.domain, data) self.assertEqual([var.name for var in domain.attributes], ["d1", "d2", "c1"]) diff --git a/Orange/widgets/visualize/owbarplot.py b/Orange/widgets/visualize/owbarplot.py index a5e61a849df..6ee77f3cc43 100644 --- a/Orange/widgets/visualize/owbarplot.py +++ b/Orange/widgets/visualize/owbarplot.py @@ -383,13 +383,13 @@ class Outputs: buttons_area_orientation = Qt.Vertical settingsHandler = DomainContextHandler() - selected_var = ContextSetting(None) - group_var = ContextSetting(None) - annot_var = ContextSetting(None) - color_var = ContextSetting(None) + selected_var: ContinuousVariable = ContextSetting(None) + group_var: DiscreteVariable = ContextSetting(None) + annot_var: Variable = ContextSetting(None) + color_var: DiscreteVariable = ContextSetting(None) auto_commit = Setting(True) - selection = Setting(None, schema_only=True) - visual_settings = Setting({}, schema_only=True) + selection: List[int] = Setting(None, schema_only=True) + visual_settings: Dict[KeyType, ValueType] = Setting({}, schema_only=True) graph = SettingProvider(BarPlotGraph) graph_name = "graph.plotItem" diff --git a/Orange/widgets/visualize/owboxplot.py b/Orange/widgets/visualize/owboxplot.py index c5c3584e64d..c9819a2862b 100644 --- a/Orange/widgets/visualize/owboxplot.py +++ b/Orange/widgets/visualize/owboxplot.py @@ -2,6 +2,8 @@ from collections import namedtuple from itertools import chain, count import numpy as np +import scipy.special +from scipy.stats import f_oneway, chi2_contingency from AnyQt.QtWidgets import ( QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem, @@ -10,10 +12,9 @@ ) from AnyQt.QtGui import QPen, QColor, QBrush, QPainterPath, QPainter, QFont from AnyQt.QtCore import Qt, QEvent, QRectF, QSize, QSortFilterProxyModel -from orangewidget.utils.listview import ListViewSearch -import scipy.special -from scipy.stats import f_oneway, chi2_contingency +from Orange.data import Variable, DiscreteVariable +from orangewidget.utils.listview import ListViewSearch import Orange.data from Orange.data.filter import FilterDiscrete, FilterContinuous, Values, \ @@ -145,10 +146,10 @@ class Warning(widget.OWWidget.Warning): # variable names! selection = ContextSetting((), schema_only=True) - attribute = ContextSetting(None) + attribute: Variable = ContextSetting(None) order_by_importance = Setting(False) order_grouping_by_importance = Setting(False) - group_var = ContextSetting(None) + group_var: DiscreteVariable = ContextSetting(None) show_annotations = Setting(True) compare = Setting(CompareMeans) stattest = Setting(0) diff --git a/Orange/widgets/visualize/owdistributions.py b/Orange/widgets/visualize/owdistributions.py index 49c6da95088..13c63aca765 100644 --- a/Orange/widgets/visualize/owdistributions.py +++ b/Orange/widgets/visualize/owdistributions.py @@ -1,5 +1,6 @@ from functools import partial, reduce from itertools import count, groupby, repeat +from typing import Set from xml.sax.saxutils import escape import numpy as np @@ -11,7 +12,8 @@ from orangewidget.utils.listview import ListViewSearch import pyqtgraph as pg -from Orange.data import Table, DiscreteVariable, ContinuousVariable, Domain +from Orange.data import Table, DiscreteVariable, ContinuousVariable, Domain, \ + Variable from Orange.preprocess.discretize import decimal_binnings, time_binnings, \ short_time_units from Orange.statistics import distribution, contingency @@ -270,9 +272,9 @@ class Warning(OWWidget.Warning): ignored_nans = Msg("Data instances with missing values are ignored") settingsHandler = settings.DomainContextHandler() - var = settings.ContextSetting(None) - cvar = settings.ContextSetting(None) - selection = settings.ContextSetting(set(), schema_only=True) + var: Variable = settings.ContextSetting(None) + cvar: DiscreteVariable = settings.ContextSetting(None) + selection: Set[int] = settings.ContextSetting(set(), schema_only=True) # number_of_bins must be a context setting because selection depends on it number_of_bins = settings.ContextSetting(5, schema_only=True) diff --git a/Orange/widgets/visualize/owheatmap.py b/Orange/widgets/visualize/owheatmap.py index c4738209e6f..b66d69eab73 100644 --- a/Orange/widgets/visualize/owheatmap.py +++ b/Orange/widgets/visualize/owheatmap.py @@ -168,13 +168,13 @@ class Outputs: legend: bool = settings.Setting(True) # Annotations #: text row annotation (row names) - annotation_var = settings.ContextSetting(None) + annotation_var: Variable = settings.ContextSetting(None) #: color row annotation - annotation_color_var = settings.ContextSetting(None) + annotation_color_var: Variable = settings.ContextSetting(None) column_annotation_color_key: Optional[Tuple[str, str]] = settings.ContextSetting(None) # Discrete variable used to split that data/heatmaps (vertically) - split_by_var = settings.ContextSetting(None) + split_by_var: DiscreteVariable = settings.ContextSetting(None) # Split heatmap columns by 'key' (horizontal) split_columns_key: Optional[Tuple[str, str]] = settings.ContextSetting(None) # Selected row/column clustering method (name) diff --git a/Orange/widgets/visualize/owlinearprojection.py b/Orange/widgets/visualize/owlinearprojection.py index 3b946afa22c..57631633b5a 100644 --- a/Orange/widgets/visualize/owlinearprojection.py +++ b/Orange/widgets/visualize/owlinearprojection.py @@ -2,9 +2,10 @@ Linear Projection widget ------------------------ """ - +from enum import IntEnum from itertools import islice, permutations, chain from math import factorial +from typing import List import numpy as np @@ -17,11 +18,10 @@ import pyqtgraph as pg -from Orange.data import Table, Domain +from Orange.data import Table, Domain, ContinuousVariable from Orange.preprocess import Normalize from Orange.preprocess.score import ReliefF, RReliefF from Orange.projection import PCA, LDA, LinearProjector -from Orange.util import Enum from Orange.widgets import gui, report from Orange.widgets.gui import OWComponent from Orange.widgets.settings import Setting, ContextSetting, SettingProvider @@ -257,8 +257,8 @@ def update_circle(self): self.circle_item.setPen(pen) -Placement = Enum("Placement", dict(Circular=0, LDA=1, PCA=2), type=int, - qualname="Placement") +Placement = IntEnum("Placement", dict(Circular=0, LDA=1, PCA=2), + qualname="Placement") class OWLinearProjection(OWAnchorProjectionWidget): @@ -276,7 +276,7 @@ class OWLinearProjection(OWAnchorProjectionWidget): settings_version = 6 placement = Setting(Placement.Circular) - selected_vars = ContextSetting([]) + selected_vars: List[ContinuousVariable] = ContextSetting([]) vizrank = SettingProvider(LinearProjectionVizRank) GRAPH_CLASS = OWLinProjGraph graph = SettingProvider(OWLinProjGraph) diff --git a/Orange/widgets/visualize/owlineplot.py b/Orange/widgets/visualize/owlineplot.py index bce51e2b49b..1eb27c56f59 100644 --- a/Orange/widgets/visualize/owlineplot.py +++ b/Orange/widgets/visualize/owlineplot.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Dict from xml.sax.saxutils import escape import numpy as np @@ -13,6 +13,7 @@ from pyqtgraph.graphicsItems.ViewBox import ViewBox from orangewidget.utils.listview import ListViewSearch +from orangewidget.utils import visual_settings_dlg as vissettings from orangewidget.utils.visual_settings_dlg import VisualSettingsDialog from Orange.data import Table, DiscreteVariable @@ -608,15 +609,15 @@ class Outputs: annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) settingsHandler = DomainContextHandler() - group_var = ContextSetting(None) + group_var: DiscreteVariable = ContextSetting(None) show_profiles = Setting(False) show_range = Setting(True) show_mean = Setting(True) show_error = Setting(False) auto_commit = Setting(True) - selection = Setting(None, schema_only=True) - visual_settings = Setting({}, schema_only=True) - + selection: List[int] = Setting(None, schema_only=True) + visual_settings: Dict[vissettings.KeyType, vissettings.ValueType] \ + = Setting({}, schema_only=True) graph_name = "graph.plotItem" class Error(OWWidget.Error): diff --git a/Orange/widgets/visualize/owmosaic.py b/Orange/widgets/visualize/owmosaic.py index 13948fd2c92..fbbec57ed2c 100644 --- a/Orange/widgets/visualize/owmosaic.py +++ b/Orange/widgets/visualize/owmosaic.py @@ -3,6 +3,7 @@ from itertools import product, chain, repeat from math import sqrt, log from operator import mul, attrgetter +from typing import Set import numpy as np from scipy.stats import distributions @@ -312,7 +313,7 @@ class Outputs: variable3: Variable = ContextSetting(None) variable4: Variable = ContextSetting(None) variable_color: DiscreteVariable = ContextSetting(None) - selection = Setting(set(), schema_only=True) + selection: Set[str] = Setting(set(), schema_only=True) BAR_WIDTH = 5 SPACING = 4 diff --git a/Orange/widgets/visualize/owscatterplot.py b/Orange/widgets/visualize/owscatterplot.py index 1aa0dfd292f..cfd6da9aa0b 100644 --- a/Orange/widgets/visualize/owscatterplot.py +++ b/Orange/widgets/visualize/owscatterplot.py @@ -240,8 +240,8 @@ class Outputs(OWDataProjectionWidget.Outputs): settings_version = 4 auto_sample = Setting(True) - attr_x = ContextSetting(None) - attr_y = ContextSetting(None) + attr_x: ContinuousVariable = ContextSetting(None) + attr_y: ContinuousVariable = ContextSetting(None) tooltip_shows_all = Setting(True) GRAPH_CLASS = OWScatterPlotGraph diff --git a/Orange/widgets/visualize/tests/test_owscatterplot.py b/Orange/widgets/visualize/tests/test_owscatterplot.py index 39cf81dcc2a..0ff0280f39b 100644 --- a/Orange/widgets/visualize/tests/test_owscatterplot.py +++ b/Orange/widgets/visualize/tests/test_owscatterplot.py @@ -298,7 +298,7 @@ def test_set_strings_settings(self): """ self.send_signal(self.widget.Inputs.data, self.data) settings = self.widget.settingsHandler.pack_data(self.widget) - plot_settings = settings["context_settings"][0].values + plot_settings = settings["context_settings"][0]["values"] plot_settings["attr_label"] = ("sepal length", -2) plot_settings["attr_color"] = ("sepal width", -2) plot_settings["attr_shape"] = ("iris", -2) diff --git a/Orange/widgets/visualize/utils/widget.py b/Orange/widgets/visualize/utils/widget.py index 11eeb9fef43..f909139b389 100644 --- a/Orange/widgets/visualize/utils/widget.py +++ b/Orange/widgets/visualize/utils/widget.py @@ -1,3 +1,4 @@ +from typing import List, Tuple, Dict from xml.sax.saxutils import escape import numpy as np @@ -5,6 +6,7 @@ from AnyQt.QtCore import QSize, Signal, Qt from AnyQt.QtWidgets import QApplication +from orangewidget.utils import visual_settings_dlg as vissettings from orangewidget.utils.visual_settings_dlg import VisualSettingsDialog from Orange.data import ( @@ -53,10 +55,10 @@ class OWProjectionWidgetBase(OWWidget, openclass=True): and a bool `np.ndarray` with indicators of valid (that is, shown) data points. """ - attr_color = ContextSetting(None, required=ContextSetting.OPTIONAL) - attr_label = ContextSetting(None, required=ContextSetting.OPTIONAL) - attr_shape = ContextSetting(None, required=ContextSetting.OPTIONAL) - attr_size = ContextSetting(None, required=ContextSetting.OPTIONAL) + attr_color: Variable = ContextSetting(None, required=ContextSetting.OPTIONAL) + attr_label: Variable = ContextSetting(None, required=ContextSetting.OPTIONAL) + attr_shape: Variable = ContextSetting(None, required=ContextSetting.OPTIONAL) + attr_size: Variable = ContextSetting(None, required=ContextSetting.OPTIONAL) class Information(OWWidget.Information): missing_size = Msg( @@ -383,8 +385,9 @@ class Warning(OWProjectionWidgetBase.Warning): "Increase opacity if subset is difficult to see") settingsHandler = DomainContextHandler() - selection = Setting(None, schema_only=True) - visual_settings = Setting({}, schema_only=True) + selection: List[Tuple[int, int]] = Setting(None, schema_only=True) + visual_settings: Dict[vissettings.KeyType, vissettings.ValueType] \ + = Setting({}, schema_only=True) auto_commit = Setting(True) GRAPH_CLASS = OWScatterPlotBase