Skip to content

Commit c488819

Browse files
authored
Merge pull request #4345 from ales-erjavec/python-script-state
[FIX] Python script serialization state
2 parents 44e75c7 + badc2fa commit c488819

File tree

3 files changed

+76
-38
lines changed

3 files changed

+76
-38
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ exclude_lines =
1919
except MemoryError
2020
assert False
2121
raise AssertionError
22+
if (typing\.)?TYPE_CHECKING:

Orange/widgets/data/owpythonscript.py

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from collections import defaultdict
99
from unittest.mock import patch
1010

11+
from typing import Optional, List, TYPE_CHECKING
12+
1113
from AnyQt.QtWidgets import (
1214
QPlainTextEdit, QListView, QSizePolicy, QMenu, QSplitter, QLineEdit,
1315
QAction, QToolButton, QFileDialog, QStyledItemDelegate,
@@ -17,7 +19,7 @@
1719
QColor, QBrush, QPalette, QFont, QTextDocument,
1820
QSyntaxHighlighter, QTextCharFormat, QTextCursor, QKeySequence,
1921
)
20-
from AnyQt.QtCore import Qt, QRegExp, QByteArray, QItemSelectionModel
22+
from AnyQt.QtCore import Qt, QRegExp, QByteArray, QItemSelectionModel, QSize
2123

2224
from Orange.data import Table
2325
from Orange.base import Learner, Model
@@ -28,6 +30,9 @@
2830
from Orange.widgets.utils.widgetpreview import WidgetPreview
2931
from Orange.widgets.widget import OWWidget, Input, Output
3032

33+
if TYPE_CHECKING:
34+
from typing_extensions import TypedDict
35+
3136
__all__ = ["OWPythonScript"]
3237

3338

@@ -339,10 +344,17 @@ def __init__(self, name, script, flags=0, filename=None):
339344
self.flags = flags
340345
self.filename = filename
341346

347+
def asdict(self) -> '_ScriptData':
348+
return dict(name=self.name, script=self.script, filename=self.filename)
349+
350+
@classmethod
351+
def fromdict(cls, state: '_ScriptData') -> 'Script':
352+
return Script(state["name"], state["script"], filename=state["filename"])
353+
342354

343355
class ScriptItemDelegate(QStyledItemDelegate):
344-
@staticmethod
345-
def displayText(script, _locale):
356+
# pylint: disable=no-self-use
357+
def displayText(self, script, _locale):
346358
if script.flags & Script.Modified:
347359
return "*" + script.name
348360
else:
@@ -357,17 +369,14 @@ def paint(self, painter, option, index):
357369
option.palette.setColor(QPalette.Highlight, QColor(Qt.darkRed))
358370
super().paint(painter, option, index)
359371

360-
@staticmethod
361-
def createEditor(parent, _option, _index):
372+
def createEditor(self, parent, _option, _index):
362373
return QLineEdit(parent)
363374

364-
@staticmethod
365-
def setEditorData(editor, index):
375+
def setEditorData(self, editor, index):
366376
script = index.data(Qt.DisplayRole)
367377
editor.setText(script.name)
368378

369-
@staticmethod
370-
def setModelData(editor, model, index):
379+
def setModelData(self, editor, model, index):
371380
model[index.row()].name = str(editor.text())
372381

373382

@@ -380,6 +389,13 @@ def select_row(view, row):
380389
QItemSelectionModel.ClearAndSelect)
381390

382391

392+
if TYPE_CHECKING:
393+
# pylint: disable=used-before-assignment
394+
_ScriptData = TypedDict("_ScriptData", {
395+
"name": str, "script": str, "filename": Optional[str]
396+
})
397+
398+
383399
class OWPythonScript(OWWidget):
384400
name = "Python Script"
385401
description = "Write a Python script and run it on input data or models."
@@ -405,13 +421,15 @@ class Outputs:
405421

406422
signal_names = ("data", "learner", "classifier", "object")
407423

408-
libraryListSource: list
409-
410-
libraryListSource = \
411-
Setting([Script("Hello world", "print('Hello world')\n")])
424+
settings_version = 2
425+
scriptLibrary: 'List[_ScriptData]' = Setting([{
426+
"name": "Hello world",
427+
"script": "print('Hello world')\n",
428+
"filename": None
429+
}])
412430
currentScriptIndex = Setting(0)
413-
scriptText = Setting(None, schema_only=True)
414-
splitterState = Setting(None)
431+
scriptText: Optional[str] = Setting(None, schema_only=True)
432+
splitterState: Optional[bytes] = Setting(None)
415433

416434
# Widgets in the same schema share namespace through a dictionary whose
417435
# key is self.signalManager. ales-erjavec expressed concern (and I fully
@@ -426,13 +444,11 @@ class Error(OWWidget.Error):
426444

427445
def __init__(self):
428446
super().__init__()
447+
self.libraryListSource = []
429448

430449
for name in self.signal_names:
431450
setattr(self, name, {})
432451

