Skip to content

Commit 13f45ff

Browse files
committed
LoadModel: Use RecentPathsWComboMixin
1 parent cdd53c8 commit 13f45ff

File tree

3 files changed

+166
-104
lines changed

3 files changed

+166
-104
lines changed
Lines changed: 39 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,29 @@
11
import os
22
import pickle
33

4+
from AnyQt.QtWidgets import QSizePolicy, QStyle, QFileDialog
45
from AnyQt.QtCore import QTimer
5-
from AnyQt.QtWidgets import (
6-
QSizePolicy, QHBoxLayout, QComboBox, QStyle, QFileDialog
7-
)
86

97
from Orange.base import Model
108
from Orange.widgets import widget, gui
119
from Orange.widgets.model import owsavemodel
12-
from Orange.widgets.settings import Setting
10+
from Orange.widgets.utils.filedialogs import RecentPathsWComboMixin
1311
from Orange.widgets.utils import stdpaths
1412
from Orange.widgets.utils.widgetpreview import WidgetPreview
1513
from Orange.widgets.widget import Msg, Output
1614

1715

18-
class OWLoadModel(widget.OWWidget):
16+
class OWLoadModel(widget.OWWidget, RecentPathsWComboMixin):
1917
name = "Load Model"
2018
description = "Load a model from an input file."
2119
priority = 3050
2220
replaces = ["Orange.widgets.classify.owloadclassifier.OWLoadClassifier"]
2321
icon = "icons/LoadModel.svg"
24-
keywords = ["file", "open"]
22+
keywords = ["file", "open", "model"]
2523

2624
class Outputs:
2725
model = Output("Model", Model)
2826

29-
#: List of recent filenames.
30-
history = Setting([])
31-
#: Current (last selected) filename or None.
32-
filename = Setting(None)
33-
3427
class Error(widget.OWWidget.Error):
3528
load_error = Msg("An error occured while reading '{}'")
3629

@@ -41,96 +34,58 @@ class Error(widget.OWWidget.Error):
4134

4235
def __init__(self):
4336
super().__init__()
44-
self.selectedIndex = -1
45-
46-
box = gui.widgetBox(
47-
self.controlArea, self.tr("File"), orientation=QHBoxLayout()
48-
)
37+
RecentPathsWComboMixin.__init__(self)
38+
self.loaded_file = ""
4939

50-
self.filesCB = gui.comboBox(
51-
box, self, "selectedIndex", callback=self._on_recent)
52-
self.filesCB.setMinimumContentsLength(20)
53-
self.filesCB.setSizeAdjustPolicy(
54-
QComboBox.AdjustToMinimumContentsLength)
40+
vbox = gui.vBox(self.controlArea, "File", addSpace=True)
41+
box = gui.hBox(vbox)
42+
self.file_combo.setMinimumWidth(300)
43+
box.layout().addWidget(self.file_combo)
44+
self.file_combo.activated[int].connect(self.select_file)
5545

56-
self.loadbutton = gui.button(box, self, "...", callback=self.browse)
57-
self.loadbutton.setIcon(
58-
self.style().standardIcon(QStyle.SP_DirOpenIcon))
59-
self.loadbutton.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
46+
button = gui.button(box, self, '...', callback=self.browse_file)
47+
button.setIcon(self.style().standardIcon(QStyle.SP_DirOpenIcon))
48+
button.setSizePolicy(
49+
QSizePolicy.Maximum, QSizePolicy.Fixed)
6050

61-
self.reloadbutton = gui.button(
51+
button = gui.button(
6252
box, self, "Reload", callback=self.reload, default=True)
63-
self.reloadbutton.setIcon(
64-
self.style().standardIcon(QStyle.SP_BrowserReload))
65-
self.reloadbutton.setSizePolicy(QSizePolicy.Maximum,
66-
QSizePolicy.Fixed)
67-
68-
# filter valid existing filenames
69-
self.history = list(filter(os.path.isfile, self.history))[:20]
70-
for filename in self.history:
71-
self.filesCB.addItem(os.path.basename(filename), userData=filename)
72-
73-
# restore the current selection if the filename is
74-
# in the history list
75-
if self.filename in self.history:
76-
self.selectedIndex = self.history.index(self.filename)
77-
else:
78-
self.selectedIndex = -1
79-
self.filename = None
80-
self.reloadbutton.setEnabled(False)
53+
button.setIcon(self.style().standardIcon(QStyle.SP_BrowserReload))
54+
button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
8155

82-
if self.filename:
83-
QTimer.singleShot(0, lambda: self.load(self.filename))
84-
85-
def browse(self):
86-
"""Select a filename using an open file dialog."""
87-
if self.filename is None:
88-
startdir = stdpaths.Documents
89-
else:
90-
startdir = os.path.dirname(self.filename)
56+
self.set_file_list()
57+
QTimer.singleShot(0, self.open_file)
9158

59+
def browse_file(self):
60+
start_file = self.last_path() or stdpaths.Documents
9261
filename, _ = QFileDialog.getOpenFileName(
93-
self, self.tr("Open"), directory=startdir, filter=self.FILTER)
62+
self, 'Open Distance File', start_file, self.FILTER)
63+
if not filename:
64+
return
65+
self.add_path(filename)
66+
self.open_file()
9467

