source: orange/Orange/OrangeCanvas/scheme/scheme.py @ 11204:500ef3f81e6f

Revision 11204:500ef3f81e6f, 13.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Added option to output pretty xml, removed contexts from the saved ows files.

Line 
1"""
2Scheme Workflow
3
4"""
5
6from operator import itemgetter
7from collections import deque
8
9import logging
10
11from PyQt4.QtCore import QObject
12from PyQt4.QtCore import pyqtSignal as Signal
13from PyQt4.QtCore import pyqtProperty as Property
14
15from .node import SchemeNode
16from .link import SchemeLink, compatible_channels
17from .annotations import BaseSchemeAnnotation
18
19from .utils import check_arg, check_type
20
21from .errors import (
22    SchemeCycleError, IncompatibleChannelTypeError
23)
24
25from .readwrite import scheme_to_ows_stream, parse_scheme
26
27from ..registry import WidgetDescription
28
29log = logging.getLogger(__name__)
30
31
32class Scheme(QObject):
33    """An QObject representing the scheme widget workflow
34    with annotations, etc.
35
36    """
37
38    node_added = Signal(SchemeNode)
39    node_removed = Signal(SchemeNode)
40
41    link_added = Signal(SchemeLink)
42    link_removed = Signal(SchemeLink)
43
44    topology_changed = Signal()
45
46    node_state_changed = Signal()
47    channel_state_changed = Signal()
48
49    annotation_added = Signal(BaseSchemeAnnotation)
50    annotation_removed = Signal(BaseSchemeAnnotation)
51
52    node_property_changed = Signal(SchemeNode, str, object)
53
54    title_changed = Signal(unicode)
55    description_changed = Signal(unicode)
56    path_changed = Signal(unicode)
57
58    def __init__(self, parent=None, title=None, description=None):
59        QObject.__init__(self, parent)
60
61        self.__path = ""
62        "Filename path associated with this schema (empty string by default)"
63
64        self.__title = title or ""
65        "Scheme title (empty string by default)."
66
67        self.__description = description or ""
68        "Scheme description (empty string by default)."
69
70        self.__annotations = []
71        self.__nodes = []
72        self.__links = []
73
74    @property
75    def nodes(self):
76        return list(self.__nodes)
77
78    @property
79    def links(self):
80        return list(self.__links)
81
82    @property
83    def annotations(self):
84        return list(self.__annotations)
85
86    def set_title(self, title):
87        if self.__title != title:
88            self.__title = title
89            self.title_changed.emit(title)
90
91    def title(self):
92        return self.__title
93
94    title = Property(unicode, fget=title, fset=set_title)
95
96    def set_description(self, description):
97        if self.__description != description:
98            self.__description = description
99            self.description_changed.emit(description)
100
101    def description(self):
102        return self.__description
103
104    description = Property(unicode, fget=description, fset=set_description)
105
106    def set_path(self, path):
107        if self.__path != path:
108            self.__path = path
109            self.path_changed.emit(path)
110
111    def path(self):
112        return self.__path
113
114    path = Property(unicode, fget=path, fset=set_path)
115
116    def add_node(self, node):
117        """Add a node to the scheme.
118
119        Parameters
120        ----------
121        node : `SchemeNode`
122            Node to add to the scheme.
123
124        """
125        check_arg(node not in self.__nodes,
126                  "Node already in scheme.")
127        check_type(node, SchemeNode)
128
129        self.__nodes.append(node)
130        log.info("Added node %r to scheme %r." % (node.title, self.title))
131        self.node_added.emit(node)
132
133    def new_node(self, description, title=None, position=None,
134                 properties=None):
135        """Create a new SchemeNode and add it to the scheme.
136        Same as:
137
138            scheme.add_node(SchemeNode(description, title, position,
139                                       properties))
140
141        """
142        if isinstance(description, WidgetDescription):
143            node = SchemeNode(description, title=title, position=position,
144                              properties=properties)
145        else:
146            raise TypeError("Expected %r, got %r." % \
147                            (WidgetDescription, type(description)))
148
149        self.add_node(node)
150        return node
151
152    def remove_node(self, node):
153        """Remove a `node` from the scheme. All links into and out of the node
154        are also removed.
155
156        """
157        check_arg(node in self.__nodes,
158                  "Node is not in the scheme.")
159
160        self.__remove_node_links(node)
161        self.__nodes.remove(node)
162        log.info("Removed node %r from scheme %r." % (node.title, self.title))
163        self.node_removed.emit(node)
164        return node
165
166    def __remove_node_links(self, node):
167        """Remove all links for node.
168        """
169        links_in, links_out = [], []
170        for link in self.__links:
171            if link.source_node is node:
172                links_out.append(link)
173            elif link.sink_node is node:
174                links_in.append(link)
175
176        for link in links_out + links_in:
177            self.remove_link(link)
178
179    def add_link(self, link):
180        """Add a link to the scheme
181        """
182        check_type(link, SchemeLink)
183        existing = self.find_links(link.source_node, link.source_channel,
184                                   link.sink_node, link.sink_channel)
185        check_arg(not existing,
186                  "Link %r already in the scheme." % link)
187
188        self.check_connect(link)
189        self.__links.append(link)
190
191        log.info("Added link %r (%r) -> %r (%r) to scheme %r." % \
192                 (link.source_node.title, link.source_channel.name,
193                  link.sink_node.title, link.sink_channel.name,
194                  self.title)
195                 )
196
197        self.link_added.emit(link)
198
199    def new_link(self, source_node, source_channel,
200                 sink_node, sink_channel):
201        """Crate a new SchemeLink and add it to the scheme.
202        Same as:
203
204            scheme.add_link(SchemeLink(source_node, source_channel,
205                                       sink_node, sink_channel)
206
207        """
208        link = SchemeLink(source_node, source_channel,
209                          sink_node, sink_channel)
210        self.add_link(link)
211        return link
212
213    def remove_link(self, link):
214        """Remove a link from the scheme.
215        """
216        check_arg(link in self.__links,
217                  "Link is not in the scheme.")
218
219        self.__links.remove(link)
220        log.info("Removed link %r (%r) -> %r (%r) from scheme %r." % \
221                 (link.source_node.title, link.source_channel.name,
222                  link.sink_node.title, link.sink_channel.name,
223                  self.title)
224                 )
225        self.link_removed.emit(link)
226
227    def check_connect(self, link):
228        """Check if the link can be added to the scheme.
229
230        Can raise:
231            - `SchemeCycleError` if the link would introduce a cycle
232            - `IncompatibleChannelTypeError` if the channel types are not
233                compatible
234
235        """
236        check_type(link, SchemeLink)
237        if self.creates_cycle(link):
238            raise SchemeCycleError("Cannot create cycles in the scheme")
239
240        if not self.compatible_channels(link):
241            raise IncompatibleChannelTypeError(
242                    "Cannot connect %r to %r" \
243                    % (link.source_channel, link.sink_channel)
244                )
245
246    def creates_cycle(self, link):
247        """Would the `link` if added to the scheme introduce a cycle.
248        """
249        check_type(link, SchemeLink)
250        source_node, sink_node = link.source_node, link.sink_node
251        upstream = self.upstream_nodes(source_node)
252        upstream.add(source_node)
253        return sink_node in upstream
254
255    def compatible_channels(self, link):
256        """Do the channels in link have compatible types.
257        """
258        check_type(link, SchemeLink)
259        return compatible_channels(link.source_channel, link.sink_channel)
260
261    def can_connect(self, link):
262        try:
263            self.check_connect(link)
264            return True
265        except (SchemeCycleError, IncompatibleChannelTypeError):
266            return False
267        except Exception:
268            raise
269
270    def upstream_nodes(self, start_node):
271        """Return a set of all nodes upstream from `start_node`.
272        """
273        visited = set()
274        queue = deque([start_node])
275        while queue:
276            node = queue.popleft()
277            snodes = [link.source_node for link in self.input_links(node)]
278            for source_node in snodes:
279                if source_node not in visited:
280                    queue.append(source_node)
281
282            visited.add(node)
283        visited.remove(start_node)
284        return visited
285
286    def downstream_nodes(self, start_node):
287        """Return a set of all nodes downstream from `start_node`.
288        """
289        visited = set()
290        queue = deque([start_node])
291        while queue:
292            node = queue.popleft()
293            snodes = [link.sink_node for link in self.output_links(node)]
294            for source_node in snodes:
295                if source_node not in visited:
296                    queue.append(source_node)
297
298            visited.add(node)
299        visited.remove(start_node)
300        return visited
301
302    def is_ancestor(self, node, child):
303        """Return True if `node` is an ancestor node of `child` (is upstream
304        of the child in the workflow). Both nodes must be in the scheme.
305
306        """
307        return child in self.downstream_nodes(node)
308
309    def children(self, node):
310        """Return a set of all children of `node`.
311        """
312        return set(link.sink_node for link in self.output_links(node))
313
314    def parents(self, node):
315        """Return a set if all parents of `node`.
316        """
317        return set(link.source_node for link in self.input_links(node))
318
319    def input_links(self, node):
320        """Return all input links connected to the `node`.
321        """
322        return self.find_links(sink_node=node)
323
324    def output_links(self, node):
325        """Return all output links connected to the `node`.
326        """
327        return self.find_links(source_node=node)
328
329    def find_links(self, source_node=None, source_channel=None,
330                   sink_node=None, sink_channel=None):
331        # TODO: Speedup - keep index of links by nodes and channels
332        result = []
333        match = lambda query, value: (query is None or value == query)
334        for link in self.__links:
335            if match(source_node, link.source_node) and \
336                    match(sink_node, link.sink_node) and \
337                    match(source_channel, link.source_channel) and \
338                    match(sink_channel, link.sink_channel):
339                result.append(link)
340
341        return result
342
343    def propose_links(self, source_node, sink_node):
344        """Return a list of ordered (`OutputSignal`, `InputSignal`, weight)
345        tuples that could be added to the scheme between `source_node` and
346        `sink_node`.
347
348        .. note:: This can depend on the links already in the scheme.
349
350        """
351        if source_node is sink_node or \
352                self.is_ancestor(sink_node, source_node):
353            # Cyclic connections are not possible.
354            return []
355
356        outputs = source_node.output_channels()
357        inputs = sink_node.input_channels()
358
359        # Get existing links to sink channels that are Single.
360        links = self.find_links(None, None, sink_node)
361        already_connected_sinks = [link.sink_channel for link in links \
362                                   if link.sink_channel.single]
363
364        def weight(out_c, in_c):
365            if out_c.explicit or in_c.explicit:
366                # Zero weight for explicit links
367                weight = 0
368            else:
369                check = [not out_c.dynamic,  # Dynamic signals are last
370                         in_c not in already_connected_sinks,
371                         bool(in_c.default),
372                         bool(out_c.default)
373                         ]
374                weights = [2 ** i for i in range(len(check), 0, -1)]
375                weight = sum([w for w, c in zip(weights, check) if c])
376            return weight
377
378        proposed_links = []
379        for out_c in outputs:
380            for in_c in inputs:
381                if compatible_channels(out_c, in_c):
382                    proposed_links.append((out_c, in_c, weight(out_c, in_c)))
383
384        return sorted(proposed_links, key=itemgetter(-1), reverse=True)
385
386    def add_annotation(self, annotation):
387        """Add an annotation (`BaseSchemeAnnotation`) subclass to the scheme.
388
389        """
390        check_arg(annotation not in self.__annotations,
391                  "Cannot add the same annotation multiple times.")
392        check_type(annotation, BaseSchemeAnnotation)
393
394        self.__annotations.append(annotation)
395        self.annotation_added.emit(annotation)
396
397    def remove_annotation(self, annotation):
398        check_arg(annotation in self.__annotations,
399                  "Annotation is not in the scheme.")
400        self.__annotations.remove(annotation)
401        self.annotation_removed.emit(annotation)
402
403    def save_to(self, stream, pretty=True):
404        """Save the scheme as an xml formated file to `stream`
405        """
406        if isinstance(stream, basestring):
407            stream = open(stream, "wb")
408
409        scheme_to_ows_stream(self, stream, pretty)
410
411    def load_from(self, stream):
412        """Load the scheme from xml formated stream.
413        """
414        if self.__nodes or self.__links or self.__annotations:
415            # TODO: should we clear the scheme and load it.
416            raise ValueError("Scheme is not empty.")
417
418        if isinstance(stream, basestring):
419            stream = open(stream, "rb")
420        elif isinstance(stream, file):
421            self.path = stream.name
422
423        parse_scheme(self, stream)
Note: See TracBrowser for help on using the repository browser.