mrv.dgfe
Covered: 439 lines
Missed: 11 lines
Skipped 356 lines
Percent: 97 %
  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"
  7
from networkx import DiGraph, NetworkXError
  8
from collections import deque
  9
import inspect
 10
import weakref
 11
from util import iDuplicatable
 13
from dge import NodeBase
 14
from dge import _PlugShell
 15
from dge import iPlug
 16
from dge import Attribute
 18
__all__ = ("FacadeNodeBase", "GraphNodeBase", "OIFacadePlug")
 23
class _OIShellMeta( type ):
 24
	"""Metaclass building the method wrappers for the _FacadeShell class - not
 25
	all methods should be overridden, just the ones important to use"""
 27
	@classmethod
 28
	def createUnfacadeMethod( cls, funcname ):
 29
		def unfacadeMethod( self, *args, **kwargs ):
 30
			return getattr( self._toIShell(), funcname )( *args, **kwargs )
 31
		return unfacadeMethod
 33
	@classmethod
 34
	def createFacadeMethod( cls, funcname ):
 35
		"""in our case, connections just are handled by our own OI plug, staying
 36
		in the main graph"""
 37
		return list()
 39
	@classmethod
 40
	def createMethod( cls,funcname, facadetype ):
 41
		method = None
 42
		if facadetype == "unfacade":
 43
			method = cls.createUnfacadeMethod( funcname )
 44
		else:
 45
			method = cls.createFacadeMethod( funcname )
 47
		if method: # could be none if we do not overwrite the method
 48
			method.__name__ = funcname
 50
		return method
 53
	def __new__( metacls, name, bases, clsdict ):
 54
		unfacadelist = clsdict.get( '__unfacade__' )
 55
		facadelist = clsdict.get( '__facade__' )
 59
		for funcnamelist, functype in ( ( unfacadelist, "unfacade" ), ( facadelist, "facade" ) ):
 60
			for funcname in funcnamelist:
 61
				method = metacls.createMethod( funcname, functype )
 62
				if method:
 63
					clsdict[ funcname ] = method
 67
		return type.__new__( metacls, name, bases, clsdict )
 70
class _IOShellMeta( _OIShellMeta ):
 71
	"""Metaclass wrapping all unfacade attributes on the plugshell trying
 72
	to get an input connection """
 74
	@classmethod
 75
	def createUnfacadeMethod( cls,funcname ):
 76
		""":return: wrapper method for funcname """
 77
		method = None
 78
		if funcname == "get":						# drection to input
 79
			def unfacadeMethod( self, *args, **kwargs ):
 80
				"""apply to the input shell"""
 88
				oshell = self._getOriginalShell( )
 89
				if oshell.hasCache():
 90
					return oshell.cache()
 92
				return getattr( self._getShells( "input" )[0], funcname )( *args, **kwargs )
 93
			method = unfacadeMethod
 94
		else:										# direction to output
 95
			def unfacadeMethod( self, *args, **kwargs ):
 96
				"""Clear caches of all output plugs as well"""
 97
				for shell in self._getShells( "output" ):
 98
					getattr( shell, funcname )( *args, **kwargs )
100
			method = unfacadeMethod
102
		return method
104
	@classmethod
105
	def createFacadeMethod( cls, funcname ):
106
		"""Call the main shell's function"""
107
		def facadeMethod( self, *args, **kwargs ):
108
			return getattr( self._getOriginalShell( ), funcname )( *args, **kwargs )
109
		return facadeMethod
112
class _OIShell( _PlugShell ):
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
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
	"""
124
	__unfacade__ = [ 'set', 'get', 'clearCache', 'hasCache','setCache', 'cache' ]
129
	__facade__ = [ 'connect','disconnect','input', 'outputs','connections',
130
					'iterShells' ]
132
	__metaclass__ = _OIShellMeta
134
	def __init__( self, *args ):
135
		"""Sanity checking"""
136
		if not isinstance( args[1], OIFacadePlug ):
137
			raise AssertionError( "Invalid PlugType: Need %r, got %r (%s)" % ( OIFacadePlug, args[1].__class__ , args[1]) )
140
		super( _OIShell, self ).__init__( *args )
143
	def __repr__ ( self ):
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 )
150
	def _toIShell( self ):
151
		""":return: convert ourselves to the real shell actually behind this facade plug"""
153
		return self.plug.inode.shellcls.origshellcls( self.plug.inode, self.plug.iplug )
156
class _IOShell( _PlugShell ):
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"""
161
	__unfacade__ = [  'get', 'clearCache' ]
166
	__facade__ = [ 'set','hasCache','setCache', 'cache',
167
					'connect','disconnect','input','connections','outputs',
168
					'iterShells' ]
170
	__metaclass__ = _IOShellMeta
172
	def __init__( self, *args ):
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
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
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
		"""
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() 							# plugname -> oiplug
191
			super( _IOShell, self ).__init__(  )			# initialize empty
199
	def __call__( self, *args ):
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
204
		:note: the shells we create are default ones with some extra handlers
205
			for exceptions"""
