Add support for Pecan *without* thread local request/response objects

Change-Id: I5a5a05e1f57ef2d8ad64e925c7ffa6907b914273
This commit is contained in:
Ryan Petrello
2014-05-19 14:10:17 -04:00
parent 9bc09ea9c6
commit e93b929edf
9 changed files with 1650 additions and 129 deletions

View File

@@ -0,0 +1,55 @@
.. _contextlocals:
Context/Thread-Locals vs. Explicit Argument Passing
===================================================
In any pecan application, the module-level ``pecan.request`` and
``pecan.response`` are proxy objects that always refer to the request and
response being handled in the current thread.
This `thread locality` ensures that you can safely access a global reference to
the current request and response in a multi-threaded environment without
constantly having to pass object references around in your code; it's a feature
of pecan that makes writing traditional web applications easier and less
verbose.
Some people feel thread-locals are too implicit or magical, and that explicit
reference passing is much clearer and more maintainable in the long run.
Additionally, the default implementation provided by pecan uses
:func:`threading.local` to associate these context-local proxy objects with the
`thread identifier` of the current server thread. In asynchronous server
models - where lots of tasks run for short amounts of time on
a `single` shared thread - supporting this mechanism involves monkeypatching
:func:`threading.local` to behave in a greenlet-local manner.
Disabling Thread-Local Proxies
------------------------------
If you're certain that you `do not` want to utilize context/thread-locals in
your project, you can do so by passing the argument
``use_context_locals=False`` in your application's configuration file::
app = {
'root': 'project.controllers.root.RootController',
'modules': ['project'],
'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/project/templates',
'debug': True,
'use_context_locals': False
}
Additionally, you'll need to update **all** of your pecan controllers to accept
positional arguments for the current request and response::
class RootController(object):
@pecan.expose('json')
def index(self, req, resp):
return dict(method=req.method) # path: /
@pecan.expose()
def greet(self, req, resp, name):
return name # path: /greet/joe
It is *imperative* that the request and response arguments come **after**
``self`` and before any positional form arguments.

View File

@@ -38,6 +38,7 @@ Narrative Documentation
secure_controller.rst secure_controller.rst
hooks.rst hooks.rst
jsonify.rst jsonify.rst
contextlocals.rst
commands.rst commands.rst
development.rst development.rst
deployment.rst deployment.rst

View File

@@ -270,11 +270,11 @@ a :func:`_route` method will enable you to have total control.
Interacting with the Request and Response Object Interacting with the Request and Response Object
------------------------------------------------ ------------------------------------------------
For every HTTP request, Pecan maintains a thread-local reference to the request For every HTTP request, Pecan maintains a :ref:`thread-local reference
and response object, ``pecan.request`` and ``pecan.response``. These are <contextlocals>` to the request and response object, ``pecan.request`` and
instances of :class:`webob.request.BaseRequest` and ``pecan.response``. These are instances of :class:`webob.request.BaseRequest`
:class:`webob.response.Response`, respectively, and can be interacted with from and :class:`webob.response.Response`, respectively, and can be interacted with
within Pecan controller code:: from within Pecan controller code::
@pecan.expose() @pecan.expose()
def login(self): def login(self):

View File

