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.canvas.scheme.parse_scheme_v_2_0()   F
last analyzed

Complexity

Conditions 26

Size

Total Lines 122

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 26
dl 0
loc 122
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Orange.canvas.scheme.parse_scheme_v_2_0() 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
"""
2
Scheme save/load routines.
3
4
"""
5
import base64
6
import sys
7
import warnings
8
9
from xml.etree.ElementTree import TreeBuilder, Element, ElementTree, parse
10
11
from collections import defaultdict, namedtuple
12
from itertools import chain, count
13
14
import pickle as pickle
15
import json
16
import pprint
17
18
import ast
19
from ast import literal_eval
20
21
import logging
22
23
from . import SchemeNode, SchemeLink
24
from .annotations import SchemeTextAnnotation, SchemeArrowAnnotation
25
from .errors import IncompatibleChannelTypeError
26
27
from ..registry import global_registry
28
29
log = logging.getLogger(__name__)
30
31
32
class UnknownWidgetDefinition(Exception):
33
    pass
34
35
36
def string_eval(source):
37
    """
38
    Evaluate a python string literal `source`. Raise ValueError if
39
    `source` is not a string literal.
40
41
    >>> string_eval("'a string'")
42
    a string
43
44
    """
45
    node = ast.parse(source, "<source>", mode="eval")
46
    if not isinstance(node.body, ast.Str):
47
        raise ValueError("%r is not a string literal" % source)
48
    return node.body.s
49
50
51
def tuple_eval(source):
52
    """
53
    Evaluate a python tuple literal `source` where the elements are
54
    constrained to be int, float or string. Raise ValueError if not
55
    a tuple literal.
56
57
    >>> tuple_eval("(1, 2, "3")")
58
    (1, 2, '3')
59
60
    """
61
    node = ast.parse(source, "<source>", mode="eval")
62
63
    if not isinstance(node.body, ast.Tuple):
64
        raise ValueError("%r is not a tuple literal" % source)
65
66
    if not all(isinstance(el, (ast.Str, ast.Num)) or
67
               # allow signed number literals in Python3 (i.e. -1|+1|-1.0)
68
               (isinstance(el, ast.UnaryOp) and
69
                isinstance(el.op, (ast.UAdd, ast.USub)) and
70
                isinstance(el.operand, ast.Num))
71
               for el in node.body.elts):
72
        raise ValueError("Can only contain numbers or strings")
73
74
    return literal_eval(source)
75
76
77
def terminal_eval(source):
78
    """
79
    Evaluate a python 'constant' (string, number, None, True, False)
80
    `source`. Raise ValueError is not a terminal literal.
81
82
    >>> terminal_eval("True")
83
    True
84
85
    """
86
    node = ast.parse(source, "<source>", mode="eval")
87
88
    try:
89
        return _terminal_value(node.body)
90
    except ValueError:
91
        raise
92
        raise ValueError("%r is not a terminal constant" % source)
0 ignored issues
show
Unused Code introduced by
This code does not seem to be reachable.
Loading history...
93
94
95
def _terminal_value(node):
96
    if isinstance(node, ast.Str):
97
        return node.s
98
    elif isinstance(node, ast.Num):
99
        return node.n
100
    elif isinstance(node, ast.Name) and \
101
            node.id in ["True", "False", "None"]:
102
        return __builtins__[node.id]
103
104
    raise ValueError("Not a terminal")
105
106
107
def sniff_version(stream):
108
    """
109
    Parse a scheme stream and return the scheme's serialization
110
    version string.
111
112
    """
113
    doc = parse(stream)
114
    scheme_el = doc.getroot()
115
    version = scheme_el.attrib.get("version", None)
116
    # Fallback: check for "widgets" tag.
117
    if scheme_el.find("widgets") is not None:
118
        version = "1.0"
119
    else:
120
        version = "2.0"
121
122
    return version
123
124
125
def parse_scheme(scheme, stream, error_handler=None,
126
                 allow_pickle_data=False):