206
		return self.__class__( *args )
210
	def _getoiplug( self ):
211
		""":return: oiplug suitable for this shell or None"""
212
		try:
214
			return self.node.shellcls.iomap[ self.plug.name() ]
215
		except KeyError:
217
			pass
223
		return None
225
	def _getOriginalShell( self ):
226
		""":return: instance of the original shell class that was replaced by our instance"""
227
		return self.node.shellcls.origshellcls( self.node, self.plug )
229
	def _getTopFacadeNodeShell( self ):
230
		"""Recursive method to find the first facade parent having an OI shell
232
		:return: topmost facade node shell or None if we are not a managed plug"""
235
		return facadeNodeShell
238
	def _getShells( self, shelltype ):
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'
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 ) ) )
253
		oiplug = self._getoiplug( )
254
		if not oiplug:
256
			return [ self._getOriginalShell( ) ]
264
		facadeNodeShell = self.node.shellcls.facadenode.toShell( oiplug )
272
		connectionShell = facadeNodeShell
273
		if facadeNodeShell.__class__ is _IOShell:
274
			connectionShell = _PlugShell( facadeNodeShell.node, facadeNodeShell.plug )
278
		outShells = list()
279
		if shelltype == "input":
285
			if not connectionShell is facadeNodeShell:
286
				aboveLevelInputShells = facadeNodeShell._getShells( shelltype )
293
				if len( aboveLevelInputShells ) == 2:		# top level orverride !
294
					return aboveLevelInputShells
300
			inputShell = connectionShell.input( )
302
			if inputShell:
307
				outShells.append( inputShell )
308
				outShells.append( self )
309
			else:
310
				outShells.append( self._getOriginalShell( ) )
313
		else:
314
			outShells.extend( connectionShell.outputs( ) )
318
			outShells.append( self._getOriginalShell( ) )
323
			if not connectionShell is facadeNodeShell:
324
				outShells.extend( facadeNodeShell._getShells( shelltype ) )
327
		return outShells
337
class FacadeNodeBase( NodeBase ):
338
	"""Node having no own plugs, but retrieves them by querying other other nodes
339
	and claiming its his own ones.
341
	Using a non-default shell it is possibly to guide all calls through to the
342
	virtual PlugShell.
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.
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.
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.
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
362
	:note: this class could also be used for facades Container nodes that provide
363
		an interface to their internal nodes"""
364
	shellcls = _OIShell		# overriden from NodeBase
367
	caching_enabled = True						# if true, the facade may cache plugs once queried
370
	def __init__( self, *args, **kwargs ):
371
		""" Initialize the instance"""
372
		self._cachedOIPlugs = list()							# simple list of names
373
		NodeBase.__init__( self, *args, **kwargs )
376
	def __getattr__( self, attr ):
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
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 )	# non long names are not garantueed to be unique
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 )
394
		if not candidates:
395
			raise AttributeError( "Attribute %s does not exist on %s" % (attr,self) )
397
		if len( candidates ) == 1:
398
			return candidates[0]
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 ) )
405
	def copyFrom( self, other, **kwargs ):
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
416
	def _getNodePlugs( self ):
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
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.
426
			The node will be altered slightly to allow input of your facade to be reached
427
			from the inside
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" )
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 """
442
		predicate = kwargs.pop( 'predicate', lambda x: True )
444
		if kwargs:		# still args that we do not know ?
445
			raise AssertionError( "Unhandled arguments found  - update this method: %s" % kwargs.keys() )
450
		if self._cachedOIPlugs:
451
			outresult = list()
452
			for oiplug in self._cachedOIPlugs:
453
				if predicate( oiplug ):
454
					outresult.append( oiplug )
456
			return outresult
462
		yourResult = self._getNodePlugs( )
465
		def toFacadePlug( node, plug ):
466
			if isinstance( plug, OIFacadePlug )\
467
			and self is plug.inode.shellcls.facadenode: 		# we can wrap other facade nodes as well
468
				return plug
469
			return OIFacadePlug( node, plug )
473
		finalres = list()
474
		for orignode, plug in yourResult:
475
			oiplug = toFacadePlug( orignode, plug )
479
			if self.caching_enabled:
480
				self._cachedOIPlugs.append( oiplug )
493
			if not isinstance( orignode.shellcls, _IOShell ):
494
				classShellCls = orignode.shellcls
495
				orignode.shellcls = _IOShell( classShellCls, self )
503
			orignode.shellcls.iomap[ oiplug.iplug.name() ] = oiplug
511
			internalshell = orignode.toShell( oiplug.iplug )
512
			all_shell_cons = internalshell.connections( 1, 1 )	 				# now we get old shells
515
			for edge in all_shell_cons:
516
				nedge = list( ( None, None ) )
517
				created_shell = False
519
				for i,shell in enumerate( edge ):
520
					nedge[ i ] = shell
525
					if shell == internalshell and not isinstance( shell, _IOShell ) :
526
						nedge[ i ] = shell.node.toShell( shell.plug )
527
						created_shell = True
530
				if created_shell:
531
					edge[0].disconnect( edge[1] )
532
					nedge[0].connect( nedge[1] )
539
			if not predicate( oiplug ):
540
				continue
542
			finalres.append( oiplug )
549
		return finalres
551
	def clearPlugCache( self ):
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
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()
561
class GraphNodeBase( FacadeNodeBase ):
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.
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
	"""