433-
for s in self.libraryListSource:
434-
s.flags = 0
435-
436452
self._cachedDocuments = {}
437453

438454
self.infoBox = gui.vBox(self.controlArea, 'Info')
@@ -544,31 +560,34 @@ def __init__(self):
544560
self.console.document().setDefaultFont(QFont(defaultFont))
545561
self.consoleBox.setAlignment(Qt.AlignBottom)
546562
self.console.setTabStopWidth(4)
547-
548-
select_row(self.libraryView, self.currentScriptIndex)
549-
550-
self.restoreScriptText()
551-
self.settingsAboutToBePacked.connect(self.saveScriptText)
552-
553563
self.splitCanvas.setSizes([2, 1])
554-
if self.splitterState is not None:
555-
self.splitCanvas.restoreState(QByteArray(self.splitterState))
556-
557564
self.setAcceptDrops(True)
565+
self.controlArea.layout().addStretch(10)
558566

559-
self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved)
560-
self.controlArea.layout().addStretch(1)
561-
self.resize(800, 600)
567+
self._restoreState()
568+
self.settingsAboutToBePacked.connect(self._saveState)
569+
570+
def sizeHint(self) -> QSize:
571+
return super().sizeHint().expandedTo(QSize(800, 600))
572+
573+
def _restoreState(self):
574+
self.libraryListSource = [Script.fromdict(s) for s in self.scriptLibrary]
575+
self.libraryList.wrap(self.libraryListSource)
576+
select_row(self.libraryView, self.currentScriptIndex)
562577

563-
def restoreScriptText(self):
564578
if self.scriptText is not None:
565579
current = self.text.toPlainText()
566580
# do not mark scripts as modified
567581
if self.scriptText != current:
568582
self.text.document().setPlainText(self.scriptText)
569583

570-
def saveScriptText(self):
584+
if self.splitterState is not None:
585+
self.splitCanvas.restoreState(QByteArray(self.splitterState))
586+
587+
def _saveState(self):
588+
self.scriptLibrary = [s.asdict() for s in self.libraryListSource]
571589
self.scriptText = self.text.toPlainText()
590+
self.splitterState = bytes(self.splitCanvas.saveState())
572591

573592
def handle_input(self, obj, sig_id, signal):
574593
sig_id = sig_id[0]
@@ -675,9 +694,6 @@ def onModificationChanged(self, modified):
675694
self.libraryList[index].flags = Script.Modified if modified else 0
676695
self.libraryList.emitDataChanged(index)
677696

678-
def onSpliterMoved(self, _pos, _ind):
679-
self.splitterState = bytes(self.splitCanvas.saveState())
680-
681697
def restoreSaved(self):
682698
index = self.selectedScriptIndex()
683699
if index is not None:
@@ -748,8 +764,7 @@ def commit(self):
748764
out_var = None
749765
getattr(self.Outputs, signal).send(out_var)
750766

751-
@staticmethod
752-
def dragEnterEvent(event):
767+
def dragEnterEvent(self, event): # pylint: disable=no-self-use
753768
urls = event.mimeData().urls()
754769
if urls:
755770
# try reading the file as text
@@ -763,6 +778,14 @@ def dropEvent(self, event):
763778
if urls:
764779
self.text.pasteFile(urls[0])
765780

781+
@classmethod
782+
def migrate_settings(cls, settings, version):
783+
if version is not None and version < 2:
784+
scripts = settings.pop("libraryListSource") # type: List[Script]
785+
library = [dict(name=s.name, script=s.script, filename=s.filename)
786+
for s in scripts] # type: List[_ScriptData]
787+
settings["scriptLibrary"] = library
788+
766789

767790
if __name__ == "__main__": # pragma: no cover
768791
WidgetPreview(OWPythonScript).run()

Orange/widgets/data/tests/test_owpythonscript.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from Orange.data import Table
99
from Orange.classification import LogisticRegressionLearner
1010
from Orange.tests import named_file
11-
from Orange.widgets.data.owpythonscript import OWPythonScript, read_file_content
11+
from Orange.widgets.data.owpythonscript import OWPythonScript, read_file_content, Script
1212
from Orange.widgets.tests.base import WidgetTest, DummySignalManager
1313
from Orange.widgets.widget import OWWidget
1414

@@ -241,3 +241,17 @@ def test_shared_namespaces(self):
241241
widget3.text.setPlainText("out_object = 2 * x")
242242
widget3.execute_button.click()
243243
self.assertIsNotNone(sys.last_traceback)
244+
245+
def test_migrate(self):
246+
w = self.create_widget(OWPythonScript, {
247+
"libraryListSource": [Script("A", "1")],
248+
"__version__": 0
249+
})
250+
self.assertEqual(w.libraryListSource[0].name, "A")
251+
252+
def test_restore(self):
253+
w = self.create_widget(OWPythonScript, {
254+
"scriptLibrary": [dict(name="A", script="1", filename=None)],
255+
"__version__": 2
256+
})
257+
self.assertEqual(w.libraryListSource[0].name, "A")

0 commit comments

Comments
 (0)