GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Orange.widgets.DomainContextHandler   F
last analyzed

Complexity

Total Complexity 102

Size/Duplication

Total Lines 253
Duplicated Lines 0 %
Metric Value
dl 0
loc 253
rs 1.5789
wmc 102

16 Methods

Rating   Name   Duplication   Size   Complexity  
A match_value() 0 8 3
C mergeBack() 0 19 15
B match_list() 0 17 7
B encode() 0 6 5
B analyze_setting() 0 7 5
F encode_domain() 0 31 9
A _var_exists() 0 10 3
A decode_setting() 0 6 2
C filter_value() 0 19 9
B fast_save() 0 14 5
F settings_to_widget() 0 32 18
A open_context() 0 8 3
C encode_setting() 0 10 8
A __init__() 0 13 1
F match() 0 26 9
B new_context() 0 13 5

How to fix   Complexity   

Complex Class

Complex classes like Orange.widgets.DomainContextHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
The argument default seems to be unused.
Loading history...
Unused Code introduced by
The argument kw_args seems to be unused.
Loading history...
Unused Code introduced by
The argument args seems to be unused.
Loading history...
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
Bug introduced by
This function was already defined on line 114.
Loading history...
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
Best Practice introduced by
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.

Loading history...
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
Comprehensibility Bug introduced by
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
Loading history...
Unused Code introduced by
The import warnings was already done on line 6. You should be able to
remove this line.
Loading history...
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
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
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
Unused Code introduced by
The variable data seems to be unused.
Loading history...
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
Unused Code introduced by
The argument args seems to be unused.
Loading history...
Coding Style introduced by
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;
Loading history...
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
Unused Code introduced by
The argument args seems to be unused.
Loading history...
Unused Code introduced by
The argument context seems to be unused.
Loading history...
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
Unused Code introduced by
The variable instance seems to be unused.
Loading history...
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
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
546
    #      this method's name would indicate.
547
    def settings_to_widget(self, widget):
0 ignored issues
show
Coding Style introduced by
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;
Loading history...
548
        widget.retrieveSpecificSettings()
549
550
    # TODO similar to settings_to_widget; update_class_defaults does this for
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
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
Unused Code introduced by
The argument context seems to be unused.
Loading history...
Unused Code introduced by
The argument setting seems to be unused.
Loading history...
Coding Style introduced by
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;
Loading history...
569
        return copy.copy(value)
570
571
    def decode_setting(self, setting, value):
0 ignored issues
show
Unused Code introduced by
The argument setting seems to be unused.
Loading history...
Coding Style introduced by
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;
Loading history...
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
Bug introduced by
Arguments number differs from overridden 'new_context' method
Loading history...
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
Bug introduced by
Arguments number differs from overridden 'open_context' method
Loading history...
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
Bug introduced by
Arguments number differs from overridden 'filter_value' method
Loading history...
Unused Code introduced by
The argument domain seems to be unused.
Loading history...
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
Bug introduced by
Arguments number differs from overridden 'decode_setting' method
Loading history...
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
Bug introduced by
Arguments number differs from overridden 'match' method
Loading history...
Unused Code introduced by
The argument domain seems to be unused.
Loading history...
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
Unused Code introduced by
The variable instance seems to be unused.
Loading history...
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
Bug introduced by
Arguments number differs from overridden 'open_context' method
Loading history...
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
Bug introduced by
Arguments number differs from overridden 'new_context' method
Loading history...
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
Bug introduced by
Arguments number differs from overridden 'match' method
Loading history...
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
Bug introduced by
Arguments number differs from overridden 'new_context' method
Loading history...
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
Bug introduced by
Arguments number differs from overridden 'match' method
Loading history...
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
Unused Code introduced by
The variable values seems to be unused.
Loading history...
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