127
    """
128
    Parse a saved scheme from `stream` and populate a `scheme`
129
    instance (:class:`Scheme`).
130
    `error_handler` if given will be called with an exception when
131
    a 'recoverable' error occurs. By default the exception is simply
132
    raised.
133
134
    Parameters
135
    ----------
136
    scheme : :class:`.Scheme`
137
        A scheme instance to populate with the contents of `stream`.
138
    stream : file-like object
139
        A file like object opened for reading.
140
    error_hander : function, optional
141
        A function to call with an exception instance when a `recoverable`
142
        error occurs.
143
    allow_picked_data : bool, optional
144
        Specifically allow parsing of picked data streams.
145
146
    """
147
    warnings.warn("Use 'scheme_load' instead", DeprecationWarning,
148
                  stacklevel=2)
149
150
    doc = parse(stream)
151
    scheme_el = doc.getroot()
152
    version = scheme_el.attrib.get("version", None)
153
    if version is None:
154
        # Fallback: check for "widgets" tag.
155
        if scheme_el.find("widgets") is not None:
156
            version = "1.0"
157
        else:
158
            version = "2.0"
159
160
    if error_handler is None:
161
        def error_handler(exc):
0 ignored issues
show
Bug introduced by
This function was already defined on line 125.
Loading history...
162
            raise exc
163
164
    if version == "1.0":
165
        parse_scheme_v_1_0(doc, scheme, error_handler=error_handler,
166
                           allow_pickle_data=allow_pickle_data)
167
        return scheme
168
    else:
169
        parse_scheme_v_2_0(doc, scheme, error_handler=error_handler,
170
                           allow_pickle_data=allow_pickle_data)
171
        return scheme
172
173
174
def scheme_node_from_element(node_el, registry):
175
    """
176
    Create a SchemeNode from an `Element` instance.
177
    """
178
    try:
179
        widget_desc = registry.widget(node_el.get("qualified_name"))
180
    except KeyError as ex:
181
        raise UnknownWidgetDefinition(*ex.args)
182
183
    title = node_el.get("title")
184
    pos = node_el.get("position")
185
186
    if pos is not None:
187
        pos = tuple_eval(pos)
188
189
    return SchemeNode(widget_desc, title=title, position=pos)
190
191
192
def parse_scheme_v_2_0(etree, scheme, error_handler, widget_registry=None,
193
                       allow_pickle_data=False):
194
    """
195
    Parse an `ElementTree` instance.
196
    """
197
    if widget_registry is None:
198
        widget_registry = global_registry()
199
200
    nodes_not_found = []
201
202
    nodes = []
203
    links = []
204
205
    id_to_node = {}
206
207
    scheme_node = etree.getroot()
208
    scheme.title = scheme_node.attrib.get("title", "")
209
    scheme.description = scheme_node.attrib.get("description", "")
210
211
    # Load and create scheme nodes.
212
    for node_el in etree.findall("nodes/node"):
213
        try:
214
            node = scheme_node_from_element(node_el, widget_registry)
215
        except UnknownWidgetDefinition as ex:
216
            # description was not found
217
            error_handler(ex)
218
            node = None
219
        except Exception:
220
            raise
221
222
        if node is not None:
223
            nodes.append(node)
224
            id_to_node[node_el.get("id")] = node
225
        else:
226
            nodes_not_found.append(node_el.get("id"))
227
228
    # Load and create scheme links.
229
    for link_el in etree.findall("links/link"):
230
        source_id = link_el.get("source_node_id")
231
        sink_id = link_el.get("sink_node_id")
232
233
        if source_id in nodes_not_found or sink_id in nodes_not_found:
234
            continue
235
236
        source = id_to_node.get(source_id)
237
        sink = id_to_node.get(sink_id)
238
239
        source_channel = link_el.get("source_channel")
240
        sink_channel = link_el.get("sink_channel")
241
        enabled = link_el.get("enabled") == "true"
242
243
        try:
244
            link = SchemeLink(source, source_channel, sink, sink_channel,
245
                              enabled=enabled)
246
        except (ValueError, IncompatibleChannelTypeError) as ex:
247
            error_handler(ex)
248
        else:
249
            links.append(link)
250
251
    # Load node properties
252
    for property_el in etree.findall("node_properties/properties"):
253
        node_id = property_el.attrib.get("node_id")
254
255
        if node_id in nodes_not_found:
256
            continue
