203 lines
8.3 KiB
Python
203 lines
8.3 KiB
Python
"""
|
|
This is the main dispatcher module.
|
|
|
|
Dispatch works as follows:
|
|
Start at the Originating dispatcher, which must
|
|
have a _dispatch function, which defines how we move
|
|
from dispatch object to dispatch object in the system.
|
|
Continue following the dispatch mechanism for a given
|
|
controller until you reach another controller with a
|
|
_dispatch method defined. Use the new _dispatch
|
|
method until another controller with _dispatch defined
|
|
or until the url has been traversed to entirety.
|
|
|
|
This module also contains the standard ObjectDispatch
|
|
class which provides the ordinary TurboGears mechanism.
|
|
|
|
"""
|
|
|
|
from crank.util import get_argspec, method_matches_args
|
|
from crank.dispatcher import Dispatcher
|
|
from webob.exc import HTTPNotFound
|
|
from inspect import ismethod
|
|
|
|
|
|
class ObjectDispatcher(Dispatcher):
|
|
"""
|
|
Object dispatch (also "object publishing") means that each portion of the
|
|
URL becomes a lookup on an object. The next part of the URL applies to the
|
|
next object, until you run out of URL. Processing starts on a "Root"
|
|
object.
|
|
|
|
Thus, /foo/bar/baz become URL portion "foo", "bar", and "baz". The
|
|
dispatch looks for the "foo" attribute on the Root URL, which returns
|
|
another object. The "bar" attribute is looked for on the new object, which
|
|
returns another object. The "baz" attribute is similarly looked for on
|
|
this object.
|
|
|
|
Dispatch does not have to be directly on attribute lookup, objects can also
|
|
have other methods to explain how to dispatch from them. The search ends
|
|
when a decorated controller method is found.
|
|
|
|
The rules work as follows:
|
|
|
|
1) If the current object under consideration is a decorated controller
|
|
method, the search is ended.
|
|
|
|
2) If the current object under consideration has a "default" method, keep a
|
|
record of that method. If we fail in our search, and the most recent
|
|
method recorded is a "default" method, then the search is ended with
|
|
that method returned.
|
|
|
|
3) If the current object under consideration has a "lookup" method, keep a
|
|
record of that method. If we fail in our search, and the most recent
|
|
method recorded is a "lookup" method, then execute the "lookup" method,
|
|
and start the search again on the return value of that method.
|
|
|
|
4) If the URL portion exists as an attribute on the object in question,
|
|
start searching again on that attribute.
|
|
|
|
5) If we fail our search, try the most recent recorded methods as per 2 and
|
|
3.
|
|
"""
|
|
|
|
#Change to True to allow extra params to pass thru the dispatch
|
|
_use_lax_params = False
|
|
_use_index_fallback = True
|
|
|
|
def _is_exposed(self, controller, name):
|
|
"""Override this function to define how a controller method is
|
|
determined to be exposed.
|
|
|
|
:Arguments:
|
|
controller - controller with methods that may or may not be exposed.
|
|
name - name of the method that is tested.
|
|
|
|
:Returns:
|
|
True or None
|
|
"""
|
|
return ismethod(getattr(controller, name, False))
|
|
|
|
def __call__(self, state, remainder=None):
|
|
return self._dispatch(state, remainder)
|
|
|
|
def _perform_security_check(self, controller):
|
|
#xxx do this better
|
|
obj = getattr(controller, 'im_self', controller)
|
|
|
|
security_check = getattr(obj, '_check_security', None)
|
|
if security_check is not None:
|
|
security_check()
|
|
|
|
def _dispatch_controller(self, current_path, controller, state, remainder):
|
|
"""
|
|
Essentially, this method defines what to do when we move to the next
|
|
layer in the url chain, if a new controller is needed.
|
|
If the new controller has a _dispatch method, dispatch proceeds to
|
|
the new controller's mechanism.
|
|
|
|
Also, this is the place where the controller is checked for
|
|
controller-level security.
|
|
"""
|
|
|
|
dispatcher = getattr(controller, '_dispatch', None)
|
|
if dispatcher is not None:
|
|
state.add_controller(current_path, controller)
|
|
return dispatcher(state, remainder)
|
|
state.add_controller(current_path, controller)
|
|
return self._dispatch(state, remainder)
|
|
|
|
def _dispatch_first_found_default_or_lookup(self, state, remainder):
|
|
"""
|
|
When the dispatch has reached the end of the tree but not found an
|
|
applicable method, so therefore we head back up the branches of the
|
|
tree until we found a method which matches with a default or lookup method.
|
|
"""
|
|
|
|
if not state._notfound_stack:
|
|
if self._use_index_fallback:
|
|
#see if there is an index
|
|
current_controller = state.controller
|
|
method = getattr(current_controller, 'index', None)
|
|
if method:
|
|
if method_matches_args(method, state.params, remainder, self._use_lax_params):
|
|
state.add_method(current_controller.index, remainder)
|
|
return state
|
|
raise HTTPNotFound
|
|
else:
|
|
|
|
m_type, meth, m_remainder, warning = state._notfound_stack.pop()
|
|
|
|
if m_type == 'lookup':
|
|
new_controller, new_remainder = meth(*m_remainder)
|
|
state.add_controller(new_controller.__class__.__name__, new_controller)
|
|
dispatcher = getattr(new_controller, '_dispatch', self._dispatch)
|
|
r = dispatcher(state, new_remainder)
|
|
return r
|
|
elif m_type == 'default':
|
|
state.add_method(meth, m_remainder)
|
|
return state
|
|
# raise HTTPNotFound
|
|
|
|
def _dispatch(self, state, remainder=None):
|
|
"""
|
|
This method defines how the object dispatch mechanism works, including
|
|
checking for security along the way.
|
|
"""
|
|
if state.dispatcher is None:
|
|
state.dispatcher = self
|
|
state.add_controller('/', self)
|
|
if remainder is None:
|
|
remainder = state.path
|
|
|
|
current_controller = state.controller
|
|
|
|
#skip any empty urls
|
|
if remainder and not(remainder[0]):
|
|
return self._dispatch(state, remainder[1:])
|
|
|
|
self._enter_controller(state, remainder)
|
|
|
|
#we are plumb out of path, check for index
|
|
if not remainder:
|
|
if self._is_exposed(current_controller, 'index') and \
|
|
method_matches_args(current_controller.index, state.params, remainder, self._use_lax_params):
|
|
state.add_method(current_controller.index, remainder)
|
|
return state
|
|
#if there is no index, head up the tree
|
|
#to see if there is a default or lookup method we can use
|
|
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
|
|
|
|
|
current_path = state.path_translator(remainder[0])
|
|
current_args = remainder[1:]
|
|
|
|
#an exposed method matching the path is found
|
|
if self._is_exposed(current_controller, current_path):
|
|
#check to see if the argspec jives
|
|
controller = getattr(current_controller, current_path)
|
|
if method_matches_args(controller, state.params, current_args, self._use_lax_params):
|
|
state.add_method(controller, current_args)
|
|
return state
|
|
|
|
#another controller is found
|
|
current_controller = getattr(current_controller, current_path, None)
|
|
if current_controller is not None:
|
|
return self._dispatch_controller(current_path, current_controller,
|
|
state, current_args)
|
|
|
|
#dispatch not found
|
|
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
|
|
|
def _enter_controller(self, state, remainder):
|
|
'''Checks security and pushes any notfound (lookup or default) handlers
|
|
onto the stack
|
|
'''
|
|
current_controller = state.controller
|
|
self._perform_security_check(current_controller)
|
|
if hasattr(current_controller, '_lookup') and self._is_exposed(current_controller, '_lookup'):
|
|
state._notfound_stack.append(('lookup', current_controller._lookup, remainder, None))
|
|
if hasattr(current_controller, '_default') and self._is_exposed(current_controller, '_default'):
|
|
state._notfound_stack.append(('default', current_controller._default, remainder, None))
|
|
|