95-
if filename:
96-
self.load(filename)
68+
def select_file(self, n):
69+
super().select_file(n)
70+
self.open_file()
9771

9872
def reload(self):
99-
"""Reload the current file."""
100-
self.load(self.filename)
73+
self.open_file()
10174

102-
def load(self, filename):
103-
"""Load the object from filename and send it to output."""
75+
def open_file(self):
76+
self.clear_messages()
77+
fn = self.last_path()
78+
if not fn:
79+
return
10480
try:
105-
with open(filename, "rb") as f:
81+
with open(fn, "rb") as f:
10682
model = pickle.load(f)
10783
except (pickle.UnpicklingError, OSError, EOFError):
108-
self.Error.load_error(os.path.split(filename)[-1])
84+
self.Error.load_error(os.path.split(fn)[-1])
85+
self.Outputs.model.send(None)
10986
else:
110-
self.Error.load_error.clear()
111-
self._remember(filename)
11287
self.Outputs.model.send(model)
11388

114-
def _remember(self, filename):
115-
"""
116-
Remember `filename` was accessed.
117-
"""
118-
if filename in self.history:
119-
index = self.history.index(filename)
120-
del self.history[index]
121-
self.filesCB.removeItem(index)
122-
123-
self.history.insert(0, filename)
124-
125-
self.filesCB.insertItem(0, os.path.basename(filename),
126-
userData=filename)
127-
self.selectedIndex = 0
128-
self.filename = filename
129-
self.reloadbutton.setEnabled(self.selectedIndex != -1)
130-
131-
def _on_recent(self):
132-
self.load(self.history[self.selectedIndex])
133-
13489

13590
if __name__ == "__main__": # pragma: no cover
13691
WidgetPreview(OWLoadModel).run()

Orange/widgets/model/tests/test_owloadmodel.py

Lines changed: 125 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,139 @@
22
# pylint: disable=missing-docstring
33
import os
44
import pickle
5-
from tempfile import mkstemp
5+
from tempfile import NamedTemporaryFile
6+
import unittest
7+
from unittest.mock import Mock, patch
68

7-
from Orange.classification.majority import ConstantModel
9+
import numpy as np
10+
11+
from orangewidget.utils.filedialogs import RecentPath
12+
from Orange.data import Table
13+
from Orange.classification.naive_bayes import NaiveBayesLearner
814
from Orange.widgets.model.owloadmodel import OWLoadModel
915
from Orange.widgets.tests.base import WidgetTest
1016

1117

1218
class TestOWLoadModel(WidgetTest):
19+
# Attribute used to store event data so it does not get garbage
20+
# collected before event is processed.
21+
event_data = None
22+
1323
def setUp(self):
14-
self.widget = self.create_widget(OWLoadModel)
24+
self.widget = self.create_widget(OWLoadModel) # type: OWLoadModel
25+
data = Table("iris")
26+
self.model = NaiveBayesLearner()(data)
27+
with NamedTemporaryFile(suffix=".pkcls", delete=False) as f:
28+
self.filename = f.name
29+
pickle.dump(self.model, f)
30+
31+
def tearDown(self):
32+
os.remove(self.filename)
33+
34+
def test_browse_file_opens_file(self):
35+
w = self.widget
36+
with patch("AnyQt.QtWidgets.QFileDialog.getOpenFileName",
37+
Mock(return_value=(self.filename, "*.pkcls"))):
38+
w.browse_file()
39+
model = self.get_output(w.Outputs.model)
40+
np.testing.assert_equal(
41+
model.log_cont_prob, self.model.log_cont_prob)
42+
43+
with patch("AnyQt.QtWidgets.QFileDialog.getOpenFileName",
44+
Mock(return_value=("", "*.pkcls"))):
45+
w.browse_file()
46+
# Keep the same model on output
47+
model2 = self.get_output(w.Outputs.model)
48+
self.assertIs(model2, model)
49+
50+
with patch("AnyQt.QtWidgets.QFileDialog.getOpenFileName",
51+
Mock(return_value=(self.filename, "*.pkcls"))):
52+
w.reload()
53+
model2 = self.get_output(w.Outputs.model)
54+
self.assertIsNot(model2, model)
55+
56+
@patch("pickle.load")
57+
def test_select_file(self, load):
58+
w = self.widget
59+
with NamedTemporaryFile(suffix=".pkcls") as f2, \
60+
NamedTemporaryFile(suffix=".pkcls", delete=False) as f3:
61+
w.add_path(self.filename)
62+
w.add_path(f2.name)
63+
w.add_path(f3.name)
64+
w.open_file()
65+
args = load.call_args[0][0]
66+
self.assertEqual(args.name, f3.name)
67+
w.select_file(2)
68+
args = load.call_args[0][0]
69+
self.assertEqual(args.name, self.filename.replace("\\", "/"))
1570