257
258
        node = id_to_node[node_id]
259
260
        format = property_el.attrib.get("format", "pickle")
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in format.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
261
262
        if "data" in property_el.attrib:
263
            # data string is 'encoded' with 'repr' i.e. unicode and
264
            # nonprintable characters are \u or \x escaped.
265
            # Could use 'codecs' module?
266
            data = string_eval(property_el.attrib.get("data"))
267
        else:
268
            data = property_el.text
269
270
        properties = None
271
        if format != "pickle" or allow_pickle_data:
272
            try:
273
                properties = loads(data, format)
274
            except Exception:
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...
275
                log.error("Could not load properties for %r.", node.title,
276
                          exc_info=True)
277
278
        if properties is not None:
279
            node.properties = properties
280
281
    annotations = []
282
    for annot_el in etree.findall("annotations/*"):
283
        if annot_el.tag == "text":
284
            rect = annot_el.attrib.get("rect", "(0, 0, 20, 20)")
285
            rect = tuple_eval(rect)
286
287
            font_family = annot_el.attrib.get("font-family", "").strip()
288
            font_size = annot_el.attrib.get("font-size", "").strip()
289
290
            font = {}
291
            if font_family:
292
                font["family"] = font_family
293
            if font_size:
294
                font["size"] = int(font_size)
295
296
            annot = SchemeTextAnnotation(rect, annot_el.text or "", font=font)
297
        elif annot_el.tag == "arrow":
298
            start = annot_el.attrib.get("start", "(0, 0)")
299
            end = annot_el.attrib.get("end", "(0, 0)")
300
            start, end = map(tuple_eval, (start, end))
301
302
            color = annot_el.attrib.get("fill", "red")
303
            annot = SchemeArrowAnnotation(start, end, color=color)
304
        annotations.append(annot)
305
306
    for node in nodes:
307
        scheme.add_node(node)
308
309
    for link in links:
310
        scheme.add_link(link)
311
312
    for annot in annotations:
313
        scheme.add_annotation(annot)
314
315
316
def parse_scheme_v_1_0(etree, scheme, error_handler, widget_registry=None,
317
                       allow_pickle_data=False):
318
    """
319
    ElementTree Instance of an old .ows scheme format.
320
    """
321
    if widget_registry is None:
322
        widget_registry = global_registry()
323
324
    widgets_not_found = []
325
326
    widgets = widget_registry.widgets()
327
    widgets_by_name = [(d.qualified_name.rsplit(".", 1)[-1], d)
328
                       for d in widgets]
329
    widgets_by_name = dict(widgets_by_name)
330
331
    nodes_by_caption = {}
332
    nodes = []
333
    links = []
334
    for widget_el in etree.findall("widgets/widget"):
335
        caption = widget_el.get("caption")
336
        name = widget_el.get("widgetName")
337
        x_pos = widget_el.get("xPos")
338
        y_pos = widget_el.get("yPos")
339
340
        if name in widgets_by_name:
341
            desc = widgets_by_name[name]
342
        else:
343
            error_handler(UnknownWidgetDefinition(name))
344
            widgets_not_found.append(caption)
345
            continue
346
347
        node = SchemeNode(desc, title=caption,
348
                          position=(int(x_pos), int(y_pos)))
349
        nodes_by_caption[caption] = node
350
        nodes.append(node)
351
352
    for channel_el in etree.findall("channels/channel"):
353
        in_caption = channel_el.get("inWidgetCaption")
354
        out_caption = channel_el.get("outWidgetCaption")
355
356
        if in_caption in widgets_not_found or \
357
                out_caption in widgets_not_found:
358
            continue
359
360
        source = nodes_by_caption[out_caption]
361
        sink = nodes_by_caption[in_caption]
362
        enabled = channel_el.get("enabled") == "1"
363
        signals = literal_eval(channel_el.get("signals"))
364
365
        for source_channel, sink_channel in signals:
366
            try:
367
                link = SchemeLink(source, source_channel, sink, sink_channel,
368
                                  enabled=enabled)
369
            except (ValueError, IncompatibleChannelTypeError) as ex:
370
                error_handler(ex)
371
            else:
372
                links.append(link)
373
374
    settings = etree.find("settings")
