[Twisted-web] Replacement for handler in LivePage?

Mike C. Fletcher mcfletch at rogers.com
Thu Sep 15 17:48:16 MDT 2005


Mike C. Fletcher wrote:
...

> Now, obviously I could override the page's locateHandler and do all of 
> the registration and lookup manually, but it seems that this kind of 
> thing would be the *common* use case for any Ajax application.  So, 
> I'm thinking we should really have an easy method for constructing 
> such callbacks.
>
> This is something along the lines of what I'm thinking (untested 
> pseudo-code):

And attached is a module that implements the functionality.  It's 
currently set up as a set of subclasses and functions that shadow those 
in nevow.livepage.

Have fun,
Mike

-- 
________________________________________________
  Mike C. Fletcher
  Designer, VR Plumber, Coder
  http://www.vrplumber.com
  http://blog.vrplumber.com

-------------- next part --------------
"""Provide classic dynamic-code-friendly handler mechanism for Nevow"""
from nevow import rend, loaders, tags, flat, livepage, inevow
from twisted.internet import defer
import weakref

log = APPLICATION.getLog( 'livepage' )

class Handler( object ):
	"""New-style handler for Nevow, uses the same basic mechanism as transient
	
	We want to be able to produce dynamically-generated trees of 
	controls, which means that we need to be able to register multi-shot
	event handlers live.
	"""
	bubble = True
	def __init__( self, identifier, callable, *args, **named ):
		"""Initialise the handler instance
		
		identifier -- unique identifier assigned by the client handle 
		callable -- the target callable object
		args -- arguments to the function (javascript arguments)
		named -- carries non-javascript arguments, currently:
			bubble -- if defined and False, prevent bubbling of the 
				generating event (i.e. "stop" after the handler)
		"""
		self.identifier = identifier
		self.callable = callable
		self.args = args 
		if named.has_key( 'bubble' ):
			self.bubble = named['bubble']
	def jsIdentifier( self ):
		"""Retrieve the javascript callback identifier for this callback"""
		return '**handler.%s'%(self.identifier)
	def jsCall( self, ctx ):
		"""Produce the javascript to call this Handler on the server"""
		client = livepage.IClientHandle(ctx)
		if self.identifier is None:
			self.identifier = client.nextId()
			client.registerHandler( self )
		base = livepage.server.handle( self.jsIdentifier(), *self.args )
		if not self.bubble:
			result = base
		else:
			result = [ 
				base,
				livepage.stop
			]
		return livepage.flat.serialize( result, ctx )
	def __call__( self, javascriptContext, *args ):
		"""Do the final calling of the handler with the client-provided values"""
		try:
			log.debug( """Callback: %s, %r""", self.callable, args )
			client = livepage.IClientHandle( javascriptContext )
			return self.callable( client, *args )
		except Exception, err:
			log.error(
				"""Failure during Javascript callback on %s %s: %s""",
				getattr(self.callable,'__name__',self.callable),
				args,
				log.getException( err ),
			)
			return None
	def deregisterOnDelete( self, targetObject, client ):
		"""Deregister this handler from the client on deletion of targetObject"""
		return weakref.ref( targetObject, _Deregister( client ))
class _Deregister( object ):
	"""Class to deregister a registered handler when target dies"""
	def __init__( self, client ):
		self.client = weakref.ref( client )
	def __call__( self, targetWeak ):
		"""De-register the javascript handler now that target is gone"""
		client = self.client()
		if client:
			try:
				del client.handlers[ self.jsIdentifier()]
			except (AttributeError,KeyError), err:
				pass 
def flattenHandler( handler, ctx ):
	"""Redirect to flatten a handler instance"""
	return handler.jsCall( ctx )
livepage.flat.registerFlattener(flattenHandler, Handler)

def handler( callable, *args, **named ):
	"""Handler call for use without reference to the client object
	
	See Handler for discussion of arguments
	"""
	return Handler( None, callable, *args, **named )

class InputHandlerResource( livepage.InputHandlerResource ):
	"""Teach to support the **handler.ID names..."""
	def renderHTTP(self, ctx):
		"""Handle incoming HTTP-based XML-RPC call
		
		This is far more verbose than it should be because the base function
		doesn't provide resolveHandler( ctx ), so we have to duplicate the 
		rest of the function just to overload that piece of functionality.
		"""
		self.clientHandle.timeoutCount = 0
	
		request = inevow.IRequest(ctx)
		livepage.neverEverCache(request)
		livepage.activeChannel(request)
		ctx.remember(self.clientHandle, livepage.IClientHandle)
		ctx.remember(livepage.jsExceptionHandler, inevow.ICanHandleException)
	
		handlerName = request.args['handler-name'][0]
		arguments = request.args.get('arguments', ())
		livepage.jslog(">>>>>>\n%s %s\n" % (handlerName, arguments))
		handler = self.resolveHandler( ctx )
	
		jsContext = livepage.JavascriptContext(ctx, tags.invisible[handler])
		towrite = []
	
		def writer(r):
			livepage.jslog("WRITE ", r)
			towrite.append(r)
	
		def finisher(r):
			livepage.jslog("FINISHED", r)
			writestr = ''.join(towrite)
			livepage.jslog("<><><>\n%s\n" % (writestr, ))
			request.write(writestr)
			request.finish()
			return r
		result = handler(jsContext, *arguments)
		livepage.jslog("RESULT ", result)
	
		if result is None:
			return defer.succeed('')
		return self.clientHandle.livePage.flattenFactory(result, jsContext,
											writer, finisher)
	def resolveHandler( self, ctx ):
		"""Resolve the handler for the given context (request)"""
		request = inevow.IRequest(ctx)
		handlerName = request.args['handler-name'][0]
		handler = self.clientHandle.getHandler( handlerName )
		if handler is None:
			if handlerName.startswith('--transient.'):
				handler = self.clientHandle.popTransient(
					handlerName.split('.')[-1]
				)
			else:
				handler = self.clientHandle.livePage.locateHandler(
					ctx, request.args['handler-path'],
					handlerName
				)
		return handler



class ClientHandle( livepage.ClientHandle ):
	"""ClientHandle providing for run-time registration of multi-use callbacks"""
	def handler( self, callable, *args, **named ):
		"""Create a new Handler object, assigning an ID automatically
		
		With the LivePage below, this allows for doing
			IClientHandle( ctx ).handler( callable, arg, arg1, arg2 )
		whereever you see a context instance.  Of course, we also support 
		the original handler top-level function as well.
		"""
		id = self.nextId()
		handle = Handler( id, callable, *args, **named )
		return self.registerHandler( handler )
	def registerHandler( self, handler ):
		"""Register a handler (must already have identifier)
		
		handler -- callable handler taking a client instance and having a 
			jsIdentifier() method.
		
		Side-effect: creates the "handlers" attribute and registers for 
			deletion of that attribute on loss of connection.
		
		returns handler
		"""
		if not hasattr( self, 'handlers' ):
			self.handlers = {}
			self.notifyOnClose().addBoth( self.cleanHandlers )
		self.handlers[ handler.jsIdentifier() ] = handler
		return handler
		
	def cleanHandlers( self, result=None ):
		"""Clean up the handler registry for this client handle
		
		Called by a notifyOnClose deferred
		"""
		log.debug( """Cleaning up handlers for client handler %s""", self )
		try:
			del self.handlers
		except AttributeError, err:
			pass
		return result
	def getHandler( self, key ):
		"""Retrieve named registered handler or None
		
		key -- identifier for the handler, exactly as returned from 
			jsIdentifier() on the handler (no pre-processing is done).
		"""
		try:
			return self.handlers[ key ]
		except KeyError, err:
			return None


class DefaultClientHandleFactory( livepage.DefaultClientHandleFactory ):
	"""Override declaration of clientHandleClass"""
	clientHandleClass = ClientHandle
theDefaultClientHandleFactory = DefaultClientHandleFactory()
class DefaultClientHandlesResource(livepage.DefaultClientHandlesResource):
	"""Override declarations of Input and Output handlers and clientFactory"""
	clientResources = {
		'input': InputHandlerResource,
		'output': livepage.OutputHandlerResource,
	}
	clientFactory = theDefaultClientHandleFactory
theDefaultClientHandlesResource = DefaultClientHandlesResource()

class LivePage( livepage.LivePage):
	"""Base class for Cinemon LivePage instances
	
	Initialise as LivePage( content=SomeFragment(), target=target )
	
	LivePages are resources, so can be directly returned from 
	traversal (i.e. don't need extra wrapping.
	"""
	clientFactory = theDefaultClientHandleFactory
	def renderHTTP(self, ctx):
		"""Override to store the client-handle in the main page context"""
		handle = self.clientFactory.newClientHandle(
			self, self.refreshInterval,self.targetTimeoutCount
		)
		ctx.remember( handle, livepage.IClientHandle )
		return super( LivePage, self ).renderHTTP( ctx )
	def render_liveglue(self, ctx, data):
		"""Override to use already-created client handle (from renderHTTP)"""
		if not self.cacheable:
			handle = livepage.IClientHandle( ctx )
			handleId = "'", handle.handleId, "'"
		else:
			handleId = 'null'
		return [
			tags.script(type="text/javascript")[
				"var nevow_clientHandleId = ", handleId ,";"],
			tags.script(type="text/javascript",
						src=livepage.url.here.child('nevow_glue.js'))
		]
	def child_livepage_client(self, ctx):
		"""Override the default client-handles-resources lookup to use local"""
		return theDefaultClientHandlesResource


More information about the Twisted-web mailing list