99 QStyle , QComboBox , QMessageBox , QGridLayout , QLabel , \
1010 QLineEdit , QSizePolicy as Policy , QCompleter
1111from AnyQt .QtCore import Qt , QTimer , QSize , QUrl
12+ from AnyQt .QtGui import QBrush
1213
14+ from orangewidget .utils .filedialogs import format_filter
1315from orangewidget .workflow .drophandler import SingleUrlDropHandler
1416
1517from Orange .data .table import Table , get_sample_datasets_dir
1618from Orange .data .io import FileFormat , UrlReader , class_from_qualified_name
19+ from Orange .data .io_base import MissingReaderException
1720from Orange .util import log_warnings
1821from Orange .widgets import widget , gui
1922from Orange .widgets .settings import Setting , ContextSetting , \
3033# module's namespace so that old saved settings still work
3134from Orange .widgets .utils .filedialogs import RecentPath
3235
36+ DEFAULT_READER_TEXT = "Automatically detect type"
37+
3338log = logging .getLogger (__name__ )
3439
3540
@@ -121,6 +126,9 @@ class Outputs:
121126
122127 domain_editor = SettingProvider (DomainEditor )
123128
129+ class Information (widget .OWWidget .Information ):
130+ no_file_selected = Msg ("No file selected." )
131+
124132 class Warning (widget .OWWidget .Warning ):
125133 file_too_big = Msg ("The file is too large to load automatically."
126134 " Press Reload to load." )
@@ -137,9 +145,6 @@ class Error(widget.OWWidget.Error):
137145 sheet_error = Msg ("Error listing available sheets." )
138146 unknown = Msg ("Read error:\n {}" )
139147
140- class NoFileSelected :
141- pass
142-
143148 UserAdviceMessages = [
144149 widget .Message (
145150 "Use CSV File Import widget for advanced options "
@@ -160,6 +165,23 @@ def __init__(self):
160165 self .loaded_file = ""
161166 self .reader = None
162167
168+ readers = [f for f in FileFormat .formats
169+ if getattr (f , 'read' , None )
170+ and getattr (f , "EXTENSIONS" , None )]
171+
172+ def group_readers_per_addon_key (w ):
173+ # readers from Orange.data.io should go first
174+ def package (w ):
175+ package = w .qualified_name ().split ("." )[:- 1 ]
176+ package = package [:2 ]
177+ if "." .join (package ) == "Orange.data" :
178+ return ["0" ] # force "Orange" to come first
179+ return package
180+ return package (w ), w .DESCRIPTION
181+
182+ self .available_readers = sorted (set (readers ),
183+ key = group_readers_per_addon_key )
184+
163185 layout = QGridLayout ()
164186 layout .setSpacing (4 )
165187 gui .widgetBox (self .controlArea , orientation = layout , box = 'Source' )
@@ -227,6 +249,19 @@ def __init__(self):
227249 completer .setCaseSensitivity (Qt .CaseSensitive )
228250 url_combo .setCompleter (completer )
229251
252+ layout = QGridLayout ()
253+ layout .setSpacing (4 )
254+ gui .widgetBox (self .controlArea , orientation = layout , box = 'File Type' )
255+
256+ box = gui .hBox (None , addToLayout = False , margin = 0 )
257+ box .setSizePolicy (Policy .MinimumExpanding , Policy .Fixed )
258+ self .reader_combo = QComboBox (self )
259+ self .reader_combo .setSizePolicy (Policy .MinimumExpanding , Policy .Fixed )
260+ self .reader_combo .activated [int ].connect (self .select_reader )
261+
262+ box .layout ().addWidget (self .reader_combo )
263+ layout .addWidget (box , 0 , 1 )
264+
230265 box = gui .vBox (self .controlArea , "Info" )
231266 self .infolabel = gui .widgetLabel (box , 'No data loaded.' )
232267
@@ -286,6 +321,23 @@ def select_sheet(self):
286321 self .recent_paths [0 ].sheet = self .sheet_combo .currentText ()
287322 self .load_data ()
288323
324+ def select_reader (self , n ):
325+ if self .source != self .LOCAL_FILE :
326+ return # ignore for URL's
327+
328+ if self .recent_paths :
329+ path = self .recent_paths [0 ]
330+ if n == 0 : # default
331+ path .file_format = None
332+ self .load_data ()
333+ elif n <= len (self .available_readers ):
334+ reader = self .available_readers [n - 1 ]
335+ path .file_format = reader .qualified_name ()
336+ self .load_data ()
337+ else : # the rest include just qualified names
338+ path .file_format = self .reader_combo .itemText (n )
339+ self .load_data ()
340+
289341 def _url_set (self ):
290342 url = self .url_combo .currentText ()
291343 pos = self .recent_urls .index (url )
@@ -310,10 +362,7 @@ def browse_file(self, in_demos=False):
310362 else :
311363 start_file = self .last_path () or os .path .expanduser ("~/" )
312364
313- readers = [f for f in FileFormat .formats
314- if getattr (f , 'read' , None )
315- and getattr (f , "EXTENSIONS" , None )]
316- filename , reader , _ = open_filename_dialog (start_file , None , readers )
365+ filename , reader , _ = open_filename_dialog (start_file , None , self .available_readers )
317366 if not filename :
318367 return
319368 self .add_path (filename )
@@ -342,19 +391,33 @@ def load_data(self):
342391 self .infolabel .setText ("No data." )
343392
344393 def _try_load (self ):
394+ self ._initialize_reader_combo ()
395+
345396 # pylint: disable=broad-except
346- if self .last_path () and not os .path .exists (self .last_path ()):
347- return self .Error .file_not_found
397+ if self .source == self .LOCAL_FILE :
398+ if self .last_path () is None :
399+ return self .Information .no_file_selected
400+ elif not os .path .exists (self .last_path ()):
401+ return self .Error .file_not_found
402+ else :
403+ url = self .url_combo .currentText ().strip ()
404+ if not url :
405+ return self .Information .no_file_selected
406+
407+ def mark_problematic_reader ():
408+ self .reader_combo .setItemData (self .reader_combo .currentIndex (),
409+ QBrush (Qt .red ), Qt .ForegroundRole )
348410
349411 try :
350- self .reader = self ._get_reader ()
412+ self .reader = self ._get_reader () # also sets current reader index
351413 assert self .reader is not None
352- except Exception :
414+ except MissingReaderException :
415+ mark_problematic_reader ()
353416 return self .Error .missing_reader
354-
355- if self . reader is self . NoFileSelected :
356- self . Outputs . data . send ( None )
357- return None
417+ except Exception as ex :
418+ mark_problematic_reader ()
419+ log . exception ( ex )
420+ return lambda x = ex : self . Error . unknown ( str ( x ))
358421
359422 try :
360423 self ._update_sheet_combo ()
@@ -365,6 +428,7 @@ def _try_load(self):
365428 try :
366429 data = self .reader .read ()
367430 except Exception as ex :
431+ mark_problematic_reader ()
368432 log .exception (ex )
369433 return lambda x = ex : self .Error .unknown (str (x ))
370434 if warnings :
@@ -382,23 +446,31 @@ def _try_load(self):
382446 def _get_reader (self ) -> FileFormat :
383447 if self .source == self .LOCAL_FILE :
384448 path = self .last_path ()
385- if path is None :
386- return self .NoFileSelected
449+ self .reader_combo .setEnabled (True )
387450 if self .recent_paths and self .recent_paths [0 ].file_format :
388451 qname = self .recent_paths [0 ].file_format
389- reader_class = class_from_qualified_name (qname )
452+ qname_index = {r .qualified_name (): i for i , r in enumerate (self .available_readers )}
453+ if qname in qname_index :
454+ self .reader_combo .setCurrentIndex (qname_index [qname ] + 1 )
455+ else :
456+ # reader may be accessible, but not in self.available_readers
457+ # (perhaps its code was moved)
458+ self .reader_combo .addItem (qname )
459+ self .reader_combo .setCurrentIndex (len (self .reader_combo ) - 1 )
460+ try :
461+ reader_class = class_from_qualified_name (qname )
462+ except Exception as ex :
463+ raise MissingReaderException (f'Can not find reader "{ qname } "' ) from ex
390464 reader = reader_class (path )
391465 else :
466+ self .reader_combo .setCurrentIndex (0 )
392467 reader = FileFormat .get_reader (path )
393468 if self .recent_paths and self .recent_paths [0 ].sheet :
394469 reader .select_sheet (self .recent_paths [0 ].sheet )
395470 return reader
396471 else :
397472 url = self .url_combo .currentText ().strip ()
398- if url :
399- return UrlReader (url )
400- else :
401- return self .NoFileSelected
473+ return UrlReader (url )
402474
403475 def _update_sheet_combo (self ):
404476 if len (self .reader .sheets ) < 2 :
@@ -420,6 +492,14 @@ def _select_active_sheet(self):
420492 self .reader .select_sheet (None )
421493 self .sheet_combo .setCurrentIndex (0 )
422494
495+ def _initialize_reader_combo (self ):
496+ self .reader_combo .clear ()
497+ filters = [format_filter (f ) for f in self .available_readers ]
498+ self .reader_combo .addItems ([DEFAULT_READER_TEXT ] + filters )
499+ self .reader_combo .setCurrentIndex (0 )
500+ self .reader_combo .setDisabled (True )
501+ # additional readers may be added in self._get_reader()
502+
423503 @staticmethod
424504 def _describe (table ):
425505 def missing_prop (prop ):
@@ -551,7 +631,7 @@ def dragEnterEvent(event):
551631 try :
552632 FileFormat .get_reader (urls [0 ].toLocalFile ())
553633 event .acceptProposedAction ()
554- except IOError :
634+ except MissingReaderException :
555635 pass
556636
557637 def dropEvent (self , event ):
0 commit comments