source: orange/Orange/OrangeWidgets/Prototypes/OWCSVFileImport.py @ 10558:6e987397cd95

Revision 10558:6e987397cd95, 14.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

More conservative reuse of variables in CSV import widget.

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