Skip to content

Commit

Permalink
Merge pull request biolab#2312 from janezd/widgets-return-descriptions
Browse files Browse the repository at this point in the history
Move creation of widget description to widgets; Reimplement signal definition lists
  • Loading branch information
astaric authored May 19, 2017
2 parents 696e786 + 25f91c5 commit e7e0a70
Show file tree
Hide file tree
Showing 20 changed files with 695 additions and 367 deletions.
4 changes: 3 additions & 1 deletion .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ engines:
enabled: true
config:
languages:
- python
- python
exclude_paths:
- doc/
pep8:
enabled: false
radon:
Expand Down
118 changes: 31 additions & 87 deletions Orange/canvas/registry/description.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
"""

import sys
import copy
import warnings

# Exceptions
from itertools import chain


class DescriptionError(Exception):
Expand Down Expand Up @@ -73,21 +72,14 @@ class InputSignal(object):
A list of names this input replaces.
"""
def __init__(self, name, type, handler, flags=Single + NonDefault,
id=None, doc=None, replaces=[]):
id=None, doc=None, replaces=()):
self.name = name
self.type = type
self.handler = handler
self.id = id
self.doc = doc
self.replaces = list(replaces)

if isinstance(flags, str):
# flags are stored as strings
warnings.warn("Passing 'flags' as string is deprecated, use "
"integer constants instead",
PendingDeprecationWarning)
flags = eval(flags)

if not (flags & Single or flags & Multiple):
flags += Single

Expand All @@ -107,15 +99,6 @@ def __str__(self):
__repr__ = __str__


def input_channel_from_args(args):
if isinstance(args, tuple):
return InputSignal(*args)
elif isinstance(args, InputSignal):
return copy.copy(args)
else:
raise TypeError("invalid declaration of widget input signal")


class OutputSignal(object):
"""
Description of an output channel.
Expand All @@ -136,20 +119,13 @@ class OutputSignal(object):
A list of names this output replaces.
"""
def __init__(self, name, type, flags=Single + NonDefault,
id=None, doc=None, replaces=[]):
id=None, doc=None, replaces=()):
self.name = name
self.type = type
self.id = id
self.doc = doc
self.replaces = list(replaces)

if isinstance(flags, str):
# flags are stored as strings
warnings.warn("Passing 'flags' as string is deprecated, use "
"integer constants instead",
PendingDeprecationWarning)
flags = eval(flags)

if not (flags & Single or flags & Multiple):
flags += Single

Expand All @@ -175,15 +151,6 @@ def __str__(self):
__repr__ = __str__


def output_channel_from_args(args):
if isinstance(args, tuple):
return OutputSignal(*args)
elif isinstance(args, OutputSignal):
return copy.copy(args)
else:
raise TypeError("invalid declaration of widget output signal")


class WidgetDescription(object):
"""
Description of a widget.
Expand Down Expand Up @@ -232,12 +199,11 @@ class WidgetDescription(object):
def __init__(self, name, id, category=None, version=None,
description=None,
qualified_name=None, package=None, project_name=None,
inputs=[], outputs=[],
inputs=(), outputs=(),
help=None, help_ref=None, url=None, keywords=None,
priority=sys.maxsize,
icon=None, background=None,
replaces=None,
):
replaces=None):

if not qualified_name:
# TODO: Should also check that the name is real.
Expand Down Expand Up @@ -277,68 +243,47 @@ def from_module(cls, module):
"""
Get the widget description from a module.
The module is inspected for global variables (upper case versions of
`WidgetDescription.__init__` parameters).
The module is inspected for classes that have a method
`get_widget_description`. The function calls this method and expects
a dictionary, which is used as keyword arguments for
:obj:`WidgetDescription`. This method also converts all signal types
into qualified names to prevent import problems when cached
descriptions are unpickled (the relevant code using this lists should
be able to handle missing types better).
Parameters
----------
module : `module` or str
A module to inspect for widget description. Can be passed
as a string (qualified import name).
module (`module` or `str`): a module to inspect
Returns
-------
An instance of :obj:`WidgetDescription`
"""
if isinstance(module, str):
module = __import__(module, fromlist=[""])

module_name = module.__name__.rsplit(".", 1)[-1]
if module.__package__:
package_name = module.__package__.rsplit(".", 1)[-1]
else:
package_name = None

default_cat_name = package_name if package_name else ""

from Orange.widgets.widget import WidgetMetaClass
for widget_cls_name, widget_class in module.__dict__.items():
if (isinstance(widget_class, WidgetMetaClass) and
widget_class.name):
break
else:
raise WidgetSpecificationError

qualified_name = "%s.%s" % (module.__name__, widget_cls_name)
description = widget_class.description
inputs = [input_channel_from_args(input_) for input_ in
widget_class.inputs]
outputs = [output_channel_from_args(output) for output in
widget_class.outputs]

# Convert all signal types into qualified names.
# This is to prevent any possible import problems when cached
# descriptions are unpickled (the relevant code using this lists
# should be able to handle missing types better).
for s in inputs + outputs:
s.type = "%s.%s" % (s.type.__module__, s.type.__name__)

return cls(
name=widget_class.name,
id=widget_class.id or module_name,
category=widget_class.category or default_cat_name,
version=widget_class.version,
description=description,
qualified_name=qualified_name,
package=module.__package__,
inputs=inputs,
outputs=outputs,
help=widget_class.help,
help_ref=widget_class.help_ref,
url=widget_class.url,
keywords=widget_class.keywords,
priority=widget_class.priority,
icon=widget_class.icon,
background=widget_class.background,
replaces=widget_class.replaces)
for widget_class in module.__dict__.values():
if not hasattr(widget_class, "get_widget_description"):
continue
description = widget_class.get_widget_description()
if description is None:
continue
for s in chain(description["inputs"], description["outputs"]):
s.type = "%s.%s" % (s.type.__module__, s.type.__name__)
description = WidgetDescription(**description)

description.package = module.__package__
description.category = widget_class.category or default_cat_name
return description

raise WidgetSpecificationError

class CategoryDescription(object):
"""
Expand Down Expand Up @@ -374,8 +319,7 @@ def __init__(self, name=None, version=None,
project_name=None,
url=None, help=None, keywords=None,
widgets=None, priority=sys.maxsize,
icon=None, background=None
):
icon=None, background=None):

self.name = name
self.version = version
Expand Down
33 changes: 19 additions & 14 deletions Orange/widgets/evaluate/owtestlearners.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,15 @@
from Orange.evaluation import scoring, Results
from Orange.preprocess.preprocess import Preprocess
from Orange.preprocess import RemoveNaNClasses
from Orange.widgets import widget, gui, settings
from Orange.widgets import gui, settings
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.widget import OWWidget, Msg
from Orange.widgets.widget import OWWidget, Msg, Input, Output
from Orange.widgets.utils.concurrent import ThreadExecutor

log = logging.getLogger(__name__)


