1
2 """Contains nodes supporting facading within a dependency graph - this can be used
3 for container tyoes or nodes containing their own subgraph even
4 """
5 __docformat__ = "restructuredtext"
6
7 from networkx import DiGraph, NetworkXError
8 from collections import deque
9 import inspect
10 import weakref
11 from util import iDuplicatable
12
13 from dge import NodeBase
14 from dge import _PlugShell
15 from dge import iPlug
16 from dge import Attribute
17
18 __all__ = ("FacadeNodeBase", "GraphNodeBase", "OIFacadePlug")
32
33 @classmethod
38
39 @classmethod
51
52
68
99
100 method = unfacadeMethod
101
102 return method
103
104 @classmethod
109 return facadeMethod
110
113 """All connections from and to the FacadeNode must actually start and end there.
114 Iteration over internal plugShells is not allowed.
115 Thus we override only the methods that matter and assure that the call is handed
116 to the acutal internal plugshell.
117 We know everything we require as we have been fed with an oiplug
118
119 * node = facacde node
120 * plug = oiplug containing inode and iplug ( internal node and internal plug )
121 * The internal node allows us to hand in calls to the native internal shell
122 """
123
124 __unfacade__ = [ 'set', 'get', 'clearCache', 'hasCache','setCache', 'cache' ]
125
126
127
128
129 __facade__ = [ 'connect','disconnect','input', 'outputs','connections',
130 'iterShells' ]
131
132 __metaclass__ = _OIShellMeta
133
141
142
144 """Cut away our name in the possible oiplug ( printing an unnecessary long name then )"""
145 plugname = str( self.plug )
146 nodename = str( self.node )
147 plugname = plugname.replace( nodename+'.', "" )
148 return "%s.%s" % ( nodename, plugname )
149
151 """:return: convert ourselves to the real shell actually behind this facade plug"""
152
153 return self.plug.inode.shellcls.origshellcls( self.plug.inode, self.plug.iplug )
154
157 """This callable class, when called, will create a IOShell using the
158 actual facade node, not the one given as input. This allows it to have the
159 facade system handle the plugshell, or simply satisfy the original request"""
160
161 __unfacade__ = [ 'get', 'clearCache' ]
162
163
164
165
166 __facade__ = [ 'set','hasCache','setCache', 'cache',
167 'connect','disconnect','input','connections','outputs',
168 'iterShells' ]
169
170 __metaclass__ = _IOShellMeta
171
173 """Initialize this instance - we can be in creator mode or in shell mode.
174 ShellMode: we behave like a shell but apply customizations, true if 3 args ( node, plug, origshellcls )
175 CreatorMode: we only create shells of our type in ShellMode, true if 2 args
176
177 :param args:
178 * origshellcls[0] = the shell class used on the manipulated node before we , must always be set as last arg
179 * facadenode[1] = the facadenode we are connected to
180
181 :todo: optimize by creating the unfacade methods exactly as we need them and bind the respective instance
182 methods - currently this is solved with a simple if conditiion.
183 """
184
185
186
187 if hasattr( args[0], '__call__' ) or isinstance( args[0], type ):
188 self.origshellcls = args[0]
189 self.facadenode = args[1]
190 self.iomap = dict()
191 super( _IOShell, self ).__init__( )
192
193
194
195
196
197
198
200 """This equals a constructor call to the shell class on the wrapped node.
201 Simply return an ordinary shell at its base, but we catch some callbacks
202 This applies to everything but connection handling
203
204 :note: the shells we create are default ones with some extra handlers
205 for exceptions"""
206 return self.__class__( *args )
207
208
209
211 """:return: oiplug suitable for this shell or None"""
212 try:
213
214 return self.node.shellcls.iomap[ self.plug.name() ]
215 except KeyError:
216
217 pass
218
219
220
221
222
223 return None
224
226 """:return: instance of the original shell class that was replaced by our instance"""
227 return self.node.shellcls.origshellcls( self.node, self.plug )
228
230 """Recursive method to find the first facade parent having an OI shell
231
232 :return: topmost facade node shell or None if we are not a managed plug"""
233
234
235 return facadeNodeShell
236
237
239 """:return: list of ( outside ) shells, depending on the shelltype and availability.
240 If no outside shell is avaiable, return the actual shell only
241 As facade nodes can be nested, we have to check each level of nesting
242 for connections into the outside world - if available, we use these, otherwise
243 we stay 'inside'
244
245 :param shelltype: "input" - outside input shell
246 "output" - output shells, and the default shell"""
247 if not isinstance( self.node.shellcls, _IOShell ):
248 raise AssertionError( "Shellclass of %s must be _IOShell, but is %s" % ( self.node, type( self.node.shellcls ) ) )
249
250
251
252
253 oiplug = self._getoiplug( )
254 if not oiplug:
255
256 return [ self._getOriginalShell( ) ]
257
258
259
260
261
262
263
264 facadeNodeShell = self.node.shellcls.facadenode.toShell( oiplug )
265
266
267
268
269
270
271
272 connectionShell = facadeNodeShell
273 if facadeNodeShell.__class__ is _IOShell:
274 connectionShell = _PlugShell( facadeNodeShell.node, facadeNodeShell.plug )
275
276
277
278 outShells = list()
279 if shelltype == "input":
280
281
282
283
284
285 if not connectionShell is facadeNodeShell:
286 aboveLevelInputShells = facadeNodeShell._getShells( shelltype )
287
288
289
290
291
292
293 if len( aboveLevelInputShells ) == 2:
294 return aboveLevelInputShells
295
296
297
298
299
300 inputShell = connectionShell.input( )
301
302 if inputShell:
303
304
305
306
307 outShells.append( inputShell )
308 outShells.append( self )
309 else:
310 outShells.append( self._getOriginalShell( ) )
311
312
313 else:
314 outShells.extend( connectionShell.outputs( ) )
315
316
317
318 outShells.append( self._getOriginalShell( ) )
319
320
321
322
323 if not connectionShell is facadeNodeShell:
324 outShells.extend( facadeNodeShell._getShells( shelltype ) )
325
326
327 return outShells
328
338 """Node having no own plugs, but retrieves them by querying other other nodes
339 and claiming its his own ones.
340
341 Using a non-default shell it is possibly to guide all calls through to the
342 virtual PlugShell.
343
344 Derived classes must override _plugshells which will be queried when
345 plugs or plugshells are requested. This node will cache the result and do
346 everything required to integrate itself.
347
348 It lies in the nature of this class that the plugs are dependent on a specific instance
349 of this node, thus classmethods of NodeBase have been overridden with instance versions
350 of it.
351
352 The facade node keeps a plug map allowing it to map plug-shells it got from
353 you back to the original shell respectively. If the map has been missed,
354 your node will be asked for information.
355
356 :note: facades are intrusive for the nodes they are facading - thus the nodes
357 returned by `_getNodePlugs` will be altered. Namely the instance will get a
358 shellcls and plug override to allow us to hook into the callchain. Thus you should have
359 your own instance of the node - otherwise things might behave differently for
360 others using your nodes from another angle
361
362 :note: this class could also be used for facades Container nodes that provide
363 an interface to their internal nodes"""
364 shellcls = _OIShell
365
366
367 caching_enabled = True
368
369
374
375
377 """:return: shell on attr made from our plugs - we do not have real ones, so we
378 need to call plugs and find it by name
379
380 :note: to make this work, you should always name the plug names equal to their
381 class attribute"""
382 check_ambigious = not attr.startswith( OIFacadePlug._fp_prefix )
383
384 candidates = list()
385 for plug in self.plugs( ):
386 if plug.name() == attr or plug.iplug.name() == attr:
387 shell = self.toShell( plug )
388 if not check_ambigious:
389 return shell
390 candidates.append( shell )
391
392
393
394 if not candidates:
395 raise AttributeError( "Attribute %s does not exist on %s" % (attr,self) )
396
397 if len( candidates ) == 1:
398 return candidates[0]
399
400
401 raise AttributeError( "More than one plug with the local name %s exist on %s - use the long name, i.e. %snode_attr" % ( attr, self, OIFacadePlug._fp_prefix ) )
402
403
404
406 """Actually, it does nothing because our plugs are linked to the internal
407 nodes in a quite complex way. The good thing is that this is just a cache that
408 will be updated once someone queries connections again.
409 Basically it comes down to the graph duplicating itself using node and plug
410 methods instead of just doing his 'internal' magic"""
411 pass
412
413
414
415
417 """Implement this as if it was your plugs method - it will be called by the
418 base - your result needs processing before it can be returned
419
420 :return: list( tuple( node, plug ) )
421 if you have an existing node that the plug or shell you gave is from,
422 return it in the tuple, otherwise set it to a node with a shell that allows you
423 to handle it - the only time the node is required is when it is used in and with
424 the shells of the node's own shell class.
425
426 The node will be altered slightly to allow input of your facade to be reached
427 from the inside
428
429 :note: a predicate is not supported as it must be applied on the converted
430 plugs, not on the ones you hand out"""
431 raise NotImplementedError( "Needs to be implemented in SubClass" )
432
433
434
435
436 - def plugs( self, **kwargs ):
437 """Calls `_getNodePlugs` method to ask you to actuallly return your
438 actual nodes and plugs or shells.
439 We prepare the returned value to assure we are being called in certain occasion,
440 which actually glues outside and inside worlds together """
441
442 predicate = kwargs.pop( 'predicate', lambda x: True )
443
444 if kwargs:
445 raise AssertionError( "Unhandled arguments found - update this method: %s" % kwargs.keys() )
446
447
448
449
450 if self._cachedOIPlugs:
451 outresult = list()
452 for oiplug in self._cachedOIPlugs:
453 if predicate( oiplug ):
454 outresult.append( oiplug )
455
456 return outresult
457
458
459
460
461
462 yourResult = self._getNodePlugs( )
463
464
465 def toFacadePlug( node, plug ):
466 if isinstance( plug, OIFacadePlug )\
467 and self is plug.inode.shellcls.facadenode:
468 return plug
469 return OIFacadePlug( node, plug )
470
471
472
473 finalres = list()
474 for orignode, plug in yourResult:
475 oiplug = toFacadePlug( orignode, plug )
476
477
478
479 if self.caching_enabled:
480 self._cachedOIPlugs.append( oiplug )
481
482
483
484
485
486
487
488
489
490
491
492
493 if not isinstance( orignode.shellcls, _IOShell ):
494 classShellCls = orignode.shellcls
495 orignode.shellcls = _IOShell( classShellCls, self )
496
497
498
499
500
501
502
503 orignode.shellcls.iomap[ oiplug.iplug.name() ] = oiplug
504
505
506
507
508
509
510
511 internalshell = orignode.toShell( oiplug.iplug )
512 all_shell_cons = internalshell.connections( 1, 1 )
513
514
515 for edge in all_shell_cons:
516 nedge = list( ( None, None ) )
517 created_shell = False
518
519 for i,shell in enumerate( edge ):
520 nedge[ i ] = shell
521
522
523
524
525 if shell == internalshell and not isinstance( shell, _IOShell ) :
526 nedge[ i ] = shell.node.toShell( shell.plug )
527 created_shell = True
528
529
530 if created_shell:
531 edge[0].disconnect( edge[1] )
532 nedge[0].connect( nedge[1] )
533
534
535
536
537
538
539 if not predicate( oiplug ):
540 continue
541
542 finalres.append( oiplug )
543
544
545
546
547
548
549 return finalres
550
552 """if a cache has been build as caching is enabled, this method clears
553 the cache forcing it to be updated on the next demand
554
555 :note: this could be more efficient by just deleting plugs that are
556 not required anymore, but probably this method can expect the whole
557 cache to be deleted right away ... so its fine"""
558 self._cachedOIPlugs = list()
559
562 """A node wrapping a graph, allowing it to be nested within the node
563 All inputs and outputs on this node are purely virtual, thus they internally connect
564 to the wrapped graph.
565
566 :todo: tests deletion of graphnodes and see whether they are being garbage collected.
567 It should work with the new collector as it can handle cyclic references - these
568 strong cycles we have a lot in this structure. Weakrefs will not work for nested
569 facade nodes as they are tuples not allowing weak refs.
570 """
571
572 duplicate_wrapped_graph = True
573 allow_auto_plugs = True
574 ignore_failed_includes = False
575
576
577
578
579 include = list()
580
581
582 exclude = list()
583
584
585 - def __init__( self, wrappedGraph, *args, **kwargs ):
593
595 """Create a copy of self and return it"""
596 return self.__class__( self.wgraph )
597
598
599
600
602 """:return: generator for nodes in our graph
603 :note: derived classes could override this to just return a filtered view on
604 their nodes"""
605 return self.wgraph.iterNodes( )
606
607
608
609
611 """Add the plugs defined in include to the given output list"""
612 missingplugs = list()
613 nodes = self.wgraph.nodes()
614 nodenames = [ str( node ) for node in nodes ]
615
616 for nodeplugname in self.include:
617 nodename = plugname = None
618
619
620
621 if nodeplugname.find( '.' ) == -1 :
622 nodename = nodeplugname
623 else:
624 nodename, plugname = tuple( nodeplugname.split( "." ) )
625
626
627
628
629 try:
630 index = nodenames.index( nodename )
631 node = nodes[ index ]
632 except ValueError:
633 missingplugs.append( nodeplugname )
634 continue
635
636
637
638
639 if not plugname:
640 outset.update( ( (node,plug) for plug in node.plugs() ) )
641 else:
642
643 try:
644 plug = getattr( node, plugname ).plug
645 except AttributeError:
646 missingplugs.append( nodeplugname )
647 else:
648
649 outset.add( ( node , plug ) )
650 continue
651
652
653
654 if not self.ignore_failed_includes and missingplugs:
655 msg = "%s: Could not find following include plugs: %s" % ( self, ",".join( missingplugs ) )
656 raise AssertionError( msg )
657
659 """remove the plugs from our exclude list and modify the outset"""
660 if not self.exclude:
661 return
662
663 excludepairs = set()
664 excludeNameTuples = [ tuple( plugname.split( "." ) ) for plugname in self.exclude ]
665 for node,plug in outset:
666 for nodeplugname in self.exclude:
667
668 nodename = plugname = None
669 if nodeplugname.find( '.' ) == -1:
670 nodename = nodeplugname
671 else:
672 nodename,plugname = nodeplugname.split( '.' )
673
674 if nodename == str( node ) and ( not plugname or plugname == plug.name() ):
675 excludepairs.add( ( node,plug ) )
676
677
678
679
680 outset -= excludepairs
681
702
708 """Facade Plugs are meant to be stored on instance level overriding the respective
709 class level plug descriptor.
710 If used directly, it will facade the internal affects relationships and just return
711 what really is affected on the facade node
712
713 Additionally they are associated to a node instance, and can thus be used to
714 find the original node once the plug is used in an OI facacde shell
715
716 Its a tuple as it will be more memory efficient that way. Additionally one
717 automatically has a proper hash and comparison if the same objects come together
718 """
719 _fp_prefix = "_FP_"
720
721
722
724 """Store only weakrefs, throw if we do not get 3 inputs
725
726 :param args:
727 * arg[0] = internal node
728 * arg[1] = internal plug"""
729 count = 2
730 if len( args ) != count:
731 raise AssertionError( "Invalid Argument count, should be %i, was %i" % ( count, len( args ) ) )
732
733
734 return tuple.__new__( cls, args )
735
736
738 """ Allow easy attribute access
739 inode: the internal node
740 iplug: the internal plug
741
742 Thus we must:
743 - Act as IOFacade returning additional information
744
745 - Act as original plug for attribute access
746
747 This will work as long as the method names are unique
748 """
749 if attr == 'inode':
750 return self[0]
751 if attr == 'iplug':
752 return self[1]
753
754
755 return getattr( self.iplug, attr )
756
757
758
759
761 """ Get name of facade plug
762
763 :return: name of (internal) plug - must be a unique key, unique enough
764 to allow connections to several nodes of the same type"""
765 return "%s%s_%s" % ( self._fp_prefix, self.inode, self.iplug )
766
767
769 """ Get affected shells into the given direction
770
771 :return: list of all oiplugs looking in direction, if
772 plugtestfunc says: False, do not prune the given shell"""
773 these = lambda shell: shell.plug is self.iplug or not isinstance( shell, _IOShell ) or shell._getoiplug() is None
774
775 iterShells = self.inode.toShell( self.iplug ).iterShells( direction=direction, prune = these, visit_once=True )
776 outlist = [ shell._getoiplug() for shell in iterShells ]
777
778 return outlist
779
781 """Affects relationships will be set on the original plug only"""
782 return self.iplug.affects( otherplug )
783
785 """Walk the internal affects using an internal plugshell
786
787 :note: only output plugs can be affected - this is a rule followed throughout the system
788 :return: tuple containing affected plugs ( plugs that are affected by our value )"""
789 return self._affectedList( "down" )
790
792 """Walk the graph upwards and return all input plugs that are being facaded
793 :return: tuple containing plugs that affect us ( plugs affecting our value )"""
794 return self._affectedList( "up" )
795
797 """:return: True if this is an output plug that can trigger computations """
798 return self.iplug.providesOutput( )
799
803
804
805
806