375
    properties = {}
376
    if settings is not None:
377
        data = settings.attrib.get("settingsDictionary", None)
378
        if data and allow_pickle_data:
379
            try:
380
                properties = literal_eval(data)
381
            except Exception:
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...
382
                log.error("Could not load properties for the scheme.",
383
                          exc_info=True)
384
385
    for node in nodes:
386
        if node.title in properties:
387
            try:
388
                node.properties = pickle.loads(properties[node.title])
389
            except Exception:
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...
390
                log.error("Could not unpickle properties for the node %r.",
391
                          node.title, exc_info=True)
392
393
        scheme.add_node(node)
394
395
    for link in links:
396
        scheme.add_link(link)
397
398
399
# Intermediate scheme representation
400
_scheme = namedtuple(
401
    "_scheme",
402
    ["title", "version", "description", "nodes", "links", "annotations"])
403
404
_node = namedtuple(
405
    "_node",
406
    ["id", "title", "name", "position", "project_name", "qualified_name",
407
     "version", "data"])
408
409
_data = namedtuple(
410
    "_data",
411
    ["format", "data"])
412
413
_link = namedtuple(
414
    "_link",
415
    ["id", "source_node_id", "sink_node_id", "source_channel", "sink_channel",
416
     "enabled"])
417
418
_annotation = namedtuple(
419
    "_annotation",
420
    ["id", "type", "params"])
421
422
_text_params = namedtuple(
423
    "_text_params",
424
    ["geometry", "text", "font"])
425
426
_arrow_params = namedtuple(
427
    "_arrow_params",
428
    ["geometry", "color"])
429
430
431
def parse_ows_etree_v_2_0(tree):
432
    scheme = tree.getroot()
433
    nodes, links, annotations = [], [], []
434
435
    # First collect all properties
436
    properties = {}
437
    for property in tree.findall("node_properties/properties"):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in property.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
438
        node_id = property.get("node_id")
439
        format = property.get("format")
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in format.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
440
        if "data" in property.attrib:
441
            data = property.get("data")
442
        else:
443
            data = property.text
444
        properties[node_id] = _data(format, data)
445
446
    # Collect all nodes
447
    for node in tree.findall("nodes/node"):
448
        node_id = node.get("id")
449
        node = _node(
450
            id=node_id,
451
            title=node.get("title"),
452
            name=node.get("name"),
453
            position=tuple_eval(node.get("position")),
454
            project_name=node.get("project_name"),
455
            qualified_name=node.get("qualified_name"),
456
            version=node.get("version", ""),
457
            data=properties.get(node_id, None)
458
        )
459
        nodes.append(node)
460
461
    for link in tree.findall("links/link"):
462
        params = _link(
463
            id=link.get("id"),
464
            source_node_id=link.get("source_node_id"),
465
            sink_node_id=link.get("sink_node_id"),
466
            source_channel=link.get("source_channel"),
467
            sink_channel=link.get("sink_channel"),
468
            enabled=link.get("enabled") == "true",
469
        )
470
        links.append(params)
471
472
    for annot in tree.findall("annotations/*"):
473
        if annot.tag == "text":
474
            rect = tuple_eval(annot.get("rect", "(0.0, 0.0, 20.0, 20.0)"))
475
476
            font_family = annot.get("font-family", "").strip()
477
            font_size = annot.get("font-size", "").strip()
478
479
            font = {}
480
            if font_family:
481
                font["family"] = font_family
482
            if font_size:
483
                font["size"] = int(font_size)
484
485
            annotation = _annotation(
486
                id=annot.get("id"),
487
                type="text",
488
                params=_text_params(rect, annot.text or "", font),
489
            )
490
        elif annot.tag == "arrow":
491
            start = tuple_eval(annot.get("start", "(0, 0)"))
492
            end = tuple_eval(annot.get("end", "(0, 0)"))
493
            color = annot.get("fill", "red")
494
            annotation = _annotation(
495
                id=annot.get("id"),
496
                type="arrow",
497
                params=_arrow_params((start, end), color)
498
            )
499
        annotations.append(annotation)
