diff --git a/Orange/widgets/settings.py b/Orange/widgets/settings.py index 73868993ada..fdc9066e98e 100644 --- a/Orange/widgets/settings.py +++ b/Orange/widgets/settings.py @@ -76,69 +76,9 @@ def __init__(self, default, *, required=2, 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 - data domain.""" - - MATCH_VALUES_NONE, MATCH_VALUES_CLASS, MATCH_VALUES_ALL = range(3) - - def __init__(self, *, match_values=0, first_match=True, **kwargs): - super().__init__() - self.match_values = match_values - self.first_match = first_match - - for name in kwargs: - warnings.warn( - "{} is not a valid parameter for DomainContextHandler" - .format(name), OrangeDeprecationWarning - ) - - def encode_domain(self, domain): - """ - domain: Orange.data.domain to encode - return: dict mapping attribute name to type or list of values - (based on the value of self.match_values attribute) - """ - - match = self.match_values - encode = self.encode_variables - if match == self.MATCH_VALUES_CLASS: - attributes = encode(domain.attributes, False) - attributes.update(encode(domain.class_vars, True)) - else: - attributes = encode(domain.variables, match == self.MATCH_VALUES_ALL) - - metas = encode(domain.metas, match == self.MATCH_VALUES_ALL) - - return attributes, metas - - @staticmethod - def encode_variables(attributes, encode_values): - """Encode variables to a list mapping name to variable type - or a list of values.""" - - if not encode_values: - return {v.name: vartype(v) for v in attributes} - - return {v.name: v.values if v.is_discrete else vartype(v) - for v in attributes} - - def new_context(self, domain, attributes, metas): - """Create a new context.""" - context = super().new_context() - context.attributes = attributes - context.metas = metas - return context +class StructuredVariableSettingMixin: - def open_context(self, widget, domain): - if domain is None: - return - if not isinstance(domain, Domain): - domain = domain.domain - super().open_context(widget, domain, *self.encode_domain(domain)) - - def _filter_value(self, setting, data, *args): + def filter_value(self, setting, data, *args): value = data.get(setting.name, None) if isinstance(value, list): new_value = [item for item in value @@ -149,22 +89,14 @@ def _filter_value(self, setting, data, *args): 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, *args): + and not self.match_variable(setting, value, *args): del data[setting.name] - def filter_value(self, setting, data, domain, *args): - self._filter_value(setting, data, *args) - - @staticmethod - def encode_variable(var): - return var.name, 100 + vartype(var) - - @classmethod - def encode_setting(cls, context, setting, value): + def encode_setting(self, context, setting, value): if isinstance(value, list): if all(e is None or isinstance(e, Variable) for e in value) \ and any(e is not None for e in value): - return ([None if e is None else cls.encode_variable(e) + return ([None if e is None else self.encode_variable(e) for e in value], -3) else: @@ -172,12 +104,12 @@ def encode_setting(cls, context, setting, value): elif isinstance(value, dict) \ and all(isinstance(e, Variable) for e in value): - return ({cls.encode_variable(e): val for e, val in value.items()}, + return ({self.encode_variable(e): val for e, val in value.items()}, -4) if isinstance(value, Variable): if isinstance(setting, WBContextSetting): - return cls.encode_variable(value) + return self.encode_variable(value) else: raise ValueError("Variables must be stored as ContextSettings; " f"change {setting.name} to ContextSetting.") @@ -204,27 +136,7 @@ def get_var(name): else: return value - @classmethod - def _var_exists(cls, setting, value, attributes, metas): - 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_metas and - metas.get(attr_name, -1) == attr_type) - - def match(self, context, domain, attrs, metas): - if context.attributes == attrs and context.metas == metas: - return self.PERFECT_MATCH - return self._match(context, attrs, metas) - - def _match(self, context, *args): + def match(self, context, *args): matches = [] try: for setting, data, _ in \ @@ -273,7 +185,7 @@ def match_value(self, setting, value, *args): if value[1] < 0: return 0, 0 - if self._var_exists(setting, value, *args): + if self.match_variable(setting, value, *args): return 1, 1 elif setting.required == setting.OPTIONAL: return 0, 1 @@ -288,7 +200,19 @@ def is_valid_item(self, setting, item, *args): """ if not isinstance(item, tuple): return True - return self._var_exists(setting, item, *args) + return self.match_variable(setting, item, *args) + + @classmethod + def match_variable(cls, setting, value, *args): + """ Return if variable described with value can be matched to *args. """ + raise NotImplementedError + + +class VariableEncoderMixin: + + @staticmethod + def encode_variable(var): + return var.name, 100 + vartype(var) @staticmethod def is_encoded_var(value): @@ -298,6 +222,101 @@ def is_encoded_var(value): and value[1] >= 0 +class DomainContextHandler(ContextHandler, StructuredVariableSettingMixin, + VariableEncoderMixin): + """Context handler for widgets with settings that depend on + the input dataset. Suitable settings are selected based on the + data domain.""" + + MATCH_VALUES_NONE, MATCH_VALUES_CLASS, MATCH_VALUES_ALL = range(3) + + def __init__(self, *, match_values=0, first_match=True, **kwargs): + super().__init__() + self.match_values = match_values + self.first_match = first_match + + for name in kwargs: + warnings.warn( + "{} is not a valid parameter for DomainContextHandler" + .format(name), OrangeDeprecationWarning + ) + + def encode_domain(self, domain): + """ + domain: Orange.data.domain to encode + return: dict mapping attribute name to type or list of values + (based on the value of self.match_values attribute) + """ + + match = self.match_values + encode = self.encode_variables + if match == self.MATCH_VALUES_CLASS: + attributes = encode(domain.attributes, False) + attributes.update(encode(domain.class_vars, True)) + else: + attributes = encode(domain.variables, match == self.MATCH_VALUES_ALL) + + metas = encode(domain.metas, match == self.MATCH_VALUES_ALL) + + return attributes, metas + + @staticmethod + def encode_variables(attributes, encode_values): + """Encode variables to a list mapping name to variable type + or a list of values.""" + + if not encode_values: + return {v.name: vartype(v) for v in attributes} + + return {v.name: v.values if v.is_discrete else vartype(v) + for v in attributes} + + def new_context(self, domain, attributes, metas): + """Create a new context.""" + context = super().new_context() + context.attributes = attributes + context.metas = metas + return context + + def open_context(self, widget, domain): + if domain is None: + return + if not isinstance(domain, Domain): + domain = domain.domain + super().open_context(widget, domain, *self.encode_domain(domain)) + + def filter_value(self, setting, data, domain, *args): + StructuredVariableSettingMixin.filter_value(self, setting, data, *args) + + def encode_setting(self, context, setting, value): + return StructuredVariableSettingMixin.encode_setting( + self, context, setting, value) + + # backward compatibility, pylint: disable=keyword-arg-before-vararg + def decode_setting(self, setting, value, domain=None, *args): + return StructuredVariableSettingMixin.decode_setting( + self, setting, value, domain, *args) + + def match_variable(self, setting, value, attributes, metas): + if not self.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_metas and + metas.get(attr_name, -1) == attr_type) + + def match(self, context, domain, attrs, metas): + if context.attributes == attrs and context.metas == metas: + return self.PERFECT_MATCH + return StructuredVariableSettingMixin.match(self, context, attrs, metas) + + class SimpleDomainContextHandler(DomainContextHandler): def encode_domain(self, domain): @@ -305,11 +324,10 @@ def encode_domain(self, domain): new_context = ContextHandler.new_context - @classmethod - def _var_exists(cls, setting, value, domain): + def match_variable(self, setting, value, domain): assert isinstance(setting, ContextSetting) - if not cls.is_encoded_var(value): + if not self.is_encoded_var(value): return False attr_name, attr_type = value @@ -324,13 +342,13 @@ def _var_exists(cls, setting, value, domain): or idx < 0 and setting.exclude_metas): return False - return cls.encode_variable(candidate)[1] == attr_type + return self.encode_variable(candidate)[1] == attr_type def filter_value(self, setting, data, domain): - self._filter_value(setting, data, domain) + StructuredVariableSettingMixin.filter_value(self, setting, data, domain) def match(self, context, domain): - return self._match(context, domain) + return StructuredVariableSettingMixin.match(self, context, domain) class ClassValuesContextHandler(ContextHandler):