source: orange/Orange/OrangeCanvas/scheme/scheme.py @ 11419:719c8b4b6b69

Revision 11419:719c8b4b6b69, 18.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Fixed docstrings for scheme subpackage.

RevLine 
[11101]1"""
[11367]2===============
[11101]3Scheme Workflow
[11367]4===============
5
6The :class:`Scheme` class defines a DAG (Directed Acyclic Graph) workflow.
[11101]7
8"""
9
10from operator import itemgetter
11from collections import deque
12
13import logging
14
15from PyQt4.QtCore import QObject
16from PyQt4.QtCore import pyqtSignal as Signal
17from PyQt4.QtCore import pyqtProperty as Property
18
19from .node import SchemeNode
20from .link import SchemeLink, compatible_channels
21from .annotations import BaseSchemeAnnotation
22
23from .utils import check_arg, check_type
24
25from .errors import (
[11275]26    SchemeCycleError, IncompatibleChannelTypeError, SinkChannelError,
27    DuplicatedLinkError
[11101]28)
29
30from .readwrite import scheme_to_ows_stream, parse_scheme
31
[11419]32from ..registry import WidgetDescription, InputSignal, OutputSignal
[11101]33
34log = logging.getLogger(__name__)
35
36
37class Scheme(QObject):
[11367]38    """
39    An :class:`QObject` subclass representing the scheme widget workflow
40    with annotations.
41
42    Parameters
43    ----------
44    parent : :class:`QObject`
45        A parent QObject item (default `None`).
46    title : str
47        The scheme title.
48    description : str
49        A longer description of the scheme.
50
51
52    Attributes
53    ----------
54    nodes : list of :class:`.SchemeNode`
55        A list of all the nodes in the scheme.
56
57    links : list of :class:`.SchemeLink`
58        A list of all links in the scheme.
59
60    annotations : list of :class:`BaseSchemeAnnotation`
61        A list of all the annotations in the scheme.
[11101]62
63    """
64
[11367]65    # Signal emitted when a `node` is added to the scheme.
[11101]66    node_added = Signal(SchemeNode)
[11367]67
68    # Signal emitted when a `node` is removed from the scheme.
[11101]69    node_removed = Signal(SchemeNode)
70
[11367]71    # Signal emitted when a `link` is added to the scheme.
[11101]72    link_added = Signal(SchemeLink)
[11367]73
74    # Signal emitted when a `link` is removed from the scheme.
[11101]75    link_removed = Signal(SchemeLink)
76
[11367]77    # Signal emitted when a `annotation` is added to the scheme.
78    annotation_added = Signal(BaseSchemeAnnotation)
79
80    # Signal emitted when a `annotation` is removed from the scheme.
81    annotation_removed = Signal(BaseSchemeAnnotation)
82
83    # Signal emitted when the title of scheme changes.
84    title_changed = Signal(unicode)
85
86    # Signal emitted when the description of scheme changes.
87    description_changed = Signal(unicode)
[11101]88
89    node_state_changed = Signal()
90    channel_state_changed = Signal()
[11367]91    topology_changed = Signal()
[11101]92
93    def __init__(self, parent=None, title=None, description=None):
94        QObject.__init__(self, parent)
95
[11189]96        self.__title = title or ""
97        "Scheme title (empty string by default)."
[11101]98
99        self.__description = description or ""
100        "Scheme description (empty string by default)."
101
102        self.__annotations = []
103        self.__nodes = []
104        self.__links = []
105
106    @property
107    def nodes(self):
[11367]108        """
109        A list of all nodes (:class:`.SchemeNode`) currently in the scheme.
110        """
[11101]111        return list(self.__nodes)
112
113    @property
114    def links(self):
[11367]115        """
116        A list of all links (:class:`.SchemeLink`) currently in the scheme.
117        """
[11101]118        return list(self.__links)
119
120    @property
121    def annotations(self):
[11367]122        """
123        A list of all annotations (:class:`.BaseSchemeAnnotation`) in the
124        scheme.
125
126        """
[11101]127        return list(self.__annotations)
128
129    def set_title(self, title):
[11367]130        """
131        Set the scheme title text.
132        """
[11101]133        if self.__title != title:
134            self.__title = title
135            self.title_changed.emit(title)
136
137    def title(self):
[11367]138        """
139        The title (human readable string) of the scheme.
140        """
[11101]141        return self.__title
142
143    title = Property(unicode, fget=title, fset=set_title)
144
145    def set_description(self, description):
[11367]146        """
147        Set the scheme description text.
148        """
[11101]149        if self.__description != description:
150            self.__description = description
151            self.description_changed.emit(description)
152
153    def description(self):
[11367]154        """
155        Scheme description text.
156        """
[11101]157        return self.__description
158
159    description = Property(unicode, fget=description, fset=set_description)
160
161    def add_node(self, node):
[11367]162        """
163        Add a node to the scheme. An error is raised if the node is
164        already in the scheme.
[11101]165
166        Parameters
167        ----------
[11367]168        node : :class:`.SchemeNode`
169            Node instance to add to the scheme.
[11101]170
171        """
172        check_arg(node not in self.__nodes,
173                  "Node already in scheme.")
174        check_type(node, SchemeNode)
175
176        self.__nodes.append(node)
177        log.info("Added node %r to scheme %r." % (node.title, self.title))
178        self.node_added.emit(node)
179
180    def new_node(self, description, title=None, position=None,
181                 properties=None):
[11367]182        """
183        Create a new :class:`.SchemeNode` and add it to the scheme.
184
185        Same as::
[11101]186
187            scheme.add_node(SchemeNode(description, title, position,
188                                       properties))
189
[11367]190        Parameters
191        ----------
192        description : :class:`WidgetDescription`
193            The new node's description.
194        title : str, optional
195            Optional new nodes title. By default `description.name` is used.
196        position : `(x, y)` tuple of floats, optional
197            Optional position in a 2D space.
198        properties : dict, optional
199            A dictionary of optional extra properties.
200
201        See also
202        --------
[11419]203        .SchemeNode, Scheme.add_node
[11367]204
[11101]205        """
206        if isinstance(description, WidgetDescription):
207            node = SchemeNode(description, title=title, position=position,
208                              properties=properties)
209        else:
210            raise TypeError("Expected %r, got %r." % \
211                            (WidgetDescription, type(description)))
212
213        self.add_node(node)
214        return node
215
216    def remove_node(self, node):
[11367]217        """
218        Remove a `node` from the scheme. All links into and out of the
219        `node` are also removed. If the node in not in the scheme an error
220        is raised.
221
222        Parameters
223        ----------
224        node : :class:`.SchemeNode`
225            Node instance to remove.
[11101]226
227        """
228        check_arg(node in self.__nodes,
229                  "Node is not in the scheme.")
230
231        self.__remove_node_links(node)
232        self.__nodes.remove(node)
233        log.info("Removed node %r from scheme %r." % (node.title, self.title))
234        self.node_removed.emit(node)
235        return node
236
237    def __remove_node_links(self, node):
[11367]238        """
239        Remove all links for node.
[11101]240        """
241        links_in, links_out = [], []
242        for link in self.__links:
243            if link.source_node is node:
244                links_out.append(link)
245            elif link.sink_node is node:
246                links_in.append(link)
247
248        for link in links_out + links_in:
249            self.remove_link(link)
250
251    def add_link(self, link):
[11275]252        """
[11367]253        Add a `link` to the scheme.
254
255        Parameters
256        ----------
257        link : :class:`.SchemeLink`
258            An initialized link instance to add to the scheme.
259
[11101]260        """
261        check_type(link, SchemeLink)
262
263        self.check_connect(link)
264        self.__links.append(link)
265
266        log.info("Added link %r (%r) -> %r (%r) to scheme %r." % \
267                 (link.source_node.title, link.source_channel.name,
268                  link.sink_node.title, link.sink_channel.name,
269                  self.title)
270                 )
271
272        self.link_added.emit(link)
273
274    def new_link(self, source_node, source_channel,
275                 sink_node, sink_channel):
[11275]276        """
[11419]277        Create a new :class:`.SchemeLink` from arguments and add it to
278        the scheme. The new link is returned.
[11367]279
280        Parameters
281        ----------
282        source_node : :class:`.SchemeNode`
283            Source node of the new link.
[11419]284        source_channel : :class:`.OutputSignal`
[11367]285            Source channel of the new node. The instance must be from
[11419]286            ``source_node.output_channels()``
[11367]287        sink_node : :class:`.SchemeNode`
288            Sink node of the new link.
[11419]289        sink_channel : :class:`.InputSignal`
[11367]290            Sink channel of the new node. The instance must be from
[11419]291            ``sink_node.input_channels()``
[11367]292
293        See also
294        --------
[11419]295        .SchemeLink, Scheme.add_link
[11101]296
297        """
298        link = SchemeLink(source_node, source_channel,
299                          sink_node, sink_channel)
300        self.add_link(link)
301        return link
302
303    def remove_link(self, link):
[11367]304        """
305        Remove a link from the scheme.
306
307        Parameters
308        ----------
309        link : :class:`.SchemeLink`
310            Link instance to remove.
311
[11101]312        """
313        check_arg(link in self.__links,
314                  "Link is not in the scheme.")
315
316        self.__links.remove(link)
317        log.info("Removed link %r (%r) -> %r (%r) from scheme %r." % \
318                 (link.source_node.title, link.source_channel.name,
319                  link.sink_node.title, link.sink_channel.name,
320                  self.title)
321                 )
322        self.link_removed.emit(link)
323
324    def check_connect(self, link):
[11275]325        """
[11419]326        Check if the `link` can be added to the scheme and raise an
327        appropriate exception.
[11101]328
329        Can raise:
[11367]330            - :class:`TypeError` if `link` is not an instance of
331              :class:`.SchemeLink`
[11419]332            - :class:`.SchemeCycleError` if the `link` would introduce a cycle
333            - :class:`.IncompatibleChannelTypeError` if the channel types are
[11367]334              not compatible
[11419]335            - :class:`.SinkChannelError` if a sink channel has a `Single` flag
[11367]336              specification and the channel is already connected.
[11419]337            - :class:`.DuplicatedLinkError` if a `link` duplicates an already
[11367]338              present link.
[11101]339
340        """
341        check_type(link, SchemeLink)
[11275]342
[11101]343        if self.creates_cycle(link):
344            raise SchemeCycleError("Cannot create cycles in the scheme")
345
346        if not self.compatible_channels(link):
347            raise IncompatibleChannelTypeError(
[11275]348                    "Cannot connect %r to %r." \
349                    % (link.source_channel.type, link.sink_channel.type)
[11101]350                )
351
[11275]352        links = self.find_links(source_node=link.source_node,
353                                source_channel=link.source_channel,
354                                sink_node=link.sink_node,
355                                sink_channel=link.sink_channel)
356
357        if links:
358            raise DuplicatedLinkError(
359                    "A link from %r (%r) -> %r (%r) already exists" \
360                    % (link.source_node.title, link.source_channel.name,
361                       link.sink_node.title, link.sink_channel.name)
362                )
363
364        if link.sink_channel.single:
365            links = self.find_links(sink_node=link.sink_node,
366                                    sink_channel=link.sink_channel)
367            if links:
368                raise SinkChannelError(
369                        "%r is already connected." % link.sink_channel.name
370                    )
371
[11101]372    def creates_cycle(self, link):
[11367]373        """
[11419]374        Return `True` if `link` would introduce a cycle in the scheme.
375
376        Parameters
377        ----------
378        link : :class:`.SchemeLink`
379
[11101]380        """
381        check_type(link, SchemeLink)
382        source_node, sink_node = link.source_node, link.sink_node
383        upstream = self.upstream_nodes(source_node)
384        upstream.add(source_node)
385        return sink_node in upstream
386
387    def compatible_channels(self, link):
[11367]388        """
[11419]389        Return `True` if the channels in `link` have compatible types.
390
391        Parameters
392        ----------
393        link : :class:`.SchemeLink`
394
[11101]395        """
396        check_type(link, SchemeLink)
397        return compatible_channels(link.source_channel, link.sink_channel)
398
399    def can_connect(self, link):
[11419]400        """
401        Return `True` if `link` can be added to the scheme.
402
403        See also
404        --------
405        Scheme.check_connect
406
407        """
408        check_type(link, SchemeLink)
[11101]409        try:
410            self.check_connect(link)
411            return True
[11419]412        except (SchemeCycleError, IncompatibleChannelTypeError,
413                SinkChannelError, DuplicatedLinkError):
[11101]414            return False
415
416    def upstream_nodes(self, start_node):
[11367]417        """
[11419]418        Return a set of all nodes upstream from `start_node` (i.e.
419        all ancestor nodes).
420
421        Parameters
422        ----------
423        start_node : :class:`.SchemeNode`
424
[11101]425        """
426        visited = set()
427        queue = deque([start_node])
428        while queue:
429            node = queue.popleft()
430            snodes = [link.source_node for link in self.input_links(node)]
431            for source_node in snodes:
432                if source_node not in visited:
433                    queue.append(source_node)
434
435            visited.add(node)
436        visited.remove(start_node)
437        return visited
438
439    def downstream_nodes(self, start_node):
[11367]440        """
441        Return a set of all nodes downstream from `start_node`.
[11419]442
443        Parameters
444        ----------
445        start_node : :class:`.SchemeNode`
446
[11101]447        """
448        visited = set()
449        queue = deque([start_node])
450        while queue:
451            node = queue.popleft()
452            snodes = [link.sink_node for link in self.output_links(node)]
453            for source_node in snodes:
454                if source_node not in visited:
455                    queue.append(source_node)
456
457            visited.add(node)
458        visited.remove(start_node)
459        return visited
460
461    def is_ancestor(self, node, child):
[11367]462        """
463        Return True if `node` is an ancestor node of `child` (is upstream
[11101]464        of the child in the workflow). Both nodes must be in the scheme.
465
[11419]466        Parameters
467        ----------
468        node : :class:`.SchemeNode`
469        child : :class:`.SchemeNode`
470
[11101]471        """
472        return child in self.downstream_nodes(node)
473
474    def children(self, node):
[11367]475        """
476        Return a set of all children of `node`.
[11101]477        """
478        return set(link.sink_node for link in self.output_links(node))
479
480    def parents(self, node):
[11367]481        """
482        Return a set of all parents of `node`.
[11101]483        """
484        return set(link.source_node for link in self.input_links(node))
485
486    def input_links(self, node):
[11367]487        """
[11419]488        Return a list of all input links (:class:`.SchemeLink`) connected
489        to the `node` instance.
490
[11101]491        """
492        return self.find_links(sink_node=node)
493
494    def output_links(self, node):
[11367]495        """
[11419]496        Return a list of all output links (:class:`.SchemeLink`) connected
497        to the `node` instance.
498
[11101]499        """
500        return self.find_links(source_node=node)
501
502    def find_links(self, source_node=None, source_channel=None,
503                   sink_node=None, sink_channel=None):
504        # TODO: Speedup - keep index of links by nodes and channels
505        result = []
506        match = lambda query, value: (query is None or value == query)
507        for link in self.__links:
508            if match(source_node, link.source_node) and \
509                    match(sink_node, link.sink_node) and \
510                    match(source_channel, link.source_channel) and \
511                    match(sink_channel, link.sink_channel):
512                result.append(link)
513
514        return result
515
516    def propose_links(self, source_node, sink_node):
[11367]517        """
518        Return a list of ordered (:class:`OutputSignal`,
519        :class:`InputSignal`, weight) tuples that could be added to
520        the scheme between `source_node` and `sink_node`.
[11101]521
522        .. note:: This can depend on the links already in the scheme.
523
524        """
525        if source_node is sink_node or \
526                self.is_ancestor(sink_node, source_node):
527            # Cyclic connections are not possible.
528            return []
529
530        outputs = source_node.output_channels()
531        inputs = sink_node.input_channels()
532
533        # Get existing links to sink channels that are Single.
534        links = self.find_links(None, None, sink_node)
[11199]535        already_connected_sinks = [link.sink_channel for link in links \
[11101]536                                   if link.sink_channel.single]
537
538        def weight(out_c, in_c):
539            if out_c.explicit or in_c.explicit:
540                # Zero weight for explicit links
541                weight = 0
542            else:
[11199]543                check = [not out_c.dynamic,  # Dynamic signals are last
[11101]544                         in_c not in already_connected_sinks,
545                         bool(in_c.default),
546                         bool(out_c.default)
547                         ]
548                weights = [2 ** i for i in range(len(check), 0, -1)]
549                weight = sum([w for w, c in zip(weights, check) if c])
550            return weight
551
552        proposed_links = []
553        for out_c in outputs:
554            for in_c in inputs:
555                if compatible_channels(out_c, in_c):
556                    proposed_links.append((out_c, in_c, weight(out_c, in_c)))
557
558        return sorted(proposed_links, key=itemgetter(-1), reverse=True)
559
560    def add_annotation(self, annotation):
[11367]561        """
562        Add an annotation (:class:`BaseSchemeAnnotation` subclass) instance
563        to the scheme.
[11101]564
565        """
566        check_arg(annotation not in self.__annotations,
567                  "Cannot add the same annotation multiple times.")
568        check_type(annotation, BaseSchemeAnnotation)
569
570        self.__annotations.append(annotation)
571        self.annotation_added.emit(annotation)
572
573    def remove_annotation(self, annotation):
[11367]574        """
575        Remove the `annotation` instance from the scheme.
576        """
[11101]577        check_arg(annotation in self.__annotations,
578                  "Annotation is not in the scheme.")
579        self.__annotations.remove(annotation)
580        self.annotation_removed.emit(annotation)
581
[11385]582    def clear(self):
583        """
584        Remove all nodes, links, and annotation items from the scheme.
585        """
586        def is_terminal(node):
587            return not bool(self.find_links(source_node=node))
588
589        while self.nodes:
590            terminal_nodes = filter(is_terminal, self.nodes)
591            for node in terminal_nodes:
592                self.remove_node(node)
593
594        for annotation in self.annotations:
595            self.remove_annotation(annotation)
596
597        assert(not (self.nodes or self.links or self.annotations))
598
[11391]599    def save_to(self, stream, pretty=True, pickle_fallback=False):
[11367]600        """
601        Save the scheme as an xml formated file to `stream`
[11391]602
603        See also
604        --------
605        .scheme_to_ows_stream
606
[11101]607        """
608        if isinstance(stream, basestring):
609            stream = open(stream, "wb")
610
[11391]611        scheme_to_ows_stream(self, stream, pretty,
612                             pickle_fallback=pickle_fallback)
[11101]613
614    def load_from(self, stream):
[11367]615        """
616        Load the scheme from xml formated stream.
[11101]617        """
618        if self.__nodes or self.__links or self.__annotations:
619            # TODO: should we clear the scheme and load it.
620            raise ValueError("Scheme is not empty.")
621
622        if isinstance(stream, basestring):
623            stream = open(stream, "rb")
624
625        parse_scheme(self, stream)
Note: See TracBrowser for help on using the repository browser.