Created
November 17, 2012 00:16
-
-
Save kurtbrose/4092110 to your computer and use it in GitHub Desktop.
middleware.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import inspect, types | |
AUTO = ['req', 'request', 'next', 'context', 'ctx'] | |
class Middleware(object): | |
provides = () | |
endpoint = None | |
render = None | |
request = None | |
def get_requirements(middleware): | |
reqs = [] | |
if middleware.request: | |
reqs += inspect.getargspec(middleware.request)[0][1:] | |
if middleware.endpoint: | |
reqs += inspect.getargspec(middleware.endpoint)[0][1:] | |
if middleware.render: | |
reqs += inspect.getargspec(middleware.render)[0][1:] | |
return set(reqs) | |
def inject(f, injectables): | |
arg_names, _, _, defaults = inspect.getargspec(f) | |
if defaults: | |
defaults = dict(reversed(zip(reversed(arg_names), reversed(defaults)))) | |
else: | |
defaults = {} | |
if isinstance(f, types.MethodType): | |
arg_names = arg_names[1:] #throw away "self" | |
args = {} | |
for n in arg_names: | |
if n in injectables: | |
args[n] = injectables[n] | |
else: | |
args[n] = defaults[n] | |
return f(**args) | |
def check_stack(resources, middlewares, endpoint): | |
#step 0: check that it doesn't overlap with automatic variables | |
#step 1: check for overlaps | |
provided_by = dict([(n, ["resources"]) for n in resources]) | |
#TODO: something better than "resources" for application provided things? | |
for mw in middlewares: | |
for p in mw.provides: | |
provided_by.setdefault(p, []).append(mw) | |
for k in provided_by: #length one things are okay, delete them | |
if len(provided_by[k]) == 1: | |
del provided_by[k] | |
if provided_by: #anything left is provided from more than one place | |
raise Exception(repr(provided_by)) #TODO: better error message/type | |
sofar = set(resources + AUTO) | |
for mw in middlewares: | |
unfulfilled = get_requirements(mw) - sofar | |
if unfulfilled: | |
#TODO: better error message | |
raise Exception('requirements not met: '+",".join(unfulfilled)) | |
sofar.update(set(mw.provides)) | |
unfulfilled = set(inspect.getargspec(endpoint)[0]) - sofar | |
#TODO: ignore things with default for unfulfilled | |
if unfulfilled: | |
raise Exception('endpoint requirements not met: '+','.join(unfulfilled)) | |
def make_mw_stack(resource_list, middlewares, endpoint, render): | |
#first step, check consistency of provides and requires | |
check_stack(resource_list, middlewares, endpoint) | |
#second step, create the callable that actually binds these things in | |
def execute(request, resource_dict): | |
injectables = {'req':request, 'request':request} | |
injectables.update(resource_dict) | |
def exec_request(i): | |
if i == len(middlewares): | |
ctx = exec_endpoint(0) | |
injectables['context'] = ctx | |
injectables['ctx'] = ctx | |
return exec_render(0) | |
cur = middlewares[i] | |
if not cur.request: | |
return exec_request(i+1) #skip if cur has no request function | |
def next(*a, **kw): | |
injectables.update(resolve_call_args("next", cur.provides, a, kw)) | |
return exec_request(i+1) | |
injectables['next'] = next | |
return inject(cur.request, injectables) | |
def exec_endpoint(i): | |
if i == len(middlewares): | |
return inject(endpoint, injectables) | |
cur = middlewares[i] | |
if not cur.endpoint: | |
return exec_endpoint(i+1) | |
injectables['next'] = lambda: exec_endpoint(i+1) | |
return inject(cur.exec_endpoint, injectables) | |
def exec_render(i): | |
if i == len(middlewares): | |
return inject(render, injectables) | |
cur = middlewares[i] | |
if not cur.render: | |
return exec_render(i+1) | |
def next(ctx): | |
injectables.update({'ctx':ctx, 'context':ctx}) | |
return exec_render(i+1) | |
injectables['next'] = next | |
return inject(cur.render, injectables) | |
return exec_request(0) | |
return execute | |
def resolve_call_args(fname, arg_list, args, kwargs): | |
pos_args = arg_list[:len(args)] | |
#check that no arguments are double specified in positional and keyword | |
for arg in pos_args: | |
if arg in kwargs: | |
raise TypeError(fname+" got multiple values for keyword argument: '"+arg+"'") | |
#check not too many arguments | |
if len(args) > len(pos_args): | |
raise TypeError(fname+" takes exactly " + str(len(pos_args)) + \ | |
" arguments (" + str(len(args)) + " given) ") | |
#check no unexpected keyword arguments | |
non_pos_argset = set(arg_list[len(args):]) | |
for k in kwargs: | |
if k not in non_pos_argset: | |
raise TypeError(fname+" got an unexpected keyword argument '"+k+"'") | |
#assign positional arguments to dictionary | |
arg_dict = dict(zip(pos_args, args)) | |
#assign keyword arguments to dictionary | |
arg_dict.update(kwargs) | |
return arg_dict | |
def test(): | |
class Producer(object): | |
provides = ('a') | |
def request(self, next): | |
print "Producer" | |
return next(1) | |
class Consumer(object): | |
provides = () | |
def request(self, next, a): | |
print "Consumer", a | |
return next() | |
def endpoint(): print "endpoint" | |
make_mw_stack({}, [Producer(), Consumer()], endpoint)('IOU_request') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import inspect, types | |
def chain_argspec(func_list, provides): | |
provided_sofar = set(['next']) #'next' is an extremely special case | |
optional_sofar = set() | |
required_sofar = set() | |
for f, p in zip(func_list, provides): | |
#middlewares can default the same parameter to different values; | |
# can't properly keep track of default values | |
arg_names, _, _, defaults = inspect.getargspec(f) | |
if isinstance(f, types.MethodType): | |
arg_names = arg_names[1:] | |
def_offs = -len(defaults) if defaults else None | |
undefaulted, defaulted = arg_names[:def_offs], arg_names[def_offs:] | |
optional_sofar.update(defaulted) | |
#keep track of defaults so that e.g. endpoint default param can pick up | |
#request injected/provided param | |
required_sofar |= set(undefaulted) - provided_sofar | |
provided_sofar.update(p) | |
#detect error, double provided? (maybe easier as separate step) | |
return required_sofar, optional_sofar | |
#NOTE: checking for double provided variables is assumed to already be done | |
#NOTE: any "hoisting" of middlewares / removing of duplicates should be done before this function | |
def make_middleware_chain(middlewares, endpoint, render, provided): | |
endpoints = [(mw.endpoint, mw.endpoint_provides) | |
for mw in middlewares if mw.endpoint] | |
renders = [mw.render for mw in middlewares if mw.render] | |
requests = [(mw.request, mw.provides) for mw in middlewares if mw.request] | |
#maybe there aren't and endpoints or requests functions defined at all | |
if endpoints: | |
endpoints, endpoints_provides = zip(*endpoints) | |
else: | |
endpoints_provides = [] | |
if requests: | |
requests, requests_provides = zip(*requests) | |
else: | |
requests_provides = [] | |
request_params, request_optional =\ | |
chain_argspec(requests, requests_provides) | |
endpoint_params, endpoint_optional =\ | |
chain_argspec(list(endpoints)+[endpoint], list(endpoints_provides)+[()]) | |
renders_params, renders_optional =\ | |
chain_argspec(list(renders)+[render], [('ctx',)]*(len(middlewares)+1)) | |
available_params = set(sum(map(tuple, requests_provides), ()) + tuple(provided)) | |
if endpoint_params - available_params: | |
raise Exception("unresolved endpoint resources") | |
if request_params - set(provided): | |
raise Exception("unresolved request resources") | |
if renders_params - available_params - set(['ctx']): | |
raise Exception("unresolved renders resources") | |
#add provided optional parametes back into actual parameters | |
endpoint_params |= available_params & endpoint_optional | |
renders_params |= available_params & renders_optional | |
endpoint = make_chain( | |
list(endpoints)+[endpoint], | |
[endpoint_params]+[mw.endpoint_provides for mw in middlewares]) | |
render = make_chain( | |
list(renders)+[render], | |
[renders_params]+[('ctx',)]*len(middlewares)) | |
def named_arg_str(args): return ','.join([a+'='+a for a in args]) | |
inner_code = \ | |
'def inner('+','.join(endpoint_params|renders_params-set(['ctx']))+'):\n'+\ | |
' ctx = endpoint('+named_arg_str(endpoint_params)+')\n'+\ | |
' resp = render('+named_arg_str(renders_params)+')\n'+\ | |
' return resp' | |
#TODO: figure out a way to update so that both ctx and context will stay in synch | |
# (maybe inject lines into return of make_call_str?) | |
# problem is, 'ctx' will update but 'context' will stay the same | |
d = {'endpoint':endpoint, 'render':render} | |
exec compile(inner_code, '<string>', 'single') in d | |
mw_exec = make_chain( | |
list(requests)+[d['inner']], [request_params]+list(requests_provides)) | |
return mw_exec | |
def make_chain(funcs, params): | |
code = compile(make_call_str(funcs, params), '<string>', 'single') | |
print make_call_str(funcs, params) | |
d = {'funcs':funcs} | |
exec code in d | |
return d['next'] | |
#funcs[0] = function to call | |
#params[0] = parameters to take | |
def make_call_str(funcs, params, params_sofar=None, level=0): | |
if not funcs: | |
return '' #stopping case | |
if params_sofar is None: | |
params_sofar = set(['next']) | |
params_sofar.update(params[0]) | |
next_args = inspect.getargspec(funcs[0])[0] | |
if isinstance(funcs[0], types.MethodType): | |
next_args = next_args[1:] | |
next_args = ','.join([a+'='+a for a in next_args if a in params_sofar]) | |
return ' '*level +'def next('+','.join(params[0])+'):\n'+\ | |
make_call_str(funcs[1:], params[1:], params_sofar, level+1)+\ | |
' '*(level+1)+'return funcs['+str(level)+']('+next_args+')\n' | |
class Middleware(object): | |
provides = () | |
endpoint_provides = () | |
request = None | |
endpoint = None | |
render = None | |
def test(): | |
def a(next,b,c): | |
return next(d=1,e=2) | |
def f(b, e): | |
return "HELLO WORLD" | |
s = make_call_str( (a,f), (('b','c'),('d','e')) ) | |
print s | |
return s | |
def test2(): | |
class MW1(Middleware): | |
provides = ('a','b') | |
endpoint_provides = ('c',) | |
def request(self, next): return next(1,2) | |
def endpoint(self, next, b): return next(2) | |
def render(self, next, ctx): return next(ctx) | |
class MW2(Middleware): | |
def request(self, next, a): | |
print 'MW2 a=', a | |
return next() | |
def endpoint(b, c): | |
print 'endpoint b=', b, 'c=',c | |
return 'ctx' | |
def render(ctx): | |
print 'render', ctx | |
return ctx | |
make_middleware_chain([MW1(), MW2()], endpoint, render, ())() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment