source: orange/Orange/OrangeCanvas/scheme/readwrite.py @ 11307:7ff71ab0def2

Revision 11307:7ff71ab0def2, 14.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Reworked ows pretty printing.

Minidom pretty printing does not preserve text section whitespace.

Line 
1"""
2Scheme save/load routines.
3"""
4import sys
5import shutil
6
7from xml.etree.ElementTree import TreeBuilder, Element, ElementTree, parse
8
9from collections import defaultdict
10
11import cPickle
12
13from ast import literal_eval
14
15import logging
16
17from . import SchemeNode, SchemeLink
18from .annotations import SchemeTextAnnotation, SchemeArrowAnnotation
19from .errors import IncompatibleChannelTypeError
20
21from .. import registry
22
23log = logging.getLogger(__name__)
24
25
26class UnknownWidgetDefinition(Exception):
27    pass
28
29
30def sniff_version(stream):
31    """
32    Parse a scheme stream and return the scheme's version string.
33    """
34    doc = parse(stream)
35    scheme_el = doc.getroot()
36    version = scheme_el.attrib.get("version", None)
37    # Fallback: check for "widgets" tag.
38    if scheme_el.find("widgets") is not None:
39        version = "1.0"
40    else:
41        version = "2.0"
42
43    return version
44
45
46def parse_scheme(scheme, stream, error_handler=None):
47    """
48    Parse a saved scheme from `stream` and populate a `scheme`
49    instance (:class:`Scheme`).
50    `error_handler` if given will be called with an exception when
51    a 'recoverable' error occurs. By default the exception is simply
52    raised.
53
54    """
55    doc = parse(stream)
56    scheme_el = doc.getroot()
57    version = scheme_el.attrib.get("version", None)
58    if version is None:
59        # Fallback: check for "widgets" tag.
60        if scheme_el.find("widgets") is not None:
61            version = "1.0"
62        else:
63            version = "2.0"
64
65    if error_handler is None:
66        def error_handler(exc):
67            raise exc
68
69    if version == "1.0":
70        parse_scheme_v_1_0(doc, scheme, error_handler=error_handler)
71        return scheme
72    else:
73        parse_scheme_v_2_0(doc, scheme, error_handler=error_handler)
74        return scheme
75
76
77def scheme_node_from_element(node_el, registry):
78    """
79    Create a SchemeNode from an `Element` instance.
80    """
81    try:
82        widget_desc = registry.widget(node_el.get("qualified_name"))
83    except KeyError as ex:
84        raise UnknownWidgetDefinition(*ex.args)
85
86    title = node_el.get("title")
87    pos = node_el.get("position")
88
89    if pos is not None:
90        pos = literal_eval(pos)
91
92    return SchemeNode(widget_desc, title=title, position=pos)
93
94
95def parse_scheme_v_2_0(etree, scheme, error_handler, widget_registry=None):
96    """
97    Parse an `ElementTree` instance.
98    """
99    if widget_registry is None:
100        widget_registry = registry.global_registry()
101
102    nodes_not_found = []
103
104    nodes = []
105    links = []
106
107    id_to_node = {}
108
109    scheme_node = etree.getroot()
110    scheme.title = scheme_node.attrib.get("title", "")
111    scheme.description = scheme_node.attrib.get("description", "")
112
113    # Load and create scheme nodes.
114    for node_el in etree.findall("nodes/node"):
115        try:
116            node = scheme_node_from_element(node_el, widget_registry)
117        except UnknownWidgetDefinition, ex:
118            # description was not found
119            error_handler(ex)
120            node = None
121        except Exception:
122            raise
123
124        if node is not None:
125            nodes.append(node)
126            id_to_node[node_el.get("id")] = node
127        else:
128            nodes_not_found.append(node_el.get("id"))
129
130    # Load and create scheme links.
131    for link_el in etree.findall("links/link"):
132        source_id = link_el.get("source_node_id")
133        sink_id = link_el.get("sink_node_id")
134
135        if source_id in nodes_not_found or sink_id in nodes_not_found:
136            continue
137
138        source = id_to_node.get(source_id)
139        sink = id_to_node.get(sink_id)
140
141        source_channel = link_el.get("source_channel")
142        sink_channel = link_el.get("sink_channel")
143        enabled = link_el.get("enabled") == "true"
144
145        try:
146            link = SchemeLink(source, source_channel, sink, sink_channel,
147                              enabled=enabled)
148        except (ValueError, IncompatibleChannelTypeError) as ex:
149            error_handler(ex)
150        else:
151            links.append(link)
152
153    # Load node properties
154    for property_el in etree.findall("node_properties/properties"):
155        node_id = property_el.attrib.get("node_id")
156
157        if node_id in nodes_not_found:
158            continue
159
160        node = id_to_node[node_id]
161
162        format = property_el.attrib.get("format", "pickle")
163
164        if "data" in property_el.attrib:
165            data = literal_eval(property_el.attrib.get("data"))
166        else:
167            data = property_el.text
168
169        properties = None
170        try:
171            if format != "pickle":
172                raise ValueError("Cannot handle %r format" % format)
173
174            properties = cPickle.loads(data)
175        except Exception:
176            log.error("Could not load properties for %r.", node.title,
177                      exc_info=True)
178
179        if properties is not None:
180            node.properties = properties
181
182    annotations = []
183    for annot_el in etree.findall("annotations/*"):
184        if annot_el.tag == "text":
185            rect = annot_el.attrib.get("rect", "(0, 0, 20, 20)")
186            rect = literal_eval(rect)
187
188            font_family = annot_el.attrib.get("font-family", "").strip()
189            font_size = annot_el.attrib.get("font-size", "").strip()
190
191            font = {}
192            if font_family:
193                font["family"] = font_family
194            if font_size:
195                font["size"] = literal_eval(font_size)
196
197            annot = SchemeTextAnnotation(rect, annot_el.text or "", font=font)
198        elif annot_el.tag == "arrow":
199            start = annot_el.attrib.get("start", "(0, 0)")
200            end = annot_el.attrib.get("end", "(0, 0)")
201            start, end = map(literal_eval, (start, end))
202
203            color = annot_el.attrib.get("fill", "red")
204            annot = SchemeArrowAnnotation(start, end, color=color)
205        annotations.append(annot)
206
207    for node in nodes:
208        scheme.add_node(node)
209
210    for link in links:
211        scheme.add_link(link)
212
213    for annot in annotations:
214        scheme.add_annotation(annot)
215
216
217def parse_scheme_v_1_0(etree, scheme, error_handler, widget_registry=None):
218    """
219    ElementTree Instance of an old .ows scheme format.
220    """
221    if widget_registry is None:
222        widget_registry = registry.global_registry()
223
224    widgets_not_found = []
225
226    widgets = widget_registry.widgets()
227    widgets_by_name = [(d.qualified_name.rsplit(".", 1)[-1], d)
228                       for d in widgets]
229    widgets_by_name = dict(widgets_by_name)
230
231    nodes_by_caption = {}
232    nodes = []
233    links = []
234    for widget_el in etree.findall("widgets/widget"):
235        caption = widget_el.get("caption")
236        name = widget_el.get("widgetName")
237        x_pos = widget_el.get("xPos")
238        y_pos = widget_el.get("yPos")
239
240        if name in widgets_by_name:
241            desc = widgets_by_name[name]
242        else:
243            error_handler(UnknownWidgetDefinition(name))
244            widgets_not_found.append(caption)
245            continue
246
247        node = SchemeNode(desc, title=caption,
248                          position=(int(x_pos), int(y_pos)))
249        nodes_by_caption[caption] = node
250        nodes.append(node)
251
252    for channel_el in etree.findall("channels/channel"):
253        in_caption = channel_el.get("inWidgetCaption")
254        out_caption = channel_el.get("outWidgetCaption")
255
256        if in_caption in widgets_not_found or \
257                out_caption in widgets_not_found:
258            continue
259
260        source = nodes_by_caption[out_caption]
261        sink = nodes_by_caption[in_caption]
262        enabled = channel_el.get("enabled") == "1"
263        signals = literal_eval(channel_el.get("signals"))
264
265        for source_channel, sink_channel in signals:
266            try:
267                link = SchemeLink(source, source_channel, sink, sink_channel,
268                                  enabled=enabled)
269            except (ValueError, IncompatibleChannelTypeError) as ex:
270                error_handler(ex)
271            else:
272                links.append(link)
273
274    settings = etree.find("settings")
275    properties = {}
276    if settings is not None:
277        data = settings.attrib.get("settingsDictionary", None)
278        if data:
279            try:
280                properties = literal_eval(data)
281            except Exception:
282                log.error("Could not load properties for the scheme.",
283                          exc_info=True)
284
285    for node in nodes:
286        if node.title in properties:
287            try:
288                node.properties = cPickle.loads(properties[node.title])
289            except Exception:
290                log.error("Could not unpickle properties for the node %r.",
291                          node.title, exc_info=True)
292
293        scheme.add_node(node)
294
295    for link in links:
296        scheme.add_link(link)
297
298
299def inf_range(start=0, step=1):
300    """Return an infinite range iterator.
301    """
302    while True:
303        yield start
304        start += step
305
306
307def scheme_to_etree(scheme):
308    """Return an `xml.etree.ElementTree` representation of the `scheme.
309    """
310    builder = TreeBuilder(element_factory=Element)
311    builder.start("scheme", {"version": "2.0",
312                             "title": scheme.title or "",
313                             "description": scheme.description or ""})
314
315    ## Nodes
316    node_ids = defaultdict(inf_range().next)
317    builder.start("nodes", {})
318    for node in scheme.nodes:
319        desc = node.description
320        attrs = {"id": str(node_ids[node]),
321                 "name": desc.name,
322                 "qualified_name": desc.qualified_name,
323                 "project_name": desc.project_name or "",
324                 "version": desc.version or "",
325                 "title": node.title,
326                 }
327        if node.position is not None:
328            attrs["position"] = str(node.position)
329
330        if type(node) is not SchemeNode:
331            attrs["scheme_node_type"] = "%s.%s" % (type(node).__name__,
332                                                   type(node).__module__)
333        builder.start("node", attrs)
334        builder.end("node")
335
336    builder.end("nodes")
337
338    ## Links
339    link_ids = defaultdict(inf_range().next)
340    builder.start("links", {})
341    for link in scheme.links:
342        source = link.source_node
343        sink = link.sink_node
344        source_id = node_ids[source]
345        sink_id = node_ids[sink]
346        attrs = {"id": str(link_ids[link]),
347                 "source_node_id": str(source_id),
348                 "sink_node_id": str(sink_id),
349                 "source_channel": link.source_channel.name,
350                 "sink_channel": link.sink_channel.name,
351                 "enabled": "true" if link.enabled else "false",
352                 }
353        builder.start("link", attrs)
354        builder.end("link")
355
356    builder.end("links")
357
358    ## Annotations
359    annotation_ids = defaultdict(inf_range().next)
360    builder.start("annotations", {})
361    for annotation in scheme.annotations:
362        annot_id = annotation_ids[annotation]
363        attrs = {"id": str(annot_id)}
364        data = None
365        if isinstance(annotation, SchemeTextAnnotation):
366            tag = "text"
367            attrs.update({"rect": repr(annotation.rect)})
368
369            # Save the font attributes
370            font = annotation.font
371            attrs.update({"font-family": font.get("family", None),
372                          "font-size": font.get("size", None)})
373            attrs = [(key, value) for key, value in attrs.items() \
374                     if value is not None]
375            attrs = dict((key, unicode(value)) for key, value in attrs)
376
377            data = annotation.text
378
379        elif isinstance(annotation, SchemeArrowAnnotation):
380            tag = "arrow"
381            attrs.update({"start": repr(annotation.start_pos),
382                          "end": repr(annotation.end_pos)})
383
384            # Save the arrow color
385            try:
386                color = annotation.color
387                attrs.update({"fill": color})
388            except AttributeError:
389                pass
390
391            data = None
392        else:
393            log.warning("Can't save %r", annotation)
394            continue
395        builder.start(tag, attrs)
396        if data is not None:
397            builder.data(data)
398        builder.end(tag)
399
400    builder.end("annotations")
401
402    builder.start("thumbnail", {})
403    builder.end("thumbnail")
404
405    # Node properties/settings
406    builder.start("node_properties", {})
407    for node in scheme.nodes:
408        data = None
409        if node.properties:
410            try:
411                data = cPickle.dumps(node.properties)
412            except Exception:
413                log.error("Error serializing properties for node %r",
414                          node.title, exc_info=True)
415            if data is not None:
416                builder.start("properties",
417                              {"node_id": str(node_ids[node]),
418                               "format": "pickle",
419#                               "data": repr(data),
420                               })
421                builder.data(data)
422                builder.end("properties")
423
424    builder.end("node_properties")
425    builder.end("scheme")
426    root = builder.close()
427    tree = ElementTree(root)
428    return tree
429
430
431def scheme_to_ows_stream(scheme, stream, pretty=False):
432    """Write scheme to a a stream in Orange Scheme .ows (v 2.0) format.
433    """
434    tree = scheme_to_etree(scheme)
435
436    if pretty:
437        indent(tree.getroot(), 0)
438
439    if sys.version_info < (2, 7):
440        # in Python 2.6 the write does not have xml_declaration parameter.
441        tree.write(stream, encoding="utf-8")
442    else:
443        tree.write(stream, encoding="utf-8", xml_declaration=True)
444
445
446def indent(element, level=0, indent="\t"):
447    """
448    Indent an instance of a :class:`Element`. Based on
449    `http://effbot.org/zone/element-lib.htm#prettyprint`_).
450
451    """
452    def empty(text):
453        return not text or not text.strip()
454
455    def indent_(element, level, last):
456        child_count = len(element)
457
458        if child_count:
459            if empty(element.text):
460                element.text = "\n" + indent * (level + 1)
461
462            if empty(element.tail):
463                element.tail = "\n" + indent * (level + (-1 if last else 0))
464
465            for i, child in enumerate(element):
466                indent_(child, level + 1, i == child_count - 1)
467
468        else:
469            if empty(element.tail):
470                element.tail = "\n" + indent * (level + (-1 if last else 0))
471
472    return indent_(element, level, True)
Note: See TracBrowser for help on using the repository browser.