Repository: biolab/orange3

Issue:
Python scripts not saving in ows file

##### Orange version
3.19 on Windows 10

##### Expected behavior
Python script object changes should save with project.


##### Actual behavior
After saving project with python script and closing Orange, the next time I open the project the python script is back to the default "Hello World" example instead of my saved script.


##### Steps to reproduce the behavior
1. Create new project with python script object
2. Modify the python script to something other than Hello World
3. Save the project
4. Close orange
5. Re-open Orange 
6. Open the project
7. The python script reverts to Hello World


##### Additional info (worksheets, data, screenshots, ...)





Discussion:

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
@@ -21,7 +21,7 @@
 from Orange.base import Learner, Model
 from Orange.widgets import widget, gui
 from Orange.widgets.utils import itemmodels
-from Orange.widgets.settings import Setting
+from Orange.widgets.settings import Setting, SettingsHandler
 from Orange.widgets.utils.widgetpreview import WidgetPreview
 from Orange.widgets.widget import OWWidget, Input, Output
 
@@ -364,6 +364,14 @@ def select_row(view, row):
                     QItemSelectionModel.ClearAndSelect)
 
 
+class PrepareSavingSettingsHandler(SettingsHandler):
+    """Calls storeSpecificSettings, which is currently not called from non-context handlers."""
+
+    def pack_data(self, widget):
+        widget.storeSpecificSettings()
+        return super().pack_data(widget)
+
+
 class OWPythonScript(widget.OWWidget):
     name = "Python Script"
     description = "Write a Python script and run it on input data or models."
@@ -389,9 +397,12 @@ class Outputs:
 
     signal_names = ("data", "learner", "classifier", "object")
 
+    settingsHandler = PrepareSavingSettingsHandler()
+
     libraryListSource = \
         Setting([Script("Hello world", "print('Hello world')\n")])
     currentScriptIndex = Setting(0)
+    scriptText = Setting(None, schema_only=True)
     splitterState = Setting(None)
 
     class Error(OWWidget.Error):
@@ -463,14 +474,17 @@ def __init__(self):
 
         new_from_file = QAction("Import Script from File", self)
         save_to_file = QAction("Save Selected Script to File", self)
+        restore_saved = QAction("Undo Changes to Selected Script", self)
         save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs))
 
         new_from_file.triggered.connect(self.onAddScriptFromFile)
         save_to_file.triggered.connect(self.saveScript)
+        restore_saved.triggered.connect(self.restoreSaved)
 
         menu = QMenu(w)
         menu.addAction(new_from_file)
         menu.addAction(save_to_file)
+        menu.addAction(restore_saved)
         action.setMenu(menu)
         button = w.addAction(action)
         button.setPopupMode(QToolButton.InstantPopup)
@@ -513,6 +527,8 @@ def __init__(self):
 
         select_row(self.libraryView, self.currentScriptIndex)
 
+        self.restoreScriptText()
+
         self.splitCanvas.setSizes([2, 1])
         if self.splitterState is not None:
             self.splitCanvas.restoreState(QByteArray(self.splitterState))
@@ -521,6 +537,19 @@ def __init__(self):
         self.controlArea.layout().addStretch(1)
         self.resize(800, 600)
 
+    def storeSpecificSettings(self):
+        self.saveScriptText()
+
+    def restoreScriptText(self):
+        if self.scriptText is not None:
+            current = self.text.toPlainText()
+            # do not mark scripts as modified
+            if self.scriptText != current:
+                self.text.document().setPlainText(self.scriptText)
+
+    def saveScriptText(self):
+        self.scriptText = self.text.toPlainText()
+
     def handle_input(self, obj, id, signal):
         id = id[0]
         dic = getattr(self, signal)
@@ -560,7 +589,7 @@ def setSelectedScript(self, index):
         select_row(self.libraryView, index)
 
     def onAddScript(self, *args):
-        self.libraryList.append(Script("New script", "", 0))
+        self.libraryList.append(Script("New script", self.text.toPlainText(), 0))
         self.setSelectedScript(len(self.libraryList) - 1)
 
     def onAddScriptFromFile(self, *args):
@@ -602,7 +631,6 @@ def onSelectedScriptChanged(self, selected, deselected):
     def documentForScript(self, script=0):
         if type(script) != Script:
             script = self.libraryList[script]
-
         if script not in self._cachedDocuments:
             doc = QTextDocument(self)
             doc.setDocumentLayout(QPlainTextDocumentLayout(doc))
@@ -630,13 +658,11 @@ def onModificationChanged(self, modified):
     def onSpliterMoved(self, pos, ind):
         self.splitterState = bytes(self.splitCanvas.saveState())
 
-    def updateSelecetdScriptState(self):
+    def restoreSaved(self):
         index = self.selectedScriptIndex()
         if index is not None:
-            script = self.libraryList[index]
-            self.libraryList[index] = Script(script.name,
-                                             self.text.toPlainText(),
-                                             0)
+            self.text.document().setPlainText(self.libraryList[index].script)
+            self.text.document().setModified(False)
 
     def saveScript(self):
         index = self.selectedScriptIndex()


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
@@ -111,3 +111,26 @@ def test_multiple_signals(self):
         click()
         self.assertIsNone(console_locals["in_data"])
         self.assertEqual(console_locals["in_datas"], [])
+
+    def test_store_new_script(self):
+        self.widget.text.setPlainText("42")
+        self.widget.onAddScript()
+        script = self.widget.text.toPlainText()
+        self.assertEqual("42", script)
+
+    def test_restore_from_library(self):
+        before = self.widget.text.toPlainText()
+        self.widget.text.setPlainText("42")
+        self.widget.restoreSaved()
+        script = self.widget.text.toPlainText()
+        self.assertEqual(before, script)
+
+    def test_store_current_script(self):
+        self.widget.text.setPlainText("42")
+        settings = self.widget.settingsHandler.pack_data(self.widget)
+        self.widget = self.create_widget(OWPythonScript)
+        script = self.widget.text.toPlainText()
+        self.assertNotEqual("42", script)
+        self.widget = self.create_widget(OWPythonScript, stored_settings=settings)
+        script = self.widget.text.toPlainText()
+        self.assertEqual("42", script)