Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ exclude_lines =
except MemoryError
assert False
raise AssertionError
if (typing\.)?TYPE_CHECKING:
97 changes: 60 additions & 37 deletions Orange/widgets/data/owpythonscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from collections import defaultdict
from unittest.mock import patch

from typing import Optional, List, TYPE_CHECKING

from AnyQt.QtWidgets import (
QPlainTextEdit, QListView, QSizePolicy, QMenu, QSplitter, QLineEdit,
QAction, QToolButton, QFileDialog, QStyledItemDelegate,
Expand All @@ -17,7 +19,7 @@
QColor, QBrush, QPalette, QFont, QTextDocument,
QSyntaxHighlighter, QTextCharFormat, QTextCursor, QKeySequence,
)
from AnyQt.QtCore import Qt, QRegExp, QByteArray, QItemSelectionModel
from AnyQt.QtCore import Qt, QRegExp, QByteArray, QItemSelectionModel, QSize

from Orange.data import Table
from Orange.base import Learner, Model
Expand All @@ -28,6 +30,9 @@
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.widget import OWWidget, Input, Output

if TYPE_CHECKING:
from typing_extensions import TypedDict

__all__ = ["OWPythonScript"]


Expand Down Expand Up @@ -339,10 +344,17 @@ def __init__(self, name, script, flags=0, filename=None):
self.flags = flags
self.filename = filename

def asdict(self) -> '_ScriptData':
return dict(name=self.name, script=self.script, filename=self.filename)

@classmethod
def fromdict(cls, state: '_ScriptData') -> 'Script':
return Script(state["name"], state["script"], filename=state["filename"])


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

@staticmethod
def createEditor(parent, _option, _index):
def createEditor(self, parent, _option, _index):
return QLineEdit(parent)

@staticmethod
def setEditorData(editor, index):
def setEditorData(self, editor, index):
script = index.data(Qt.DisplayRole)
editor.setText(script.name)

@staticmethod
def setModelData(editor, model, index):
def setModelData(self, editor, model, index):
model[index.row()].name = str(editor.text())


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


if TYPE_CHECKING:
# pylint: disable=used-before-assignment
_ScriptData = TypedDict("_ScriptData", {
"name": str, "script": str, "filename": Optional[str]
})


class OWPythonScript(OWWidget):
name = "Python Script"
description = "Write a Python script and run it on input data or models."
Expand All @@ -405,13 +421,15 @@ class Outputs:

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

libraryListSource: list

libraryListSource = \
Setting([Script("Hello world", "print('Hello world')\n")])
settings_version = 2
scriptLibrary: 'List[_ScriptData]' = Setting([{
"name": "Hello world",
"script": "print('Hello world')\n",
"filename": None
}])
currentScriptIndex = Setting(0)
scriptText = Setting(None, schema_only=True)
splitterState = Setting(None)
scriptText: Optional[str] = Setting(None, schema_only=True)
splitterState: Optional[bytes] = Setting(None)

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

def __init__(self):
super().__init__()
self.libraryListSource = []

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

for s in self.libraryListSource:
s.flags = 0

self._cachedDocuments = {}

self.infoBox = gui.vBox(self.controlArea, 'Info')
Expand Down Expand Up @@ -544,31 +560,34 @@ def __init__(self):
self.console.document().setDefaultFont(QFont(defaultFont))
self.consoleBox.setAlignment(Qt.AlignBottom)
self.console.setTabStopWidth(4)

select_row(self.libraryView, self.currentScriptIndex)

self.restoreScriptText()
self.settingsAboutToBePacked.connect(self.saveScriptText)

self.splitCanvas.setSizes([2, 1])
if self.splitterState is not None:
self.splitCanvas.restoreState(QByteArray(self.splitterState))

self.setAcceptDrops(True)
self.controlArea.layout().addStretch(10)

self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved)
self.controlArea.layout().addStretch(1)
self.resize(800, 600)
self._restoreState()
self.settingsAboutToBePacked.connect(self._saveState)

def sizeHint(self) -> QSize:
return super().sizeHint().expandedTo(QSize(800, 600))

def _restoreState(self):
self.libraryListSource = [Script.fromdict(s) for s in self.scriptLibrary]
self.libraryList.wrap(self.libraryListSource)
select_row(self.libraryView, self.currentScriptIndex)

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):
if self.splitterState is not None:
self.splitCanvas.restoreState(QByteArray(self.splitterState))

def _saveState(self):
self.scriptLibrary = [s.asdict() for s in self.libraryListSource]
self.scriptText = self.text.toPlainText()
self.splitterState = bytes(self.splitCanvas.saveState())

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

def onSpliterMoved(self, _pos, _ind):
self.splitterState = bytes(self.splitCanvas.saveState())

def restoreSaved(self):
index = self.selectedScriptIndex()
if index is not None:
Expand Down Expand Up @@ -748,8 +764,7 @@ def commit(self):
out_var = None
getattr(self.Outputs, signal).send(out_var)

@staticmethod
def dragEnterEvent(event):
def dragEnterEvent(self, event): # pylint: disable=no-self-use
urls = event.mimeData().urls()
if urls:
# try reading the file as text
Expand All @@ -763,6 +778,14 @@ def dropEvent(self, event):
if urls:
self.text.pasteFile(urls[0])

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


if __name__ == "__main__": # pragma: no cover
WidgetPreview(OWPythonScript).run()
16 changes: 15 additions & 1 deletion Orange/widgets/data/tests/test_owpythonscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from Orange.data import Table
from Orange.classification import LogisticRegressionLearner
from Orange.tests import named_file
from Orange.widgets.data.owpythonscript import OWPythonScript, read_file_content
from Orange.widgets.data.owpythonscript import OWPythonScript, read_file_content, Script
from Orange.widgets.tests.base import WidgetTest, DummySignalManager
from Orange.widgets.widget import OWWidget

Expand Down Expand Up @@ -241,3 +241,17 @@ def test_shared_namespaces(self):
widget3.text.setPlainText("out_object = 2 * x")
widget3.execute_button.click()
self.assertIsNotNone(sys.last_traceback)

def test_migrate(self):
w = self.create_widget(OWPythonScript, {
"libraryListSource": [Script("A", "1")],
"__version__": 0
})
self.assertEqual(w.libraryListSource[0].name, "A")

def test_restore(self):
w = self.create_widget(OWPythonScript, {
"scriptLibrary": [dict(name="A", script="1", filename=None)],
"__version__": 2
})
self.assertEqual(w.libraryListSource[0].name, "A")