Skip to content

Instantly share code, notes, and snippets.

@kurtbrose
Created November 17, 2012 00:16
Show Gist options
  • Save kurtbrose/4092110 to your computer and use it in GitHub Desktop.
Save kurtbrose/4092110 to your computer and use it in GitHub Desktop.
middleware.py
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')
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