500
501
    return _scheme(
502
        version=scheme.get("version"),
503
        title=scheme.get("title", ""),
504
        description=scheme.get("description"),
505
        nodes=nodes,
506
        links=links,
507
        annotations=annotations
508
    )
509
510
511
def parse_ows_etree_v_1_0(tree):
512
    nodes, links = [], []
513
    id_gen = count()
514
515
    settings = tree.find("settings")
516
    properties = {}
517
    if settings is not None:
518
        data = settings.get("settingsDictionary", None)
519
        if data:
520
            try:
521
                properties = literal_eval(data)
522
            except Exception:
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...
523
                log.error("Could not decode properties data.",
524
                          exc_info=True)
525
526
    for widget in tree.findall("widgets/widget"):
527
        title = widget.get("caption")
528
        data = properties.get(title, None)
529
        node = _node(
530
            id=next(id_gen),
531
            title=widget.get("caption"),
532
            name=None,
533
            position=(float(widget.get("xPos")),
534
                      float(widget.get("yPos"))),
535
            project_name=None,
536
            qualified_name=widget.get("widgetName"),
537
            version="",
538
            data=_data("pickle", data)
539
        )
540
        nodes.append(node)
541
542
    nodes_by_title = dict((node.title, node) for node in nodes)
543
544
    for channel in tree.findall("channels/channel"):
545
        in_title = channel.get("inWidgetCaption")
546
        out_title = channel.get("outWidgetCaption")
547
548
        source = nodes_by_title[out_title]
549
        sink = nodes_by_title[in_title]
550
        enabled = channel.get("enabled") == "1"
551
        # repr list of (source_name, sink_name) tuples.
552
        signals = literal_eval(channel.get("signals"))
553
554
        for source_channel, sink_channel in signals:
555
            links.append(
556
                _link(id=next(id_gen),
557
                      source_node_id=source.id,
558
                      sink_node_id=sink.id,
559
                      source_channel=source_channel,
560
                      sink_channel=sink_channel,
561
                      enabled=enabled)
562
            )
563
    return _scheme(title="", description="", version="1.0",
564
                   nodes=nodes, links=links, annotations=[])
565
566
567
def parse_ows_stream(stream):
568
    doc = parse(stream)
569
    scheme_el = doc.getroot()
570
    version = scheme_el.get("version", None)
571
    if version is None:
572
        # Fallback: check for "widgets" tag.
573
        if scheme_el.find("widgets") is not None:
574
            version = "1.0"
575
        else:
576
            log.warning("<scheme> tag does not have a 'version' attribute")
577
            version = "2.0"
578
579
    if version == "1.0":
580
        return parse_ows_etree_v_1_0(doc)
581
    elif version == "2.0":
582
        return parse_ows_etree_v_2_0(doc)
583
    else:
584
        raise ValueError()
585
586
587
def resolve_1_0(scheme_desc, registry):
588
    widgets = registry.widgets()
589
    widgets_by_name = dict((d.qualified_name.rsplit(".", 1)[-1], d)
590
                           for d in widgets)
591
    nodes = scheme_desc.nodes
592
    for i, node in list(enumerate(nodes)):
593
        # 1.0's qualified name is the class name only, need to replace it
594
        # with the full qualified import name
595
        qname = node.qualified_name
596
        if qname in widgets_by_name:
597
            desc = widgets_by_name[qname]
598
            nodes[i] = node._replace(qualified_name=desc.qualified_name,
599
                                     project_name=desc.project_name)
600
601
    return scheme_desc._replace(nodes=nodes)
602
603
604
def resolve_replaced(scheme_desc, registry):
605
    widgets = registry.widgets()
606
    replacements = {}
607
    for desc in widgets:
608
        if desc.replaces:
609
            for repl_qname in desc.replaces:
610
                replacements[repl_qname] = desc.qualified_name
611
612
    nodes = scheme_desc.nodes
613
    for i, node in list(enumerate(nodes)):
614
        if not registry.has_widget(node.qualified_name) and \
615
                node.qualified_name in replacements:
616
            qname = replacements[node.qualified_name]
617
            desc = registry.widget(qname)
618
            nodes[i] = node._replace(qualified_name=desc.qualified_name,
619
                                     project_name=desc.project_name)
620
621
    return scheme_desc._replace(nodes=nodes)