@@ -27,10 +27,31 @@ state = None
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RoutingState(object):
def __init__(self, request, response, app, hooks=[], controller=None):
self.request = request
self.response = response
self.app = app
self.hooks = hooks
self.controller = controller
def proxy(key): def proxy(key):
class ObjectProxy(object): class ObjectProxy(object):
explanation_ = AttributeError(
"`pecan.state` is not bound to a context-local context.\n"
"Ensure that you're accessing `pecan.request` or `pecan.response` "
"from within the context of a WSGI `__call__` and that "
"`use_context_locals` = True."
)
def __getattr__(self, attr): def __getattr__(self, attr):
obj = getattr(state, key) try:
obj = getattr(state, key)
except AttributeError:
raise self.explanation_
return getattr(obj, attr) return getattr(obj, attr)
def __setattr__(self, attr, value): def __setattr__(self, attr, value):
@@ -87,7 +108,7 @@ def abort(status_code=None, detail='', headers=None, comment=None, **kw):
def redirect(location=None, internal=False, code=None, headers={}, def redirect(location=None, internal=False, code=None, headers={},
add_slash=False): add_slash=False, request=None):
''' '''
Perform a redirect, either internal or external. An internal redirect Perform a redirect, either internal or external. An internal redirect
performs the redirect server-side, while the external redirect utilizes performs the redirect server-side, while the external redirect utilizes
@@ -99,12 +120,14 @@ def redirect(location=None, internal=False, code=None, headers={},
:param code: The HTTP status code to use for the redirect. Defaults to 302. :param code: The HTTP status code to use for the redirect. Defaults to 302.
:param headers: Any HTTP headers to send with the response, as a :param headers: Any HTTP headers to send with the response, as a
dictionary. dictionary.
:param request: The :class:`webob.request.BaseRequest` instance to use.
''' '''
request = request or state.request
if add_slash: if add_slash:
if location is None: if location is None:
split_url = list(urlparse.urlsplit(state.request.url)) split_url = list(urlparse.urlsplit(request.url))
new_proto = state.request.environ.get( new_proto = request.environ.get(
'HTTP_X_FORWARDED_PROTO', split_url[0] 'HTTP_X_FORWARDED_PROTO', split_url[0]
) )
split_url[0] = new_proto split_url[0] = new_proto
@@ -126,7 +149,7 @@ def redirect(location=None, internal=False, code=None, headers={},
raise exc.status_map[code](location=location, headers=headers) raise exc.status_map[code](location=location, headers=headers)
def render(template, namespace): def render(template, namespace, app=None):
''' '''
Render the specified template using the Pecan rendering framework Render the specified template using the Pecan rendering framework
with the specified template namespace as a dictionary. Useful in a with the specified template namespace as a dictionary. Useful in a
@@ -136,9 +159,10 @@ def render(template, namespace):
``@expose``. ``@expose``.
:param namespace: The namespace to use for rendering the template, as a :param namespace: The namespace to use for rendering the template, as a
dictionary. dictionary.
:param app: The instance of :class:`pecan.Pecan` to use
''' '''
app = app or state.app
return state.app.render(template, namespace) return app.render(template, namespace)
def load_app(config, **kwargs): def load_app(config, **kwargs):
@@ -165,31 +189,7 @@ def load_app(config, **kwargs):
) )
class Pecan(object): class PecanBase(object):
'''
Base Pecan application object. Generally created using ``pecan.make_app``,
rather than being created manually.
Creates a Pecan application instance, which is a WSGI application.
:param root: A string representing a root controller object (e.g.,
"myapp.controller.root.RootController")
:param default_renderer: The default template rendering engine to use.
Defaults to mako.
:param template_path: A relative file system path (from the project root)
where template files live. Defaults to 'templates'.
:param hooks: A callable which returns a list of
:class:`pecan.hooks.PecanHook`
:param custom_renderers: Custom renderer objects, as a dictionary keyed
by engine name.
:param extra_template_vars: Any variables to inject into the template
namespace automatically.
:param force_canonical: A boolean indicating if this project should
require canonical URLs.
:param guess_content_type_from_ext: A boolean indicating if this project
should use the extension in the URL for guessing
the content type to return.
'''
SIMPLEST_CONTENT_TYPES = ( SIMPLEST_CONTENT_TYPES = (
['text/html'], ['text/html'],
@@ -201,9 +201,6 @@ class Pecan(object):
custom_renderers={}, extra_template_vars={}, custom_renderers={}, extra_template_vars={},
force_canonical=True, guess_content_type_from_ext=True, force_canonical=True, guess_content_type_from_ext=True,
context_local_factory=None, **kw): context_local_factory=None, **kw):
self.init_context_local(context_local_factory)
if isinstance(root, six.string_types): if isinstance(root, six.string_types):
root = self.__translate_root__(root) root = self.__translate_root__(root)
@@ -223,12 +220,6 @@ class Pecan(object):
self.force_canonical = force_canonical self.force_canonical = force_canonical
self.guess_content_type_from_ext = guess_content_type_from_ext self.guess_content_type_from_ext = guess_content_type_from_ext
def init_context_local(self, local_factory):
global state
if local_factory is None:
from threading import local as local_factory
state = local_factory()
def __translate_root__(self, item): def __translate_root__(self, item):
''' '''
Creates a root controller instance from a string root, e.g., Creates a root controller instance from a string root, e.g.,
@@ -259,10 +250,9 @@ class Pecan(object):
:param node: The node, such as a root controller object. :param node: The node, such as a root controller object.
:param path: The path to look up on this node. :param path: The path to look up on this node.
''' '''
path = path.split('/')[1:] path = path.split('/')[1:]
try: try:
node, remainder = lookup_controller(node, path) node, remainder = lookup_controller(node, path, req)
return node, remainder return node, remainder
except NonCanonicalPath as e: except NonCanonicalPath as e:
if self.force_canonical and \ if self.force_canonical and \
@@ -276,7 +266,7 @@ class Pecan(object):
(req.pecan['routing_path'], (req.pecan['routing_path'],
req.pecan['routing_path']) req.pecan['routing_path'])
) )
redirect(code=302, add_slash=True) redirect(code=302, add_slash=True, request=req)
return e.controller, e.remainder return e.controller, e.remainder
def determine_hooks(self, controller=None): def determine_hooks(self, controller=None):
@@ -299,7 +289,7 @@ class Pecan(object):
) )
return self.hooks return self.hooks
def handle_hooks(self, hook_type, *args): def handle_hooks(self, hooks, hook_type, *args):
''' '''
Processes hooks of the specified type. Processes hooks of the specified type.
@@ -307,10 +297,8 @@ class Pecan(object):
``on_error``, and ``on_route``. ``on_error``, and ``on_route``.
:param \*args: Arguments to pass to the hooks. :param \*args: Arguments to pass to the hooks.
''' '''
if hook_type in ['before', 'on_route']: if hook_type not in ['before', 'on_route']:
hooks = state.hooks hooks = reversed(hooks)
else:
hooks = reversed(state.hooks)
for hook in hooks: for hook in hooks:
result = getattr(hook, hook_type)(*args) result = getattr(hook, hook_type)(*args)
@@ -319,14 +307,15 @@ class Pecan(object):
if hook_type == 'on_error' and isinstance(result, Response): if hook_type == 'on_error' and isinstance(result, Response):
return result return result
def get_args(self, pecan_state, all_params, remainder, argspec, im_self): def get_args(self, state, all_params, remainder, argspec, im_self):
''' '''
Determines the arguments for a controller based upon parameters Determines the arguments for a controller based upon parameters
passed the argument specification for the controller. passed the argument specification for the controller.
''' '''
args = [] args = []
kwargs = dict() kwargs = dict()
valid_args = argspec[0][1:] valid_args = argspec.args[1:] # pop off `self`
pecan_state = state.request.pecan
def _decode(x): def _decode(x):
return unquote_plus(x) if isinstance(x, six.string_types) \ return unquote_plus(x) if isinstance(x, six.string_types) \
@@ -392,13 +381,12 @@ class Pecan(object):
template = template.split(':')[1] template = template.split(':')[1]
return renderer.render(template, namespace) return renderer.render(template, namespace)
def handle_request(self, req, resp): def find_controller(self, state):
''' '''
The main request handler for Pecan applications. The main request handler for Pecan applications.
''' '''
# get a sorted list of hooks, by priority (no controller hooks yet) # get a sorted list of hooks, by priority (no controller hooks yet)
state.hooks = self.hooks req = state.request
pecan_state = req.pecan pecan_state = req.pecan
# store the routing path for the current application to allow hooks to # store the routing path for the current application to allow hooks to
@@ -406,7 +394,7 @@ class Pecan(object):
pecan_state['routing_path'] = path = req.encget('PATH_INFO') pecan_state['routing_path'] = path = req.encget('PATH_INFO')
# handle "on_route" hooks # handle "on_route" hooks
self.handle_hooks('on_route', state) self.handle_hooks(self.hooks, 'on_route', state)
# lookup the controller, respecting content-type as requested # lookup the controller, respecting content-type as requested
# by the file extension on the URI # by the file extension on the URI
@@ -491,11 +479,8 @@ class Pecan(object):
) )
raise exc.HTTPNotFound raise exc.HTTPNotFound
# get a sorted list of hooks, by priority
state.hooks = self.determine_hooks(controller)
# handle "before" hooks # handle "before" hooks
self.handle_hooks('before', state) self.handle_hooks(self.determine_hooks(controller), 'before', state)
# fetch any parameters # fetch any parameters
if req.method == 'GET': if req.method == 'GET':
@@ -505,13 +490,25 @@ class Pecan(object):
# fetch the arguments for the controller # fetch the arguments for the controller
args, kwargs = self.get_args( args, kwargs = self.get_args(
pecan_state, state,
params, params,
remainder, remainder,
cfg['argspec'], cfg['argspec'],
im_self im_self
) )
return controller, args, kwargs
def invoke_controller(self, controller, args, kwargs, state):
'''
The main request handler for Pecan applications.
'''
cfg = _cfg(controller)
content_types = cfg.get('content_types', {})
req = state.request
resp = state.response
pecan_state = req.pecan
# get the result from the controller # get the result from the controller
result = controller(*args, **kwargs) result = controller(*args, **kwargs)
@@ -570,11 +567,10 @@ class Pecan(object):
''' '''
# create the request and response object # create the request and response object
state.request = req = Request(environ) req = Request(environ)
state.response = resp = Response() resp = Response()
state.hooks = [] state = RoutingState(req, resp, self)
state.app = self controller = None
state.controller = None
# handle the request # handle the request
try: try:
@@ -582,7 +578,8 @@ class Pecan(object):
req.context = environ.get('pecan.recursive.context', {}) req.context = environ.get('pecan.recursive.context', {})
req.pecan = dict(content_type=None) req.pecan = dict(content_type=None)
self.handle_request(req, resp) controller, args, kwargs = self.find_controller(state)
self.invoke_controller(controller, args, kwargs, state)
except Exception as e: except Exception as e:
# if this is an HTTP Exception, set it as the response # if this is an HTTP Exception, set it as the response
if isinstance(e, exc.HTTPException): if isinstance(e, exc.HTTPException):
@@ -592,7 +589,12 @@ class Pecan(object):
# if this is not an internal redirect, run error hooks # if this is not an internal redirect, run error hooks
on_error_result = None on_error_result = None
if not isinstance(e, ForwardRequestException): if not isinstance(e, ForwardRequestException):
on_error_result = self.handle_hooks('on_error', state, e) on_error_result = self.handle_hooks(
self.determine_hooks(state.controller),
'on_error',
state,
e
)
# if the on_error handler returned a Response, use it. # if the on_error handler returned a Response, use it.
if isinstance(on_error_result, Response): if isinstance(on_error_result, Response):
@@ -602,15 +604,111 @@ class Pecan(object):
raise raise
finally: finally:
# handle "after" hooks # handle "after" hooks
self.handle_hooks('after', state) self.handle_hooks(
self.determine_hooks(state.controller), 'after', state
)
# get the response # get the response
return state.response(environ, start_response)
class ExplicitPecan(PecanBase):
def get_args(self, state, all_params, remainder, argspec, im_self):
# When comparing the argspec of the method to GET/POST params,
# ignore the implicit (req, resp) at the beginning of the function
# signature
signature_error = TypeError(
'When `use_context_locals` is `False`, pecan passes an explicit '
'reference to the request and response as the first two arguments '
'to the controller.\nChange the `%s.%s.%s` signature to accept '
'exactly 2 initial arguments (req, resp)' % (
state.controller.__self__.__class__.__module__,
state.controller.__self__.__class__.__name__,
state.controller.__name__
)
)
try: try:
return state.response(environ, start_response) positional = argspec.args[:]
positional.pop(1) # req
positional.pop(1) # resp
argspec = argspec._replace(args=positional)
except IndexError:
raise signature_error
args, kwargs = super(ExplicitPecan, self).get_args(
state, all_params, remainder, argspec, im_self
)
args = [state.request, state.response] + args
return args, kwargs
class Pecan(PecanBase):
'''
Pecan application object. Generally created using ``pecan.make_app``,
rather than being created manually.
Creates a Pecan application instance, which is a WSGI application.
:param root: A string representing a root controller object (e.g.,
"myapp.controller.root.RootController")
:param default_renderer: The default template rendering engine to use.
Defaults to mako.
:param template_path: A relative file system path (from the project root)
where template files live. Defaults to 'templates'.
:param hooks: A callable which returns a list of
:class:`pecan.hooks.PecanHook`
:param custom_renderers: Custom renderer objects, as a dictionary keyed
by engine name.
:param extra_template_vars: Any variables to inject into the template
namespace automatically.
:param force_canonical: A boolean indicating if this project should
require canonical URLs.
:param guess_content_type_from_ext: A boolean indicating if this project
should use the extension in the URL for guessing
the content type to return.
:param use_context_locals: When `True`, `pecan.request` and
`pecan.response` will be available as
thread-local references.
'''
def __new__(cls, *args, **kw):
if kw.get('use_context_locals') is False:
self = super(Pecan, cls).__new__(ExplicitPecan, *args, **kw)
self.__init__(*args, **kw)
return self
return super(Pecan, cls).__new__(cls)
def __init__(self, *args, **kw):
self.init_context_local(kw.get('context_local_factory'))
super(Pecan, self).__init__(*args, **kw)
def __call__(self, environ, start_response):
try:
state.hooks = []
state.app = self
state.controller = None
return super(Pecan, self).__call__(environ, start_response)
finally: finally:
# clean up state
del state.hooks del state.hooks
del state.request del state.request
del state.response del state.response
del state.controller del state.controller
del state.app del state.app
def init_context_local(self, local_factory):
global state
if local_factory is None:
from threading import local as local_factory
state = local_factory()
def find_controller(self, _state):
state.request = _state.request
state.response = _state.response
controller, args, kw = super(Pecan, self).find_controller(_state)
state.controller = controller
return controller, args, kw
def handle_hooks(self, hooks, *args, **kw):
state.hooks = hooks
return super(Pecan, self).handle_hooks(hooks, *args, **kw)

