initial framework for crank, no tests yet
This commit is contained in:
commit
bfc9714eda
15
.hgignore
Normal file
15
.hgignore
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
syntax:glob
|
||||||
|
|
||||||
|
.coverage
|
||||||
|
*.pyc
|
||||||
|
*.bak
|
||||||
|
*.db
|
||||||
|
*.sw?
|
||||||
|
*.orig
|
||||||
|
*.class
|
||||||
|
build/*
|
||||||
|
docs/.build/*
|
||||||
|
dist/*
|
||||||
|
.svn
|
||||||
|
*.egg-info/*
|
||||||
|
.noseids
|
0
crank/__init__.py
Normal file
0
crank/__init__.py
Normal file
160
crank/dispatcher.py
Normal file
160
crank/dispatcher.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
"""
|
||||||
|
This is the main dispatcher module.
|
||||||
|
|
||||||
|
Dispatch works as follows:
|
||||||
|
Start at the RootController, the root controller must
|
||||||
|
have a _dispatch function, which defines how we move
|
||||||
|
from object to 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 anther 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 urllib import url2pathname
|
||||||
|
from inspect import ismethod, isclass, getargspec
|
||||||
|
|
||||||
|
def remove_argspec_params_from_params(func, params, remainder):
|
||||||
|
"""Remove parameters from the argument list that are
|
||||||
|
not named parameters
|
||||||
|
Returns: params, remainder"""
|
||||||
|
|
||||||
|
# figure out which of the vars in the argspec are required
|
||||||
|
argspec = _get_argspec(func)
|
||||||
|
argvars = argspec[0][1:]
|
||||||
|
|
||||||
|
# if there are no required variables, or the remainder is none, we
|
||||||
|
# have nothing to do
|
||||||
|
if not argvars or not remainder:
|
||||||
|
return params, remainder
|
||||||
|
|
||||||
|
# this is a work around for a crappy api choice in getargspec
|
||||||
|
argvals = argspec[3]
|
||||||
|
if argvals is None:
|
||||||
|
argvals = []
|
||||||
|
|
||||||
|
required_vars = argvars
|
||||||
|
optional_vars = []
|
||||||
|
if argvals:
|
||||||
|
required_vars = argvars[:-len(argvals)]
|
||||||
|
optional_vars = argvars[-len(argvals):]
|
||||||
|
|
||||||
|
# make a copy of the params so that we don't modify the existing one
|
||||||
|
params=params.copy()
|
||||||
|
|
||||||
|
# replace the existing required variables with the values that come in
|
||||||
|
# from params these could be the parameters that come off of validation.
|
||||||
|
remainder = list(remainder)
|
||||||
|
for i, var in enumerate(required_vars):
|
||||||
|
if i < len(remainder):
|
||||||
|
remainder[i] = params[var]
|
||||||
|
elif params.get(var):
|
||||||
|
remainder.append(params[var])
|
||||||
|
if var in params:
|
||||||
|
del params[var]
|
||||||
|
|
||||||
|
#remove the optional positional variables (remainder) from the named parameters
|
||||||
|
# until we run out of remainder, that is, avoid creating duplicate parameters
|
||||||
|
for i,(original,var) in enumerate(zip(remainder[len(required_vars):],optional_vars)):
|
||||||
|
if var in params:
|
||||||
|
remainder[ len(required_vars)+i ] = params[var]
|
||||||
|
del params[var]
|
||||||
|
|
||||||
|
return params, tuple(remainder)
|
||||||
|
|
||||||
|
_cached_argspecs = {}
|
||||||
|
def get_argspec(self, func):
|
||||||
|
try:
|
||||||
|
argspec = _cached_argspecs[func.im_func]
|
||||||
|
except KeyError:
|
||||||
|
argspec = _cached_argspecs[func.im_func] = getargspec(func)
|
||||||
|
return argspec
|
||||||
|
|
||||||
|
def get_params_with_argspec(func, params, remainder):
|
||||||
|
params = params.copy()
|
||||||
|
argspec = get_argspec(func)
|
||||||
|
argvars = argspec[0][1:]
|
||||||
|
if argvars and enumerate(remainder):
|
||||||
|
for i, var in enumerate(argvars):
|
||||||
|
if i >= len(remainder):
|
||||||
|
break
|
||||||
|
params[var] = remainder[i]
|
||||||
|
return params
|
||||||
|
|
||||||
|
def method_matches_args(self, method, state, remainder):
|
||||||
|
"""
|
||||||
|
This method matches the params from the request along with the remainder to the
|
||||||
|
method's function signiture. If the two jive, it returns true.
|
||||||
|
|
||||||
|
It is very likely that this method would go into ObjectDispatch in the future.
|
||||||
|
"""
|
||||||
|
argspec = get_argspec(method)
|
||||||
|
argvars = argspec[0][1:]
|
||||||
|
argvals = argspec[3]
|
||||||
|
|
||||||
|
required_vars = argvars
|
||||||
|
if argvals:
|
||||||
|
required_vars = argvars[:-len(argvals)]
|
||||||
|
else:
|
||||||
|
argvals = []
|
||||||
|
|
||||||
|
#remove the appropriate remainder quotient
|
||||||
|
if len(remainder)<len(required_vars):
|
||||||
|
#pull the first few off with the remainder
|
||||||
|
required_vars = required_vars[len(remainder):]
|
||||||
|
else:
|
||||||
|
#there is more of a remainder than there is non optional vars
|
||||||
|
required_vars = []
|
||||||
|
|
||||||
|
#remove vars found in the params list
|
||||||
|
params = state.params
|
||||||
|
for var in required_vars[:]:
|
||||||
|
if var in params:
|
||||||
|
required_vars.pop(0)
|
||||||
|
else:
|
||||||
|
break;
|
||||||
|
|
||||||
|
var_in_params = 0
|
||||||
|
for var in argvars:
|
||||||
|
if var in params:
|
||||||
|
var_in_params+=1
|
||||||
|
|
||||||
|
#make sure all of the non-optional-vars are there
|
||||||
|
if not required_vars:
|
||||||
|
var_args = argspec[0][1:]
|
||||||
|
#there are more args in the remainder than are available in the argspec
|
||||||
|
if len(var_args)<len(remainder) and not argspec[1]:
|
||||||
|
return False
|
||||||
|
defaults = argspec[3] or []
|
||||||
|
var_args = var_args[len(remainder):-len(defaults)]
|
||||||
|
for arg in var_args:
|
||||||
|
if arg not in state.params:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
class Dispatcher(object):
|
||||||
|
"""
|
||||||
|
Extend this class to define your own mechanism for dispatch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _dispatch(self, state, remainder):
|
||||||
|
"""override this to define how your controller should dispatch.
|
||||||
|
returns: dispatcher, controller_path, remainder
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _setup_wsgiorg_routing_args(self, url_path, remainder, params):
|
||||||
|
"""
|
||||||
|
This is expected to be overridden by any subclass that wants to set
|
||||||
|
the routing_args.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _setup_wsgi_script_name(self, url_path, remainder, params):
|
||||||
|
pass
|
||||||
|
|
54
crank/dispatchstate.py
Normal file
54
crank/dispatchstate.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
This module implements the :class:`DispatchState` class
|
||||||
|
"""
|
||||||
|
from util import odict
|
||||||
|
|
||||||
|
class DispatchState(object):
|
||||||
|
"""
|
||||||
|
This class keeps around all the pertainent info for the state
|
||||||
|
of the dispatch as it traverses through the tree. This allows
|
||||||
|
us to attach things like routing args and to keep track of the
|
||||||
|
path the controller takes along the system.
|
||||||
|
"""
|
||||||
|
def __init__(self, request, params=None):
|
||||||
|
self.request = request
|
||||||
|
self.url_path = request.path_info
|
||||||
|
|
||||||
|
if params:
|
||||||
|
self.params = params
|
||||||
|
else:
|
||||||
|
self.params = request.params
|
||||||
|
|
||||||
|
self.controller_path = odict()
|
||||||
|
self.routing_args = {}
|
||||||
|
self.method = None
|
||||||
|
self.remainder = None
|
||||||
|
self.dispatcher = None
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
def add_controller(self, location, controller):
|
||||||
|
"""Add a controller object to the stack"""
|
||||||
|
self.controller_path[location] = controller
|
||||||
|
|
||||||
|
def add_method(self, method, remainder):
|
||||||
|
"""Add the final method that will be called in the _call method"""
|
||||||
|
self.method = method
|
||||||
|
self.remainder = remainder
|
||||||
|
|
||||||
|
def add_routing_args(self, current_path, remainder, fixed_args, var_args):
|
||||||
|
"""
|
||||||
|
Add the "intermediate" routing args for a given controller mounted
|
||||||
|
at the current_path
|
||||||
|
"""
|
||||||
|
for i, arg in enumerate(fixed_args):
|
||||||
|
if i >= len(remainder):
|
||||||
|
break
|
||||||
|
self.routing_args[arg] = remainder[i]
|
||||||
|
remainder = remainder[i:]
|
||||||
|
if var_args and remainder:
|
||||||
|
self.routing_args[current_path] = remainder
|
||||||
|
|
||||||
|
@property
|
||||||
|
def controller(self):
|
||||||
|
"""returns the current controller"""
|
||||||
|
return self.controller_path.getitem(-1)
|
179
crank/objectdispatcher.py
Normal file
179
crank/objectdispatcher.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
"""
|
||||||
|
This is the main dispatcher module.
|
||||||
|
|
||||||
|
Dispatch works as follows:
|
||||||
|
Start at the RootController, the root controller must
|
||||||
|
have a _dispatch function, which defines how we move
|
||||||
|
from object to 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 anther 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 dispatcher import get_argspec, method_matches_args, Dispatcher
|
||||||
|
from webob.exc import HTTPNotFound
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
def _find_first_exposed(self, controller, methods):
|
||||||
|
for method in methods:
|
||||||
|
if self._is_exposed(controller, method):
|
||||||
|
return getattr(controller, method)
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
if hasattr(controller, name) and ismethod(getattr(controller, name)):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _is_controller(self, controller, name):
|
||||||
|
"""
|
||||||
|
Override this function to define how an object is determined to be a
|
||||||
|
controller.
|
||||||
|
"""
|
||||||
|
return hasattr(controller, name) and not ismethod(getattr(controller, name))
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
#xxx: add logging?
|
||||||
|
if hasattr(controller, '_dispatch'):
|
||||||
|
if hasattr(controller, "im_self"):
|
||||||
|
obj = controller.im_self
|
||||||
|
else:
|
||||||
|
obj = controller
|
||||||
|
|
||||||
|
if hasattr(obj, '_check_security'):
|
||||||
|
obj._check_security()
|
||||||
|
state.add_controller(current_path, controller)
|
||||||
|
state.dispatcher = controller
|
||||||
|
return controller._dispatch(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.
|
||||||
|
"""
|
||||||
|
orig_url_path = state.url_path
|
||||||
|
if len(remainder):
|
||||||
|
state.url_path = state.url_path[:-len(remainder)]
|
||||||
|
for i in xrange(len(state.controller_path)):
|
||||||
|
controller = state.controller
|
||||||
|
if self._is_exposed(controller, '_default'):
|
||||||
|
state.add_method(controller._default, remainder)
|
||||||
|
state.dispatcher = self
|
||||||
|
return state
|
||||||
|
if self._is_exposed(controller, '_lookup'):
|
||||||
|
controller, remainder = controller._lookup(*remainder)
|
||||||
|
state.url_path = '/'.join(remainder)
|
||||||
|
return self._dispatch_controller(
|
||||||
|
'_lookup', controller, state, remainder)
|
||||||
|
state.controller_path.pop()
|
||||||
|
if len(state.url_path):
|
||||||
|
remainder = list(remainder)
|
||||||
|
remainder.insert(0, state.url_path[-1])
|
||||||
|
state.url_path.pop()
|
||||||
|
raise HTTPNotFound
|
||||||
|
|
||||||
|
def _dispatch(self, state, remainder):
|
||||||
|
"""
|
||||||
|
This method defines how the object dispatch mechanism works, including
|
||||||
|
checking for security along the way.
|
||||||
|
"""
|
||||||
|
current_controller = state.controller
|
||||||
|
|
||||||
|
if hasattr(current_controller, '_check_security'):
|
||||||
|
current_controller._check_security()
|
||||||
|
#we are plumb out of path, check for index
|
||||||
|
if not remainder:
|
||||||
|
if hasattr(current_controller, 'index'):
|
||||||
|
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 = remainder[0]
|
||||||
|
|
||||||
|
#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 self._method_matches_args(controller, state, remainder[1:]):
|
||||||
|
state.add_method(controller, remainder[1:])
|
||||||
|
return state
|
||||||
|
|
||||||
|
#another controller is found
|
||||||
|
if hasattr(current_controller, current_path):
|
||||||
|
current_controller = getattr(current_controller, current_path)
|
||||||
|
return self._dispatch_controller(
|
||||||
|
current_path, current_controller, state, remainder[1:])
|
||||||
|
|
||||||
|
#dispatch not found
|
||||||
|
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||||
|
|
||||||
|
def _setup_wsgiorg_routing_args(self, url_path, remainder, params):
|
||||||
|
"""
|
||||||
|
This is expected to be overridden by any subclass that wants to set
|
||||||
|
the routing_args (RestController). Do not delete.
|
||||||
|
"""
|
43
crank/restcontroller.py
Normal file
43
crank/restcontroller.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
"""
|
||||||
|
import web.core
|
||||||
|
from restdispatcher import RestDispatcher
|
||||||
|
from dispatcher import DispatchState
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
class RestController(RestDispatcher):
|
||||||
|
|
||||||
|
def __call__(self, *args, **kw):
|
||||||
|
verb = kw.get('_method', None)
|
||||||
|
|
||||||
|
request = web.core.request
|
||||||
|
url_path = '/'.join(args)
|
||||||
|
state = DispatchState(url_path, kw)
|
||||||
|
state.request = request
|
||||||
|
state.add_controller('/', self)
|
||||||
|
state.dispatcher = self
|
||||||
|
state = state.controller._dispatch(state, args)
|
||||||
|
|
||||||
|
verb = kw.pop('_verb', request.method).lower()
|
||||||
|
|
||||||
|
# attach the request to the controller for use without the
|
||||||
|
# cost of a SOP.
|
||||||
|
# also, save the dispatch state
|
||||||
|
try:
|
||||||
|
state.controller.request
|
||||||
|
state.controller.dispatch_state
|
||||||
|
except AttributeError:
|
||||||
|
state.controller.request = request
|
||||||
|
state.controller.dispatch_state = state
|
||||||
|
|
||||||
|
return state.method(*state.remainder, **kw)
|
||||||
|
|
||||||
|
def __before__(self, *args, **kw):
|
||||||
|
return (args, kw)
|
||||||
|
|
||||||
|
def __after__(self, result, *args, **kw):
|
||||||
|
"""The __after__ method can modify the value returned by the final method call."""
|
||||||
|
return result
|
||||||
|
|
||||||
|
index = __call__
|
||||||
|
__default__ = __call__
|
228
crank/restdispatcher.py
Normal file
228
crank/restdispatcher.py
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
"""
|
||||||
|
This module contains the RestDispatcher implementation
|
||||||
|
|
||||||
|
Rest controller provides a RESTful dispatch mechanism, and
|
||||||
|
combines controller decoration for TG-Controller behavior.
|
||||||
|
"""
|
||||||
|
from webob.exc import HTTPMethodNotAllowed
|
||||||
|
from objectdispatcher import ObjectDispatcher
|
||||||
|
|
||||||
|
class RestDispatcher(ObjectDispatcher):
|
||||||
|
"""Defines a restful interface for a set of HTTP verbs.
|
||||||
|
Please see RestController for a rundown of the controller
|
||||||
|
methods used.
|
||||||
|
"""
|
||||||
|
def _setup_wsgiorg_routing_args(self, url_path, remainder, params):
|
||||||
|
pass
|
||||||
|
#request.environ['wsgiorg.routing_args'] = (tuple(remainder), params)
|
||||||
|
|
||||||
|
def _handle_put_or_post(self, method, state, remainder):
|
||||||
|
current_controller = state.controller
|
||||||
|
if remainder:
|
||||||
|
current_path = remainder[0]
|
||||||
|
if self._is_exposed(current_controller, current_path):
|
||||||
|
state.add_method(getattr(current_controller, current_path), remainder[1:])
|
||||||
|
return state
|
||||||
|
|
||||||
|
if self._is_controller(current_controller, current_path):
|
||||||
|
current_controller = getattr(current_controller, current_path)
|
||||||
|
return self._dispatch_controller(current_path, current_controller, state, remainder[1:])
|
||||||
|
|
||||||
|
method_name = method
|
||||||
|
method = self._find_first_exposed(current_controller, [method,])
|
||||||
|
if method and self._method_matches_args(method, state, remainder):
|
||||||
|
state.add_method(method, remainder)
|
||||||
|
return state
|
||||||
|
|
||||||
|
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||||
|
|
||||||
|
def _handle_delete(self, method, state, remainder):
|
||||||
|
current_controller = state.controller
|
||||||
|
method_name = method
|
||||||
|
method = self._find_first_exposed(current_controller, ('post_delete', 'delete'))
|
||||||
|
|
||||||
|
if method and self._method_matches_args(method, state, remainder):
|
||||||
|
state.add_method(method, remainder)
|
||||||
|
return state
|
||||||
|
|
||||||
|
#you may not send a delete request to a non-delete function
|
||||||
|
if remainder and self._is_exposed(current_controller, remainder[0]):
|
||||||
|
raise HTTPMethodNotAllowed
|
||||||
|
|
||||||
|
# there might be a sub-controller with a delete method, let's go see
|
||||||
|
if remainder:
|
||||||
|
sub_controller = getattr(current_controller, remainder[0], None)
|
||||||
|
if sub_controller:
|
||||||
|
remainder = remainder[1:]
|
||||||
|
state.current_controller = sub_controller
|
||||||
|
state.url_path = '/'.join(remainder)
|
||||||
|
r = self._dispatch_controller(state.url_path, sub_controller, state, remainder)
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||||
|
|
||||||
|
def _check_for_sub_controllers(self, state, remainder):
|
||||||
|
current_controller = state.controller
|
||||||
|
method = None
|
||||||
|
for find in ('get_one', 'get'):
|
||||||
|
if hasattr(current_controller, find):
|
||||||
|
method = find
|
||||||
|
break
|
||||||
|
if method is None:
|
||||||
|
return
|
||||||
|
args = self._get_argspec(getattr(current_controller, method))
|
||||||
|
fixed_args = args[0][1:]
|
||||||
|
fixed_arg_length = len(fixed_args)
|
||||||
|
var_args = args[1]
|
||||||
|
if var_args:
|
||||||
|
for i, item in enumerate(remainder):
|
||||||
|
if hasattr(current_controller, item) and self._is_controller(current_controller, item):
|
||||||
|
current_controller = getattr(current_controller, item)
|
||||||
|
state.add_routing_args(item, remainder[:i], fixed_args, var_args)
|
||||||
|
return self._dispatch_controller(item, current_controller, state, remainder[i+1:])
|
||||||
|
elif fixed_arg_length< len(remainder) and hasattr(current_controller, remainder[fixed_arg_length]):
|
||||||
|
item = remainder[fixed_arg_length]
|
||||||
|
if hasattr(current_controller, item):
|
||||||
|
if self._is_controller(current_controller, item):
|
||||||
|
state.add_routing_args(item, remainder, fixed_args, var_args)
|
||||||
|
return self._dispatch_controller(item, getattr(current_controller, item), state, remainder[fixed_arg_length+1:])
|
||||||
|
|
||||||
|
def _handle_delete_edit_or_new(self, state, remainder):
|
||||||
|
method_name = remainder[-1]
|
||||||
|
if method_name not in ('new', 'edit', 'delete'):
|
||||||
|
return
|
||||||
|
if method_name == 'delete':
|
||||||
|
method_name = 'get_delete'
|
||||||
|
|
||||||
|
current_controller = state.controller
|
||||||
|
|
||||||
|
if self._is_exposed(current_controller, method_name):
|
||||||
|
method = getattr(current_controller, method_name)
|
||||||
|
new_remainder = remainder[:-1]
|
||||||
|
if method and self._method_matches_args(method, state, new_remainder):
|
||||||
|
state.add_method(method, new_remainder)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _handle_custom_get(self, state, remainder):
|
||||||
|
method_name = remainder[-1]
|
||||||
|
if method_name not in getattr(self, '_custom_actions', []):
|
||||||
|
return
|
||||||
|
|
||||||
|
current_controller = state.controller
|
||||||
|
|
||||||
|
if (self._is_exposed(current_controller, method_name) or
|
||||||
|
self._is_exposed(current_controller, 'get_%s' % method_name)):
|
||||||
|
method = self._find_first_exposed(current_controller, ('get_%s' % method_name, method_name))
|
||||||
|
new_remainder = remainder[:-1]
|
||||||
|
if method and self._method_matches_args(method, state, new_remainder):
|
||||||
|
state.add_method(method, new_remainder)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _handle_custom_method(self, method, state, remainder):
|
||||||
|
current_controller = state.controller
|
||||||
|
method_name = method
|
||||||
|
method = self._find_first_exposed(current_controller, ('post_%s' % method_name, method_name))
|
||||||
|
|
||||||
|
if method and self._method_matches_args(method, state, remainder):
|
||||||
|
state.add_method(method, remainder)
|
||||||
|
return state
|
||||||
|
|
||||||
|
#you may not send a delete request to a non-delete function
|
||||||
|
if remainder and self._is_exposed(current_controller, remainder[0]):
|
||||||
|
raise HTTPMethodNotAllowed
|
||||||
|
|
||||||
|
# there might be a sub-controller with a delete method, let's go see
|
||||||
|
if remainder:
|
||||||
|
sub_controller = getattr(current_controller, remainder[0], None)
|
||||||
|
if sub_controller:
|
||||||
|
remainder = remainder[1:]
|
||||||
|
state.current_controller = sub_controller
|
||||||
|
state.url_path = '/'.join(remainder)
|
||||||
|
r = self._dispatch_controller(state.url_path, sub_controller, state, remainder)
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||||
|
|
||||||
|
def _handle_get(self, method, state, remainder):
|
||||||
|
current_controller = state.controller
|
||||||
|
if not remainder:
|
||||||
|
method = self._find_first_exposed(current_controller, ('get_all', 'get'))
|
||||||
|
if method:
|
||||||
|
state.add_method(method, remainder)
|
||||||
|
return state
|
||||||
|
if self._is_exposed(current_controller, 'get_one'):
|
||||||
|
method = current_controller.get_one
|
||||||
|
if method and self._method_matches_args(method, state, remainder):
|
||||||
|
state.add_method(method, remainder)
|
||||||
|
return state
|
||||||
|
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||||
|
|
||||||
|
#test for "delete", "edit" or "new"
|
||||||
|
r = self._handle_delete_edit_or_new(state, remainder)
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
|
||||||
|
#test for custom REST-like attribute
|
||||||
|
r = self._handle_custom_get(state, remainder)
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
|
||||||
|
current_path = remainder[0]
|
||||||
|
if self._is_exposed(current_controller, current_path):
|
||||||
|
state.add_method(getattr(current_controller, current_path), remainder[1:])
|
||||||
|
return state
|
||||||
|
|
||||||
|
if self._is_controller(current_controller, current_path):
|
||||||
|
current_controller = getattr(current_controller, current_path)
|
||||||
|
return self._dispatch_controller(current_path, current_controller, state, remainder[1:])
|
||||||
|
|
||||||
|
if self._is_exposed(current_controller, 'get_one') or self._is_exposed(current_controller, 'get'):
|
||||||
|
|
||||||
|
if self._is_exposed(current_controller, 'get_one'):
|
||||||
|
method = current_controller.get_one
|
||||||
|
else:
|
||||||
|
method = current_controller.get
|
||||||
|
|
||||||
|
if method and self._method_matches_args(method, state, remainder):
|
||||||
|
state.add_method(method, remainder)
|
||||||
|
return state
|
||||||
|
|
||||||
|
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||||
|
|
||||||
|
_handler_lookup = {
|
||||||
|
'put':_handle_put_or_post,
|
||||||
|
'post':_handle_put_or_post,
|
||||||
|
'delete':_handle_delete,
|
||||||
|
'get':_handle_get,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _dispatch(self, state, remainder):
|
||||||
|
"""returns: populated DispachState object
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.debug('Entering dispatch for remainder: %s in controller %s'%(remainder, self))
|
||||||
|
if not hasattr(state, 'http_method'):
|
||||||
|
method = state.request.method.lower()
|
||||||
|
params = state.params
|
||||||
|
|
||||||
|
#conventional hack for handling methods which are not supported by most browsers
|
||||||
|
request_method = params.get('_method', None)
|
||||||
|
if request_method:
|
||||||
|
request_method = request_method.lower()
|
||||||
|
#make certain that DELETE and PUT requests are not sent with GET
|
||||||
|
if method == 'get' and request_method == 'put':
|
||||||
|
raise HTTPMethodNotAllowed
|
||||||
|
if method == 'get' and request_method == 'delete':
|
||||||
|
raise HTTPMethodNotAllowed
|
||||||
|
method = request_method
|
||||||
|
state.http_method = method
|
||||||
|
|
||||||
|
r = self._check_for_sub_controllers(state, remainder)
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
|
||||||
|
if state.http_method in self._handler_lookup.keys():
|
||||||
|
r = self._handler_lookup[state.http_method](self, state.http_method, state, remainder)
|
||||||
|
else:
|
||||||
|
r = self._handle_custom_method(state.http_method, state, remainder)
|
||||||
|
return r
|
67
crank/util.py
Normal file
67
crank/util.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
This is the main dispatcher module.
|
||||||
|
|
||||||
|
Dispatch works as follows:
|
||||||
|
Start at the RootController, the root controller must
|
||||||
|
have a _dispatch function, which defines how we move
|
||||||
|
from object to 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 anther 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
class odict(dict):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
self._ordering = []
|
||||||
|
dict.__init__(self, *args, **kw)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self._ordering.append(key)
|
||||||
|
dict.__setitem__(self, key, value)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return self._ordering
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self._ordering = []
|
||||||
|
dict.clear(self)
|
||||||
|
|
||||||
|
def getitem(self, n):
|
||||||
|
return self[self._ordering[n]]
|
||||||
|
|
||||||
|
def __slice__(self, a, b, n):
|
||||||
|
return self.values()[a:b:n]
|
||||||
|
|
||||||
|
def iteritems(self):
|
||||||
|
for item in self._ordering:
|
||||||
|
yield item, self[item]
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return [i for i in self.iteritems()]
|
||||||
|
|
||||||
|
def itervalues(self):
|
||||||
|
for item in self._ordering:
|
||||||
|
yield self[item]
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
return [i for i in self.values()]
|
||||||
|
|
||||||
|
def __delete__(self, key):
|
||||||
|
self._ordering.remove(key)
|
||||||
|
dict.__delete__(self, key)
|
||||||
|
|
||||||
|
def pop(self):
|
||||||
|
item = self._ordering[-1]
|
||||||
|
del self[item]
|
||||||
|
self._ordering.remove(item)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.items())
|
||||||
|
|
3
setup.cfg
Normal file
3
setup.cfg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[egg_info]
|
||||||
|
tag_build = dev
|
||||||
|
tag_svn_revision = true
|
26
setup.py
Normal file
26
setup.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from setuptools import setup, find_packages
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
version = '0.1a1'
|
||||||
|
|
||||||
|
setup(name='crank',
|
||||||
|
version=version,
|
||||||
|
description="Generalization of dispatch mechanism for use across frameworks.",
|
||||||
|
long_description="""\
|
||||||
|
""",
|
||||||
|
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||||
|
keywords='',
|
||||||
|
author='Christopher Perkins',
|
||||||
|
author_email='chris@percious.com',
|
||||||
|
url='',
|
||||||
|
license='MIT',
|
||||||
|
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
|
||||||
|
include_package_data=True,
|
||||||
|
zip_safe=True,
|
||||||
|
install_requires=[
|
||||||
|
# -*- Extra requirements: -*-
|
||||||
|
],
|
||||||
|
entry_points="""
|
||||||
|
# -*- Entry points: -*-
|
||||||
|
""",
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user