From 5896dd818ff2f81e091b0e18cc82923a4bae4c68 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Fri, 2 Feb 2024 15:41:25 +0100 Subject: [PATCH] first working prototype --- Orange/widgets/settings.py | 93 ++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/Orange/widgets/settings.py b/Orange/widgets/settings.py index 3bbe31eb2c0..1d1d4e67f4e 100644 --- a/Orange/widgets/settings.py +++ b/Orange/widgets/settings.py @@ -38,10 +38,11 @@ import warnings from orangewidget.settings import ( - Setting, SettingProvider, SettingsHandler, ContextSetting, + Setting, SettingProvider, SettingsHandler, ContextHandler, Context, IncompatibleContext, SettingsPrinter, rename_setting, widget_settings_dir ) +from orangewidget.settings import ContextSetting as WBContextSetting from orangewidget.settings import _apply_setting from Orange.data import Domain, Variable @@ -62,6 +63,19 @@ ] +class ContextSetting(WBContextSetting): + """Description of a context dependent setting""" + + def __init__(self, default, *, required=2, + exclude_attributes=False, exclude_class_vars=False, exclude_metas=False, + **data): + super().__init__(default, required=required, **data) + self.exclude_attributes = exclude_attributes + self.exclude_class_vars = exclude_class_vars + self.exclude_metas = exclude_metas + self.required = required + + class DomainContextHandler(ContextHandler): """Context handler for widgets with settings that depend on the input dataset. Suitable settings are selected based on the @@ -124,18 +138,18 @@ def open_context(self, widget, domain): domain = domain.domain super().open_context(widget, domain, *self.encode_domain(domain)) - def filter_value(self, setting, data, domain, attrs, metas): + def filter_value(self, setting, data, domain, *args): value = data.get(setting.name, None) if isinstance(value, list): new_value = [item for item in value - if self.is_valid_item(setting, item, attrs, metas)] + if self.is_valid_item(setting, item, *args)] data[setting.name] = new_value elif isinstance(value, dict): new_value = {item: val for item, val in value.items() - if self.is_valid_item(setting, item, attrs, metas)} + if self.is_valid_item(setting, item, *args)} data[setting.name] = new_value elif self.is_encoded_var(value) \ - and not self._var_exists(setting, value, attrs, metas): + and not self._var_exists(setting, value, *args): del data[setting.name] @staticmethod @@ -159,7 +173,7 @@ def encode_setting(cls, context, setting, value): -4) if isinstance(value, Variable): - if isinstance(setting, ContextSetting): + if isinstance(setting, WBContextSetting): return cls.encode_variable(value) else: raise ValueError("Variables must be stored as ContextSettings; " @@ -205,28 +219,30 @@ def _var_exists(cls, setting, value, attributes, metas): def match(self, context, domain, attrs, metas): if context.attributes == attrs and context.metas == metas: return self.PERFECT_MATCH + return self._match(context, domain, attrs, metas) + def _match(self, context, domain, *args): matches = [] try: for setting, data, _ in \ self.provider.traverse_settings(data=context.values): - if not isinstance(setting, ContextSetting): + if not isinstance(setting, WBContextSetting): continue value = data.get(setting.name, None) if isinstance(value, list): matches.append( - self.match_list(setting, value, context, attrs, metas)) + self.match_list(setting, value, context, *args)) # type check is a (not foolproof) check in case of a pair that # would, by conincidence, have -3 or -4 as the second element elif isinstance(value, tuple) and len(value) == 2 \ and (value[1] == -3 and isinstance(value[0], list) or (value[1] == -4 and isinstance(value[0], dict))): matches.append(self.match_list(setting, value[0], context, - attrs, metas)) + *args)) elif value is not None: matches.append( - self.match_value(setting, value, attrs, metas)) + self.match_value(setting, value, *args)) except IncompatibleContext: return self.NO_MATCH @@ -237,31 +253,31 @@ def match(self, context, domain, attrs, metas): matched, available = [sum(m) for m in zip(*matches)] return matched / available if available else 0.1 - def match_list(self, setting, value, context, attrs, metas): + def match_list(self, setting, value, context, *args): """Match a list of values with the given context. returns a tuple containing number of matched and all values. """ matched = 0 for item in value: - if self.is_valid_item(setting, item, attrs, metas): + if self.is_valid_item(setting, item, *args): matched += 1 - elif setting.required == ContextSetting.REQUIRED: + elif setting.required == WBContextSetting.REQUIRED: raise IncompatibleContext() return matched, len(value) - def match_value(self, setting, value, attrs, metas): + def match_value(self, setting, value, *args): """Match a single value """ if value[1] < 0: return 0, 0 - if self._var_exists(setting, value, attrs, metas): + if self._var_exists(setting, value, *args): return 1, 1 elif setting.required == setting.OPTIONAL: return 0, 1 else: raise IncompatibleContext() - def is_valid_item(self, setting, item, attrs, metas): + def is_valid_item(self, setting, item, *args): """Return True if given item can be used with attrs and metas Subclasses can override this method to checks data in alternative @@ -269,7 +285,7 @@ def is_valid_item(self, setting, item, attrs, metas): """ if not isinstance(item, tuple): return True - return self._var_exists(setting, item, attrs, metas) + return self._var_exists(setting, item, *args) @staticmethod def is_encoded_var(value): @@ -278,6 +294,47 @@ def is_encoded_var(value): and isinstance(value[0], str) and isinstance(value[1], int) \ and value[1] >= 0 + +class DomainContextHandlerPosition(DomainContextHandler): + + def __init__(self, *, match_values=0, first_match=True): + super().__init__(match_values=match_values, first_match=first_match) + + def encode_domain(self, domain): + match = self.match_values + encode = self.encode_variables + attributes = encode(domain.attributes, match == self.MATCH_VALUES_ALL) + class_vars = encode(domain.class_vars, + match in (self.MATCH_VALUES_ALL, self.MATCH_VALUES_CLASS)) + metas = encode(domain.metas, match == self.MATCH_VALUES_ALL) + + return attributes, class_vars, metas + + new_context = ContextHandler.new_context + + @classmethod + def _var_exists(cls, setting, value, attributes, class_vars, metas): + assert isinstance(setting, ContextSetting) + + if not cls.is_encoded_var(value): + return False + + attr_name, attr_type = value + # attr_type used to be either 1-4 for variables stored as string + # settings, and 101-104 for variables stored as variables. The former is + # no longer supported, but we play it safe and still handle both here. + attr_type %= 100 + return (not setting.exclude_attributes and + attributes.get(attr_name, -1) == attr_type or + not setting.exclude_class_vars and + class_vars.get(attr_name, -1) == attr_type or + not setting.exclude_metas and + metas.get(attr_name, -1) == attr_type) + + def match(self, context, domain, attrs, class_vars, metas): + return self._match(context, domain, attrs, class_vars, metas) + + class ClassValuesContextHandler(ContextHandler): """Context handler used for widgets that work with a single discrete variable""" @@ -358,7 +415,7 @@ def encode_setting(self, context, setting, value): """Same as is domain context handler, but handles separately stored class_vars.""" - if isinstance(setting, ContextSetting) and isinstance(value, str): + if isinstance(setting, WBContextSetting) and isinstance(value, str): def _candidate_variables(): if not setting.exclude_attributes: