Repository: biolab/orange3

Issue:
Using a function in separate Python Scripts
Hello,

Is it possible to define a function in one Python Script in my workflow and call it in a different script with or without them being connected? The closest I've found is using the function as the in_object and out_object.
![Functions](https://user-images.githubusercontent.com/50418145/58351112-6e00d380-7e42-11e9-8e5b-e685fe174dd4.png)




Discussion:
This is doable, but we must think if we're ready to accept the consequences. I suppose that you would also like to be able to define a class in one widget and use it in another. Or prepare some data in one widget and use it in others? In other words, all Python Script widgets would share a common namespace.

Setting `x = 1` in one widget and `x = 2` in another and `x = 3` in the third one would set x to ... well, it depends upon which widget was executed the last. If we agree that this is user's problem, not ours, I suppose implementing shared namespaces shouldn't be a problem.

(Ehm. If you open multiple schemas at the same time, we wouldn't like to share namespaces across them. I suppose we could share namespace across all widgets that share the same instance of signal manager? This is not very nice.) 
I wouldn't want a shared namespace. Even something like being able to say 'from Function import F' would be helpful. Sharing variables across scripts would be a problem.
Why is out_object not ok?
Using out_object works, but it's tedious when I need the same function in four or five different scripts, or need to pass in more important objects.
Can I connect multiple in_objects to a single script?

Resolving patch:
diff --git a/Orange/widgets/data/owpythonscript.py b/Orange/widgets/data/owpythonscript.py
--- a/Orange/widgets/data/owpythonscript.py
+++ b/Orange/widgets/data/owpythonscript.py
@@ -4,6 +4,8 @@
 import keyword
 import itertools
 import unicodedata
+from functools import reduce
+from collections import defaultdict
 from unittest.mock import patch
 
 from AnyQt.QtWidgets import (
@@ -416,6 +418,14 @@ class Outputs:
     scriptText = Setting(None, schema_only=True)
     splitterState = Setting(None)
 
+    # Widgets in the same schema share namespace through a dictionary whose
+    # key is self.signalManager. ales-erjavec expressed concern (and I fully
+    # agree!) about widget being aware of the outside world. I am leaving this
+    # anyway. If this causes any problems in the future, replace this with
+    # shared_namespaces = {} and thus use a common namespace for all instances
+    # of # PythonScript even if they are in different schemata.
+    shared_namespaces = defaultdict(dict)
+
     class Error(OWWidget.Error):
         pass
 
@@ -706,7 +716,7 @@ def saveScript(self):
             f.close()
 
     def initial_locals_state(self):
-        d = {}
+        d = self.shared_namespaces[self.signalManager].copy()
         for name in self.signal_names:
             value = getattr(self, name)
             all_values = list(value.values())
@@ -715,6 +725,14 @@ def initial_locals_state(self):
             d["in_" + name] = one_value
         return d
 
+    def update_namespace(self, namespace):
+        not_saved = reduce(set.union,
+                           ({f"in_{name}s", f"in_{name}", f"out_{name}"}
+                            for name in self.signal_names))
+        self.shared_namespaces[self.signalManager].update(
+            {name: value for name, value in namespace.items()
+             if name not in not_saved})
+
     def commit(self):
         self.Error.clear()
         lcls = self.initial_locals_state()
@@ -723,6 +741,7 @@ def commit(self):
         self.console.write("\nRunning script:\n")
         self.console.push("exec(_script)")
         self.console.new_prompt(sys.ps1)
+        self.update_namespace(self.console.locals)
         for signal in self.signal_names:
             out_var = self.console.locals.get("out_" + signal)
             signal_type = getattr(self.Outputs, signal).type


Test patch:
diff --git a/Orange/widgets/data/tests/test_owpythonscript.py b/Orange/widgets/data/tests/test_owpythonscript.py
--- a/Orange/widgets/data/tests/test_owpythonscript.py
+++ b/Orange/widgets/data/tests/test_owpythonscript.py
@@ -1,5 +1,7 @@
 # Test methods with long descriptive names can omit docstrings
 # pylint: disable=missing-docstring
+import sys
+
 from AnyQt.QtCore import QMimeData, QUrl, QPoint, Qt
 from AnyQt.QtGui import QDragEnterEvent, QDropEvent
 
@@ -7,7 +9,7 @@
 from Orange.classification import LogisticRegressionLearner
 from Orange.tests import named_file
 from Orange.widgets.data.owpythonscript import OWPythonScript, read_file_content
-from Orange.widgets.tests.base import WidgetTest
+from Orange.widgets.tests.base import WidgetTest, DummySignalManager
 from Orange.widgets.widget import OWWidget
 
 
@@ -212,3 +214,30 @@ def _drop_event(self, url):
         return QDropEvent(
             QPoint(0, 0), Qt.MoveAction, data,
             Qt.NoButton, Qt.NoModifier, QDropEvent.Drop)
+
+    def test_shared_namespaces(self):
+        widget1 = self.create_widget(OWPythonScript)
+        widget2 = self.create_widget(OWPythonScript)
+        self.signal_manager = DummySignalManager()
+        widget3 = self.create_widget(OWPythonScript)
+
+        self.send_signal(widget1.Inputs.data, self.iris, (1,), widget=widget1)
+        widget1.text.setPlainText("x = 42\n"
+                                  "out_data = in_data\n")
+        widget1.execute_button.click()
+        self.assertIs(
+            self.get_output(widget1.Outputs.data, widget=widget1),
+            self.iris)
+
+        widget2.text.setPlainText("out_object = 2 * x\n"
+                                  "out_data = in_data")
+        widget2.execute_button.click()
+        self.assertEqual(
+            self.get_output(widget1.Outputs.object, widget=widget2),
+            84)
+        self.assertIsNone(self.get_output(widget1.Outputs.data, widget=widget2))
+
+        sys.last_traceback = None
+        widget3.text.setPlainText("out_object = 2 * x")
+        widget3.execute_button.click()
+        self.assertIsNotNone(sys.last_traceback)
diff --git a/Orange/widgets/tests/base.py b/Orange/widgets/tests/base.py
--- a/Orange/widgets/tests/base.py
+++ b/Orange/widgets/tests/base.py
@@ -356,7 +356,7 @@ def get_output(self, output, widget=None, wait=DEFAULT_TIMEOUT):
         outputs = widget.outputs or widget.Outputs.__dict__.values()
         assert output in (out.name for out in outputs), \
             "widget {} has no output {}".format(widget.name, output)
-        return self.signal_manager.outputs.get((widget, output), None)
+        return widget.signalManager.outputs.get((widget, output), None)
 
     @contextmanager
     def modifiers(self, modifiers):