16-
def test_show_error(self):
17-
self.widget.load("no-such-file.pckls")
18-
self.assertTrue(self.widget.Error.load_error.is_shown())
71+
def test_load_error(self):
72+
w = self.widget
73+
with patch("AnyQt.QtWidgets.QFileDialog.getOpenFileName",
74+
Mock(return_value=(self.filename, "*.pkcls"))):
75+
with patch("pickle.load", side_effect=pickle.UnpicklingError):
76+
w.browse_file()
77+
self.assertTrue(w.Error.load_error.is_shown())
78+
self.assertIsNone(self.get_output(w.Outputs.model))
1979

20-
clsf = ConstantModel([1, 1, 1])
21-
fd, fname = mkstemp(suffix='.pkcls')
22-
os.close(fd)
80+
w.reload()
81+
self.assertFalse(w.Error.load_error.is_shown())
82+
model = self.get_output(w.Outputs.model)
83+
self.assertIsNotNone(model)
84+
85+
with patch.object(w, "last_path", Mock(return_value="")), \
86+
patch("pickle.load") as load:
87+
w.reload()
88+
load.assert_not_called()
89+
self.assertFalse(w.Error.load_error.is_shown())
90+
self.assertIs(self.get_output(w.Outputs.model), model)
91+
92+
with patch("pickle.load", side_effect=pickle.UnpicklingError):
93+
w.reload()
94+
self.assertTrue(w.Error.load_error.is_shown())
95+
self.assertIsNone(self.get_output(w.Outputs.model))
96+
97+
with patch("AnyQt.QtWidgets.QFileDialog.getOpenFileName",
98+
Mock(return_value=("foo", "*.pkcls"))):
99+
w.browse_file()
100+
self.assertTrue(w.Error.load_error.is_shown())
101+
self.assertIsNone(self.get_output(w.Outputs.model))
102+
103+
def test_no_last_path(self):
104+
self.widget = \
105+
self.create_widget(OWLoadModel,
106+
stored_settings={"recent_paths": []})
107+
# Doesn't crash and contains a single item, (none).
108+
self.assertEqual(self.widget.file_combo.count(), 1)
109+
110+
@patch("Orange.widgets.widget.OWWidget.workflowEnv",
111+
Mock(return_value={"basedir": os.getcwd()}))
112+
@patch("pickle.load")
113+
def test_open_moved_workflow(self, load):
114+
"""
115+
Test opening workflow that has been moved to another location
116+
(i.e. sent by email), considering data file is stored in the same
117+
directory as the workflow.
118+
"""
119+
temp_file = NamedTemporaryFile(dir=os.getcwd(), delete=False)
120+
file_name = temp_file.name
121+
temp_file.close()
122+
base_name = os.path.basename(file_name)
23123
try:
24-
with open(fname, 'wb') as f:
25-
pickle.dump(clsf, f)
26-
self.widget.load(fname)
27-
self.assertFalse(self.widget.Error.load_error.is_shown())
28-
29-
with open(fname, "w") as f:
30-
f.write("X")
31-
self.widget.load(fname)
32-
self.assertTrue(self.widget.Error.load_error.is_shown())
124+
recent_path = RecentPath(
125+
os.path.join("temp/models", base_name), "",
126+
os.path.join("models", base_name)
127+
)
128+
stored_settings = {"recent_paths": [recent_path]}
129+
w = self.create_widget(OWLoadModel,
130+
stored_settings=stored_settings)
131+
w.open_file()
132+
self.assertEqual(w.file_combo.count(), 1)
133+
args = load.call_args[0][0]
134+
self.assertEqual(args.name, file_name.replace("\\", "/"))
33135
finally:
34-
os.remove(fname)
136+
os.remove(file_name)
137+
138+
139+
if __name__ == "__main__":
140+
unittest.main()

doc/widgets.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,8 @@
572572
"background": "#FAC1D9",
573573
"keywords": [
574574
"file",
575-
"open"
575+
"open",
576+
"model"
576577
]
577578
}
578579
]

0 commit comments

Comments
 (0)