Context-based dispatch¶
Introduction¶
Consider this advanced use case for Reg: we have a runtime with multiple contexts. For each context, you want the dispatch behavior to be different. Concretely, if you have an application where you can call a view dispatch function, you want it to execute a different function and return a different value in each separate context.
The Morepath web framework uses this feature of Reg to allow the developer to compose a larger application from multiple smaller ones.
You can define application context as a class. This context class defines dispatch methods. When you subclass the context class, you establish a new context: each subclass has entirely different dispatch registrations, and shares nothing with its base class.
A Context Class¶
Here is a concrete example. First we define a context class we call
A
, and a view
dispatch method on it:
import reg
class A(object):
@reg.dispatch_method(
reg.match_instance('obj'),
reg.match_key('request_method',
lambda self, obj, request: request.request_method))
def view(self, obj, request):
return "default"
Note that since view
is a method we define a self
argument.
To have something to view, We define Document
and Image
content classes:
class Document(object):
def __init__(self, text):
self.text = text
class Image(object):
def __init__(self, bytes):
self.bytes = bytes
We also need a request class:
class Request(object):
def __init__(self, request_method, body=''):
self.request_method = request_method
self.body = body
To try this out, we need to create an instance of the context class:
a = A()
Before we register anything, we get the default result we defined in the method:
>>> doc = Document('Hello world!')
>>> a.view(doc, Request('GET'))
'default'
>>> a.view(doc, Request('POST', 'new content'))
'default'
>>> image = Image('abc')
>>> a.view(image, Request('GET'))
'default'
Here are the functions we are going to register:
def document_get(obj, request):
return "Document text is: " + obj.text
def document_post(obj, request):
obj.text = request.body
return "We changed the document"
def image_get(obj, request):
return obj.bytes
def image_post(obj, request):
obj.bytes = request.body
return "We changed the image"
We now want to register them with our context. To do so, we need to
access the dispatch function through its class (A
), not its
instance (a
). All instances of A
(but not instances of its
subclasses as we will see later) share the same registrations.
We use reg.methodify()
to do the registration, to keep our view
functions the same as when context is not in use. We will see an
example without reg.methodify()
later:
from reg import methodify
A.view.register(methodify(document_get),
request_method='GET',
obj=Document)
A.view.register(methodify(document_post),
request_method='POST',
obj=Document)
A.view.register(methodify(image_get),
request_method='GET',
obj=Image)
A.view.register(methodify(image_post),
request_method='POST',
obj=Image)
Now that we’ve registered some functions, we get the expected behavior
when we call a.view
:
>>> a.view(doc, Request('GET'))
'Document text is: Hello world!'
>>> a.view(doc, Request('POST', 'New content'))
'We changed the document'
>>> doc.text
'New content'
>>> a.view(image, Request('GET'))
'abc'
>>> a.view(image, Request('POST', "new data"))
'We changed the image'
>>> image.bytes
'new data'
A new context¶
Okay, we associate a dispatch method with a context class, but what is the
point? The point is that we can introduce a new context that has
different behavior now. To do, we subclass A
:
class B(A):
pass
At this point the new B
context is empty of specific behavior,
even though it subclasses A
:
>>> b = B()
>>> b.view(doc, Request('GET'))
'default'
>>> b.view(doc, Request('POST', 'New content'))
'default'
>>> b.view(image, Request('GET'))
'default'
>>> b.view(image, Request('POST', "new data"))
'default'
We can now do our registrations. Let’s register the same
behavior for documents as we did for Context
:
B.view.register(methodify(document_get),
request_method='GET',
obj=Document)
B.view.register(methodify(document_post),
request_method='POST',
obj=Document)
But we install different behavior for Image
:
def b_image_get(obj, request):
return 'New image GET'
def b_image_post(obj, request):
return 'New image POST'
B.view.register(methodify(b_image_get),
request_method='GET',
obj=Image)
B.view.register(methodify(b_image_post),
request_method='POST',
obj=Image)
Calling view
for Document
works as before:
>>> b.view(doc, Request('GET'))
'Document text is: New content'
But the behavior for Image
instances is different in the B
context:
>>> b.view(image, Request('GET'))
'New image GET'
>>> b.view(image, Request('POST', "new data"))
'New image POST'
Note that the original context A
is of course unaffected and still
has the behavior we registered for it:
>>> a.view(image, Request('GET'))
'new data'
The idea is that you can create a framework around your base context
class. Where this base context class needs to have dispatch behavior,
you define dispatch methods. You then create different subclasses of
the base context class and register different behaviors for them. This
is what Morepath does with its App
class.
Call method in the same context¶
What if in a dispatch implementation you find you need to call another
dispatch method? How to access the context? You can do this by
registering a function that get a context as its first argument. As an
example, we modify our document functions so that document_post
uses the other:
def c_document_get(context, obj, request):
return "Document text is: " + obj.text
def c_document_post(context, obj, request):
obj.text = request.body
return "Changed: " + context.view(obj, Request('GET'))
Now c_document_post
uses the view
dispatch method on the
context. We need to register these methods using
reg.Dispatch.register()
without reg.methodify()
. This way
they get the context as the first argument. Let’s create a new context
and do so:
class C(A):
pass
C.view.register(c_document_get,
request_method='GET',
obj=Document)
C.view.register(c_document_post,
request_method='POST',
obj=Document)
We now get the expected behavior:
>>> c = C()
>>> c.view(doc, Request('GET'))
'Document text is: New content'
>>> c.view(doc, Request('POST', 'Very new content'))
'Changed: Document text is: Very new content'
You could have used reg.methodify()
for this too, as
methodify
inspects the first argument and if it’s identical to the
second argument to methodify
, it will pass in the context as that
argument.
class D(A):
pass
D.view.register(methodify(c_document_get, 'context'),
request_method='GET',
obj=Document)
D.view.register(methodify(c_document_post, 'context'),
request_method='POST',
obj=Document)
>>> d = D()
>>> d.view(doc, Request('GET'))
'Document text is: Very new content'
>>> d.view(doc, Request('POST', 'Even newer content'))
'Changed: Document text is: Even newer content'
The default value for the second argument to methodify
is app
.