Skip to content

Commit a26fada

Browse files
authored
Merge pull request #2814 from markotoplak/refactor_owsave
Refactor OWSave
2 parents 3f819e7 + 13d7741 commit a26fada

File tree

4 files changed

+132
-50
lines changed

4 files changed

+132
-50
lines changed

Orange/widgets/data/owsave.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ class Inputs:
2525
last_filter = Setting("")
2626
auto_save = Setting(False)
2727

28-
writers = FileFormat.writers
29-
sparse_writers = {ext: w for ext, w in FileFormat.writers.items()
30-
if getattr(w, 'SUPPORT_SPARSE_DATA', False)}
28+
@classmethod
29+
def get_writers(cls, sparse):
30+
return [f for f in FileFormat.formats
31+
if getattr(f, 'write_file', None) and getattr(f, "EXTENSIONS", None)
32+
and (not sparse or getattr(f, 'SUPPORT_SPARSE_DATA', False))]
3133

3234
def __init__(self):
3335
super().__init__()
@@ -63,9 +65,8 @@ def save_file_as(self):
6365
file_name = self.filename or \
6466
os.path.join(self.last_dir or os.path.expanduser("~"),
6567
getattr(self.data, 'name', ''))
66-
filename, writer, filter = filedialogs.get_file_name(
67-
file_name, self.last_filter,
68-
self.sparse_writers if self.data.is_sparse() else self.writers)
68+
filename, writer, filter = filedialogs.open_filename_dialog_save(
69+
file_name, self.last_filter, self.get_writers(self.data.is_sparse()))
6970
if not filename:
7071
return
7172
self.filename = filename

Orange/widgets/data/tests/test_owfile.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import Orange
1818
from Orange.data import FileFormat, dataset_dirs, StringVariable, Table, \
1919
Domain, DiscreteVariable
20+
from Orange.util import OrangeDeprecationWarning
2021
from Orange.data.io import TabReader
2122
from Orange.tests import named_file
2223
from Orange.widgets.data.owfile import OWFile
@@ -27,14 +28,6 @@
2728
TITANIC_PATH = path.join(path.dirname(Orange.__file__), 'datasets', 'titanic.tab')
2829

2930

30-
class AddedFormat(FileFormat):
31-
EXTENSIONS = ('.123',)
32-
DESCRIPTION = "Test if a dialog format works after reading OWFile"
33-
34-
def read(self):
35-
pass
36-
37-
3831
class FailedSheetsFormat(FileFormat):
3932
EXTENSIONS = ('.failed_sheet',)
4033
DESCRIPTION = "Make a sheet function that fails"
@@ -338,10 +331,22 @@ def test_drop_data_when_everything_skipped(self):
338331
data = self.get_output(self.widget.Outputs.data)
339332
self.assertIsNone(data)
340333

334+
def test_call_deprecated_dialog_formats(self):
335+
with self.assertWarns(OrangeDeprecationWarning):
336+
self.assertIn("Tab", dialog_formats())
337+
341338
def test_add_new_format(self):
342339
# test adding file formats after registering the widget
343-
formats = dialog_formats()
344-
self.assertTrue(".123" in formats)
340+
called = False
341+
with named_file("", suffix=".tab") as filename:
342+
def test_format(sd, sf, ff, **kwargs):
343+
nonlocal called
344+
called = True
345+
self.assertIn(FailedSheetsFormat, ff)
346+
return filename, TabReader, ""
347+
with patch("Orange.widgets.data.owfile.open_filename_dialog", test_format):
348+
self.widget.browse_file()
349+
self.assertTrue(called)
345350