View File

@@ -4,7 +4,6 @@ from inspect import getmembers
from webob.exc import HTTPFound from webob.exc import HTTPFound
from .util import iscontroller, _cfg from .util import iscontroller, _cfg
from .routing import lookup_controller
__all__ = [ __all__ = [
'PecanHook', 'TransactionHook', 'HookController', 'PecanHook', 'TransactionHook', 'HookController',
@@ -334,8 +333,7 @@ class RequestViewerHook(PecanHook):
Specific to Pecan (not available in the request object) Specific to Pecan (not available in the request object)
''' '''
path = state.request.pecan['routing_path'].split('/')[1:] path = state.request.pecan['routing_path'].split('/')[1:]
controller, reminder = lookup_controller(state.app.root, path) return state.controller.__str__().split()[2]
return controller.__str__().split()[2]
def format_hooks(self, hooks): def format_hooks(self, hooks):
''' '''

View File

@@ -1,9 +1,10 @@
from inspect import getargspec, ismethod from inspect import getargspec, ismethod
import warnings
from webob import exc from webob import exc
import six import six
from .core import abort, request from .core import abort
from .decorators import expose from .decorators import expose
from .routing import lookup_controller, handle_lookup_traversal from .routing import lookup_controller, handle_lookup_traversal
from .util import iscontroller from .util import iscontroller
@@ -26,31 +27,41 @@ class RestController(object):
''' '''
_custom_actions = {} _custom_actions = {}
def _get_args_for_controller(self, controller):
"""
Retrieve the arguments we actually care about. For Pecan applications
that utilize thread locals, we should truncate the first argument,
`self`. For applications that explicitly pass request/response
references as the first controller arguments, we should truncate the
first three arguments, `self, req, resp`.
"""
argspec = getargspec(controller)
from pecan import request
try:
request.path
except AttributeError:
return argspec.args[3:]
return argspec.args[1:]
@expose() @expose()
def _route(self, args): def _route(self, args, request=None):
''' '''
Routes a request to the appropriate controller and returns its result. Routes a request to the appropriate controller and returns its result.
Performs a bit of validation - refuses to route delete and put actions Performs a bit of validation - refuses to route delete and put actions
via a GET request). via a GET request).
''' '''
if request is None:
from pecan import request
# convention uses "_method" to handle browser-unsupported methods # convention uses "_method" to handle browser-unsupported methods
if request.environ.get('pecan.validation_redirected', False) is True: method = request.params.get('_method', request.method).lower()
#
# If the request has been internally redirected due to a validation
# exception, we want the request method to be enforced as GET, not
# the `_method` param which may have been passed for REST support.
#
method = request.method.lower()
else:
method = request.params.get('_method', request.method).lower()
# make sure DELETE/PUT requests don't use GET # make sure DELETE/PUT requests don't use GET
if request.method == 'GET' and method in ('delete', 'put'): if request.method == 'GET' and method in ('delete', 'put'):
abort(405) abort(405)
# check for nested controllers # check for nested controllers
result = self._find_sub_controllers(args) result = self._find_sub_controllers(args, request)
if result: if result:
return result return result
@@ -62,17 +73,17 @@ class RestController(object):
) )
try: try:
result = handler(method, args) result = handler(method, args, request)
# #
# If the signature of the handler does not match the number # If the signature of the handler does not match the number
# of remaining positional arguments, attempt to handle # of remaining positional arguments, attempt to handle
# a _lookup method (if it exists) # a _lookup method (if it exists)
# #
argspec = getargspec(result[0]) argspec = self._get_args_for_controller(result[0])
num_args = len(argspec[0][1:]) num_args = len(argspec)
if num_args < len(args): if num_args < len(args):
_lookup_result = self._handle_lookup(args) _lookup_result = self._handle_lookup(args, request)
if _lookup_result: if _lookup_result:
return _lookup_result return _lookup_result
except exc.HTTPNotFound: except exc.HTTPNotFound:
@@ -80,7 +91,7 @@ class RestController(object):
# If the matching handler results in a 404, attempt to handle # If the matching handler results in a 404, attempt to handle
# a _lookup method (if it exists) # a _lookup method (if it exists)
# #
_lookup_result = self._handle_lookup(args) _lookup_result = self._handle_lookup(args, request)
if _lookup_result: if _lookup_result:
return _lookup_result return _lookup_result
raise raise
@@ -88,7 +99,7 @@ class RestController(object):
# return the result # return the result
return result return result
def _handle_lookup(self, args): def _handle_lookup(self, args, request):
# filter empty strings from the arg list # filter empty strings from the arg list
args = list(six.moves.filter(bool, args)) args = list(six.moves.filter(bool, args))
@@ -97,7 +108,8 @@ class RestController(object):
if args and iscontroller(lookup): if args and iscontroller(lookup):
result = handle_lookup_traversal(lookup, args) result = handle_lookup_traversal(lookup, args)
if result: if result:
return lookup_controller(*result) obj, remainder = result
return lookup_controller(obj, remainder, request)
def _find_controller(self, *args): def _find_controller(self, *args):
''' '''
@@ -109,7 +121,7 @@ class RestController(object):
return obj return obj
return None return None
def _find_sub_controllers(self, remainder): def _find_sub_controllers(self, remainder, request):
''' '''
Identifies the correct controller to route to by analyzing the Identifies the correct controller to route to by analyzing the
request URI. request URI.
@@ -124,31 +136,33 @@ class RestController(object):
return return
# get the args to figure out how much to chop off # get the args to figure out how much to chop off
args = getargspec(getattr(self, method)) args = self._get_args_for_controller(getattr(self, method))
fixed_args = len(args[0][1:]) - len( fixed_args = len(args) - len(
request.pecan.get('routing_args', []) request.pecan.get('routing_args', [])
) )
var_args = args[1] var_args = getargspec(getattr(self, method)).varargs
# attempt to locate a sub-controller # attempt to locate a sub-controller
if var_args: if var_args:
for i, item in enumerate(remainder): for i, item in enumerate(remainder):
controller = getattr(self, item, None) controller = getattr(self, item, None)
if controller and not ismethod(controller): if controller and not ismethod(controller):
self._set_routing_args(remainder[:i]) self._set_routing_args(request, remainder[:i])
return lookup_controller(controller, remainder[i + 1:]) return lookup_controller(controller, remainder[i + 1:],
request)
elif fixed_args < len(remainder) and hasattr( elif fixed_args < len(remainder) and hasattr(
self, remainder[fixed_args] self, remainder[fixed_args]
): ):
controller = getattr(self, remainder[fixed_args]) controller = getattr(self, remainder[fixed_args])
if not ismethod(controller): if not ismethod(controller):
self._set_routing_args(remainder[:fixed_args]) self._set_routing_args(request, remainder[:fixed_args])
return lookup_controller( return lookup_controller(
controller, controller,
remainder[fixed_args + 1:] remainder[fixed_args + 1:],
request
) )
def _handle_unknown_method(self, method, remainder): def _handle_unknown_method(self, method, remainder, request):
''' '''
Routes undefined actions (like RESET) to the appropriate controller. Routes undefined actions (like RESET) to the appropriate controller.
''' '''
@@ -164,11 +178,12 @@ class RestController(object):
abort(405) abort(405)
sub_controller = getattr(self, remainder[0], None) sub_controller = getattr(self, remainder[0], None)
if sub_controller: if sub_controller:
return lookup_controller(sub_controller, remainder[1:]) return lookup_controller(sub_controller, remainder[1:],
request)
abort(404) abort(404)
def _handle_get(self, method, remainder): def _handle_get(self, method, remainder, request):
''' '''
Routes ``GET`` actions to the appropriate controller. Routes ``GET`` actions to the appropriate controller.
''' '''
@@ -176,8 +191,8 @@ class RestController(object):
if not remainder or remainder == ['']: if not remainder or remainder == ['']:
controller = self._find_controller('get_all', 'get') controller = self._find_controller('get_all', 'get')
if controller: if controller:
argspec = getargspec(controller) argspec = self._get_args_for_controller(controller)
fixed_args = len(argspec.args[1:]) - len( fixed_args = len(argspec) - len(
request.pecan.get('routing_args', []) request.pecan.get('routing_args', [])
) )
if len(remainder) < fixed_args: if len(remainder) < fixed_args:
@@ -194,13 +209,13 @@ class RestController(object):
if controller: if controller:
return controller, remainder[:-1] return controller, remainder[:-1]
match = self._handle_custom_action(method, remainder) match = self._handle_custom_action(method, remainder, request)
if match: if match:
return match return match
controller = getattr(self, remainder[0], None) controller = getattr(self, remainder[0], None)
if controller and not ismethod(controller): if controller and not ismethod(controller):
return lookup_controller(controller, remainder[1:]) return lookup_controller(controller, remainder[1:], request)
# finally, check for the regular get_one/get requests # finally, check for the regular get_one/get requests
controller = self._find_controller('get_one', 'get') controller = self._find_controller('get_one', 'get')
@@ -209,18 +224,18 @@ class RestController(object):
abort(404) abort(404)
def _handle_delete(self, method, remainder): def _handle_delete(self, method, remainder, request):
''' '''
Routes ``DELETE`` actions to the appropriate controller. Routes ``DELETE`` actions to the appropriate controller.
''' '''
if remainder: if remainder:
match = self._handle_custom_action(method, remainder) match = self._handle_custom_action(method, remainder, request)
if match: if match:
return match return match
controller = getattr(self, remainder[0], None) controller = getattr(self, remainder[0], None)
if controller and not ismethod(controller): if controller and not ismethod(controller):
return lookup_controller(controller, remainder[1:]) return lookup_controller(controller, remainder[1:], request)
# check for post_delete/delete requests first # check for post_delete/delete requests first
controller = self._find_controller('post_delete', 'delete') controller = self._find_controller('post_delete', 'delete')
@@ -234,23 +249,24 @@ class RestController(object):
abort(405) abort(405)
sub_controller = getattr(self, remainder[0], None) sub_controller = getattr(self, remainder[0], None)
if sub_controller: if sub_controller:
return lookup_controller(sub_controller, remainder[1:]) return lookup_controller(sub_controller, remainder[1:],
request)
abort(404) abort(404)
def _handle_post(self, method, remainder): def _handle_post(self, method, remainder, request):
''' '''
Routes ``POST`` requests. Routes ``POST`` requests.
''' '''
# check for custom POST/PUT requests # check for custom POST/PUT requests
if remainder: if remainder:
match = self._handle_custom_action(method, remainder) match = self._handle_custom_action(method, remainder, request)
if match: if match:
return match return match
controller = getattr(self, remainder[0], None) controller = getattr(self, remainder[0], None)
if controller and not ismethod(controller): if controller and not ismethod(controller):
return lookup_controller(controller, remainder[1:]) return lookup_controller(controller, remainder[1:], request)
# check for regular POST/PUT requests # check for regular POST/PUT requests
controller = self._find_controller(method) controller = self._find_controller(method)
@@ -259,10 +275,10 @@ class RestController(object):
abort(404) abort(404)
def _handle_put(self, method, remainder): def _handle_put(self, method, remainder, request):
return self._handle_post(method, remainder) return self._handle_post(method, remainder, request)
def _handle_custom_action(self, method, remainder): def _handle_custom_action(self, method, remainder, request):
remainder = [r for r in remainder if r] remainder = [r for r in remainder if r]
if remainder: if remainder:
if method in ('put', 'delete'): if method in ('put', 'delete'):
@@ -281,7 +297,7 @@ class RestController(object):
if controller: if controller:
return controller, remainder return controller, remainder
def _set_routing_args(self, args): def _set_routing_args(self, request, args):
''' '''
Sets default routing arguments. Sets default routing arguments.
''' '''

