initial framework for crank, no tests yet

This commit is contained in:
percious 2010-03-12 14:55:06 -05:00
commit bfc9714eda
10 changed files with 775 additions and 0 deletions

15
.hgignore Normal file
View 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
View File

160
crank/dispatcher.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
[egg_info]
tag_build = dev
tag_svn_revision = true

26
setup.py Normal file
View 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: -*-
""",
)