622
623
624
def scheme_load(scheme, stream, registry=None, error_handler=None):
625
    desc = parse_ows_stream(stream)
626
627
    if registry is None:
628
        registry = global_registry()
629
630
    if error_handler is None:
631
        def error_handler(exc):
0 ignored issues
show
Bug introduced by
This function was already defined on line 624.
Loading history...
632
            raise exc
633
634
    if desc.version == "1.0":
635
        desc = resolve_1_0(desc, registry)
636
637
    desc = resolve_replaced(desc, registry)
638
    nodes_not_found = []
639
    nodes = []
640
    nodes_by_id = {}
641
    links = []
642
    annotations = []
643
644
    scheme.title = desc.title
645
    scheme.description = desc.description
646
647
    for node_d in desc.nodes:
648
        try:
649
            w_desc = registry.widget(node_d.qualified_name)
650
        except KeyError as ex:
651
            error_handler(UnknownWidgetDefinition(*ex.args))
652
            nodes_not_found.append(node_d.id)
653
        else:
654
            node = SchemeNode(
655
                w_desc, title=node_d.title, position=node_d.position)
656
            data = node_d.data
657
658
            if data:
659
                try:
660
                    properties = loads(data.data, data.format)
661
                except Exception:
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...
662
                    log.error("Could not load properties for %r.", node.title,
663
                              exc_info=True)
664
                else:
665
                    node.properties = properties
666
667
            nodes.append(node)
668
            nodes_by_id[node_d.id] = node
669
670
    for link_d in desc.links:
671
        source_id = link_d.source_node_id
672
        sink_id = link_d.sink_node_id
673
674
        if source_id in nodes_not_found or sink_id in nodes_not_found:
675
            continue
676
677
        source = nodes_by_id[source_id]
678
        sink = nodes_by_id[sink_id]
679
        try:
680
            link = SchemeLink(source, link_d.source_channel,
681
                              sink, link_d.sink_channel,
682
                              enabled=link_d.enabled)
683
        except (ValueError, IncompatibleChannelTypeError) as ex:
684
            error_handler(ex)
685
        else:
686
            links.append(link)
687
688
    for annot_d in desc.annotations:
689
        params = annot_d.params
690
        if annot_d.type == "text":
691
            annot = SchemeTextAnnotation(params.geometry, params.text,
692
                                         params.font)
693
        elif annot_d.type == "arrow":
694
            start, end = params.geometry
695
            annot = SchemeArrowAnnotation(start, end, params.color)
696
697
        else:
698
            log.warning("Ignoring unknown annotation type: %r", annot_d.type)
699
        annotations.append(annot)
700
701
    for node in nodes:
702
        scheme.add_node(node)
703
704
    for link in links:
705
        scheme.add_link(link)
706
707
    for annot in annotations:
708
        scheme.add_annotation(annot)
709
710
    return scheme
711
712
713
def inf_range(start=0, step=1):
714
    """Return an infinite range iterator.
715
    """
716
    while True:
717
        yield start
718
        start += step
719
720
721
def scheme_to_etree(scheme, data_format="literal", pickle_fallback=False):
722
    """
723
    Return an `xml.etree.ElementTree` representation of the `scheme.
724
    """
725
    builder = TreeBuilder(element_factory=Element)
726
    builder.start("scheme", {"version": "2.0",
727
                             "title": scheme.title or "",
728
                             "description": scheme.description or ""})
729
730
    ## Nodes
731
    node_ids = defaultdict(inf_range().__next__)
732
    builder.start("nodes", {})
733
    for node in scheme.nodes:
734
        desc = node.description
735
        attrs = {"id": str(node_ids[node]),
736
                 "name": desc.name,
737
                 "qualified_name": desc.qualified_name,
738
                 "project_name": desc.project_name or "",
739
                 "version": desc.version or "",
740
                 "title": node.title,
741
                 }
742
        if node.position is not None:
743
            attrs["position"] = str(node.position)
744
745
        if type(node) is not SchemeNode:
746
            attrs["scheme_node_type"] = "%s.%s" % (type(node).__name__,
747
                                                   type(node).__module__)
748
        builder.start("node", attrs)
749
        builder.end("node")
