Total Complexity | 90 |
Total Lines | 631 |
Duplicated Lines | 0 % |
Complex classes like Orange.canvas.scheme.Scheme often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | """ |
||
37 | class Scheme(QObject): |
||
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. |
||
62 | |||
63 | """ |
||
64 | |||
65 | # Signal emitted when a `node` is added to the scheme. |
||
66 | node_added = Signal(SchemeNode) |
||
67 | |||
68 | # Signal emitted when a `node` is removed from the scheme. |
||
69 | node_removed = Signal(SchemeNode) |
||
70 | |||
71 | # Signal emitted when a `link` is added to the scheme. |
||
72 | link_added = Signal(SchemeLink) |
||
73 | |||
74 | # Signal emitted when a `link` is removed from the scheme. |
||
75 | link_removed = Signal(SchemeLink) |
||
76 | |||
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(str) |
||
85 | |||
86 | # Signal emitted when the description of scheme changes. |
||
87 | description_changed = Signal(str) |
||
88 | |||
89 | node_state_changed = Signal() |
||
90 | channel_state_changed = Signal() |
||
91 | topology_changed = Signal() |
||
92 | |||
93 | #: Emitted when the associated runtime environment changes |
||
94 | #: runtime_env_changed(key: str, newvalue: Option[str], |
||
95 | #: oldvalue: Option[str]) |
||
96 | runtime_env_changed = Signal(str, object, object) |
||
97 | |||
98 | def __init__(self, parent=None, title=None, description=None, env={}): |
||
99 | QObject.__init__(self, parent) |
||
100 | |||
101 | self.__title = title or "" |
||
102 | "Workflow title (empty string by default)." |
||
103 | |||
104 | self.__description = description or "" |
||
105 | "Workflow description (empty string by default)." |
||
106 | |||
107 | self.__annotations = [] |
||
108 | self.__nodes = [] |
||
109 | self.__links = [] |
||
110 | self.__env = dict(env) |
||
111 | |||
112 | @property |
||
113 | def nodes(self): |
||
114 | """ |
||
115 | A list of all nodes (:class:`.SchemeNode`) currently in the scheme. |
||
116 | """ |
||
117 | return list(self.__nodes) |
||
118 | |||
119 | @property |
||
120 | def links(self): |
||
121 | """ |
||
122 | A list of all links (:class:`.SchemeLink`) currently in the scheme. |
||
123 | """ |
||
124 | return list(self.__links) |
||
125 | |||
126 | @property |
||
127 | def annotations(self): |
||
128 | """ |
||
129 | A list of all annotations (:class:`.BaseSchemeAnnotation`) in the |
||
130 | scheme. |
||
131 | |||
132 | """ |
||
133 | return list(self.__annotations) |
||
134 | |||
135 | def set_title(self, title): |
||
136 | """ |
||
137 | Set the scheme title text. |
||
138 | """ |
||
139 | if self.__title != title: |
||
140 | self.__title = title |
||
141 | self.title_changed.emit(title) |
||
142 | |||
143 | def title(self): |
||
144 | """ |
||
145 | The title (human readable string) of the scheme. |
||
146 | """ |
||
147 | return self.__title |
||
148 | |||
149 | title = Property(str, fget=title, fset=set_title) |
||
150 | |||
151 | def set_description(self, description): |
||
152 | """ |
||
153 | Set the scheme description text. |
||
154 | """ |
||
155 | if self.__description != description: |
||
156 | self.__description = description |
||
157 | self.description_changed.emit(description) |
||
158 | |||
159 | def description(self): |
||
160 | """ |
||
161 | Scheme description text. |
||
162 | """ |
||
163 | return self.__description |
||
164 | |||
165 | description = Property(str, fget=description, fset=set_description) |
||
166 | |||
167 | def add_node(self, node): |
||
168 | """ |
||
169 | Add a node to the scheme. An error is raised if the node is |
||
170 | already in the scheme. |
||
171 | |||
172 | Parameters |
||
173 | ---------- |
||
174 | node : :class:`.SchemeNode` |
||
175 | Node instance to add to the scheme. |
||
176 | |||
177 | """ |
||
178 | check_arg(node not in self.__nodes, |
||
179 | "Node already in scheme.") |
||
180 | check_type(node, SchemeNode) |
||
181 | |||
182 | self.__nodes.append(node) |
||
183 | log.info("Added node %r to scheme %r." % (node.title, self.title)) |
||
184 | self.node_added.emit(node) |
||
185 | |||
186 | def new_node(self, description, title=None, position=None, |
||
187 | properties=None): |
||
188 | """ |
||
189 | Create a new :class:`.SchemeNode` and add it to the scheme. |
||
190 | |||
191 | Same as:: |
||
192 | |||
193 | scheme.add_node(SchemeNode(description, title, position, |
||
194 | properties)) |
||
195 | |||
196 | Parameters |
||
197 | ---------- |
||
198 | description : :class:`WidgetDescription` |
||
199 | The new node's description. |
||
200 | title : str, optional |
||
201 | Optional new nodes title. By default `description.name` is used. |
||
202 | position : `(x, y)` tuple of floats, optional |
||
203 | Optional position in a 2D space. |
||
204 | properties : dict, optional |
||
205 | A dictionary of optional extra properties. |
||
206 | |||
207 | See also |
||
208 | -------- |
||
209 | .SchemeNode, Scheme.add_node |
||
210 | |||
211 | """ |
||
212 | if isinstance(description, WidgetDescription): |
||
213 | node = SchemeNode(description, title=title, position=position, |
||
214 | properties=properties) |
||
215 | else: |
||
216 | raise TypeError("Expected %r, got %r." % \ |
||
217 | (WidgetDescription, type(description))) |
||
218 | |||
219 | self.add_node(node) |
||
220 | return node |
||
221 | |||
222 | def remove_node(self, node): |
||
223 | """ |
||
224 | Remove a `node` from the scheme. All links into and out of the |
||
225 | `node` are also removed. If the node in not in the scheme an error |
||
226 | is raised. |
||
227 | |||
228 | Parameters |
||
229 | ---------- |
||
230 | node : :class:`.SchemeNode` |
||
231 | Node instance to remove. |
||
232 | |||
233 | """ |
||
234 | check_arg(node in self.__nodes, |
||
235 | "Node is not in the scheme.") |
||
236 | |||
237 | self.__remove_node_links(node) |
||
238 | self.__nodes.remove(node) |
||
239 | log.info("Removed node %r from scheme %r." % (node.title, self.title)) |
||
240 | self.node_removed.emit(node) |
||
241 | return node |
||
242 | |||
243 | def __remove_node_links(self, node): |
||
244 | """ |
||
245 | Remove all links for node. |
||
246 | """ |
||
247 | links_in, links_out = [], [] |
||
248 | for link in self.__links: |
||
249 | if link.source_node is node: |
||
250 | links_out.append(link) |
||
251 | elif link.sink_node is node: |
||
252 | links_in.append(link) |
||
253 | |||
254 | for link in links_out + links_in: |
||
255 | self.remove_link(link) |
||
256 | |||
257 | def add_link(self, link): |
||
258 | """ |
||
259 | Add a `link` to the scheme. |
||
260 | |||
261 | Parameters |
||
262 | ---------- |
||
263 | link : :class:`.SchemeLink` |
||
264 | An initialized link instance to add to the scheme. |
||
265 | |||
266 | """ |
||
267 | check_type(link, SchemeLink) |
||
268 | |||
269 | self.check_connect(link) |
||
270 | self.__links.append(link) |
||
271 | |||
272 | log.info("Added link %r (%r) -> %r (%r) to scheme %r." % \ |
||
273 | (link.source_node.title, link.source_channel.name, |
||
274 | link.sink_node.title, link.sink_channel.name, |
||
275 | self.title) |
||
276 | ) |
||
277 | |||
278 | self.link_added.emit(link) |
||
279 | |||
280 | def new_link(self, source_node, source_channel, |
||
281 | sink_node, sink_channel): |
||
282 | """ |
||
283 | Create a new :class:`.SchemeLink` from arguments and add it to |
||
284 | the scheme. The new link is returned. |
||
285 | |||
286 | Parameters |
||
287 | ---------- |
||
288 | source_node : :class:`.SchemeNode` |
||
289 | Source node of the new link. |
||
290 | source_channel : :class:`.OutputSignal` |
||
291 | Source channel of the new node. The instance must be from |
||
292 | ``source_node.output_channels()`` |
||
293 | sink_node : :class:`.SchemeNode` |
||
294 | Sink node of the new link. |
||
295 | sink_channel : :class:`.InputSignal` |
||
296 | Sink channel of the new node. The instance must be from |
||
297 | ``sink_node.input_channels()`` |
||
298 | |||
299 | See also |
||
300 | -------- |
||
301 | .SchemeLink, Scheme.add_link |
||
302 | |||
303 | """ |
||
304 | link = SchemeLink(source_node, source_channel, |
||
305 | sink_node, sink_channel) |
||
306 | self.add_link(link) |
||
307 | return link |
||
308 | |||
309 | def remove_link(self, link): |
||
310 | """ |
||
311 | Remove a link from the scheme. |
||
312 | |||
313 | Parameters |
||
314 | ---------- |
||
315 | link : :class:`.SchemeLink` |
||
316 | Link instance to remove. |
||
317 | |||
318 | """ |
||
319 | check_arg(link in self.__links, |
||
320 | "Link is not in the scheme.") |
||
321 | |||
322 | self.__links.remove(link) |
||
323 | log.info("Removed link %r (%r) -> %r (%r) from scheme %r." % \ |
||
324 | (link.source_node.title, link.source_channel.name, |
||
325 | link.sink_node.title, link.sink_channel.name, |
||
326 | self.title) |
||
327 | ) |
||
328 | self.link_removed.emit(link) |
||
329 | |||
330 | def check_connect(self, link): |
||
331 | """ |
||
332 | Check if the `link` can be added to the scheme and raise an |
||
333 | appropriate exception. |
||
334 | |||
335 | Can raise: |
||
336 | - :class:`TypeError` if `link` is not an instance of |
||
337 | :class:`.SchemeLink` |
||
338 | - :class:`.SchemeCycleError` if the `link` would introduce a cycle |
||
339 | - :class:`.IncompatibleChannelTypeError` if the channel types are |
||
340 | not compatible |
||
341 | - :class:`.SinkChannelError` if a sink channel has a `Single` flag |
||
342 | specification and the channel is already connected. |
||
343 | - :class:`.DuplicatedLinkError` if a `link` duplicates an already |
||
344 | present link. |
||
345 | |||
346 | """ |
||
347 | check_type(link, SchemeLink) |
||
348 | |||
349 | if self.creates_cycle(link): |
||
350 | raise SchemeCycleError("Cannot create cycles in the scheme") |
||
351 | |||
352 | if not self.compatible_channels(link): |
||
353 | raise IncompatibleChannelTypeError( |
||
354 | "Cannot connect %r to %r." \ |
||
355 | % (link.source_channel.type, link.sink_channel.type) |
||
356 | ) |
||
357 | |||
358 | links = self.find_links(source_node=link.source_node, |
||
359 | source_channel=link.source_channel, |
||
360 | sink_node=link.sink_node, |
||
361 | sink_channel=link.sink_channel) |
||
362 | |||
363 | if links: |
||
364 | raise DuplicatedLinkError( |
||
365 | "A link from %r (%r) -> %r (%r) already exists" \ |
||
366 | % (link.source_node.title, link.source_channel.name, |
||
367 | link.sink_node.title, link.sink_channel.name) |
||
368 | ) |
||
369 | |||
370 | if link.sink_channel.single: |
||
371 | links = self.find_links(sink_node=link.sink_node, |
||
372 | sink_channel=link.sink_channel) |
||
373 | if links: |
||
374 | raise SinkChannelError( |
||
375 | "%r is already connected." % link.sink_channel.name |
||
376 | ) |
||
377 | |||
378 | def creates_cycle(self, link): |
||
379 | """ |
||
380 | Return `True` if `link` would introduce a cycle in the scheme. |
||
381 | |||
382 | Parameters |
||
383 | ---------- |
||
384 | link : :class:`.SchemeLink` |
||
385 | |||
386 | """ |
||
387 | check_type(link, SchemeLink) |
||
388 | source_node, sink_node = link.source_node, link.sink_node |
||
389 | upstream = self.upstream_nodes(source_node) |
||
390 | upstream.add(source_node) |
||
391 | return sink_node in upstream |
||
392 | |||
393 | def compatible_channels(self, link): |
||
394 | """ |
||
395 | Return `True` if the channels in `link` have compatible types. |
||
396 | |||
397 | Parameters |
||
398 | ---------- |
||
399 | link : :class:`.SchemeLink` |
||
400 | |||
401 | """ |
||
402 | check_type(link, SchemeLink) |
||
403 | return compatible_channels(link.source_channel, link.sink_channel) |
||
404 | |||
405 | def can_connect(self, link): |
||
406 | """ |
||
407 | Return `True` if `link` can be added to the scheme. |
||
408 | |||
409 | See also |
||
410 | -------- |
||
411 | Scheme.check_connect |
||
412 | |||
413 | """ |
||
414 | check_type(link, SchemeLink) |
||
415 | try: |
||
416 | self.check_connect(link) |
||
417 | return True |
||
418 | except (SchemeCycleError, IncompatibleChannelTypeError, |
||
419 | SinkChannelError, DuplicatedLinkError): |
||
420 | return False |
||
421 | |||
422 | def upstream_nodes(self, start_node): |
||
423 | """ |
||
424 | Return a set of all nodes upstream from `start_node` (i.e. |
||
425 | all ancestor nodes). |
||
426 | |||
427 | Parameters |
||
428 | ---------- |
||
429 | start_node : :class:`.SchemeNode` |
||
430 | |||
431 | """ |
||
432 | visited = set() |
||
433 | queue = deque([start_node]) |
||
434 | while queue: |
||
435 | node = queue.popleft() |
||
436 | snodes = [link.source_node for link in self.input_links(node)] |
||
437 | for source_node in snodes: |
||
438 | if source_node not in visited: |
||
439 | queue.append(source_node) |
||
440 | |||
441 | visited.add(node) |
||
442 | visited.remove(start_node) |
||
443 | return visited |
||
444 | |||
445 | def downstream_nodes(self, start_node): |
||
446 | """ |
||
447 | Return a set of all nodes downstream from `start_node`. |
||
448 | |||
449 | Parameters |
||
450 | ---------- |
||
451 | start_node : :class:`.SchemeNode` |
||
452 | |||
453 | """ |
||
454 | visited = set() |
||
455 | queue = deque([start_node]) |
||
456 | while queue: |
||
457 | node = queue.popleft() |
||
458 | snodes = [link.sink_node for link in self.output_links(node)] |
||
459 | for source_node in snodes: |
||
460 | if source_node not in visited: |
||
461 | queue.append(source_node) |
||
462 | |||
463 | visited.add(node) |
||
464 | visited.remove(start_node) |
||
465 | return visited |
||
466 | |||
467 | def is_ancestor(self, node, child): |
||
468 | """ |
||
469 | Return True if `node` is an ancestor node of `child` (is upstream |
||
470 | of the child in the workflow). Both nodes must be in the scheme. |
||
471 | |||
472 | Parameters |
||
473 | ---------- |
||
474 | node : :class:`.SchemeNode` |
||
475 | child : :class:`.SchemeNode` |
||
476 | |||
477 | """ |
||
478 | return child in self.downstream_nodes(node) |
||
479 | |||
480 | def children(self, node): |
||
481 | """ |
||
482 | Return a set of all children of `node`. |
||
483 | """ |
||
484 | return set(link.sink_node for link in self.output_links(node)) |
||
485 | |||
486 | def parents(self, node): |
||
487 | """ |
||
488 | Return a set of all parents of `node`. |
||
489 | """ |
||
490 | return set(link.source_node for link in self.input_links(node)) |
||
491 | |||
492 | def input_links(self, node): |
||
493 | """ |
||
494 | Return a list of all input links (:class:`.SchemeLink`) connected |
||
495 | to the `node` instance. |
||
496 | |||
497 | """ |
||
498 | return self.find_links(sink_node=node) |
||
499 | |||
500 | def output_links(self, node): |
||
501 | """ |
||
502 | Return a list of all output links (:class:`.SchemeLink`) connected |
||
503 | to the `node` instance. |
||
504 | |||
505 | """ |
||
506 | return self.find_links(source_node=node) |
||
507 | |||
508 | def find_links(self, source_node=None, source_channel=None, |
||
509 | sink_node=None, sink_channel=None): |
||
510 | # TODO: Speedup - keep index of links by nodes and channels |
||
511 | result = [] |
||
512 | match = lambda query, value: (query is None or value == query) |
||
513 | for link in self.__links: |
||
514 | if match(source_node, link.source_node) and \ |
||
515 | match(sink_node, link.sink_node) and \ |
||
516 | match(source_channel, link.source_channel) and \ |
||
517 | match(sink_channel, link.sink_channel): |
||
518 | result.append(link) |
||
519 | |||
520 | return result |
||
521 | |||
522 | def propose_links(self, source_node, sink_node): |
||
523 | """ |
||
524 | Return a list of ordered (:class:`OutputSignal`, |
||
525 | :class:`InputSignal`, weight) tuples that could be added to |
||
526 | the scheme between `source_node` and `sink_node`. |
||
527 | |||
528 | .. note:: This can depend on the links already in the scheme. |
||
529 | |||
530 | """ |
||
531 | if source_node is sink_node or \ |
||
532 | self.is_ancestor(sink_node, source_node): |
||
533 | # Cyclic connections are not possible. |
||
534 | return [] |
||
535 | |||
536 | outputs = source_node.output_channels() |
||
537 | inputs = sink_node.input_channels() |
||
538 | |||
539 | # Get existing links to sink channels that are Single. |
||
540 | links = self.find_links(None, None, sink_node) |
||
541 | already_connected_sinks = [link.sink_channel for link in links \ |
||
542 | if link.sink_channel.single] |
||
543 | |||
544 | def weight(out_c, in_c): |
||
545 | if out_c.explicit or in_c.explicit: |
||
546 | # Zero weight for explicit links |
||
547 | weight = 0 |
||
548 | else: |
||
549 | check = [not out_c.dynamic, # Dynamic signals are last |
||
550 | in_c not in already_connected_sinks, |
||
551 | bool(in_c.default), |
||
552 | bool(out_c.default) |
||
553 | ] |
||
554 | weights = [2 ** i for i in range(len(check), 0, -1)] |
||
555 | weight = sum([w for w, c in zip(weights, check) if c]) |
||
556 | return weight |
||
557 | |||
558 | proposed_links = [] |
||
559 | for out_c in outputs: |
||
560 | for in_c in inputs: |
||
561 | if compatible_channels(out_c, in_c): |
||
562 | proposed_links.append((out_c, in_c, weight(out_c, in_c))) |
||
563 | |||
564 | return sorted(proposed_links, key=itemgetter(-1), reverse=True) |
||
565 | |||
566 | def add_annotation(self, annotation): |
||
567 | """ |
||
568 | Add an annotation (:class:`BaseSchemeAnnotation` subclass) instance |
||
569 | to the scheme. |
||
570 | |||
571 | """ |
||
572 | check_arg(annotation not in self.__annotations, |
||
573 | "Cannot add the same annotation multiple times.") |
||
574 | check_type(annotation, BaseSchemeAnnotation) |
||
575 | |||
576 | self.__annotations.append(annotation) |
||
577 | self.annotation_added.emit(annotation) |
||
578 | |||
579 | def remove_annotation(self, annotation): |
||
580 | """ |
||
581 | Remove the `annotation` instance from the scheme. |
||
582 | """ |
||
583 | check_arg(annotation in self.__annotations, |
||
584 | "Annotation is not in the scheme.") |
||
585 | self.__annotations.remove(annotation) |
||
586 | self.annotation_removed.emit(annotation) |
||
587 | |||
588 | def clear(self): |
||
589 | """ |
||
590 | Remove all nodes, links, and annotation items from the scheme. |
||
591 | """ |
||
592 | def is_terminal(node): |
||
593 | return not bool(self.find_links(source_node=node)) |
||
594 | |||
595 | while self.nodes: |
||
596 | terminal_nodes = list(filter(is_terminal, self.nodes)) |
||
597 | for node in terminal_nodes: |
||
598 | self.remove_node(node) |
||
599 | |||
600 | for annotation in self.annotations: |
||
601 | self.remove_annotation(annotation) |
||
602 | |||
603 | assert(not (self.nodes or self.links or self.annotations)) |
||
604 | |||
605 | def sync_node_properties(self): |
||
606 | """ |
||
607 | Called before saving, allowing a subclass to update/sync. |
||
608 | |||
609 | The default implementation does nothing. |
||
610 | |||
611 | """ |
||
612 | pass |
||
613 | |||
614 | def save_to(self, stream, pretty=True, pickle_fallback=False): |
||
615 | """ |
||
616 | Save the scheme as an xml formated file to `stream` |
||
617 | |||
618 | See also |
||
619 | -------- |
||
620 | .scheme_to_ows_stream |
||
621 | |||
622 | """ |
||
623 | if isinstance(stream, str): |
||
624 | stream = open(stream, "wb") |
||
625 | |||
626 | self.sync_node_properties() |
||
627 | |||
628 | readwrite.scheme_to_ows_stream(self, stream, pretty, |
||
629 | pickle_fallback=pickle_fallback) |
||
630 | |||
631 | def load_from(self, stream): |
||
632 | """ |
||
633 | Load the scheme from xml formated stream. |
||
634 | """ |
||
635 | if self.__nodes or self.__links or self.__annotations: |
||
636 | # TODO: should we clear the scheme and load it. |
||
637 | raise ValueError("Scheme is not empty.") |
||
638 | |||
639 | if isinstance(stream, str): |
||
640 | stream = open(stream, "rb") |
||
641 | readwrite.scheme_load(self, stream) |
||
642 | # parse_scheme(self, stream) |
||
643 | |||
644 | def set_runtime_env(self, key, value): |
||
645 | """ |
||
646 | Set a runtime environment variable `key` to `value` |
||
647 | """ |
||
648 | oldvalue = self.__env.get(key, None) |
||
649 | if value != oldvalue: |
||
650 | self.__env[key] = value |
||
651 | self.runtime_env_changed.emit(key, value, oldvalue) |
||
652 | |||
653 | def get_runtime_env(self, key, default=None): |
||
654 | """ |
||
655 | Return a runtime environment variable for `key`. |
||
656 | """ |
||
657 | return self.__env.get(key, default) |
||
658 | |||
659 | def runtime_env(self): |
||
660 | """ |
||
661 | Return (a view to) the full runtime environment. |
||
662 | |||
663 | The return value is a types.MappingProxyType of the |
||
664 | underlying environment dictionary. Changes to the env. |
||
665 | will be reflected in it. |
||
666 | """ |
||
667 | return types.MappingProxyType(self.__env) |
||
668 |
This can be caused by one of the following:
1. Missing Dependencies
This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.
2. Missing __init__.py files
This error could also result from missing
__init__.py
files in your module folders. Make sure that you place one file in each sub-folder.