source: orange/Orange/OrangeCanvas/scheme/readwrite.py @ 11683:7b9dcf8abcc4

Revision 11683:7b9dcf8abcc4, 29.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 8 months ago (diff)

Refactored scheme parsing.

Using widget description's "replaces" list to resolve widgets.

Line 
1"""
2Scheme save/load routines.
3
4"""
5import sys
6import warnings
7
8from xml.etree.ElementTree import TreeBuilder, Element, ElementTree, parse
9
10from collections import defaultdict, namedtuple
11from itertools import chain, count
12
13import cPickle as pickle
14import json
15import pprint
16
17import ast
18from ast import literal_eval
19
20import logging
21
22from . import SchemeNode, SchemeLink
23from .annotations import SchemeTextAnnotation, SchemeArrowAnnotation
24from .errors import IncompatibleChannelTypeError
25
26from ..registry import global_registry
27
28log = logging.getLogger(__name__)
29
30
31class UnknownWidgetDefinition(Exception):
32    pass
33
34
35def string_eval(source):
36    """
37    Evaluate a python string literal `source`. Raise ValueError if
38    `source` is not a string literal.
39
40    >>> string_eval("'a string'")
41    a string
42
43    """
44    node = ast.parse(source, "<source>", mode="eval")
45    if not isinstance(node.body, ast.Str):
46        raise ValueError("%r is not a string literal" % source)
47    return node.body.s
48
49
50def tuple_eval(source):
51    """
52    Evaluate a python tuple literal `source` where the elements are
53    constrained to be int, float or string. Raise ValueError if not
54    a tuple literal.
55
56    >>> tuple_eval("(1, 2, "3")")
57    (1, 2, '3')
58
59    """
60    node = ast.parse(source, "<source>", mode="eval")
61
62    if not isinstance(node.body, ast.Tuple):
63        raise ValueError("%r is not a tuple literal" % source)
64
65    if not all(isinstance(el, (ast.Str, ast.Num))
66               for el in node.body.elts):
67        raise ValueError("Can only contain numbers or strings")
68
69    return literal_eval(source)
70
71
72def terminal_eval(source):
73    """
74    Evaluate a python 'constant' (string, number, None, True, False)
75    `source`. Raise ValueError is not a terminal literal.
76
77    >>> terminal_eval("True")
78    True
79
80    """
81    node = ast.parse(source, "<source>", mode="eval")
82
83    try:
84        return _terminal_value(node.body)
85    except ValueError:
86        raise
87        raise ValueError("%r is not a terminal constant" % source)
88
89
90def _terminal_value(node):
91    if isinstance(node, ast.Str):
92        return node.s
93    elif isinstance(node, ast.Num):
94        return node.n
95    elif isinstance(node, ast.Name) and \
96            node.id in ["True", "False", "None"]:
97        return __builtins__[node.id]
98
99    raise ValueError("Not a terminal")
100
101
102def sniff_version(stream):
103    """
104    Parse a scheme stream and return the scheme's serialization
105    version string.
106
107    """
108    doc = parse(stream)
109    scheme_el = doc.getroot()
110    version = scheme_el.attrib.get("version", None)
111    # Fallback: check for "widgets" tag.
112    if scheme_el.find("widgets") is not None:
113        version = "1.0"
114    else:
115        version = "2.0"
116
117    return version
118
119
120def parse_scheme(scheme, stream, error_handler=None,
121                 allow_pickle_data=False):
122    """
123    Parse a saved scheme from `stream` and populate a `scheme`
124    instance (:class:`Scheme`).
125    `error_handler` if given will be called with an exception when
126    a 'recoverable' error occurs. By default the exception is simply
127    raised.
128
129    Parameters
130    ----------
131    scheme : :class:`.Scheme`
132        A scheme instance to populate with the contents of `stream`.
133    stream : file-like object
134        A file like object opened for reading.
135    error_hander : function, optional
136        A function to call with an exception instance when a `recoverable`
137        error occurs.
138    allow_picked_data : bool, optional
139        Specifically allow parsing of picked data streams.
140
141    """
142    warnings.warn("Use 'scheme_load' instead", DeprecationWarning,
143                  stacklevel=2)
144
145    doc = parse(stream)
146    scheme_el = doc.getroot()
147    version = scheme_el.attrib.get("version", None)
148    if version is None:
149        # Fallback: check for "widgets" tag.
150        if scheme_el.find("widgets") is not None:
151            version = "1.0"
152        else:
153            version = "2.0"
154
155    if error_handler is None:
156        def error_handler(exc):
157            raise exc
158
159    if version == "1.0":
160        parse_scheme_v_1_0(doc, scheme, error_handler=error_handler,
161                           allow_pickle_data=allow_pickle_data)
162        return scheme
163    else:
164        parse_scheme_v_2_0(doc, scheme, error_handler=error_handler,
165                           allow_pickle_data=allow_pickle_data)
166        return scheme
167
168
169def scheme_node_from_element(node_el, registry):
170    """
171    Create a SchemeNode from an `Element` instance.
172    """
173    try:
174        widget_desc = registry.widget(node_el.get("qualified_name"))
175    except KeyError as ex:
176        raise UnknownWidgetDefinition(*ex.args)
177
178    title = node_el.get("title")
179    pos = node_el.get("position")
180
181    if pos is not None:
182        pos = tuple_eval(pos)
183
184    return SchemeNode(widget_desc, title=title, position=pos)
185
186
187def parse_scheme_v_2_0(etree, scheme, error_handler, widget_registry=None,
188                       allow_pickle_data=False):
189    """
190    Parse an `ElementTree` instance.
191    """
192    if widget_registry is None:
193        widget_registry = global_registry()
194
195    nodes_not_found = []
196
197    nodes = []
198    links = []
199
200    id_to_node = {}
201
202    scheme_node = etree.getroot()
203    scheme.title = scheme_node.attrib.get("title", "")
204    scheme.description = scheme_node.attrib.get("description", "")
205
206    # Load and create scheme nodes.
207    for node_el in etree.findall("nodes/node"):
208        try:
209            node = scheme_node_from_element(node_el, widget_registry)
210        except UnknownWidgetDefinition, ex:
211            # description was not found
212            error_handler(ex)
213            node = None
214        except Exception:
215            raise
216
217        if node is not None:
218            nodes.append(node)
219            id_to_node[node_el.get("id")] = node
220        else:
221            nodes_not_found.append(node_el.get("id"))
222
223    # Load and create scheme links.
224    for link_el in etree.findall("links/link"):
225        source_id = link_el.get("source_node_id")
226        sink_id = link_el.get("sink_node_id")
227
228        if source_id in nodes_not_found or sink_id in nodes_not_found:
229            continue
230
231        source = id_to_node.get(source_id)
232        sink = id_to_node.get(sink_id)
233
234        source_channel = link_el.get("source_channel")
235        sink_channel = link_el.get("sink_channel")
236        enabled = link_el.get("enabled") == "true"
237
238        try:
239            link = SchemeLink(source, source_channel, sink, sink_channel,
240                              enabled=enabled)
241        except (ValueError, IncompatibleChannelTypeError) as ex:
242            error_handler(ex)
243        else:
244            links.append(link)
245
246    # Load node properties
247    for property_el in etree.findall("node_properties/properties"):
248        node_id = property_el.attrib.get("node_id")
249
250        if node_id in nodes_not_found:
251            continue
252
253        node = id_to_node[node_id]
254
255        format = property_el.attrib.get("format", "pickle")
256
257        if "data" in property_el.attrib:
258            # data string is 'encoded' with 'repr' i.e. unicode and
259            # nonprintable characters are \u or \x escaped.
260            # Could use 'codecs' module?
261            data = string_eval(property_el.attrib.get("data"))
262        else:
263            data = property_el.text
264
265        properties = None
266        if format != "pickle" or allow_pickle_data:
267            try:
268                properties = loads(data, format)
269            except Exception:
270                log.error("Could not load properties for %r.", node.title,
271                          exc_info=True)
272
273        if properties is not None:
274            node.properties = properties
275
276    annotations = []
277    for annot_el in etree.findall("annotations/*"):
278        if annot_el.tag == "text":
279            rect = annot_el.attrib.get("rect", "(0, 0, 20, 20)")
280            rect = tuple_eval(rect)
281
282            font_family = annot_el.attrib.get("font-family", "").strip()
283            font_size = annot_el.attrib.get("font-size", "").strip()
284
285            font = {}
286            if font_family:
287                font["family"] = font_family
288            if font_size:
289                font["size"] = int(font_size)
290
291            annot = SchemeTextAnnotation(rect, annot_el.text or "", font=font)
292        elif annot_el.tag == "arrow":
293            start = annot_el.attrib.get("start", "(0, 0)")
294            end = annot_el.attrib.get("end", "(0, 0)")
295            start, end = map(tuple_eval, (start, end))
296
297            color = annot_el.attrib.get("fill", "red")
298            annot = SchemeArrowAnnotation(start, end, color=color)
299        annotations.append(annot)
300
301    for node in nodes:
302        scheme.add_node(node)
303
304    for link in links:
305        scheme.add_link(link)
306
307    for annot in annotations:
308        scheme.add_annotation(annot)
309
310
311def parse_scheme_v_1_0(etree, scheme, error_handler, widget_registry=None,
312                       allow_pickle_data=False):
313    """
314    ElementTree Instance of an old .ows scheme format.
315    """
316    if widget_registry is None:
317        widget_registry = global_registry()
318
319    widgets_not_found = []
320
321    widgets = widget_registry.widgets()
322    widgets_by_name = [(d.qualified_name.rsplit(".", 1)[-1], d)
323                       for d in widgets]
324    widgets_by_name = dict(widgets_by_name)
325
326    nodes_by_caption = {}
327    nodes = []
328    links = []
329    for widget_el in etree.findall("widgets/widget"):
330        caption = widget_el.get("caption")
331        name = widget_el.get("widgetName")
332        x_pos = widget_el.get("xPos")
333        y_pos = widget_el.get("yPos")
334
335        if name in widgets_by_name:
336            desc = widgets_by_name[name]
337        else:
338            error_handler(UnknownWidgetDefinition(name))
339            widgets_not_found.append(caption)
340            continue
341
342        node = SchemeNode(desc, title=caption,
343                          position=(int(x_pos), int(y_pos)))
344        nodes_by_caption[caption] = node
345        nodes.append(node)
346
347    for channel_el in etree.findall("channels/channel"):
348        in_caption = channel_el.get("inWidgetCaption")
349        out_caption = channel_el.get("outWidgetCaption")
350
351        if in_caption in widgets_not_found or \
352                out_caption in widgets_not_found:
353            continue
354
355        source = nodes_by_caption[out_caption]
356        sink = nodes_by_caption[in_caption]
357        enabled = channel_el.get("enabled") == "1"
358        signals = literal_eval(channel_el.get("signals"))
359
360        for source_channel, sink_channel in signals:
361            try:
362                link = SchemeLink(source, source_channel, sink, sink_channel,
363                                  enabled=enabled)
364            except (ValueError, IncompatibleChannelTypeError) as ex:
365                error_handler(ex)
366            else:
367                links.append(link)
368
369    settings = etree.find("settings")
370    properties = {}
371    if settings is not None:
372        data = settings.attrib.get("settingsDictionary", None)
373        if data and allow_pickle_data:
374            try:
375                properties = literal_eval(data)
376            except Exception:
377                log.error("Could not load properties for the scheme.",
378                          exc_info=True)
379
380    for node in nodes:
381        if node.title in properties:
382            try:
383                node.properties = pickle.loads(properties[node.title])
384            except Exception:
385                log.error("Could not unpickle properties for the node %r.",
386                          node.title, exc_info=True)
387
388        scheme.add_node(node)
389
390    for link in links:
391        scheme.add_link(link)
392
393
394# Intermediate scheme representation
395_scheme = namedtuple(
396    "_scheme",
397    ["title", "version", "description", "nodes", "links", "annotations"])
398
399_node = namedtuple(
400    "_node",
401    ["id", "title", "name", "position", "project_name", "qualified_name",
402     "version", "data"])
403
404_data = namedtuple(
405    "_data",
406    ["format", "data"])
407
408_link = namedtuple(
409    "_link",
410    ["id", "source_node_id", "sink_node_id", "source_channel", "sink_channel",
411     "enabled"])
412
413_annotation = namedtuple(
414    "_annotation",
415    ["id", "type", "params"])
416
417_text_params = namedtuple(
418    "_text_params",
419    ["geometry", "text", "font"])
420
421_arrow_params = namedtuple(
422    "_arrow_params",
423    ["geometry", "color"])
424
425
426def parse_ows_etree_v_2_0(tree):
427    scheme = tree.getroot()
428    nodes, links, annotations = [], [], []
429
430    # First collect all properties
431    properties = {}
432    for property in tree.findall("node_properties/properties"):
433        node_id = property.get("node_id")
434        format = property.get("format")
435        if "data" in property.attrib:
436            data = property.get("data")
437        else:
438            data = property.text
439        properties[node_id] = _data(format, data)
440
441    # Collect all nodes
442    for node in tree.findall("nodes/node"):
443        node_id = node.get("id")
444        node = _node(
445            id=node_id,
446            title=node.get("title"),
447            name=node.get("name"),
448            position=tuple_eval(node.get("position")),
449            project_name=node.get("project_name"),
450            qualified_name=node.get("qualified_name"),
451            version=node.get("version", ""),
452            data=properties.get(node_id, None)
453        )
454        nodes.append(node)
455
456    for link in tree.findall("links/link"):
457        params = _link(
458            id=link.get("id"),
459            source_node_id=link.get("source_node_id"),
460            sink_node_id=link.get("sink_node_id"),
461            source_channel=link.get("source_channel"),
462            sink_channel=link.get("sink_channel"),
463            enabled=link.get("enabled") == "true",
464        )
465        links.append(params)
466
467    for annot in tree.findall("annotations/*"):
468        if annot.tag == "text":
469            rect = tuple_eval(annot.get("rect", "(0.0, 0.0, 20.0, 20.0)"))
470
471            font_family = annot.get("font-family", "").strip()
472            font_size = annot.get("font-size", "").strip()
473
474            font = {}
475            if font_family:
476                font["family"] = font_family
477            if font_size:
478                font["size"] = int(font_size)
479
480            annotation = _annotation(
481                id=annot.get("id"),
482                type="text",
483                params=_text_params(rect, annot.text or "", font),
484            )
485        elif annot.tag == "arrow":
486            start = tuple_eval(annot.get("start", "(0, 0)"))
487            end = tuple_eval(annot.get("end", "(0, 0)"))
488            color = annot.get("fill", "red")
489            annotation = _annotation(
490                id=annot.get("id"),
491                type="arrow",
492                params=_arrow_params((start, end), color)
493            )
494        annotations.append(annotation)
495
496    return _scheme(
497        version=scheme.get("version"),
498        title=scheme.get("title", ""),
499        description=scheme.get("description"),
500        nodes=nodes,
501        links=links,
502        annotations=annotations
503    )
504
505
506def parse_ows_etree_v_1_0(tree):
507    nodes, links = [], []
508    id_gen = count()
509
510    settings = tree.find("settings")
511    properties = {}
512    if settings is not None:
513        data = settings.get("settingsDictionary", None)
514        if data:
515            try:
516                properties = literal_eval(data)
517            except Exception:
518                log.error("Could not decode properties data.",
519                          exc_info=True)
520
521    for widget in tree.findall("widgets/widget"):
522        title = widget.get("caption")
523        data = properties.get(title, None)
524        node = _node(
525            id=next(id_gen),
526            title=widget.get("caption"),
527            name=None,
528            position=(float(widget.get("xPos")),
529                      float(widget.get("yPos"))),
530            project_name=None,
531            qualified_name=widget.get("widgetName"),
532            version="",
533            data=_data("pickle", data)
534        )
535        nodes.append(node)
536
537    nodes_by_title = dict((node.title, node) for node in nodes)
538
539    for channel in tree.findall("channels/channel"):
540        in_title = channel.get("inWidgetCaption")
541        out_title = channel.get("outWidgetCaption")
542
543        source = nodes_by_title[out_title]
544        sink = nodes_by_title[in_title]
545        enabled = channel.get("enabled") == "1"
546        # repr list of (source_name, sink_name) tuples.
547        signals = literal_eval(channel.get("signals"))
548
549        for source_channel, sink_channel in signals:
550            links.append(
551                _link(id=next(id_gen),
552                      source_node_id=source.id,
553                      sink_node_id=sink.id,
554                      source_channel=source_channel,
555                      sink_channel=sink_channel,
556                      enabled=enabled)
557            )
558    return _scheme(title="", description="", version="1.0",
559                   nodes=nodes, links=links, annotations=[])
560
561
562def parse_ows_stream(stream):
563    doc = parse(stream)
564    scheme_el = doc.getroot()
565    version = scheme_el.get("version", None)
566    if version is None:
567        # Fallback: check for "widgets" tag.
568        if scheme_el.find("widgets") is not None:
569            version = "1.0"
570        else:
571            log.warning("<scheme> tag does not have a 'version' attribute")
572            version = "2.0"
573
574    if version == "1.0":
575        return parse_ows_etree_v_1_0(doc)
576    elif version == "2.0":
577        return parse_ows_etree_v_2_0(doc)
578    else:
579        raise ValueError()
580
581
582def resolve_1_0(scheme_desc, registry):
583    widgets = registry.widgets()
584    widgets_by_name = dict((d.qualified_name.rsplit(".", 1)[-1], d)
585                           for d in widgets)
586    nodes = scheme_desc.nodes
587    for i, node in list(enumerate(nodes)):
588        # 1.0's qualified name is the class name only, need to replace it
589        # with the full qualified import name
590        qname = node.qualified_name
591        if qname in widgets_by_name:
592            desc = widgets_by_name[qname]
593            nodes[i] = node._replace(qualified_name=desc.qualified_name,
594                                     project_name=desc.project_name)
595
596    return scheme_desc._replace(nodes=nodes)
597
598
599def resolve_replaced(scheme_desc, registry):
600    widgets = registry.widgets()
601    replacements = {}
602    for desc in widgets:
603        if desc.replaces:
604            for repl_qname in desc.replaces:
605                replacements[repl_qname] = desc.qualified_name
606
607    nodes = scheme_desc.nodes
608    for i, node in list(enumerate(nodes)):
609        if not registry.has_widget(node.qualified_name) and \
610                node.qualified_name in replacements:
611            qname = replacements[node.qualified_name]
612            desc = registry.widget(qname)
613            nodes[i] = node._replace(qualified_name=desc.qualified_name,
614                                     project_name=desc.project_name)
615
616    return scheme_desc._replace(nodes=nodes)
617
618
619def scheme_load(scheme, stream, registry=None, error_handler=None):
620    desc = parse_ows_stream(stream)
621
622    if registry is None:
623        registry = global_registry()
624
625    if error_handler is None:
626        def error_handler(exc):
627            raise exc
628
629    if desc.version == "1.0":
630        desc = resolve_1_0(desc, registry, error_handler)
631
632    desc = resolve_replaced(desc, registry)
633    nodes_not_found = []
634    nodes = []
635    nodes_by_id = {}
636    links = []
637    annotations = []
638
639    scheme.title = desc.title
640    scheme.description = desc.description
641
642    for node_d in desc.nodes:
643        try:
644            w_desc = registry.widget(node_d.qualified_name)
645        except KeyError as ex:
646            error_handler(UnknownWidgetDefinition(*ex.args))
647            nodes_not_found.append(node_d.id)
648        else:
649            node = SchemeNode(
650                w_desc, title=node_d.title, position=node_d.position)
651            data = node_d.data
652
653            if data:
654                try:
655                    properties = loads(data.data, data.format)
656                except Exception:
657                    log.error("Could not load properties for %r.", node.title,
658                              exc_info=True)
659                else:
660                    node.properties = properties
661
662            nodes.append(node)
663            nodes_by_id[node_d.id] = node
664
665    for link_d in desc.links:
666        source_id = link_d.source_node_id
667        sink_id = link_d.sink_node_id
668
669        if source_id in nodes_not_found or sink_id in nodes_not_found:
670            continue
671
672        source = nodes_by_id[source_id]
673        sink = nodes_by_id[sink_id]
674        try:
675            link = SchemeLink(source, link_d.source_channel,
676                              sink, link_d.sink_channel,
677                              enabled=link_d.enabled)
678        except (ValueError, IncompatibleChannelTypeError) as ex:
679            error_handler(ex)
680        else:
681            links.append(link)
682
683    for annot_d in desc.annotations:
684        params = annot_d.params
685        if annot_d.type == "text":
686            annot = SchemeTextAnnotation(params.geometry, params.text,
687                                         params.font)
688        elif annot_d.type == "arrow":
689            start, end = params.geometry
690            annot = SchemeArrowAnnotation(start, end, params.color)
691
692        else:
693            log.warning("Ignoring unknown annotation type: %r", annot_d.type)
694        annotations.append(annot)
695
696    for node in nodes:
697        scheme.add_node(node)
698
699    for link in links:
700        scheme.add_link(link)
701
702    for annot in annotations:
703        scheme.add_annotation(annot)
704
705    return scheme
706
707
708def inf_range(start=0, step=1):
709    """Return an infinite range iterator.
710    """
711    while True:
712        yield start
713        start += step
714
715
716def scheme_to_etree(scheme, data_format="literal", pickle_fallback=False):
717    """
718    Return an `xml.etree.ElementTree` representation of the `scheme.
719    """
720    builder = TreeBuilder(element_factory=Element)
721    builder.start("scheme", {"version": "2.0",
722                             "title": scheme.title or "",
723                             "description": scheme.description or ""})
724
725    ## Nodes
726    node_ids = defaultdict(inf_range().next)
727    builder.start("nodes", {})
728    for node in scheme.nodes:
729        desc = node.description
730        attrs = {"id": str(node_ids[node]),
731                 "name": desc.name,
732                 "qualified_name": desc.qualified_name,
733                 "project_name": desc.project_name or "",
734                 "version": desc.version or "",
735                 "title": node.title,
736                 }
737        if node.position is not None:
738            attrs["position"] = str(node.position)
739
740        if type(node) is not SchemeNode:
741            attrs["scheme_node_type"] = "%s.%s" % (type(node).__name__,
742                                                   type(node).__module__)
743        builder.start("node", attrs)
744        builder.end("node")
745
746    builder.end("nodes")
747
748    ## Links
749    link_ids = defaultdict(inf_range().next)
750    builder.start("links", {})
751    for link in scheme.links:
752        source = link.source_node
753        sink = link.sink_node
754        source_id = node_ids[source]
755        sink_id = node_ids[sink]
756        attrs = {"id": str(link_ids[link]),
757                 "source_node_id": str(source_id),
758                 "sink_node_id": str(sink_id),
759                 "source_channel": link.source_channel.name,
760                 "sink_channel": link.sink_channel.name,
761                 "enabled": "true" if link.enabled else "false",
762                 }
763        builder.start("link", attrs)
764        builder.end("link")
765
766    builder.end("links")
767
768    ## Annotations
769    annotation_ids = defaultdict(inf_range().next)
770    builder.start("annotations", {})
771    for annotation in scheme.annotations:
772        annot_id = annotation_ids[annotation]
773        attrs = {"id": str(annot_id)}
774        data = None
775        if isinstance(annotation, SchemeTextAnnotation):
776            tag = "text"
777            attrs.update({"rect": repr(annotation.rect)})
778
779            # Save the font attributes
780            font = annotation.font
781            attrs.update({"font-family": font.get("family", None),
782                          "font-size": font.get("size", None)})
783            attrs = [(key, value) for key, value in attrs.items() \
784                     if value is not None]
785            attrs = dict((key, unicode(value)) for key, value in attrs)
786
787            data = annotation.text
788
789        elif isinstance(annotation, SchemeArrowAnnotation):
790            tag = "arrow"
791            attrs.update({"start": repr(annotation.start_pos),
792                          "end": repr(annotation.end_pos)})
793
794            # Save the arrow color
795            try:
796                color = annotation.color
797                attrs.update({"fill": color})
798            except AttributeError:
799                pass
800
801            data = None
802        else:
803            log.warning("Can't save %r", annotation)
804            continue
805        builder.start(tag, attrs)
806        if data is not None:
807            builder.data(data)
808        builder.end(tag)
809
810    builder.end("annotations")
811
812    builder.start("thumbnail", {})
813    builder.end("thumbnail")
814
815    # Node properties/settings
816    builder.start("node_properties", {})
817    for node in scheme.nodes:
818        data = None
819        if node.properties:
820            try:
821                data, format = dumps(node.properties, format=data_format,
822                                     pickle_fallback=pickle_fallback)
823            except Exception:
824                log.error("Error serializing properties for node %r",
825                          node.title, exc_info=True)
826            if data is not None:
827                builder.start("properties",
828                              {"node_id": str(node_ids[node]),
829                               "format": format})
830                builder.data(data)
831                builder.end("properties")
832
833    builder.end("node_properties")
834    builder.end("scheme")
835    root = builder.close()
836    tree = ElementTree(root)
837    return tree
838
839
840def scheme_to_ows_stream(scheme, stream, pretty=False, pickle_fallback=False):
841    """
842    Write scheme to a a stream in Orange Scheme .ows (v 2.0) format.
843
844    Parameters
845    ----------
846    scheme : :class:`.Scheme`
847        A :class:`.Scheme` instance to serialize.
848    stream : file-like object
849        A file-like object opened for writing.
850    pretty : bool, optional
851        If `True` the output xml will be pretty printed (indented).
852    pickle_fallback : bool, optional
853        If `True` allow scheme node properties to be saves using pickle
854        protocol if properties cannot be saved using the default
855        notation.
856
857    """
858    tree = scheme_to_etree(scheme, data_format="literal",
859                           pickle_fallback=pickle_fallback)
860
861    if pretty:
862        indent(tree.getroot(), 0)
863
864    if sys.version_info < (2, 7):
865        # in Python 2.6 the write does not have xml_declaration parameter.
866        tree.write(stream, encoding="utf-8")
867    else:
868        tree.write(stream, encoding="utf-8", xml_declaration=True)
869
870
871def indent(element, level=0, indent="\t"):
872    """
873    Indent an instance of a :class:`Element`. Based on
874    (http://effbot.org/zone/element-lib.htm#prettyprint).
875
876    """
877    def empty(text):
878        return not text or not text.strip()
879
880    def indent_(element, level, last):
881        child_count = len(element)
882
883        if child_count:
884            if empty(element.text):
885                element.text = "\n" + indent * (level + 1)
886
887            if empty(element.tail):
888                element.tail = "\n" + indent * (level + (-1 if last else 0))
889
890            for i, child in enumerate(element):
891                indent_(child, level + 1, i == child_count - 1)
892
893        else:
894            if empty(element.tail):
895                element.tail = "\n" + indent * (level + (-1 if last else 0))
896
897    return indent_(element, level, True)
898
899
900def dumps(obj, format="literal", prettyprint=False, pickle_fallback=False):
901    """
902    Serialize `obj` using `format` ('json' or 'literal') and return its
903    string representation and the used serialization format ('literal',
904    'json' or 'pickle').
905
906    If `pickle_fallback` is True and the serialization with `format`
907    fails object's pickle representation will be returned
908
909    """
910    if format == "literal":
911        try:
912            return (literal_dumps(obj, prettyprint=prettyprint, indent=1),
913                    "literal")
914        except (ValueError, TypeError) as ex:
915            if not pickle_fallback:
916                raise
917
918            log.warning("Could not serialize to a literal string",
919                        exc_info=True)
920
921    elif format == "json":
922        try:
923            return (json.dumps(obj, indent=1 if prettyprint else None),
924                    "json")
925        except (ValueError, TypeError):
926            if not pickle_fallback:
927                raise
928
929            log.warning("Could not serialize to a json string",
930                        exc_info=True)
931
932    elif format == "pickle":
933        return pickle.dumps(obj), "pickle"
934
935    else:
936        raise ValueError("Unsupported format %r" % format)
937
938    if pickle_fallback:
939        log.warning("Using pickle fallback")
940        return pickle.dumps(obj), "pickle"
941    else:
942        raise Exception("Something strange happened.")
943
944
945def loads(string, format):
946    if format == "literal":
947        return literal_eval(string)
948    elif format == "json":
949        return json.loads(string)
950    elif format == "pickle":
951        return pickle.loads(string)
952    else:
953        raise ValueError("Unknown format")
954
955
956# This is a subset of PyON serialization.
957def literal_dumps(obj, prettyprint=False, indent=4):
958    """
959    Write obj into a string as a python literal.
960    """
961    memo = {}
962    NoneType = type(None)
963
964    def check(obj):
965        if type(obj) in [int, long, float, bool, NoneType, unicode, str]:
966            return True
967
968        if id(obj) in memo:
969            raise ValueError("{0} is a recursive structure".format(obj))
970
971        memo[id(obj)] = obj
972
973        if type(obj) in [list, tuple]:
974            return all(map(check, obj))
975        elif type(obj) is dict:
976            return all(map(check, chain(obj.iterkeys(), obj.itervalues())))
977        else:
978            raise TypeError("{0} can not be serialized as a python "
979                             "literal".format(type(obj)))
980
981    check(obj)
982
983    if prettyprint:
984        return pprint.pformat(obj, indent=indent)
985    else:
986        return repr(obj)
987
988
989literal_loads = literal_eval
Note: See TracBrowser for help on using the repository browser.