88from collections import defaultdict
99from unittest .mock import patch
1010
11+ from typing import Optional , List , TYPE_CHECKING
12+
1113from AnyQt .QtWidgets import (
1214 QPlainTextEdit , QListView , QSizePolicy , QMenu , QSplitter , QLineEdit ,
1315 QAction , QToolButton , QFileDialog , QStyledItemDelegate ,
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
2224from Orange .data import Table
2325from Orange .base import Learner , Model
2830from Orange .widgets .utils .widgetpreview import WidgetPreview
2931from 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
343355class 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+
383399class 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
767790if __name__ == "__main__" : # pragma: no cover
768791 WidgetPreview (OWPythonScript ).run ()
0 commit comments