source: orange/Orange/OrangeCanvas/scheme/readwrite.py @ 11303:9401b375eba0

Revision 11303:9401b375eba0, 13.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Error handling/recovery in scheme parsing code.

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