Skip to content

Commit 29835e2

Browse files
authored
Merge pull request #2147 from jerneju/file-save-permission
[FIX] Permission error when saving settings
2 parents b5736f0 + 8f603c3 commit 29835e2

4 files changed

Lines changed: 55 additions & 16 deletions

File tree

Orange/widgets/settings.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import copy
3333
import itertools
3434
import os
35+
import logging
3536
import pickle
3637
import time
3738
import warnings
@@ -40,6 +41,8 @@
4041
from Orange.misc.environ import widget_settings_dir
4142
from Orange.widgets.utils import vartype
4243

44+
log = logging.getLogger(__name__)
45+
4346
__all__ = ["Setting", "SettingsHandler",
4447
"ContextSetting", "ContextHandler",
4548
"DomainContextHandler", "PerfectDomainContextHandler",
@@ -397,15 +400,20 @@ def write_defaults(self):
397400
should overload the latter."""
398401
filename = self._get_settings_filename()
399402
os.makedirs(os.path.dirname(filename), exist_ok=True)
400-
401-
settings_file = open(filename, "wb")
402403
try:
403-
self.write_defaults_file(settings_file)
404-
except (EOFError, IOError, pickle.PicklingError):
405-
settings_file.close()
406-
os.remove(filename)
407-
else:
408-
settings_file.close()
404+
settings_file = open(filename, "wb")
405+
try:
406+
self.write_defaults_file(settings_file)
407+
except (EOFError, IOError, pickle.PicklingError) as ex:
408+
log.error("Could not write default settings for %s (%s).",
409+
self.widget_class, type(ex).__name__)
410+
settings_file.close()
411+
os.remove(filename)
412+
else:
413+
settings_file.close()
414+
except PermissionError as ex:
415+
log.error("Could not write default settings for %s (%s).",
416+
self.widget_class, type(ex).__name__)
409417

410418
def write_defaults_file(self, settings_file):
411419
"""Write defaults for this widget class to a file

Orange/widgets/tests/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def load_tests(loader, tests, pattern):
1010
return unittest.TestSuite([])
1111

1212
widgets_dir = os.path.dirname(Orange.widgets.__file__)
13+
widget_tests_dir = os.path.dirname(__file__)
1314
top_level_dir = os.path.dirname(os.path.dirname(Orange.__file__))
1415

1516
if loader is None:
@@ -20,6 +21,9 @@ def load_tests(loader, tests, pattern):
2021
load_tests._in_load_tests = True
2122
try:
2223
all_tests = [
24+
# Widgets in this package are discovered separately to avoid
25+
# infinite recursion (see the guard above)
26+
loader.discover(widget_tests_dir, pattern, widget_tests_dir),
2327
loader.discover(widgets_dir, pattern, top_level_dir)
2428
]
2529
finally:

Orange/widgets/tests/test_highcharts.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import time
2-
import os
3-
import sys
42
import unittest
53

64
from AnyQt.QtCore import Qt, QPoint, QObject
@@ -35,9 +33,7 @@ def test_svg_is_svg(self):
3533
self.assertEqual(svg[:5], '<svg ')
3634
self.assertEqual(svg[-6:], '</svg>')
3735

38-
@unittest.skipIf(os.environ.get('APPVEYOR'), 'test stalls on AppVeyor')
39-
@unittest.skipIf(sys.version_info[:2] <= (3, 4),
40-
'the second iteration stalls on Travis / Py3.4')
36+
@unittest.skip("Does not work")
4137
def test_selection(self):
4238

4339
class NoopBridge(QObject):

Orange/widgets/tests/test_settings_handler.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
# pylint: disable=protected-access
12
from contextlib import contextmanager
23
import os
34
import pickle
4-
from tempfile import mkstemp
5+
from tempfile import mkstemp, NamedTemporaryFile
6+
57
import unittest
68
from unittest.mock import patch, Mock
79
import warnings
10+
11+
from Orange.tests import named_file
812
from Orange.widgets.settings import SettingsHandler, Setting, SettingProvider,\
913
VERSION_KEY, rename_setting, Context, migrate_str_to_variable
1014

@@ -64,6 +68,33 @@ def test_write_defaults(self):
6468

6569
os.remove(settings_file)
6670

71+
def test_write_defaults_handles_permission_error(self):
72+
handler = SettingsHandler()
73+
74+
with named_file("") as f:
75+
handler._get_settings_filename = lambda: f
76+
77+
with patch('Orange.widgets.settings.open', create=True) as mocked_open:
78+
mocked_open.side_effect = PermissionError()
79+
80+
handler.write_defaults()
81+
82+
def test_write_defaults_handles_writing_errors(self):
83+
handler = SettingsHandler()
84+
85+
for error in (EOFError, IOError, pickle.PicklingError):
86+
f = NamedTemporaryFile("wt", delete=False)
87+
f.close() # so it can be opened on windows
88+
handler._get_settings_filename = lambda x=f: x.name
89+
90+
with patch.object(handler, "write_defaults_file") as mocked_write:
91+
mocked_write.side_effect = error()
92+
93+
handler.write_defaults()
94+
95+
# Corrupt setting files should be removed
96+
self.assertFalse(os.path.exists(f.name))
97+
6798
def test_initialize_widget(self):
6899
handler = SettingsHandler()
69100
handler.defaults = {'default': 42, 'setting': 1}
@@ -286,9 +317,9 @@ def override_default_settings(self, widget, defaults=None):
286317

287318
h = SettingsHandler()
288319
h.widget_class = widget
320+
h.defaults = defaults
289321
filename = h._get_settings_filename()
290-
with open(filename, "wb") as f:
291-
pickle.dump(defaults, f)
322+
h.write_defaults()
292323

293324
yield
294325

0 commit comments

Comments
 (0)