source: orange/Orange/OrangeWidgets/Data/OWFile.py @ 11745:21144138c193

Revision 11745:21144138c193, 14.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Slight GUI and code style fixes for 'File' widget.

Line 
1"""
2<name>File</name>
3<description>Reads data from a file.</description>
4<icon>icons/File.svg</icon>
5<contact>Janez Demsar (janez.demsar(@at@)fri.uni-lj.si)</contact>
6<priority>10</priority>
7"""
8
9import os
10import sys
11import warnings
12
13import Orange
14
15from OWWidget import *
16import OWGUI
17
18NAME = "File"
19
20DESCRIPTION = """
21Reads data from an input file.
22"""
23
24LONG_DESCRIPTION = ""
25"""
26This is the widget you will probably use at the start of every schema to read
27the input data file (data table with examples). The widget maintains a
28history of most recently used data files. For convenience, the history
29also includes a directory with the sample data sets that come with Orange.
30"""
31
32ICON = "icons/File.svg"
33AUTHOR = "Janez Demsar"
34MAINTAINER_EMAIL = "janez.demsar(@at@)fri.uni-lj.si"
35PRIORITY = 10
36CATEGORY = "Data"
37
38KEYWORDS = ["data", "file", "load", "read"]
39
40OUTPUTS = (
41    {"name": "Data",
42     "type": Orange.data.Table,
43     "doc": "Attribute-valued data set read from the input file.",
44     },
45)
46
47WIDGET_CLASS = "OWFile"
48
49
50def call(f, *args, **kwargs):
51    return f(*args, **kwargs)
52
53# Make any KernelWarning raise an error if called through the 'call' function
54# defined above.
55warnings.filterwarnings(
56    "error", ".*", Orange.core.KernelWarning,
57    __name__, call.func_code.co_firstlineno + 1
58)
59
60
61class FileNameContextHandler(ContextHandler):
62    def match(self, context, imperfect, filename):
63        return context.filename == filename and 2
64
65
66def addOrigin(examples, filename):
67    vars = examples.domain.variables + examples.domain.getmetas().values()
68    strings = [var for var in vars if isinstance(var, Orange.feature.String)]
69    dirname, basename = os.path.split(filename)
70    for var in strings:
71        if "type" in var.attributes and "origin" not in var.attributes:
72            var.attributes["origin"] = dirname
73
74
75class OWFile(OWWidget):
76    settingsList = ["recentFiles", "createNewOn", "showAdvanced"]
77    contextHandlers = {"": FileNameContextHandler()}
78
79    registeredFileTypes = [ft[:2] for ft in Orange.core.getRegisteredFileTypes()
80                           if len(ft) > 2 and ft[2]]
81    dlgFormats = (
82        'Tab-delimited files (*.tab *.txt)\n'
83        'C4.5 files (*.data)\n'
84        'Assistant files (*.dat)\n'
85        'Retis files (*.rda *.rdo)\n'
86        'Basket files (*.basket)\n' +
87        "\n".join("%s (%s)" % (ft[:2]) for ft in registeredFileTypes) +
88        "\nAll files(*.*)"
89    )
90
91    formats = {
92        ".tab": "Tab-delimited file",
93        ".txt": "Tab-delimited file",
94        ".data": "C4.5 file",
95        ".dat": "Assistant file",
96        ".rda": "Retis file",
97        ".rdo": "Retis file",
98        ".basket": "Basket file"
99    }
100    formats.update(dict((ext.lstrip("*."), name)
101                        for name, ext in registeredFileTypes))
102
103    def __init__(self, parent=None, signalManager=None):
104        OWWidget.__init__(self, parent, signalManager, "File", wantMainArea=0)
105
106        self.inputs = []
107        self.outputs = [("Data", ExampleTable)]
108
109        self.recentFiles = []
110        self.symbolDC = "?"
111        self.symbolDK = "~"
112        self.createNewOn = 1
113        self.domain = None
114        self.loadedFile = ""
115        self.showAdvanced = 0
116        self.loadSettings()
117
118        self.dataReport = None
119
120        box = OWGUI.widgetBox(self.controlArea, "Data File", addSpace=True,
121                              orientation="horizontal")
122        self.filecombo = QComboBox(box)
123        self.filecombo.setMinimumWidth(150)
124        self.filecombo.activated[int].connect(self.selectFile)
125
126        box.layout().addWidget(self.filecombo)
127        button = OWGUI.button(box, self, '...', callback=self.browse)
128        button.setIcon(self.style().standardIcon(QStyle.SP_DirOpenIcon))
129        button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
130
131        self.reloadBtn = OWGUI.button(
132            box, self, "Reload", callback=self.reload, default=True)
133
134        self.reloadBtn.setIcon(
135            self.style().standardIcon(QStyle.SP_BrowserReload)
136        )
137        self.reloadBtn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
138
139        box = OWGUI.widgetBox(self.controlArea, "Info", addSpace=True)
140        self.infoa = OWGUI.widgetLabel(box, 'No data loaded.')
141        self.infob = OWGUI.widgetLabel(box, ' ')
142        self.warnings = OWGUI.widgetLabel(box, ' ')
143
144        #Set word wrap so long warnings won't expand the widget
145        self.warnings.setWordWrap(True)
146        self.warnings.setSizePolicy(QSizePolicy.Ignored,
147                                    QSizePolicy.MinimumExpanding)
148
149        smallWidget = OWGUI.collapsableWidgetBox(
150            self.controlArea, "Advanced settings", self, "showAdvanced",
151            callback=self.adjustSize0)
152
153        box = QGroupBox("Missing Value Symbols")
154        form = QFormLayout(fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow)
155
156        form.addRow(
157            "Don't care:",
158            OWGUI.lineEdit(None, self, "symbolDC",
159                           tooltip="Default values: '~' or '*'")
160        )
161        form.addRow(
162            "Don't know:",
163            OWGUI.lineEdit(None, self, "symbolDK",
164                           tooltip="Default values: empty fields (space), "
165                                   "'?' or 'NA'")
166        )
167        box.setLayout(form)
168        smallWidget.layout().addWidget(box)
169        smallWidget.layout().addSpacing(8)
170
171        OWGUI.radioButtonsInBox(
172            smallWidget, self, "createNewOn", box="New Attributes",
173            label="Create a new attribute when existing attribute(s) ...",
174            btnLabels=["Have mismatching order of values",
175                       "Have no common values with the new (recommended)",
176                       "Miss some values of the new attribute",
177                       "... Always create a new attribute"]
178        )
179
180        OWGUI.rubber(smallWidget)
181        smallWidget.updateControls()
182
183        OWGUI.rubber(self.controlArea)
184
185        # remove missing data set names
186        def exists(path):
187            if not os.path.exists(path):
188                dirpath, basename = os.path.split(path)
189                return os.path.exists(os.path.join("./", basename))
190            else:
191                return True
192
193        self.recentFiles = filter(exists, self.recentFiles)
194        self.setFileList()
195
196        if len(self.recentFiles) > 0 and exists(self.recentFiles[0]):
197            self.openFile(self.recentFiles[0])
198
199    def adjustSize0(self):
200        qApp.processEvents()
201        QTimer.singleShot(50, self.adjustSize)
202
203    def setFileList(self):
204        self.filecombo.clear()
205        model = self.filecombo.model()
206        iconprovider = QFileIconProvider()
207        if not self.recentFiles:
208            item = QStandardItem("(none)")
209            item.setEnabled(False)
210            item.setSelectable(False)
211            model.appendRow([item])
212        else:
213            for fname in self.recentFiles:
214                item = QStandardItem(os.path.basename(fname))
215                item.setToolTip(fname)
216                item.setIcon(iconprovider.icon(QFileInfo(fname)))
217                model.appendRow(item)
218
219        self.filecombo.insertSeparator(self.filecombo.count())
220        self.filecombo.addItem("Browse documentation data sets...")
221        item = model.item(self.filecombo.count() - 1)
222        item.setEnabled(os.path.isdir(Orange.utils.environ.dataset_install_dir))
223
224    def reload(self):
225        if self.recentFiles:
226            return self.openFile(self.recentFiles[0])
227
228    def settingsFromWidgetCallback(self, handler, context):
229        context.filename = self.loadedFile
230        context.symbolDC, context.symbolDK = self.symbolDC, self.symbolDK
231
232    def settingsToWidgetCallback(self, handler, context):
233        self.symbolDC, self.symbolDK = context.symbolDC, context.symbolDK
234
235    def selectFile(self, n):
236        if n < len(self.recentFiles):
237            name = self.recentFiles[n]
238            self.recentFiles.remove(name)
239            self.recentFiles.insert(0, name)
240        elif n >= 0:
241            self.browseDocDatasets()
242
243        if len(self.recentFiles) > 0:
244            self.setFileList()
245            self.openFile(self.recentFiles[0])
246
247    def browseDocDatasets(self):
248        """
249        Display a FileDialog with the documentation datasets folder.
250        """
251        self.browse(Orange.utils.environ.dataset_install_dir)
252
253    def browse(self, startpath=None):
254        """
255        Display a FileDialog and select a file to open.
256        """
257        if startpath is None:
258            if len(self.recentFiles) == 0 or self.recentFiles[0] == "(none)":
259                startpath = os.path.expanduser("~/")
260            else:
261                startpath = self.recentFiles[0]
262
263        filename = QFileDialog.getOpenFileName(
264            self, 'Open Orange Data File', startpath, self.dlgFormats)
265
266        filename = unicode(filename)
267
268        if filename == "":
269            return
270
271        if filename in self.recentFiles:
272            self.recentFiles.remove(filename)
273        self.recentFiles.insert(0, filename)
274
275        self.setFileList()
276
277        self.openFile(filename)
278
279    # Open a file, create data from it and send it over the data channel
280    def openFile(self, fn):
281        self.error()
282        self.warning()
283        self.information()
284
285        if not os.path.exists(fn):
286            basename = os.path.basename(fn)
287            if os.path.exists(os.path.join("./", basename)):
288                fn = os.path.join("./", basename)
289                self.information("Loading '%s' from the current directory." %
290                                 basename)
291
292        self.closeContext()
293        self.loadedFile = ""
294
295        if fn == "(none)":
296            self.send("Data", None)
297            self.infoa.setText("No data loaded")
298            self.infob.setText("")
299            self.warnings.setText("")
300            return
301
302        self.openContext("", fn)
303
304        self.loadedFile = ""
305
306        argdict = {"createNewOn": 3 - self.createNewOn}
307        if self.symbolDK:
308            argdict["DK"] = str(self.symbolDK)
309        if self.symbolDC:
310            argdict["DC"] = str(self.symbolDC)
311
312        data = None
313        try:
314            data = call(Orange.data.Table, fn, **argdict)
315            self.loadedFile = fn
316        except Exception as ex:
317            if "is being loaded as" in str(ex):
318                try:
319                    data = Orange.data.Table(fn, **argdict)
320                    self.warning(0, str(ex))
321                except:
322                    pass
323
324            if data is None:
325                self.error(str(ex))
326                self.infoa.setText('Data was not loaded due to an error.')
327                self.infob.setText('Error:')
328                self.warnings.setText(str(ex))
329                return
330
331        self.infoa.setText('%d example(s), ' % len(data) +
332                           '%d attribute(s), ' % len(data.domain.attributes) +
333                           '%d meta attribute(s).' % len(data.domain.getmetas()))
334        cl = data.domain.class_var
335        if cl is not None:
336            if isinstance(cl, Orange.feature.Continuous):
337                self.infob.setText('Regression; Numerical class.')
338            elif isinstance(cl, Orange.feature.Discrete):
339                self.infob.setText(
340                    'Classification; Discrete class with %d value(s).' %
341                    len(cl.values)
342                )
343            else:
344                self.infob.setText("Class is neither discrete nor continuous.")
345        else:
346            self.infob.setText("Data has no dependent variable.")
347
348        self.warnings.setText(
349            feature_load_status_report(data, self.createNewOn))
350
351        addOrigin(data, fn)
352        # make new data and send it
353        name = os.path.basename(fn)
354        name, _ = os.path.splitext(name)
355        data.name = name
356
357        self.dataReport = self.prepareDataReport(data)
358
359        self.send("Data", data)
360
361    def sendReport(self):
362        if self.dataReport:
363            _, ext = os.path.splitext(self.loadedFile)
364            format = self.formats.get(ext, "unknown format")
365            self.reportSettings(
366                "File", [("File name", self.loadedFile),
367                         ("Format", format)])
368            self.reportData(self.dataReport)
369
370
371def feature_load_status_report(data, create_new_on):
372    warnings = ""
373    metas = data.domain.getmetas()
374    attr_status = []
375    meta_status = {}
376
377    if hasattr(data, "attribute_load_status"):
378        attr_status = data.attribute_load_status
379    elif hasattr(data, "attributeLoadStatus"):
380        attr_status = data.attributeLoadStatus
381
382    if hasattr(data, "meta_attribute_load_status"):
383        meta_status = data.meta_attribute_load_status
384    elif hasattr(data, "metaAttributeLoadStatus"):
385        meta_status = data.metaAttributeLoadStatus
386
387    for status, message_used, message_not_used in STATUS_MESASGES:
388        if create_new_on > status:
389            message = message_used
390        else:
391            message = message_not_used
392        if not message:
393            continue
394
395        attrs = [attr.name for attr, stat in zip(data.domain, attr_status)
396                 if stat == status] + \
397                [attr.name for id, attr in metas.items()
398                 if meta_status.get(id, -99) == status]
399
400        if attrs:
401            jattrs = ", ".join(attrs)
402            if len(jattrs) > 80:
403                jattrs = jattrs[:80] + "..."
404            if len(jattrs) > 30:
405                warnings += "<li>%s:<br/> %s</li>" % (message, jattrs)
406            else:
407                warnings += "<li>%s: %s</li>" % (message, jattrs)
408    return warnings
409
410
411STATUS_MESASGES = [
412    (Orange.feature.Descriptor.MakeStatus.Incompatible,
413     "",
414     "The following attributes already existed but had a different order " +
415     "of values, so new attributes needed to be created"),
416    (Orange.feature.Descriptor.MakeStatus.NoRecognizedValues,
417     "The following attributes were reused although they share no " +
418     "common values with the existing attribute of the same names",
419     "The following attributes were not reused since they share no " +
420     "common values with the existing attribute of the same names"),
421    (Orange.feature.Descriptor.MakeStatus.MissingValues,
422     "The following attribute(s) were reused although some values " +
423     "needed to be added",
424     "The following attribute(s) were not reused since they miss some values")
425]
426
427
428if __name__ == "__main__":
429    a = QApplication(sys.argv)
430    ow = OWFile()
431    ow.show()
432    a.exec_()
433    ow.saveSettings()
Note: See TracBrowser for help on using the repository browser.