750
751
    builder.end("nodes")
752
753
    ## Links
754
    link_ids = defaultdict(inf_range().__next__)
755
    builder.start("links", {})
756
    for link in scheme.links:
757
        source = link.source_node
758
        sink = link.sink_node
759
        source_id = node_ids[source]
760
        sink_id = node_ids[sink]
761
        attrs = {"id": str(link_ids[link]),
762
                 "source_node_id": str(source_id),
763
                 "sink_node_id": str(sink_id),
764
                 "source_channel": link.source_channel.name,
765
                 "sink_channel": link.sink_channel.name,
766
                 "enabled": "true" if link.enabled else "false",
767
                 }
768
        builder.start("link", attrs)
769
        builder.end("link")
770
771
    builder.end("links")
772
773
    ## Annotations
774
    annotation_ids = defaultdict(inf_range().__next__)
775
    builder.start("annotations", {})
776
    for annotation in scheme.annotations:
777
        annot_id = annotation_ids[annotation]
778
        attrs = {"id": str(annot_id)}
779
        data = None
780
        if isinstance(annotation, SchemeTextAnnotation):
781
            tag = "text"
782
            attrs.update({"rect": repr(annotation.rect)})
783
784
            # Save the font attributes
785
            font = annotation.font
786
            attrs.update({"font-family": font.get("family", None),
787
                          "font-size": font.get("size", None)})
788
            attrs = [(key, value) for key, value in attrs.items()
789
                     if value is not None]
790
            attrs = dict((key, str(value)) for key, value in attrs)
791
792
            data = annotation.text
793
794
        elif isinstance(annotation, SchemeArrowAnnotation):
795
            tag = "arrow"
796
            attrs.update({"start": repr(annotation.start_pos),
797
                          "end": repr(annotation.end_pos)})
798
799
            # Save the arrow color
800
            try:
801
                color = annotation.color
802
                attrs.update({"fill": color})
803
            except AttributeError:
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
804
                pass
805
806
            data = None
807
        else:
808
            log.warning("Can't save %r", annotation)
809
            continue
810
        builder.start(tag, attrs)
811
        if data is not None:
812
            builder.data(data)
813
        builder.end(tag)
814
815
    builder.end("annotations")
816
817
    builder.start("thumbnail", {})
818
    builder.end("thumbnail")
819
820
    # Node properties/settings
821
    builder.start("node_properties", {})
822
    for node in scheme.nodes:
823
        data = None
824
        if node.properties:
825
            try:
826
                data, format = dumps(node.properties, format=data_format,
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in format.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
827
                                     pickle_fallback=pickle_fallback)
828
            except Exception:
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...
829
                log.error("Error serializing properties for node %r",
830
                          node.title, exc_info=True)
831
            if data is not None:
832
                builder.start("properties",
833
                              {"node_id": str(node_ids[node]),
834
                               "format": format})
835
                builder.data(data)
836
                builder.end("properties")
837
838
    builder.end("node_properties")
839
    builder.end("scheme")
840
    root = builder.close()
841
    tree = ElementTree(root)
842
    return tree
843
844
845
def scheme_to_ows_stream(scheme, stream, pretty=False, pickle_fallback=False):
846
    """
847
    Write scheme to a a stream in Orange Scheme .ows (v 2.0) format.
848
849
    Parameters
850
    ----------
851
    scheme : :class:`.Scheme`
852
        A :class:`.Scheme` instance to serialize.
853
    stream : file-like object
854
        A file-like object opened for writing.
855
    pretty : bool, optional
856
        If `True` the output xml will be pretty printed (indented).
857
    pickle_fallback : bool, optional
858
        If `True` allow scheme node properties to be saves using pickle
859
        protocol if properties cannot be saved using the default
860
        notation.
861
862
    """
863
    tree = scheme_to_etree(scheme, data_format="literal",
864
                           pickle_fallback=pickle_fallback)
865
866
    if pretty:
867
        indent(tree.getroot(), 0)
868
869
    if sys.version_info < (2, 7):
870
        # in Python 2.6 the write does not have xml_declaration parameter.
871
        tree.write(stream, encoding="utf-8")
872
    else:
873
        tree.write(stream, encoding="utf-8", xml_declaration=True)
