Skip to content

Commit 7ea8531

Browse files
authored
Merge pull request #1724 from astaric/setting-migration
[ENH] Setting migration
2 parents 5b60248 + b26edaf commit 7ea8531

File tree

5 files changed

+295
-32
lines changed

5 files changed

+295
-32
lines changed

Orange/widgets/settings.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747

4848
_IMMUTABLES = (str, int, bytes, bool, float, tuple)
4949

50+
VERSION_KEY = "__version__"
51+
5052

5153
class Setting:
5254
"""Description of a setting.
@@ -386,6 +388,7 @@ def read_defaults_file(self, settings_file):
386388
for key, value in defaults.items()
387389
if not isinstance(value, Setting)
388390
}
391+
self._migrate_settings(self.defaults)
389392

390393
def write_defaults(self):
391394
"""Write (global) defaults for this widget class to a file.
@@ -434,12 +437,18 @@ def initialize(self, instance, data=None):
434437

435438
if isinstance(data, bytes):
436439
data = pickle.loads(data)
440+
self._migrate_settings(data)
437441

438442
if provider is self.provider:
439443
data = self._add_defaults(data)
440444

441445
provider.initialize(instance, data)
442446

447+
def _migrate_settings(self, settings):
448+
"""Ask widget to migrate settings to the latest version."""
449+
if settings:
450+
self.widget_class.migrate_settings(settings, settings.pop(VERSION_KEY, None))
451+
443452
def _select_provider(self, instance):
444453
provider = self.provider.get_provider(instance.__class__)
445454
if provider is None:
@@ -473,7 +482,9 @@ def pack_data(self, widget):
473482
----------
474483
widget : OWWidget
475484
"""
476-
return self.provider.pack(widget)
485+
packed_settings = self.provider.pack(widget)
486+
packed_settings[VERSION_KEY] = self.widget_class.settings_version
487+
return packed_settings
477488

478489
def update_defaults(self, widget):
479490
"""
@@ -588,6 +599,7 @@ def initialize(self, instance, data=None):
588599
super().initialize(instance, data)
589600
if data and "context_settings" in data:
590601
instance.context_settings = data["context_settings"]
602+
self._migrate_contexts(instance.context_settings)
591603
else:
592604
instance.context_settings = []
593605

