source: orange/Orange/OrangeWidgets/Prototypes/OWCSVFileImport.py @ 10957:660b3532fb57

Revision 10957:660b3532fb57, 14.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 21 months ago (diff)

Handle csv.Sniffer errors.

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