View File

@@ -1,4 +1,5 @@
import warnings import warnings
from inspect import getargspec
from webob import exc from webob import exc
@@ -23,7 +24,7 @@ class NonCanonicalPath(Exception):
self.remainder = remainder self.remainder = remainder
def lookup_controller(obj, remainder): def lookup_controller(obj, remainder, request):
''' '''
Traverses the requested url path and returns the appropriate controller Traverses the requested url path and returns the appropriate controller
object, including default routes. object, including default routes.
@@ -33,7 +34,8 @@ def lookup_controller(obj, remainder):
notfound_handlers = [] notfound_handlers = []
while True: while True:
try: try:
obj, remainder = find_object(obj, remainder, notfound_handlers) obj, remainder = find_object(obj, remainder, notfound_handlers,
request)
handle_security(obj) handle_security(obj)
return obj, remainder return obj, remainder
except (exc.HTTPNotFound, PecanNotFound): except (exc.HTTPNotFound, PecanNotFound):
@@ -55,7 +57,8 @@ def lookup_controller(obj, remainder):
and len(obj._pecan['argspec'].args) > 1 and len(obj._pecan['argspec'].args) > 1
): ):
raise exc.HTTPNotFound raise exc.HTTPNotFound
return lookup_controller(*result) obj_, remainder_ = result
return lookup_controller(obj_, remainder_, request)
else: else:
raise exc.HTTPNotFound raise exc.HTTPNotFound
@@ -77,7 +80,7 @@ def handle_lookup_traversal(obj, args):
) )
def find_object(obj, remainder, notfound_handlers): def find_object(obj, remainder, notfound_handlers, request):
''' '''
'Walks' the url path in search of an action for which a controller is 'Walks' the url path in search of an action for which a controller is
implemented and returns that controller object along with what's left implemented and returns that controller object along with what's left
@@ -114,7 +117,21 @@ def find_object(obj, remainder, notfound_handlers):
route = getattr(obj, '_route', None) route = getattr(obj, '_route', None)
if iscontroller(route): if iscontroller(route):
next_obj, next_remainder = route(remainder) if len(getargspec(route).args) == 2:
warnings.warn(
(
"The function signature for %s.%s._route is changing "
"in the next version of pecan.\nPlease update to: "
"`def _route(self, args, request)`." % (
obj.__class__.__module__,
obj.__class__.__name__
)
),
DeprecationWarning
)
next_obj, next_remainder = route(remainder)
else:
next_obj, next_remainder = route(remainder, request)
cross_boundary(route, next_obj) cross_boundary(route, next_obj)
return next_obj, next_remainder return next_obj, next_remainder

View File

@@ -285,7 +285,7 @@ class TestControllerArguments(PecanTestCase):
) )
@expose() @expose()
def _route(self, args): def _route(self, args, request):
if hasattr(self, args[0]): if hasattr(self, args[0]):
return getattr(self, args[0]), args[1:] return getattr(self, args[0]), args[1:]
else: else:
@@ -1519,3 +1519,27 @@ class TestEngines(PecanTestCase):
r = app.get('/') r = app.get('/')
assert r.status_int == 200 assert r.status_int == 200
assert b_("<h1>Hello, Jonathan!</h1>") in r.body assert b_("<h1>Hello, Jonathan!</h1>") in r.body
class TestDeprecatedRouteMethod(PecanTestCase):
@property
def app_(self):
class RootController(object):
@expose()
def index(self, *args):
return ', '.join(args)
@expose()
def _route(self, args):
return self.index, args
return TestApp(Pecan(RootController()))
def test_required_argument(self):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
r = self.app_.get('/foo/bar/')
assert r.status_int == 200
assert b_('foo, bar') in r.body

File diff suppressed because it is too large Load Diff