source: orange/Orange/OrangeCanvas/scheme/scheme.py @ 11219:d24b63d1c4db

Revision 11219:d24b63d1c4db, 12.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Removed 'path' property from 'Scheme' class and added it to SchemeEditWidget.

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