Skip to content

Commit

Permalink
DomainContextHandler: refactor into mixins
Browse files Browse the repository at this point in the history
  • Loading branch information
markotoplak committed Aug 2, 2024
1 parent 9a2b01a commit 761506b
Showing 1 changed file with 122 additions and 104 deletions.
226 changes: 122 additions & 104 deletions Orange/widgets/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -149,35 +89,27 @@ 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:
return copy.copy(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.")
Expand All @@ -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 \
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -298,18 +222,112 @@ 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

Check warning on line 302 in Orange/widgets/settings.py

View check run for this annotation

Codecov / codecov/patch

Orange/widgets/settings.py#L302

Added line #L302 was not covered by tests

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):
return tuple()

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

Check warning on line 331 in Orange/widgets/settings.py

View check run for this annotation

Codecov / codecov/patch

Orange/widgets/settings.py#L331

Added line #L331 was not covered by tests

attr_name, attr_type = value
Expand All @@ -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):
Expand Down

0 comments on commit 761506b

Please sign in to comment.