Input = namedtuple(
"Input",
InputLearner = namedtuple(
"InputLearner",
["learner", # :: Orange.base.Learner
"results", # :: Option[Try[Orange.evaluation.Results]]
"stats"] # :: Option[Sequence[Try[float]]]
Expand Down Expand Up @@ -163,13 +162,15 @@ class OWTestLearners(OWWidget):
icon = "icons/TestLearners1.svg"
priority = 100

inputs = [("Learner", Learner, "set_learner", widget.Multiple),
("Data", Table, "set_train_data", widget.Default),
("Test Data", Table, "set_test_data"),
("Preprocessor", Preprocess, "set_preprocessor")]
class Inputs:
train_data = Input("Data", Table, default=True)
test_data = Input("Test Data", Table)
learner = Input("Learner", Learner, multiple=True)
preprocessor = Input("Preprocessor", Preprocess)

outputs = [("Predictions", Table),
("Evaluation Results", Results)]
class Outputs:
predictions = Output("Predictions", Table)
evaluations_results = Output("Evaluation Results", Results)

settings_version = 3
settingsHandler = settings.PerfectDomainContextHandler(metas_in_res=True)
Expand Down Expand Up @@ -332,6 +333,7 @@ def _update_controls(self):
if self.resampling == OWTestLearners.FeatureFold and not enabled:
self.resampling = OWTestLearners.KFold

@Inputs.learner
def set_learner(self, learner, key):
"""
Set the input `learner` for `key`.
Expand All @@ -346,9 +348,10 @@ def set_learner(self, learner, key):
self._invalidate([key])
del self.learners[key]
else:
self.learners[key] = Input(learner, None, None)
self.learners[key] = InputLearner(learner, None, None)
self._invalidate([key])

@Inputs.train_data
def set_train_data(self, data):
"""
Set the input training dataset.
Expand Down Expand Up @@ -400,6 +403,7 @@ def set_train_data(self, data):
self.resampling = OWTestLearners.FeatureFold
self._invalidate()

@Inputs.test_data
def set_test_data(self, data):
# type: (Orange.data.Table) -> None
"""
Expand Down Expand Up @@ -448,6 +452,7 @@ def _which_missing_data(self):
(False, True): " test "}[(self.train_data_missing_vals,
self.test_data_missing_vals)]

@Inputs.preprocessor
def set_preprocessor(self, preproc):
"""
Set the input preprocessor to apply on the training data.
Expand Down Expand Up @@ -656,8 +661,8 @@ def commit(self):
else:
combined = None
predictions = None
self.send("Evaluation Results", combined)
self.send("Predictions", predictions)
self.Outputs.evaluations_results.send(combined)
self.Outputs.predictions.send(predictions)

def send_report(self):
"""Report on the testing schema and results"""
Expand Down
7 changes: 5 additions & 2 deletions Orange/widgets/evaluate/tests/test_owtestlearners.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_basic(self):
data = Table("iris")[::3]
self.send_signal("Data", data)
self.send_signal("Learner", MajorityLearner(), 0, wait=5000)
res = self.get_output("Evaluation Results")
res = self.get_output(self.widget.Outputs.evaluations_results)
self.assertIsInstance(res, Results)
self.assertIsNotNone(res.domain)
self.assertIsNotNone(res.data)
Expand All @@ -48,7 +48,7 @@ def test_basic(self):

def test_testOnTest(self):
data = Table("iris")
self.send_signal("Data", data)
self.send_signal(self.widget.Inputs.train_data, data)
self.widget.resampling = OWTestLearners.TestOnTest
self.send_signal("Test Data", data)

Expand Down Expand Up @@ -168,3 +168,6 @@ def test_results_one_vs_rest(self):
np.testing.assert_equal(r1.row_indices, res.row_indices)
np.testing.assert_equal(r2.row_indices, res.row_indices)
np.testing.assert_equal(r3.row_indices, res.row_indices)

if __name__ == "__main__":
unittest.main()
24 changes: 14 additions & 10 deletions Orange/widgets/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def show(self, widget=None):
widget.show()
app.exec()

def send_signal(self, input_name, value, *args, widget=None, wait=-1):
def send_signal(self, input, value, *args, widget=None, wait=-1):
""" Send signal to widget by calling appropriate triggers.
Parameters
Expand All @@ -207,19 +207,21 @@ def send_signal(self, input_name, value, *args, widget=None, wait=-1):
"""
if widget is None:
widget = self.widget
for input_signal in widget.inputs:
if input_signal.name == input_name:
getattr(widget, input_signal.handler)(value, *args)
break
else:
raise ValueError("'{}' is not an input name for widget {}"
.format(input_name, type(widget).__name__))
if isinstance(input, str):
for input_signal in widget.get_signals("inputs"):
if input_signal.name == input:
input = input_signal
break
else:
raise ValueError("'{}' is not an input name for widget {}"
.format(input, type(widget).__name__))
getattr(widget, input.handler)(value, *args)
widget.handleNewSignals()
if wait >= 0 and widget.isBlocking():
spy = QSignalSpy(widget.blockingStateChanged)
self.assertTrue(spy.wait(timeout=wait))

def get_output(self, output_name, widget=None):
def get_output(self, output, widget=None):
"""Return the last output that has been sent from the widget.
Parameters
Expand All @@ -234,7 +236,9 @@ def get_output(self, output_name, widget=None):
"""
if widget is None:
widget = self.widget
return self.signal_manager.outputs.get((widget, output_name), None)
if not isinstance(output, str):
output = output.name
return self.signal_manager.outputs.get((widget, output), None)

@contextmanager
def modifiers(self, modifiers):
Expand Down
Loading

0 comments on commit e7e0a70

Please sign in to comment.