source: orange/Orange/OrangeCanvas/scheme/readwrite.py @ 11305:b85791927494

Revision 11305:b85791927494, 13.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Prevent a overwrite of the old version 1.0 ows scheme format.

Old schemes should not be allowed to be overwritten in a way that would
prevent the old interface from reading them.

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