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): |
||
0 ignored issues
–
show
Unused Code
introduced
by
![]() |
|||
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): |
||
0 ignored issues
–
show
|
|||
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: |
||
0 ignored issues
–
show
Catching very general exceptions such as
Exception is usually not recommended.
Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed. So, unless you specifically plan to handle any error, consider adding a more specific exception. ![]() |
|||
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 |
||
0 ignored issues
–
show
warnings is re-defining a name which is already available in the outer-scope (previously defined on line 6 ).
It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior: param = 5
class Foo:
def __init__(self, param): # "param" would be flagged here
self.param = param
![]() |
|||
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): |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
This method could be written as a function/class method.
If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example class Foo:
def some_method(self, x, y):
return x + y;
could be written as class Foo:
@classmethod
def some_method(cls, x, y):
return x + y;
![]() |
|||
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): |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
|
|||
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 |
||
0 ignored issues
–
show
|
|||
546 | # this method's name would indicate. |
||
547 | def settings_to_widget(self, widget): |
||
0 ignored issues
–
show
This method could be written as a function/class method.
If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example class Foo:
def some_method(self, x, y):
return x + y;
could be written as class Foo:
@classmethod
def some_method(cls, x, y):
return x + y;
![]() |
|||
548 | widget.retrieveSpecificSettings() |
||
549 | |||
550 | # TODO similar to settings_to_widget; update_class_defaults does this for |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
This method could be written as a function/class method.
If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example class Foo:
def some_method(self, x, y):
return x + y;
could be written as class Foo:
@classmethod
def some_method(cls, x, y):
return x + y;
![]() |
|||
569 | return copy.copy(value) |
||
570 | |||
571 | def decode_setting(self, setting, value): |
||
0 ignored issues
–
show
This method could be written as a function/class method.
If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example class Foo:
def some_method(self, x, y):
return x + y;
could be written as class Foo:
@classmethod
def some_method(cls, x, y):
return x + y;
![]() |
|||
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): |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
|
|||
756 | if (attrs, metas) == (context.attributes, context.metas): |
||
757 | return 2 |
||
758 | |||
759 | matches = [] |
||
760 | try: |
||
761 | for setting, data, instance in \ |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
|
|||
845 | context = super().new_context() |
||
846 | context.classes = classes |
||
847 | return context |
||
848 | |||
849 | #noinspection PyMethodOverriding |
||
850 | def match(self, context, classes): |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
|
|||
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): |
||
0 ignored issues
–
show
|
|||
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, |
||
0 ignored issues
–
show
|
|||
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 |