@@ -596,16 +608,23 @@ def read_defaults_file(self, settings_file):
596608
pickle."""
597609
super().read_defaults_file(settings_file)
598610
self.global_contexts = pickle.load(settings_file)
611+
self._migrate_contexts(self.global_contexts)
612+
613+
def _migrate_contexts(self, contexts):
614+
for context in contexts:
615+
self.widget_class.migrate_context(context, context.values.pop(VERSION_KEY, None))
599616

600617
def write_defaults_file(self, settings_file):
601618
"""Call the inherited method, then add global context to the pickle."""
602619
super().write_defaults_file(settings_file)
603620
pickle.dump(self.global_contexts, settings_file, -1)
604621

605622
def pack_data(self, widget):
606-
"""Call the inherited method, then add local contexts to the pickle."""
623+
"""Call the inherited method, then add local contexts to the dict."""
607624
data = super().pack_data(widget)
608625
self.settings_from_widget(widget)
626+
for context in widget.context_settings:
627+
context.values[VERSION_KEY] = self.widget_class.settings_version
609628
data["context_settings"] = widget.context_settings
610629
return data
611630

Orange/widgets/tests/test_context_handler.py

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,112 @@
1-
from copy import copy
1+
import pickle
2+
from copy import copy, deepcopy
3+
from io import BytesIO
24
from unittest import TestCase
3-
from unittest.mock import Mock
4-
from Orange.widgets.settings import ContextHandler, Setting, ContextSetting, Context
5+
from unittest.mock import Mock, patch, call
6+
from Orange.widgets.settings import ContextHandler, Setting, ContextSetting, Context, VERSION_KEY
57

68
__author__ = 'anze'
79

810

911
class SimpleWidget:
12+
settings_version = 1
13+
1014
setting = Setting(42)
1115

1216
context_setting = ContextSetting(42)
1317

18+
migrate_settings = Mock()
19+
migrate_context = Mock()
20+
21+
22+
class DummyContext(Context):
23+
id = 0
24+
25+
def __init__(self, version=None):
26+
super().__init__()
27+
DummyContext.id += 1
28+
self.id = DummyContext.id
29+
if version:
30+
self.values[VERSION_KEY] = version
31+
32+
def __repr__(self):
33+
return "Context(id={})".format(self.id)
34+
__str__ = __repr__
35+
36+
def __eq__(self, other):
37+
if not isinstance(other, DummyContext):
38+
return False
39+
return self.id == other.id
40+
41+
42+
def create_defaults_file(contexts):
43+
b = BytesIO()
44+
pickle.dump({"x": 5}, b)
45+
pickle.dump(contexts, b)
46+
b.seek(0)
47+
return b
48+
1449

1550
class TestContextHandler(TestCase):
51+
def test_read_defaults(self):
52+
contexts = [DummyContext() for _ in range(3)]
53+
54+
handler = ContextHandler()
55+
handler.widget_class = SimpleWidget
56+
57+
# Old settings without version
58+
migrate_context = Mock()
59+
with patch.object(SimpleWidget, "migrate_context", migrate_context):
60+
handler.read_defaults_file(create_defaults_file(contexts))
61+
self.assertSequenceEqual(handler.global_contexts, contexts)
62+
migrate_context.assert_has_calls([call(c, None) for c in contexts])
63+
64+
# Settings with version
65+
contexts = [DummyContext(version=i) for i in range(1, 4)]
66+
migrate_context.reset_mock()
67+
with patch.object(SimpleWidget, "migrate_context", migrate_context):
68+
handler.read_defaults_file(create_defaults_file(contexts))
69+
self.assertSequenceEqual(handler.global_contexts, contexts)
70+
migrate_context.assert_has_calls([call(c, c.values[VERSION_KEY]) for c in contexts])
71+
1672
def test_initialize(self):
1773
handler = ContextHandler()
1874
handler.provider = Mock()
75+
handler.widget_class = SimpleWidget
1976

2077
# Context settings from data
2178
widget = SimpleWidget()
22-
handler.initialize(widget, {'context_settings': 5})
79+
context_settings = [DummyContext()]
80+
handler.initialize(widget, {'context_settings': context_settings})
2381
self.assertTrue(hasattr(widget, 'context_settings'))
24-
self.assertEqual(widget.context_settings, 5)
82+
self.assertEqual(widget.context_settings, context_settings)
2583

2684
# Default (global) context settings
2785
widget = SimpleWidget()
2886
handler.initialize(widget)
2987
self.assertTrue(hasattr(widget, 'context_settings'))
3088
self.assertEqual(widget.context_settings, handler.global_contexts)
3189

90+
def test_initialize_migrates_contexts(self):
91+
handler = ContextHandler()
92+
handler.bind(SimpleWidget)
93+
94+
widget = SimpleWidget()
95+
96+
# Old settings without version
97+
contexts = [DummyContext() for _ in range(3)]
98+
migrate_context = Mock()
99+
with patch.object(SimpleWidget, "migrate_context", migrate_context):
100+
handler.initialize(widget, dict(context_settings=contexts))
101+
migrate_context.assert_has_calls([call(c, None) for c in contexts])
102+
103+
# Settings with version
104+
contexts = [DummyContext(version=i) for i in range(1, 4)]
105+
migrate_context = Mock()
106+
with patch.object(SimpleWidget, "migrate_context", migrate_context):
107+
handler.initialize(widget, dict(context_settings=deepcopy(contexts)))
108+
migrate_context.assert_has_calls([call(c, c.values[VERSION_KEY]) for c in contexts])
109+
32110
def test_fast_save(self):
33111
handler = ContextHandler()
34112
handler.bind(SimpleWidget)
@@ -68,3 +146,16 @@ def test_find_or_create_context(self):
68146
self.assertEqual(context.i, 5)
69147
self.assertEqual([c.i for c in widget.context_settings], [5, 2])
70148
self.assertEqual([c.i for c in handler.global_contexts], [3, 7])
149+
150+
def test_pack_settings_stores_version(self):
151+
handler = ContextHandler()
152+
handler.bind(SimpleWidget)
153+
154+
widget = SimpleWidget()
155+
handler.initialize(widget)
156+
widget.context_setting = [DummyContext() for _ in range(3)]
157+
158+
settings = handler.pack_data(widget)
159+
self.assertIn("context_settings", settings)
160+
for c in settings["context_settings"]:
161+
self.assertIn(VERSION_KEY, c.values)

0 commit comments

Comments
 (0)