346351
def test_domain_editor_conversions(self):
347352
dat = """V0\tV1\tV2\tV3\tV4\tV5\tV6
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Test methods with long descriptive names can omit docstrings
2+
# pylint: disable=missing-docstring
3+
from unittest.mock import patch
4+
5+
from Orange.data import Table
6+
from Orange.data.io import TabReader, PickleReader, FileFormat
7+
from Orange.tests import named_file
8+
from Orange.widgets.tests.base import WidgetTest
9+
from Orange.widgets.utils.filedialogs import format_filter, fix_extension, open_filename_dialog_save
10+
from Orange.widgets.data.owsave import OWSave
11+
12+
13+
class AddedFormat(FileFormat):
14+
EXTENSIONS = ('.234',)
15+
DESCRIPTION = "Test if a dialog format works after reading OWSave"
16+
17+
def write_file(self):
18+
pass
19+
20+
21+
class TestOWSave(WidgetTest):
22+
23+
def setUp(self):
24+
self.widget = self.create_widget(OWSave) # type: OWSave
25+
26+
def test_ordinary_save(self):
27+
self.send_signal(self.widget.Inputs.data, Table("iris"))
28+
29+
for ext, writer in [('.tab', TabReader), ('.pickle', PickleReader)]:
30+
with named_file("", suffix=ext) as filename:
31+
def choose_file(a, b, c, d, e, fn=filename, w=writer):
32+
return fn, format_filter(w)
33+
with patch("AnyQt.QtWidgets.QFileDialog.getSaveFileName", choose_file):
34+
self.widget.save_file_as()
35+
self.assertEqual(len(Table(filename)), 150)
36+
37+
def test_filename_with_fix_extension(self):
38+
39+
def mock_fix_choice(ret):
40+
f = lambda *x: ret
41+
f.__dict__.update(fix_extension.__dict__)
42+
return f
43+
44+
change_filter = iter([PickleReader, TabReader])
45+
46+
for file_choice, fix in [
47+
[lambda *x: ("o.pickle", format_filter(TabReader)),
48+
mock_fix_choice(fix_extension.CHANGE_EXT)],
49+
[lambda *x: ("o.tab", format_filter(PickleReader)),
50+
mock_fix_choice(fix_extension.CHANGE_FORMAT)],
51+
[lambda *x: ("o.tab", format_filter(next(change_filter))),
52+
mock_fix_choice(fix_extension.CANCEL)]
53+
]:
54+
55+
with patch("AnyQt.QtWidgets.QFileDialog.getSaveFileName", file_choice),\
56+
patch("Orange.widgets.utils.filedialogs.fix_extension", fix):
57+
saved_filename, format, filter = \
58+
open_filename_dialog_save(".", None, OWSave.get_writers(False))
59+
self.assertEqual(saved_filename, "o.tab")
60+
self.assertEqual(format, TabReader)
61+
self.assertEqual(filter, format_filter(TabReader))
62+
63+
def test_added_format(self):
64+
"""Test that a format added after widget initialization is recognized"""
65+
self.send_signal(self.widget.Inputs.data, Table("iris"))
66+
called = False
67+
with named_file("", suffix=".tab") as filename:
68+
def test_format(sd, sf, ff, **kwargs):
69+
nonlocal called
70+
called = True
71+
self.assertIn(AddedFormat, ff)
72+
return filename, TabReader, ""
73+
with patch("Orange.widgets.utils.filedialogs.open_filename_dialog", test_format):
74+
self.widget.save_file_as()
75+
self.assertTrue(called)

Orange/widgets/utils/filedialogs.py

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from Orange.data.io import FileFormat
99
from Orange.widgets.settings import Setting
10+
from Orange.util import deprecated
1011

1112

1213
def fix_extension(ext, format, suggested_ext, suggested_format):
@@ -41,6 +42,7 @@ def format_filter(writer):
4142
return '{} (*{})'.format(writer.DESCRIPTION, ' *'.join(writer.EXTENSIONS))
4243

4344

45+
@deprecated
4446
def dialog_formats():
4547
"""
4648
Return readable file types for QFileDialogs.
@@ -53,59 +55,56 @@ def dialog_formats():
5355

5456