572
	duplicate_wrapped_graph	 = True			# an independent copy of the wrapped graph usually is required - duplication assures that ( or the caller )
573
	allow_auto_plugs = True					# if True, plugs can be found automatically by iterating nodes on the graph and using their plugs
574
	ignore_failed_includes = False			# if True, node will not raise if a plug to be included cannot be found
579
	include = list()
582
	exclude = list()
585
	def __init__( self, wrappedGraph, *args, **kwargs ):
586
		""" Initialize the instance
587
		:param wrappedGraph: graph we are wrapping"""
588
		self.wgraph = wrappedGraph
589
		if self.duplicate_wrapped_graph:
590
			self.wgraph = self.wgraph.duplicate( )
592
		FacadeNodeBase.__init__( self, *args, **kwargs )
594
	def createInstance( self , **kwargs ):
595
		"""Create a copy of self and return it"""
596
		return self.__class__( self.wgraph )	# graph will be duplicated in the constructor
601
	def _iterNodes( self ):
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( )
610
	def _addIncludeNodePlugs( self, outset ):
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 ]
616
		for nodeplugname in self.include:
617
			nodename = plugname = None
621
			if nodeplugname.find( '.' ) == -1 :
622
				nodename = nodeplugname
623
			else:
624
				nodename, plugname = tuple( nodeplugname.split( "." ) )
629
			try:
630
				index = nodenames.index( nodename )
631
				node = nodes[ index ]
632
			except ValueError:
633
				missingplugs.append( nodeplugname )
634
				continue
639
			if not plugname:
640
				outset.update( ( (node,plug) for plug in node.plugs() ) )
641
			else:
643
				try:
644
					plug = getattr( node, plugname ).plug
645
				except AttributeError:
646
					missingplugs.append( nodeplugname )
647
				else:
649
					outset.add( ( node , plug ) )
650
					continue
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 )
658
	def _removeExcludedPlugs( self, outset ):
659
		"""remove the plugs from our exclude list and modify the outset"""
660
		if not self.exclude:
661
			return
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:
668
				nodename = plugname = None
669
				if nodeplugname.find( '.' ) == -1:			# node mode
670
					nodename = nodeplugname
671
				else:
672
					nodename,plugname = nodeplugname.split( '.' ) # node plug mode
674
				if nodename == str( node ) and ( not plugname or plugname == plug.name() ):
675
					excludepairs.add( ( node,plug ) )
680
		outset -= excludepairs
682
	def _getNodePlugs( self ):
683
		""":return: all plugs on nodes we wrap ( as node,plug tuple )"""
684
		outset = set()
687
		self._addIncludeNodePlugs( outset )
689
		if self.allow_auto_plugs:
690
			for node in self._iterNodes():
691
				plugresult = node.plugs(  )
692
				outset.update( set( ( (node,plug) for plug in plugresult ) ) )
698
		self._removeExcludedPlugs( outset )
701
		return outset
707
class OIFacadePlug( tuple , iPlug ):
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
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
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_"
723
	def __new__( cls, *args ):
724
		"""Store only weakrefs, throw if we do not get 3 inputs
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 ) ) )
734
		return tuple.__new__( cls,  args )		# NOTE: have to use string refs for recursive facade plugs
737
	def __getattr__( self, attr ):
738
		""" Allow easy attribute access
739
		inode: the internal node
740
		iplug: the internal plug
742
		Thus we must:
743
		 - Act as IOFacade returning additional information
745
		 - Act as original plug for attribute access
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]
755
		return getattr( self.iplug, attr )
760
	def name( self ):
761
		""" Get name of facade plug
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 )
768
	def _affectedList( self, direction ):
769
		""" Get affected shells into the given direction
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
775
		iterShells = self.inode.toShell( self.iplug ).iterShells( direction=direction, prune = these, visit_once=True )
776
		outlist = [ shell._getoiplug() for shell in iterShells ]
778
		return outlist
780
	def affects( self, otherplug ):
781
		"""Affects relationships will be set on the original plug only"""
782
		return self.iplug.affects( otherplug )
784
	def affected( self ):
785
		"""Walk the internal affects using an internal plugshell
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" )
791
	def affectedBy( self ):
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" )
796
	def providesOutput( self ):
797
		""":return: True if this is an output plug that can trigger computations """
798
		return self.iplug.providesOutput( )
800
	def providesInput( self ):
801
		""":return: True if this is an input plug that will never cause computations"""
802
		return self.iplug.providesInput( )