1
|
|
|
import os |
2
|
|
|
import time |
3
|
|
|
import copy |
4
|
|
|
import itertools |
5
|
|
|
import pickle |
6
|
|
|
import warnings |
7
|
|
|
|
8
|
|
|
from Orange.misc.environ import widget_settings_dir |
9
|
|
|
from Orange.data import Domain, Variable |
10
|
|
|
from Orange.widgets.utils import vartype |
11
|
|
|
|
12
|
|
|
__all__ = ["Setting", "SettingsHandler", |
13
|
|
|
"ContextSetting", "ContextHandler", |
14
|
|
|
"DomainContextHandler", "PerfectDomainContextHandler", |
15
|
|
|
"ClassValuesContextHandler", "widget_settings_dir"] |
16
|
|
|
|
17
|
|
|
_immutables = (str, int, bytes, bool, float, tuple) |
18
|
|
|
|
19
|
|
|
|
20
|
|
|
class Setting: |
21
|
|
|
"""Description of a setting. |
22
|
|
|
""" |
23
|
|
|
|
24
|
|
|
# A misleading docstring for providing type hints for Settings to PyCharm |
25
|
|
|
def __new__(cls, default, *args, **kw_args): |
|
|
|
|
26
|
|
|
""" |
27
|
|
|
:type: default: T |
28
|
|
|
:rtype: T |
29
|
|
|
""" |
30
|
|
|
return super().__new__(cls) |
31
|
|
|
|
32
|
|
|
def __init__(self, default, **data): |
33
|
|
|
self.name = None # Name gets set in widget's meta class |
34
|
|
|
self.default = default |
35
|
|
|
self.__dict__.update(data) |
36
|
|
|
|
37
|
|
|
def __str__(self): |
38
|
|
|
return "%s \"%s\"" % (self.__class__.__name__, self.name) |
39
|
|
|
|
40
|
|
|
__repr__ = __str__ |
41
|
|
|
|
42
|
|
|
def __getnewargs__(self): |
43
|
|
|
return (self.default, ) |
44
|
|
|
|
45
|
|
|
|
46
|
|
|
class SettingProvider: |
47
|
|
|
"""A hierarchical structure keeping track of settings belonging to |
48
|
|
|
a class and child setting providers. |
49
|
|
|
|
50
|
|
|
At instantiation, it creates a dict of all Setting and SettingProvider |
51
|
|
|
members of the class. This dict is used to get/set values of settings |
52
|
|
|
from/to the instances of the class this provider belongs to. |
53
|
|
|
""" |
54
|
|
|
|
55
|
|
|
def __init__(self, provider_class): |
56
|
|
|
""" Construct a new instance of SettingProvider. |
57
|
|
|
|
58
|
|
|
Traverse provider_class members and store all instances of |
59
|
|
|
Setting and SettingProvider. |
60
|
|
|
""" |
61
|
|
|
self.name = None |
62
|
|
|
self.provider_class = provider_class |
63
|
|
|
self.providers = {} |
64
|
|
|
""":type: dict[str, SettingProvider]""" |
65
|
|
|
self.settings = {} |
66
|
|
|
""":type: dict[str, Setting]""" |
67
|
|
|
self.initialization_data = None |
68
|
|
|
|
69
|
|
|
for name in dir(provider_class): |
70
|
|
|
value = getattr(provider_class, name, None) |
71
|
|
|
if isinstance(value, Setting): |
72
|
|
|
value = copy.deepcopy(value) |
73
|
|
|
value.name = name |
74
|
|
|
self.settings[name] = value |
75
|
|
|
if isinstance(value, SettingProvider): |
76
|
|
|
value = copy.deepcopy(value) |
77
|
|
|
value.name = name |
78
|
|
|
self.providers[name] = value |
79
|
|
|
|
80
|
|
|
def initialize(self, instance, data=None): |
81
|
|
|
"""Initialize instance settings to their default values. |
82
|
|
|
|
83
|
|
|
If default value is mutable, create a shallow copy before assigning it to the instance. |
84
|
|
|
|
85
|
|
|
If data is provided, setting values from data will override defaults. |
86
|
|
|
""" |
87
|
|
|
if data is None and self.initialization_data is not None: |
88
|
|
|
data = self.initialization_data |
89
|
|
|
|
90
|
|
|
for name, setting in self.settings.items(): |
91
|
|
|
if data and name in data: |
92
|
|
|
setattr(instance, name, data[name]) |
93
|
|
|
elif isinstance(setting.default, _immutables): |
94
|
|
|
setattr(instance, name, setting.default) |
95
|
|
|
else: |
96
|
|
|
setattr(instance, name, copy.copy(setting.default)) |
97
|
|
|
|
98
|
|
|
for name, provider in self.providers.items(): |
99
|
|
|
if data and name in data: |
100
|
|
|
if hasattr(instance, name) and not isinstance(getattr(instance, name), SettingProvider): |
101
|
|
|
provider.initialize(getattr(instance, name), data[name]) |
102
|
|
|
else: |
103
|
|
|
provider.store_initialization_data(data[name]) |
104
|
|
|
|
105
|
|
|
def store_initialization_data(self, initialization_data): |
106
|
|
|
"""Store initialization data for later use. |
107
|
|
|
|
108
|
|
|
Used when settings handler is initialized, but member for this provider |
109
|
|
|
does not exists yet (because handler.initialize is called in __new__, but |
110
|
|
|
member will be created in __init__. |
111
|
|
|
""" |
112
|
|
|
self.initialization_data = initialization_data |
113
|
|
|
|
114
|
|
|
def pack(self, instance, packer=None): |
115
|
|
|
"""Pack instance settings in name: value dict. |
116
|
|
|
|
117
|
|
|
packer: optional packing function |
118
|
|
|
it will be called with setting and instance parameters and should |
119
|
|
|
yield (name, value) pairs that will be added to the packed_settings. |
120
|
|
|
""" |
121
|
|
|
if packer is None: |
122
|
|
|
# noinspection PyShadowingNames |
123
|
|
|
def packer(setting, instance): |
|
|
|
|
124
|
|
|
if type(setting) == Setting and hasattr(instance, setting.name): |
125
|
|
|
yield setting.name, getattr(instance, setting.name) |
126
|
|
|
|
127
|
|
|
packed_settings = dict(itertools.chain( |
128
|
|
|
*(packer(setting, instance) for setting in self.settings.values()) |
129
|
|
|
)) |
130
|
|
|
|
131
|
|
|
packed_settings.update({ |
132
|
|
|
name: provider.pack(getattr(instance, name), packer) |
133
|
|
|
for name, provider in self.providers.items() |
134
|
|
|
if hasattr(instance, name) |
135
|
|
|
}) |
136
|
|
|
return packed_settings |
137
|
|
|
|
138
|
|
|
def unpack(self, instance, data): |
139
|
|
|
"""Restore settings from data to the instance. |
140
|
|
|
|
141
|
|
|
instance: instance to restore settings to |
142
|
|
|
data: dictionary containing packed data |
143
|
|
|
""" |
144
|
|
|
for setting, data, instance in self.traverse_settings(data, instance): |
145
|
|
|
if setting.name in data and instance is not None: |
146
|
|
|
setattr(instance, setting.name, data[setting.name]) |
147
|
|
|
|
148
|
|
|
def get_provider(self, provider_class): |
149
|
|
|
"""Return provider for provider_class. |
150
|
|
|
|
151
|
|
|
If this provider matches, return it, otherwise pass |
152
|
|
|
the call to child providers. |
153
|
|
|
""" |
154
|
|
|
if issubclass(provider_class, self.provider_class): |
155
|
|
|
return self |
156
|
|
|
|
157
|
|
|
for subprovider in self.providers.values(): |
158
|
|
|
provider = subprovider.get_provider(provider_class) |
159
|
|
|
if provider: |
160
|
|
|
return provider |
161
|
|
|
|
162
|
|
|
def traverse_settings(self, data=None, instance=None): |
163
|
|
|
"""Generator of tuples (setting, data, instance) for each setting |
164
|
|
|
in this and child providers.. |
165
|
|
|
|
166
|
|
|
:param data: dictionary with setting values |
167
|
|
|
:type data: dict |
168
|
|
|
:param instance: instance matching setting_provider |
169
|
|
|
""" |
170
|
|
|
data = data if data is not None else {} |
171
|
|
|
select_data = lambda x: data.get(x.name, {}) |
172
|
|
|
select_instance = lambda x: getattr(instance, x.name, None) |
173
|
|
|
|
174
|
|
|
for setting in self.settings.values(): |
175
|
|
|
yield setting, data, instance |
176
|
|
|
|
177
|
|
|
for provider in self.providers.values(): |
178
|
|
|
for setting, component_data, component_instance in \ |
179
|
|
|
provider.traverse_settings(select_data(provider), select_instance(provider)): |
180
|
|
|
yield setting, component_data, component_instance |
181
|
|
|
|
182
|
|
|
|
183
|
|
|
class SettingsHandler: |
184
|
|
|
"""Reads widget setting files and passes them to appropriate providers.""" |
185
|
|
|
|
186
|
|
|
def __init__(self): |
187
|
|
|
"""Create a setting handler template. |
188
|
|
|
|
189
|
|
|
Used in class definition. Bound instance will be created |
190
|
|
|
when SettingsHandler.create is called. |
191
|
|
|
""" |
192
|
|
|
self.widget_class = None |
193
|
|
|
self.provider = None |
194
|
|
|
""":type: SettingProvider""" |
195
|
|
|
self.defaults = {} |
196
|
|
|
self.known_settings = {} |
197
|
|
|
|
198
|
|
|
@staticmethod |
199
|
|
|
def create(widget_class, template=None): |
200
|
|
|
"""Create a new settings handler based on the template and bind it to |
201
|
|
|
widget_class. |
202
|
|
|
|
203
|
|
|
:type template: SettingsHandler |
204
|
|
|
:rtype: SettingsHandler |
205
|
|
|
""" |
206
|
|
|
|
207
|
|
|
if template is None: |
208
|
|
|
template = SettingsHandler() |
209
|
|
|
|
210
|
|
|
setting_handler = copy.copy(template) |
211
|
|
|
setting_handler.bind(widget_class) |
212
|
|
|
return setting_handler |
213
|
|
|
|
214
|
|
|
def bind(self, widget_class): |
215
|
|
|
"""Bind settings handler instance to widget_class.""" |
216
|
|
|
self.widget_class = widget_class |
217
|
|
|
self.provider = SettingProvider(widget_class) |
218
|
|
|
self.known_settings = {} |
219
|
|
|
self.analyze_settings(self.provider, "") |
220
|
|
|
self.read_defaults() |
221
|
|
|
|
222
|
|
|
def analyze_settings(self, provider, prefix): |
223
|
|
|
for setting in provider.settings.values(): |
224
|
|
|
self.analyze_setting(prefix, setting) |
225
|
|
|
|
226
|
|
|
for name, sub_provider in provider.providers.items(): |
227
|
|
|
new_prefix = '%s%s.' % (prefix, name) if prefix else '%s.' % name |
228
|
|
|
self.analyze_settings(sub_provider, new_prefix) |
229
|
|
|
|
230
|
|
|
def analyze_setting(self, prefix, setting): |
231
|
|
|
self.known_settings[prefix + setting.name] = setting |
232
|
|
|
|
233
|
|
|
|
234
|
|
|
# noinspection PyBroadException |
235
|
|
|
def read_defaults(self): |
236
|
|
|
"""Read (global) defaults for this widget class from a file. |
237
|
|
|
Opens a file and calls :obj:`read_defaults_file`. Derived classes |
238
|
|
|
should overload the latter.""" |
239
|
|
|
filename = self._get_settings_filename() |
240
|
|
|
if os.path.isfile(filename): |
241
|
|
|
settings_file = open(filename, "rb") |
242
|
|
|
try: |
243
|
|
|
self.read_defaults_file(settings_file) |
244
|
|
|
except Exception as e: |
|
|
|
|
245
|
|
|
warnings.warn("Could not read defaults for widget %s.\n" % self.widget_class + |
246
|
|
|
"The following error occurred:\n\n%s" % e) |
247
|
|
|
finally: |
248
|
|
|
settings_file.close() |
249
|
|
|
|
250
|
|
|
def read_defaults_file(self, settings_file): |
251
|
|
|
"""Read (global) defaults for this widget class from a file.""" |
252
|
|
|
defaults = pickle.load(settings_file) |
253
|
|
|
self.defaults = { |
254
|
|
|
key: value |
255
|
|
|
for key, value in defaults.items() |
256
|
|
|
if not isinstance(value, Setting) |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
def write_defaults(self): |
260
|
|
|
"""Write (global) defaults for this widget class to a file. |
261
|
|
|
Opens a file and calls :obj:`write_defaults_file`. Derived classes |
262
|
|
|
should overload the latter.""" |
263
|
|
|
filename = self._get_settings_filename() |
264
|
|
|
os.makedirs(os.path.dirname(filename), exist_ok=True) |
265
|
|
|
|
266
|
|
|
settings_file = open(filename, "wb") |
267
|
|
|
try: |
268
|
|
|
self.write_defaults_file(settings_file) |
269
|
|
|
except (EOFError, IOError, pickle.PicklingError): |
270
|
|
|
settings_file.close() |
271
|
|
|
os.remove(filename) |
272
|
|
|
else: |
273
|
|
|
settings_file.close() |
274
|
|
|
|
275
|
|
|
def write_defaults_file(self, settings_file): |
276
|
|
|
"""Write defaults for this widget class to a file""" |
277
|
|
|
pickle.dump(self.defaults, settings_file, -1) |
278
|
|
|
|
279
|
|
|
def _get_settings_filename(self): |
280
|
|
|
"""Return the name of the file with default settings for the widget""" |
281
|
|
|
return os.path.join(widget_settings_dir(), |
282
|
|
|
"{0.__module__}.{0.__qualname__}.pickle" |
283
|
|
|
.format(self.widget_class)) |
284
|
|
|
|
285
|
|
|
def initialize(self, instance, data=None): |
286
|
|
|
""" |
287
|
|
|
Initialize widget's settings. |
288
|
|
|
|
289
|
|
|
Replace all instance settings with their default values. |
290
|
|
|
|
291
|
|
|
:param instance: the instance whose settings will be initialized |
292
|
|
|
:param data: dict of values that will be used instead of defaults. |
293
|
|
|
:type data: `dict` or `bytes` that unpickle into a `dict` |
294
|
|
|
""" |
295
|
|
|
provider = self._select_provider(instance) |
296
|
|
|
|
297
|
|
|
if isinstance(data, bytes): |
298
|
|
|
data = pickle.loads(data) |
299
|
|
|
|
300
|
|
|
if provider is self.provider: |
301
|
|
|
data = self._add_defaults(data) |
302
|
|
|
|
303
|
|
|
provider.initialize(instance, data) |
304
|
|
|
|
305
|
|
|
def _select_provider(self, instance): |
306
|
|
|
provider = self.provider.get_provider(instance.__class__) |
307
|
|
|
if provider is None: |
308
|
|
|
import warnings |
|
|
|
|
309
|
|
|
|
310
|
|
|
message = "%s has not been declared as setting provider in %s. " \ |
311
|
|
|
"Settings will not be saved/loaded properly. Defaults will be used instead." \ |
312
|
|
|
% (instance.__class__, self.widget_class) |
313
|
|
|
warnings.warn(message) |
314
|
|
|
provider = SettingProvider(instance.__class__) |
315
|
|
|
return provider |
316
|
|
|
|
317
|
|
|
def _add_defaults(self, data): |
318
|
|
|
if data is None: |
319
|
|
|
return self.defaults |
320
|
|
|
|
321
|
|
|
new_data = self.defaults.copy() |
322
|
|
|
new_data.update(data) |
323
|
|
|
return new_data |
324
|
|
|
|
325
|
|
|
def pack_data(self, widget): |
326
|
|
|
""" |
327
|
|
|
Pack the settings for the given widget. This method is used when |
328
|
|
|
saving schema, so that when the schema is reloaded the widget is |
329
|
|
|
initialized with its proper data and not the class-based defaults. |
330
|
|
|
See :obj:`SettingsHandler.initialize` for detailed explanation of its |
331
|
|
|
use. |
332
|
|
|
|
333
|
|
|
Inherited classes add other data, in particular widget-specific |
334
|
|
|
local contexts. |
335
|
|
|
""" |
336
|
|
|
return self.provider.pack(widget) |
337
|
|
|
|
338
|
|
|
def update_defaults(self, widget): |
339
|
|
|
""" |
340
|
|
|
Writes widget instance's settings to class defaults. Called when the |
341
|
|
|
widget is deleted. |
342
|
|
|
""" |
343
|
|
|
self.defaults = self.provider.pack(widget) |
344
|
|
|
self.write_defaults() |
345
|
|
|
|
346
|
|
|
def fast_save(self, widget, name, value): |
|
|
|
|
347
|
|
|
"""Store the (changed) widget's setting immediately to the context.""" |
348
|
|
|
if name in self.known_settings: |
349
|
|
|
self.known_settings[name].default = value |
350
|
|
|
|
351
|
|
|
@staticmethod |
352
|
|
|
def update_packed_data(data, name, value): |
353
|
|
|
split_name = name.split('.') |
354
|
|
|
prefixes, name = split_name[:-1], split_name[-1] |
355
|
|
|
for prefix in prefixes: |
356
|
|
|
data = data.setdefault(prefix, {}) |
357
|
|
|
data[name] = value |
358
|
|
|
|
359
|
|
|
def reset_settings(self, instance): |
360
|
|
|
for setting, data, instance in self.provider.traverse_settings(instance=instance): |
|
|
|
|
361
|
|
|
if type(setting) == Setting: |
362
|
|
|
setattr(instance, setting.name, setting.default) |
363
|
|
|
|
364
|
|
|
|
365
|
|
|
class ContextSetting(Setting): |
366
|
|
|
OPTIONAL = 0 |
367
|
|
|
IF_SELECTED = 1 |
368
|
|
|
REQUIRED = 2 |
369
|
|
|
|
370
|
|
|
# These flags are not general - they assume that the setting has to do |
371
|
|
|
# something with the attributes. Large majority does, so this greatly |
372
|
|
|
# simplifies the declaration of settings in widget at no (visible) |
373
|
|
|
# cost to those settings that don't need it |
374
|
|
|
def __init__(self, default, not_attribute=False, required=0, |
375
|
|
|
exclude_attributes=False, exclude_metas=True, **data): |
376
|
|
|
super().__init__(default, **data) |
377
|
|
|
self.not_attribute = not_attribute |
378
|
|
|
self.exclude_attributes = exclude_attributes |
379
|
|
|
self.exclude_metas = exclude_metas |
380
|
|
|
self.required = required |
381
|
|
|
|
382
|
|
|
|
383
|
|
|
class Context: |
384
|
|
|
def __init__(self, **argkw): |
385
|
|
|
self.time = time.time() |
386
|
|
|
self.values = {} |
387
|
|
|
self.__dict__.update(argkw) |
388
|
|
|
|
389
|
|
|
def __getstate__(self): |
390
|
|
|
s = dict(self.__dict__) |
391
|
|
|
for nc in getattr(self, "no_copy", []): |
392
|
|
|
if nc in s: |
393
|
|
|
del s[nc] |
394
|
|
|
return s |
395
|
|
|
|
396
|
|
|
|
397
|
|
|
class ContextHandler(SettingsHandler): |
398
|
|
|
"""Base class for setting handlers that can handle contexts.""" |
399
|
|
|
|
400
|
|
|
MAX_SAVED_CONTEXTS = 50 |
401
|
|
|
|
402
|
|
|
def __init__(self): |
403
|
|
|
super().__init__() |
404
|
|
|
self.global_contexts = [] |
405
|
|
|
self.known_settings = {} |
406
|
|
|
|
407
|
|
|
def analyze_setting(self, prefix, setting): |
408
|
|
|
super().analyze_setting(prefix, setting) |
409
|
|
|
if isinstance(setting, ContextSetting): |
410
|
|
|
if hasattr(setting, 'selected'): |
411
|
|
|
self.known_settings[prefix + setting.selected] = setting |
412
|
|
|
|
413
|
|
|
def initialize(self, instance, data=None): |
414
|
|
|
"""Initialize the widget: call the inherited initialization and |
415
|
|
|
add an attribute 'context_settings' to the widget. This method |
416
|
|
|
does not open a context.""" |
417
|
|
|
instance.current_context = None |
418
|
|
|
super().initialize(instance, data) |
419
|
|
|
if data and "context_settings" in data: |
420
|
|
|
instance.context_settings = data["context_settings"] |
421
|
|
|
else: |
422
|
|
|
instance.context_settings = self.global_contexts |
423
|
|
|
|
424
|
|
|
def read_defaults_file(self, settings_file): |
425
|
|
|
"""Call the inherited method, then read global context from the |
426
|
|
|
pickle.""" |
427
|
|
|
super().read_defaults_file(settings_file) |
428
|
|
|
self.global_contexts = pickle.load(settings_file) |
429
|
|
|
|
430
|
|
|
def write_defaults_file(self, settings_file): |
431
|
|
|
"""Call the inherited method, then add global context to the pickle.""" |
432
|
|
|
super().write_defaults_file(settings_file) |
433
|
|
|
pickle.dump(self.global_contexts, settings_file, -1) |
434
|
|
|
|
435
|
|
|
def pack_data(self, widget): |
436
|
|
|
"""Call the inherited method, then add local contexts to the pickle.""" |
437
|
|
|
data = super().pack_data(widget) |
438
|
|
|
data["context_settings"] = widget.context_settings |
439
|
|
|
return data |
440
|
|
|
|
441
|
|
|
def update_defaults(self, widget): |
442
|
|
|
"""Call the inherited method, then merge the local context into the |
443
|
|
|
global contexts. This make sense only when the widget does not use |
444
|
|
|
global context (i.e. `widget.context_settings is not |
445
|
|
|
self.global_contexts`); this happens when the widget was initialized by |
446
|
|
|
an instance-specific data that was passed to :obj:`initialize`.""" |
447
|
|
|
super().update_defaults(widget) |
448
|
|
|
globs = self.global_contexts |
449
|
|
|
if widget.context_settings is not globs: |
450
|
|
|
ids = {id(c) for c in globs} |
451
|
|
|
globs += (c for c in widget.context_settings if id(c) not in ids) |
452
|
|
|
globs.sort(key=lambda c: -c.time) |
453
|
|
|
del globs[self.MAX_SAVED_CONTEXTS:] |
454
|
|
|
|
455
|
|
|
def new_context(self, *args): |
|
|
|
|
456
|
|
|
"""Create a new context.""" |
457
|
|
|
return Context() |
458
|
|
|
|
459
|
|
|
def open_context(self, widget, *args): |
460
|
|
|
"""Open a context by finding one and setting the widget data or |
461
|
|
|
creating one and fill with the data from the widget.""" |
462
|
|
|
widget.current_context, is_new = \ |
463
|
|
|
self.find_or_create_context(widget, *args) |
464
|
|
|
if is_new: |
465
|
|
|
self.settings_from_widget(widget) |
466
|
|
|
else: |
467
|
|
|
self.settings_to_widget(widget) |
468
|
|
|
|
469
|
|
|
def match(self, context, *args): |
|
|
|
|
470
|
|
|
"""Return the degree to which the stored `context` matches the data |
471
|
|
|
passed in additional arguments). A match of 0 zero indicates that |
472
|
|
|
the context cannot be used and 2 means a perfect match, so no further |
473
|
|
|
search is necessary. |
474
|
|
|
|
475
|
|
|
If imperfect matching is not desired, match should only |
476
|
|
|
return 0 and 2. |
477
|
|
|
|
478
|
|
|
Derived classes must overload this method.""" |
479
|
|
|
raise TypeError(self.__class__.__name__ + " does not overload match") |
480
|
|
|
|
481
|
|
|
def find_or_create_context(self, widget, *args): |
482
|
|
|
"""Find the best matching context or create a new one if nothing |
483
|
|
|
useful is found. The returned context is moved to or added to the top |
484
|
|
|
of the context list.""" |
485
|
|
|
best_context, best_score = None, 0 |
486
|
|
|
for i, context in enumerate(widget.context_settings): |
487
|
|
|
score = self.match(context, *args) |
488
|
|
|
if score == 2: |
489
|
|
|
self.move_context_up(widget, i) |
490
|
|
|
return context, False |
491
|
|
|
if score > best_score: # 0 is not OK! |
492
|
|
|
best_context, best_score = context, score |
493
|
|
|
if best_context: |
494
|
|
|
context = self.clone_context(best_context, *args) |
495
|
|
|
else: |
496
|
|
|
context = self.new_context(*args) |
497
|
|
|
self.add_context(widget, context) |
498
|
|
|
return context, best_context is None |
499
|
|
|
|
500
|
|
|
@staticmethod |
501
|
|
|
def move_context_up(widget, index): |
502
|
|
|
"""Move the context to the top of the context list and set the time |
503
|
|
|
stamp to current.""" |
504
|
|
|
setting = widget.context_settings.pop(index) |
505
|
|
|
setting.time = time.time() |
506
|
|
|
widget.context_settings.insert(0, setting) |
507
|
|
|
|
508
|
|
|
@staticmethod |
509
|
|
|
def add_context(widget, setting): |
510
|
|
|
"""Add the context to the top of the list.""" |
511
|
|
|
s = widget.context_settings |
512
|
|
|
s.insert(0, setting) |
513
|
|
|
del s[len(s):] |
514
|
|
|
|
515
|
|
|
def clone_context(self, old_context, *args): |
516
|
|
|
"""Construct a copy of the context settings suitable for the context |
517
|
|
|
described by additional arguments. The method is called by |
518
|
|
|
find_or_create_context with the same arguments. A class that overloads |
519
|
|
|
:obj:`match` to accept additional arguments must also overload |
520
|
|
|
:obj:`clone_context`.""" |
521
|
|
|
context = self.new_context(*args) |
522
|
|
|
context.values = copy.deepcopy(old_context.values) |
523
|
|
|
|
524
|
|
|
traverse = self.provider.traverse_settings |
525
|
|
|
for setting, data, instance in traverse(data=context.values): |
|
|
|
|
526
|
|
|
if not isinstance(setting, ContextSetting): |
527
|
|
|
continue |
528
|
|
|
|
529
|
|
|
self.filter_value(setting, data, *args) |
530
|
|
|
return context |
531
|
|
|
|
532
|
|
|
@staticmethod |
533
|
|
|
def filter_value(setting, data, *args): |
534
|
|
|
"""Remove values related to setting that are invalid given args.""" |
535
|
|
|
|
536
|
|
|
def close_context(self, widget): |
537
|
|
|
"""Close the context by calling :obj:`settings_from_widget` to write |
538
|
|
|
any relevant widget settings to the context.""" |
539
|
|
|
if widget.current_context is None: |
540
|
|
|
return |
541
|
|
|
|
542
|
|
|
self.settings_from_widget(widget) |
543
|
|
|
widget.current_context = None |
544
|
|
|
|
545
|
|
|
# TODO this method has misleading name (method 'initialize' does what |
|
|
|
|
546
|
|
|
# this method's name would indicate. |
547
|
|
|
def settings_to_widget(self, widget): |
|
|
|
|
548
|
|
|
widget.retrieveSpecificSettings() |
549
|
|
|
|
550
|
|
|
# TODO similar to settings_to_widget; update_class_defaults does this for |
|
|
|
|
551
|
|
|
# context independent settings |
552
|
|
|
def settings_from_widget(self, widget): |
553
|
|
|
widget.storeSpecificSettings() |
554
|
|
|
|
555
|
|
|
context = widget.current_context |
556
|
|
|
if context is None: |
557
|
|
|
return |
558
|
|
|
|
559
|
|
|
def packer(setting, instance): |
560
|
|
|
if hasattr(instance, setting.name): |
561
|
|
|
value = getattr(instance, setting.name) |
562
|
|
|
yield setting.name, self.encode_setting(context, setting, value) |
563
|
|
|
if hasattr(setting, "selected"): |
564
|
|
|
yield setting.selected, list(getattr(instance, setting.selected)) |
565
|
|
|
|
566
|
|
|
context.values = self.provider.pack(widget, packer=packer) |
567
|
|
|
|
568
|
|
|
def encode_setting(self, context, setting, value): |
|
|
|
|
569
|
|
|
return copy.copy(value) |
570
|
|
|
|
571
|
|
|
def decode_setting(self, setting, value): |
|
|
|
|
572
|
|
|
return value |
573
|
|
|
|
574
|
|
|
|
575
|
|
|
class DomainContextHandler(ContextHandler): |
576
|
|
|
MATCH_VALUES_NONE, MATCH_VALUES_CLASS, MATCH_VALUES_ALL = range(3) |
577
|
|
|
|
578
|
|
|
def __init__(self, max_vars_to_pickle=100, match_values=0, |
579
|
|
|
reservoir=None, attributes_in_res=True, metas_in_res=False): |
580
|
|
|
super().__init__() |
581
|
|
|
self.max_vars_to_pickle = max_vars_to_pickle |
582
|
|
|
self.match_values = match_values |
583
|
|
|
self.reservoir = reservoir |
584
|
|
|
self.attributes_in_res = attributes_in_res |
585
|
|
|
self.metas_in_res = metas_in_res |
586
|
|
|
|
587
|
|
|
self.has_ordinary_attributes = attributes_in_res |
588
|
|
|
self.has_meta_attributes = metas_in_res |
589
|
|
|
|
590
|
|
|
self.known_settings = {} |
591
|
|
|
|
592
|
|
|
def analyze_setting(self, prefix, setting): |
593
|
|
|
super().analyze_setting(prefix, setting) |
594
|
|
|
if isinstance(setting, ContextSetting) and not setting.not_attribute: |
595
|
|
|
if not setting.exclude_attributes: |
596
|
|
|
self.has_ordinary_attributes = True |
597
|
|
|
if not setting.exclude_metas: |
598
|
|
|
self.has_meta_attributes = True |
599
|
|
|
|
600
|
|
|
def encode_domain(self, domain): |
601
|
|
|
""" |
602
|
|
|
domain: Orange.data.domain to encode |
603
|
|
|
return: dict mapping attribute name to type or list of values |
604
|
|
|
(based on the value of self.match_values attribute) |
605
|
|
|
""" |
606
|
|
|
|
607
|
|
|
# noinspection PyShadowingNames |
608
|
|
|
def encode(attributes, encode_values): |
609
|
|
|
if not encode_values: |
610
|
|
|
return {v.name: vartype(v) for v in attributes} |
611
|
|
|
|
612
|
|
|
return {v.name: v.values if v.is_discrete else vartype(v) |
613
|
|
|
for v in attributes} |
614
|
|
|
|
615
|
|
|
match = self.match_values |
616
|
|
|
if self.has_ordinary_attributes: |
617
|
|
|
if match == self.MATCH_VALUES_CLASS: |
618
|
|
|
attributes = encode(domain.attributes, False) |
619
|
|
|
attributes.update(encode(domain.class_vars, True)) |
620
|
|
|
else: |
621
|
|
|
attributes = encode(domain, match == self.MATCH_VALUES_ALL) |
622
|
|
|
else: |
623
|
|
|
attributes = {} |
624
|
|
|
|
625
|
|
|
if self.has_meta_attributes: |
626
|
|
|
metas = encode(domain.metas, match == self.MATCH_VALUES_ALL) |
627
|
|
|
else: |
628
|
|
|
metas = {} |
629
|
|
|
|
630
|
|
|
return attributes, metas |
631
|
|
|
|
632
|
|
|
def new_context(self, domain, attributes, metas): |
|
|
|
|
633
|
|
|
"""Create a new context.""" |
634
|
|
|
context = super().new_context() |
635
|
|
|
context.attributes = attributes |
636
|
|
|
context.metas = metas |
637
|
|
|
context.ordered_domain = [] |
638
|
|
|
if self.has_ordinary_attributes: |
639
|
|
|
context.ordered_domain += [(attr.name, vartype(attr)) |
640
|
|
|
for attr in domain] |
641
|
|
|
if self.has_meta_attributes: |
642
|
|
|
context.ordered_domain += [(attr.name, vartype(attr)) |
643
|
|
|
for attr in domain.metas] |
644
|
|
|
return context |
645
|
|
|
|
646
|
|
|
# noinspection PyMethodOverriding |
647
|
|
|
def open_context(self, widget, domain): |
|
|
|
|
648
|
|
|
if not domain: |
649
|
|
|
return None, False |
650
|
|
|
|
651
|
|
|
if not isinstance(domain, Domain): |
652
|
|
|
domain = domain.domain |
653
|
|
|
|
654
|
|
|
super().open_context(widget, domain, *self.encode_domain(domain)) |
655
|
|
|
|
656
|
|
|
# noinspection PyMethodOverriding |
657
|
|
|
def filter_value(self, setting, data, domain, attributes, metas): |
|
|
|
|
658
|
|
|
value = data.get(setting.name, None) |
659
|
|
|
if isinstance(value, list): |
660
|
|
|
sel_name = getattr(setting, "selected", None) |
661
|
|
|
selected = set(data.pop(sel_name, [])) |
662
|
|
|
new_selected, new_value = [], [] |
663
|
|
|
for i, val in enumerate(value): |
664
|
|
|
if self._var_exists(setting, val, attributes, metas): |
665
|
|
|
if i in selected: |
666
|
|
|
new_selected.append(len(new_value)) |
667
|
|
|
new_value.append(val) |
668
|
|
|
|
669
|
|
|
data[setting.name] = new_value |
670
|
|
|
if hasattr(setting, 'selected'): |
671
|
|
|
data[setting.selected] = new_selected |
672
|
|
|
elif value is not None: |
673
|
|
|
if (value[1] >= 0 and |
674
|
|
|
not self._var_exists(setting, value, attributes, metas)): |
675
|
|
|
del data[setting.name] |
676
|
|
|
|
677
|
|
|
def settings_to_widget(self, widget): |
678
|
|
|
super().settings_to_widget(widget) |
679
|
|
|
|
680
|
|
|
context = widget.current_context |
681
|
|
|
if context is None: |
682
|
|
|
return |
683
|
|
|
|
684
|
|
|
excluded = set() |
685
|
|
|
|
686
|
|
|
for setting, data, instance in \ |
687
|
|
|
self.provider.traverse_settings(data=context.values, instance=widget): |
688
|
|
|
if not isinstance(setting, ContextSetting) or setting.name not in data: |
689
|
|
|
continue |
690
|
|
|
|
691
|
|
|
value = self.decode_setting(setting, data[setting.name]) |
692
|
|
|
setattr(instance, setting.name, value) |
693
|
|
|
if hasattr(setting, "selected") and setting.selected in data: |
694
|
|
|
setattr(instance, setting.selected, data[setting.selected]) |
695
|
|
|
|
696
|
|
|
if isinstance(value, list): |
697
|
|
|
excluded |= set(value) |
698
|
|
|
else: |
699
|
|
|
if setting.not_attribute: |
700
|
|
|
excluded.add(value) |
701
|
|
|
|
702
|
|
|
if self.reservoir is not None: |
703
|
|
|
get_attribute = lambda name: context.attributes.get(name, None) |
704
|
|
|
get_meta = lambda name: context.metas.get(name, None) |
705
|
|
|
ll = [a for a in context.ordered_domain if a not in excluded and ( |
706
|
|
|
self.attributes_in_res and get_attribute(a[0]) == a[1] or |
707
|
|
|
self.metas_in_res and get_meta(a[0]) == a[1])] |
708
|
|
|
setattr(widget, self.reservoir, ll) |
709
|
|
|
|
710
|
|
|
def fast_save(self, widget, name, value): |
711
|
|
|
context = widget.current_context |
712
|
|
|
if not context: |
713
|
|
|
return |
714
|
|
|
|
715
|
|
|
if name in self.known_settings: |
716
|
|
|
setting = self.known_settings[name] |
717
|
|
|
|
718
|
|
|
if name == setting.name or name.endswith(".%s" % setting.name): |
719
|
|
|
value = self.encode_setting(context, setting, value) |
720
|
|
|
else: |
721
|
|
|
value = list(value) |
722
|
|
|
|
723
|
|
|
self.update_packed_data(context.values, name, value) |
724
|
|
|
|
725
|
|
|
def encode_setting(self, context, setting, value): |
726
|
|
|
value = copy.copy(value) |
727
|
|
|
if isinstance(value, list): |
728
|
|
|
return value |
729
|
|
|
elif isinstance(setting, ContextSetting) and isinstance(value, str): |
730
|
|
|
if not setting.exclude_attributes and value in context.attributes: |
731
|
|
|
return value, context.attributes[value] |
732
|
|
|
if not setting.exclude_metas and value in context.metas: |
733
|
|
|
return value, context.metas[value] |
734
|
|
|
return value, -2 |
735
|
|
|
|
736
|
|
|
@staticmethod |
737
|
|
|
def decode_setting(setting, value): |
|
|
|
|
738
|
|
|
if isinstance(value, tuple): |
739
|
|
|
return value[0] |
740
|
|
|
else: |
741
|
|
|
return value |
742
|
|
|
|
743
|
|
|
@staticmethod |
744
|
|
|
def _var_exists(setting, value, attributes, metas): |
745
|
|
|
if not isinstance(value, tuple) or len(value) != 2: |
746
|
|
|
return False |
747
|
|
|
|
748
|
|
|
attr_name, attr_type = value |
749
|
|
|
return (not setting.exclude_attributes and |
750
|
|
|
attributes.get(attr_name, -1) == attr_type or |
751
|
|
|
not setting.exclude_metas and |
752
|
|
|
metas.get(attr_name, -1) == attr_type) |
753
|
|
|
|
754
|
|
|
#noinspection PyMethodOverriding |
755
|
|
|
def match(self, context, domain, attrs, metas): |
|
|
|
|
756
|
|
|
if (attrs, metas) == (context.attributes, context.metas): |
757
|
|
|
return 2 |
758
|
|
|
|
759
|
|
|
matches = [] |
760
|
|
|
try: |
761
|
|
|
for setting, data, instance in \ |
|
|
|
|
762
|
|
|
self.provider.traverse_settings(data=context.values): |
763
|
|
|
if not isinstance(setting, ContextSetting): |
764
|
|
|
continue |
765
|
|
|
value = data.get(setting.name, None) |
766
|
|
|
|
767
|
|
|
if isinstance(value, list): |
768
|
|
|
matches.append( |
769
|
|
|
self.match_list(setting, value, context, attrs, metas)) |
770
|
|
|
elif value is not None: |
771
|
|
|
matches.append( |
772
|
|
|
self.match_value(setting, value, attrs, metas)) |
773
|
|
|
except IncompatibleContext: |
774
|
|
|
return 0 |
775
|
|
|
|
776
|
|
|
matched, available = map(sum, zip(*matches)) if matches else (0, 0) |
777
|
|
|
if not available: |
778
|
|
|
return 0.1 |
779
|
|
|
else: |
780
|
|
|
return matched / available |
781
|
|
|
|
782
|
|
|
def match_list(self, setting, value, context, attrs, metas): |
783
|
|
|
matched = 0 |
784
|
|
|
if hasattr(setting, 'selected'): |
785
|
|
|
selected = set(context.values.get(setting.selected, [])) |
786
|
|
|
else: |
787
|
|
|
selected = set() |
788
|
|
|
|
789
|
|
|
for i, item in enumerate(value): |
790
|
|
|
if self._var_exists(setting, item, attrs, metas): |
791
|
|
|
matched += 1 |
792
|
|
|
else: |
793
|
|
|
if setting.required == ContextSetting.REQUIRED: |
794
|
|
|
raise IncompatibleContext() |
795
|
|
|
if setting.IF_SELECTED and i in selected: |
796
|
|
|
raise IncompatibleContext() |
797
|
|
|
|
798
|
|
|
return matched, len(value) |
799
|
|
|
|
800
|
|
|
def match_value(self, setting, value, attrs, metas): |
801
|
|
|
if value[1] < 0: |
802
|
|
|
return 0, 0 |
803
|
|
|
|
804
|
|
|
if self._var_exists(setting, value, attrs, metas): |
805
|
|
|
return 1, 1 |
806
|
|
|
else: |
807
|
|
|
raise IncompatibleContext() |
808
|
|
|
|
809
|
|
|
def mergeBack(self, widget): |
810
|
|
|
glob = self.global_contexts |
811
|
|
|
mp = self.max_vars_to_pickle |
812
|
|
|
if widget.context_settings is not glob: |
813
|
|
|
ids = {id(c) for c in glob} |
814
|
|
|
glob += (c for c in widget.context_settings if id(c) not in ids and |
815
|
|
|
((c.attributes and len(c.attributes) or 0) + |
816
|
|
|
(c.class_vars and len(c.class_vars) or 0) + |
817
|
|
|
(c.metas and len(c.metas) or 0)) <= mp) |
818
|
|
|
glob.sort(key=lambda context: -context.time) |
819
|
|
|
del glob[self.MAX_SAVED_CONTEXTS:] |
820
|
|
|
else: |
821
|
|
|
for i in range(len(glob) - 1, -1, -1): |
822
|
|
|
c = glob[i] |
823
|
|
|
n_attrs = ((c.attributes and len(c.attributes) or 0) + |
824
|
|
|
(c.class_vars and len(c.class_vars) or 0) + |
825
|
|
|
(c.metas and len(c.metas) or 0)) |
826
|
|
|
if n_attrs >= mp: |
827
|
|
|
del glob[i] |
828
|
|
|
|
829
|
|
|
|
830
|
|
|
class IncompatibleContext(Exception): |
831
|
|
|
pass |
832
|
|
|
|
833
|
|
|
|
834
|
|
|
class ClassValuesContextHandler(ContextHandler): |
835
|
|
|
def open_context(self, widget, classes): |
|
|
|
|
836
|
|
|
if isinstance(classes, Variable): |
837
|
|
|
if classes.is_discrete: |
838
|
|
|
classes = classes.values |
839
|
|
|
else: |
840
|
|
|
classes = None |
841
|
|
|
|
842
|
|
|
super().open_context(widget, classes) |
843
|
|
|
|
844
|
|
|
def new_context(self, classes): |
|
|
|
|
845
|
|
|
context = super().new_context() |
846
|
|
|
context.classes = classes |
847
|
|
|
return context |
848
|
|
|
|
849
|
|
|
#noinspection PyMethodOverriding |
850
|
|
|
def match(self, context, classes): |
|
|
|
|
851
|
|
|
if isinstance(classes, Variable) and classes.is_continuous: |
852
|
|
|
return context.classes is None and 2 |
853
|
|
|
else: |
854
|
|
|
return context.classes == classes and 2 |
855
|
|
|
|
856
|
|
|
def settings_to_widget(self, widget): |
857
|
|
|
super().settings_to_widget(widget) |
858
|
|
|
context = widget.current_context |
859
|
|
|
self.provider.unpack(widget, context.values) |
860
|
|
|
|
861
|
|
|
def fast_save(self, widget, name, value): |
862
|
|
|
if widget.current_context is None: |
863
|
|
|
return |
864
|
|
|
|
865
|
|
|
if name in self.known_settings: |
866
|
|
|
self.update_packed_data(widget.current_context.values, name, copy.copy(value)) |
867
|
|
|
|
868
|
|
|
|
869
|
|
|
### Requires the same the same attributes in the same order |
870
|
|
|
### The class overloads domain encoding and matching. |
871
|
|
|
### Due to different encoding, it also needs to overload encode_setting and |
872
|
|
|
### clone_context (which is the same as the ContextHandler's) |
873
|
|
|
### We could simplify some other methods, but prefer not to replicate the code |
874
|
|
|
class PerfectDomainContextHandler(DomainContextHandler): |
875
|
|
|
#noinspection PyMethodOverriding |
876
|
|
|
def new_context(self, domain, attributes, class_vars, metas): |
|
|
|
|
877
|
|
|
context = super().new_context(domain, attributes, metas) |
878
|
|
|
context.class_vars = class_vars |
879
|
|
|
return context |
880
|
|
|
|
881
|
|
|
def encode_domain(self, domain): |
882
|
|
|
if self.match_values == 2: |
883
|
|
|
def encode(attrs): |
884
|
|
|
return tuple((v.name, v.values if v.is_discrete else vartype(v)) |
885
|
|
|
for v in attrs) |
886
|
|
|
else: |
887
|
|
|
def encode(attrs): |
888
|
|
|
return tuple((v.name, vartype(v)) for v in attrs) |
889
|
|
|
return (encode(domain.attributes), |
890
|
|
|
encode(domain.class_vars), |
891
|
|
|
encode(domain.metas)) |
892
|
|
|
|
893
|
|
|
#noinspection PyMethodOverriding |
894
|
|
|
def match(self, context, domain, attributes, class_vars, metas): |
|
|
|
|
895
|
|
|
return (attributes, class_vars, metas) == ( |
896
|
|
|
context.attributes, context.class_vars, context.metas) and 2 |
897
|
|
|
|
898
|
|
|
def encode_setting(self, context, setting, value): |
899
|
|
|
if isinstance(value, str): |
900
|
|
|
atype = -1 |
901
|
|
|
if not setting.exclude_attributes: |
902
|
|
|
for aname, atype in itertools.chain(context.attributes, |
903
|
|
|
context.class_vars): |
904
|
|
|
if aname == value: |
905
|
|
|
break |
906
|
|
|
if atype == -1 and not setting.exclude_metas: |
907
|
|
|
for aname, values in itertools.chain(context.attributes, |
|
|
|
|
908
|
|
|
context.class_vars): |
909
|
|
|
if aname == value: |
910
|
|
|
break |
911
|
|
|
return value, copy.copy(atype) |
912
|
|
|
else: |
913
|
|
|
return super().encode_setting(context, setting, value) |
914
|
|
|
|
915
|
|
|
def clone_context(self, context, _, *__): |
916
|
|
|
return copy.deepcopy(context) |
917
|
|
|
|