5557
def get_file_name(start_dir, start_filter, file_formats):
56-
"""
57-
Get filename for the given possible file formats
58+
return open_filename_dialog_save(start_dir, start_filter,
59+
sorted(set(file_formats.values()), key=lambda x: x.PRIORITY))
60+
5861

62+
def open_filename_dialog_save(start_dir, start_filter, file_formats):
63+
"""
5964
The function uses the standard save file dialog with filters from the
6065
given file formats. Extension is added automatically, if missing. If the
6166
user enters file extension that does not match the file format, (s)he is
6267
given a dialog to decide whether to fix the extension or the format.
6368
64-
Function also returns the writer and filter to cover the case where the
65-
same extension appears in multiple filters. Although `file_format` is a
66-
dictionary that associates its extension with one writer, writers can
67-
still have other extensions that are allowed.
68-
6969
Args:
7070
start_dir (str): initial directory, optionally including the filename
7171
start_filter (str): initial filter
72-
file_formats (dict {extension: Orange.data.io.FileFormat}): file formats
72+
file_formats (a list of Orange.data.io.FileFormat): file formats
7373
Returns:
7474
(filename, writer, filter), or `(None, None, None)` on cancel
7575
"""
76-
writers = sorted(set(file_formats.values()), key=lambda w: w.PRIORITY)
77-
filters = [format_filter(w) for w in writers]
78-
if start_filter not in filters:
79-
start_filter = filters[0]
80-
8176
while True:
82-
filename, filter = QFileDialog.getSaveFileName(
83-
None, 'Save As...', start_dir, ';;'.join(filters), start_filter)
77+
dialog = QFileDialog.getSaveFileName
78+
filename, format, filter = \
79+
open_filename_dialog(start_dir, start_filter, file_formats,
80+
add_all=False, title="Save as...", dialog=dialog)
8481
if not filename:
8582
return None, None, None
8683

87-
writer = writers[filters.index(filter)]
8884
base, ext = os.path.splitext(filename)
8985
if not ext:
90-
filename += writer.EXTENSIONS[0]
91-
elif ext not in writer.EXTENSIONS:
92-
format = writer.DESCRIPTION
93-
suggested_ext = writer.EXTENSIONS[0]
94-
suggested_format = \
95-
ext in file_formats and file_formats[ext].DESCRIPTION
96-
res = fix_extension(ext, format, suggested_ext, suggested_format)
86+
filename += format.EXTENSIONS[0]
87+
elif ext not in format.EXTENSIONS:
88+
suggested_ext = format.EXTENSIONS[0]
89+
suggested_format = False
90+
for f in file_formats: # find the first format
91+
if ext in f.EXTENSIONS:
92+
suggested_format = f
93+
break
94+
res = fix_extension(ext, format.DESCRIPTION, suggested_ext,
95+
suggested_format.DESCRIPTION if suggested_format else False)
9796
if res == fix_extension.CANCEL:
9897
continue
9998
if res == fix_extension.CHANGE_EXT:
10099
filename = base + suggested_ext
101100
elif res == fix_extension.CHANGE_FORMAT:
102-
writer = file_formats[ext]
103-
filter = format_filter(writer)
104-
return filename, writer, filter
101+
format = suggested_format
102+
filter = format_filter(format)
103+
return filename, format, filter
105104

106105

107-
def open_filename_dialog(start_dir, start_filter, file_formats, title="Open...",
108-
dialog=None):
106+
def open_filename_dialog(start_dir, start_filter, file_formats,
107+
add_all=True, title="Open...", dialog=None):
109108
"""
110109
Open file dialog with file formats.
111110
@@ -116,6 +115,7 @@ def open_filename_dialog(start_dir, start_filter, file_formats, title="Open...",
116115
start_dir (str): initial directory, optionally including the filename
117116
start_filter (str): initial filter
118117
file_formats (a list of Orange.data.io.FileFormat): file formats
118+
add_all (bool): add a filter for all supported extensions
119119
title (str): title of the dialog
120120
dialog: a function that creates a QT dialog
121121
Returns:
@@ -125,12 +125,13 @@ def open_filename_dialog(start_dir, start_filter, file_formats, title="Open...",
125125
filters = [format_filter(f) for f in file_formats]
126126

127127
# add all readable files option
128-
all_extensions = set()
129-
for f in file_formats:
130-
all_extensions.update(f.EXTENSIONS)
131-
file_formats.insert(0, None)
132-
filters.insert(0, "All readable files (*{})".format(
133-
' *'.join(sorted(all_extensions))))
128+
if add_all:
129+
all_extensions = set()
130+
for f in file_formats:
131+
all_extensions.update(f.EXTENSIONS)
132+
file_formats.insert(0, None)
133+
filters.insert(0, "All readable files (*{})".format(
134+
' *'.join(sorted(all_extensions))))
134135

135136
if start_filter not in filters:
136137
start_filter = filters[0]

0 commit comments

Comments
 (0)