source: orange/Orange/OrangeWidgets/Prototypes/OWCSVFileImport.py @ 10648:1f4675e7b566

Revision 10648:1f4675e7b566, 14.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Replace files in the recent list.

Line 
1"""
2<name>CSV File import</name>
3<description>Import .csv file</description>
4
5"""
6import os
7import csv
8from StringIO import StringIO
9
10from PyQt4 import QtCore, QtGui
11
12from OWWidget import *
13import OWGUI
14import Orange
15
16MakeStatus = Orange.feature.Descriptor.MakeStatus
17
18from OWDataTable import ExampleTableModel
19
20class standard_icons(object):
21    def __init__(self, qwidget=None, style=None):
22        self.qwidget = qwidget
23        if qwidget is None:
24            self.style = QApplication.instance().style()
25        else:
26            self.style = qwidget.style()
27   
28    @property
29    def dir_open_icon(self):
30        return self.style.standardIcon(QStyle.SP_DirOpenIcon)
31   
32    @property
33    def reload_icon(self):
34        return self.style.standardIcon(QStyle.SP_BrowserReload)
35   
36   
37class OWCSVFileImport(OWWidget):
38#    contextHandlers = {"": PathContextHandler("")}
39    settingsList = ["recent_files"]
40   
41    DELIMITERS = [("Tab", "\t"),
42                  ("Comma", ","),
43                  ("Semicolon", ";"),
44                  ("Space", " "),
45                  ("Others", None),
46                  ]
47   
48    def __init__(self, parent=None, signalManager=None, 
49                 title="CSV File Import"):
50       
51        OWWidget.__init__(self, parent, signalManager, title, 
52                          wantMainArea=False)
53       
54        self.inputs = []
55        self.outputs = [("Data", Orange.data.Table)]
56       
57        # Settings
58        self.delimiter = ","
59        self.other_delimiter = None
60        self.quote = '"'
61        self.missing = ""
62       
63        self.skipinitialspace = True
64        self.has_header = True
65        self.has_orange_header = True
66       
67        self.recent_files = []
68       
69        # Hints for the recent files
70        self.hints = {}
71       
72        self.loadSettings()
73       
74        self.recent_files = filter(os.path.exists, self.recent_files)
75       
76        layout = QHBoxLayout()
77        box = OWGUI.widgetBox(self.controlArea, "File", orientation=layout)
78        self.style().standardIcon(QStyle.SP_DirOpenIcon)
79        icons = standard_icons(self)
80       
81        self.recent_combo = QComboBox(self, objectName="recent_combo",
82                                      toolTip="Recent files.",
83                                      activated=self.on_select_recent)
84        self.recent_combo.addItems([os.path.basename(p) for p in self.recent_files])
85       
86        self.browse_button = QPushButton("...", icon=icons.dir_open_icon,
87                                         toolTip="Browse filesystem",
88                                         clicked=self.on_open_dialog)
89       
90#        self.reload_button = QPushButton("Reload", icon=icons.reload_icon,
91#                                         clicked=self.reload_file)
92       
93        layout.addWidget(self.recent_combo, 2)
94        layout.addWidget(self.browse_button)
95#        layout.addWidget(self.reload_button)
96
97        #################
98        # Cell separators
99        #################
100        grid_layout = QGridLayout()
101        grid_layout.setVerticalSpacing(4)
102        grid_layout.setHorizontalSpacing(4)
103        box = OWGUI.widgetBox(self.controlArea, "Cell Separator",
104                              orientation=grid_layout)
105       
106        button_group = QButtonGroup(box)
107        QObject.connect(button_group, 
108                        SIGNAL("buttonPressed(int)"),
109                        self.delimiter_changed
110                        )
111       
112        for i, (name, char) in  enumerate(self.DELIMITERS[:-1]):
113            button = QRadioButton(name, box,
114                                  toolTip="Use %r as cell separator" % char)
115           
116            button_group.addButton(button, i)
117            grid_layout.addWidget(button, i / 3, i % 3)
118           
119        button = QRadioButton("Other", box,
120                              toolTip="Use other character")
121       
122        button_group.addButton(button, i + 1)
123        grid_layout.addWidget(button, i / 3 + 1, 0)
124        self.delimiter_button_group = button_group
125           
126        self.delimiter_edit = QLineEdit(objectName="delimiter_edit",
127                                        text=self.other_delimiter or self.delimiter,
128                                        editingFinished=self.delimiter_changed,
129                                        toolTip="Cell delimiter character.")
130       
131        grid_layout.addWidget(self.delimiter_edit, i / 3 + 1, 1, -1, -1)
132       
133        preset = [d[1] for d in self.DELIMITERS[:-1]]
134        if self.delimiter in preset:
135            index = preset.index(self.delimiter)
136            b = button_group.button(index)
137            b.setChecked(True)
138            self.delimiter_edit.setEnabled(False)
139        else:
140            button.setChecked(True)
141            self.delimiter_edit.setEnabled(True)
142       
143        ###############
144        # Other options
145        ###############
146        form = QFormLayout()
147        box = OWGUI.widgetBox(self.controlArea, "Other Options",
148                              orientation=form)
149       
150        self.quote_edit = QLineEdit(objectName="quote_edit",
151                                    text=self.quote,
152                                    editingFinished=self.quote_changed,
153                                    toolTip="Text quote character.")
154       
155        form.addRow("Quote", self.quote_edit)
156       
157        self.missing_edit = QLineEdit(objectName="missing_edit",
158                                      text=self.missing,
159                                      editingFinished=self.missing_changed,
160                                      toolTip="Missing value flags (separated by a comma)."
161                                      )
162       
163        form.addRow("Missing values", self.missing_edit)
164       
165        self.skipinitialspace_check = \
166                QCheckBox(objectName="skipinitialspace_check",
167                          checked=self.skipinitialspace,
168                          text="Skip initial whitespace",
169                          toolTip="Skip any whitespace at the beginning of each cell.",
170                          toggled=self.skipinitialspace_changed
171                          )
172               
173        form.addRow(self.skipinitialspace_check)
174               
175        self.has_header_check = \
176                QCheckBox(objectName="has_header_check",
177                          checked=self.has_header,
178                          text="Header line",
179                          toolTip="Use the first line as a header",
180                          toggled=self.has_header_changed
181                          )
182       
183        form.addRow(self.has_header_check)
184       
185        self.has_orange_header_check = \
186                QCheckBox(objectName="has_orange_header_check",
187                          checked=self.has_orange_header,
188                          text="Has orange variable type definitions",
189                          toolTip="Use second and third line as a orange style '.tab' format feature definitions.",
190                          toggled=self.has_orange_header_changed
191                          )
192               
193        form.addRow(self.has_orange_header_check)
194       
195        box = OWGUI.widgetBox(self.controlArea, "Preview")
196        self.preview_view = QTableView()
197        box.layout().addWidget(self.preview_view)
198       
199        OWGUI.button(self.controlArea, self, "Send", callback=self.send_data)
200       
201        self.selected_file = None
202        self.data = None
203       
204        self.resize(450, 500)
205        if self.recent_files:
206            QTimer.singleShot(1, lambda: self.set_selected_file(self.recent_files[0]))
207       
208    def on_select_recent(self, recent):
209        if isinstance(recent, int):
210            recent = self.recent_files[recent]
211       
212        self.set_selected_file(recent)
213   
214    def on_open_dialog(self): 
215        last = os.path.expanduser("~/Documents")
216        path = QFileDialog.getOpenFileName(self, "Open File", last)
217        path = unicode(path)
218        if path:
219            self.set_selected_file(path)
220   
221    def delimiter_changed(self, index=-1):
222        self.delimiter = self.DELIMITERS[index][1]
223        if self.delimiter is None:
224            self.other_delimiter = str(self.delimiter_edit.text())
225        self.update_preview()
226   
227    def quote_changed(self):
228        if self.quote_edit.text():
229            self.quote = str(self.quote_edit.text())
230            self.update_preview()
231           
232    def missing_changed(self):
233        self.missing = str(self.missing_edit.text())
234        self.update_preview()
235           
236    def has_header_changed(self):
237        self.has_header = self.has_header_check.isChecked()
238        self.update_preview()
239       
240    def has_orange_header_changed(self):
241        self.has_orange_header = self.has_orange_header_check.isChecked()
242        self.update_preview()
243       
244    def skipinitialspace_changed(self):
245        self.skipinitialspace = self.skipinitialspace_check.isChecked()
246        self.update_preview()
247           
248    def set_selected_file(self, filename):
249        basedir, name = os.path.split(filename)
250       
251        index_to_remove = None
252        if filename in self.recent_files:
253            index_to_remove = self.recent_files.index(filename)
254        elif self.recent_combo.count() > 20:
255            # Always keep 20 latest files in the list.
256            index_to_remove = self.recent_combo.count() - 1
257       
258        self.recent_combo.insertItem(0, name)
259        self.recent_combo.setCurrentIndex(0)
260        self.recent_files.insert(0, filename)
261           
262        if index_to_remove is not None:   
263            self.recent_combo.removeItem(index_to_remove + 1)
264            self.recent_files.pop(index_to_remove + 1)
265            if filename in self.hints:
266                del self.hints[filename]
267       
268        if filename in self.hints:
269            hints = self.hints[filename]
270        else:
271            hints = sniff_csv(filename)
272            self.hints[filename] = hints
273       
274        delimiter = hints["delimiter"]
275       
276        # Update the widget state (GUI) from the saved hints for the file
277        preset = [d[1] for d in self.DELIMITERS[:-1]]
278        if delimiter not in preset:
279            self.delimiter = None
280            self.other_delimiter = delimiter
281            index = len(self.DELIMITERS) - 1
282            button = self.delimiter_button_group.button(index)
283            button.setChecked(True)
284            self.delimiter_edit.setText(self.other_delimiter)
285            self.delimiter_edit.setEnabled(True)
286        else:
287            self.delimiter = delimiter
288            index = preset.index(delimiter)
289            button = self.delimiter_button_group.button(index)
290            button.setChecked(True)
291            self.delimiter_edit.setEnabled(False)
292       
293        self.quote = hints["quotechar"]
294        self.quote_edit.setText(self.quote)
295       
296        self.missing = hints["DK"] or ""
297        self.missing_edit.setText(self.missing)
298       
299        self.has_header = hints["has_header"]
300        self.has_header_check.setChecked(self.has_header)
301       
302        self.has_orange_header = hints["has_orange_header"]
303        self.has_orange_header_check.setChecked(self.has_orange_header)
304       
305        self.skipinitialspace = hints["skipinitialspace"]
306        self.skipinitialspace_check.setChecked(self.skipinitialspace)
307       
308        self.selected_file = filename
309        self.selected_file_head = []
310        with open(self.selected_file, "rb") as f:
311            for i, line in zip(range(30), f):
312                self.selected_file_head.append(line)
313       
314        self.update_preview()
315       
316    def update_preview(self):
317        self.error(0)
318        if self.selected_file:
319            head = StringIO("".join(self.selected_file_head))
320            hints = self.hints[self.selected_file]
321           
322            # Save hints for the selected file
323            hints["quote"] = self.quote
324            hints["delimiter"] = self.delimiter or self.other_delimiter
325            hints["has_header"] = self.has_header
326            hints["has_orange_header"] = self.has_orange_header
327            hints["skipinitialspace"] = self.skipinitialspace
328            hints["DK"] = self.missing or None
329            try:
330                data = Orange.data.io.load_csv(head, delimiter=self.delimiter,
331                                               quotechar=self.quote,
332                                               has_header=self.has_header,
333                                               has_types=self.has_orange_header,
334                                               has_annotations=self.has_orange_header,
335                                               skipinitialspace=self.skipinitialspace,
336                                               DK=self.missing or None,
337                                               create_new_on=MakeStatus.OK)
338            except Exception, ex:
339                self.error(0, "Cannot parse (%r)" % ex)
340                data = None
341               
342            if data is not None:
343                model = ExampleTableModel(data, None, self)
344            else:
345                model = None
346            self.preview_view.setModel(model)
347           
348    def send_data(self):
349        self.error(0)
350        if self.selected_file:
351            hints = self.hints[self.selected_file]
352            try:
353                data = Orange.data.io.load_csv(self.selected_file,
354                                               delimiter=self.delimiter,
355                                               quotechar=self.quote,
356                                               has_header=self.has_header,
357                                               has_annotations=self.has_orange_header,
358                                               skipinitialspace=self.skipinitialspace,
359                                               DK=self.missing or None,
360                                               create_new_on=MakeStatus.OK
361                                               )
362            except Exception, ex:
363                self.error(0, "An error occurred while loading the file:\n\t%r" % ex)
364                data = None
365            self.data = data
366        self.send("Data", self.data)
367       
368       
369def sniff_csv(file):
370    snifer = csv.Sniffer()
371    if isinstance(file, basestring):
372        file = open(file, "rb")
373   
374    sample = file.read(2 ** 20) # max 1MB sample
375    dialect = snifer.sniff(sample)
376    has_header = snifer.has_header(sample)
377   
378    return {"delimiter": dialect.delimiter,
379            "doublequote": dialect.doublequote,
380            "escapechar": dialect.escapechar,
381            "quotechar": dialect.quotechar,
382            "quoting": dialect.quoting,
383            "skipinitialspace": dialect.skipinitialspace,
384            "has_header": has_header,
385            "has_orange_header": False,
386            "skipinitialspace": True,
387            "DK": None,
388            }
389   
390if __name__ == "__main__":
391    import sys
392    app = QApplication(sys.argv)
393    w = OWCSVFileImport()
394    w.show()
395    app.exec_()
396    w.saveSettings()
397       
398       
399       
400       
Note: See TracBrowser for help on using the repository browser.