874
875
876
def indent(element, level=0, indent="\t"):
0 ignored issues
show
Comprehensibility Bug introduced by
indent is re-defining a name which is already available in the outer-scope (previously defined on line 876).

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...
877
    """
878
    Indent an instance of a :class:`Element`. Based on
879
    (http://effbot.org/zone/element-lib.htm#prettyprint).
880
881
    """
882
    def empty(text):
883
        return not text or not text.strip()
884
885
    def indent_(element, level, last):
886
        child_count = len(element)
887
888
        if child_count:
889
            if empty(element.text):
890
                element.text = "\n" + indent * (level + 1)
891
892
            if empty(element.tail):
893
                element.tail = "\n" + indent * (level + (-1 if last else 0))
894
895
            for i, child in enumerate(element):
896
                indent_(child, level + 1, i == child_count - 1)
897
898
        else:
899
            if empty(element.tail):
900
                element.tail = "\n" + indent * (level + (-1 if last else 0))
901
902
    return indent_(element, level, True)
903
904
905
def dumps(obj, format="literal", prettyprint=False, pickle_fallback=False):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in format.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
906
    """
907
    Serialize `obj` using `format` ('json' or 'literal') and return its
908
    string representation and the used serialization format ('literal',
909
    'json' or 'pickle').
910
911
    If `pickle_fallback` is True and the serialization with `format`
912
    fails object's pickle representation will be returned
913
914
    """
915
    if format == "literal":
916
        try:
917
            return (literal_dumps(obj, prettyprint=prettyprint, indent=1),
918
                    "literal")
919
        except (ValueError, TypeError) as ex:
0 ignored issues
show
Unused Code introduced by
The variable ex seems to be unused.
Loading history...
920
            if not pickle_fallback:
921
                raise
922
923
            log.warning("Could not serialize to a literal string",
924
                        exc_info=True)
925
926
    elif format == "json":
927
        try:
928
            return (json.dumps(obj, indent=1 if prettyprint else None),
929
                    "json")
930
        except (ValueError, TypeError):
931
            if not pickle_fallback:
932
                raise
933
934
            log.warning("Could not serialize to a json string",
935
                        exc_info=True)
936
937
    elif format == "pickle":
938
        return base64.encodebytes(pickle.dumps(obj)).decode('ascii'), "pickle"
939
940
    else:
941
        raise ValueError("Unsupported format %r" % format)
942
943
    if pickle_fallback:
944
        log.warning("Using pickle fallback")
945
        return base64.encodebytes(pickle.dumps(obj)).decode('ascii'), "pickle"
946
    else:
947
        raise Exception("Something strange happened.")
948
949
950
def loads(string, format):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in format.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
951
    if format == "literal":
952
        return literal_eval(string)
953
    elif format == "json":
954
        return json.loads(string)
955
    elif format == "pickle":
956
        return pickle.loads(base64.decodebytes(string.encode('ascii')))
957
    else:
958
        raise ValueError("Unknown format")
959
960
961
# This is a subset of PyON serialization.
962
def literal_dumps(obj, prettyprint=False, indent=4):
0 ignored issues
show
Comprehensibility Bug introduced by
indent is re-defining a name which is already available in the outer-scope (previously defined on line 876).

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...
963
    """
964
    Write obj into a string as a python literal.
965
    """
966
    memo = {}
967
    NoneType = type(None)
968
969
    def check(obj):
970
        if type(obj) in [int, int, float, bool, NoneType, str, str]:
971
            return True
972
973
        if id(obj) in memo:
974
            raise ValueError("{0} is a recursive structure".format(obj))
975
976
        memo[id(obj)] = obj
977
978
        if type(obj) in [list, tuple]:
979
            return all(map(check, obj))
980
        elif type(obj) is dict:
981
            return all(map(check, chain(iter(obj.keys()), iter(obj.values()))))
982
        else:
983
            raise TypeError("{0} can not be serialized as a python "
984
                             "literal".format(type(obj)))
985
986
    check(obj)
987
988
    if prettyprint:
989
        return pprint.pformat(obj, indent=indent)
990
    else:
991
        return repr(obj)
992
993
994
literal_loads = literal_eval
995