summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Petrello <lists@ryanpetrello.com>2014-05-19 14:10:17 -0400
committerRyan Petrello <lists@ryanpetrello.com>2014-05-28 16:06:30 -0400
commite93b929edf9cdbec6711e5672029981cb303fff3 (patch)
tree06729d0ce8878f67763aede44a9580fdf5eb2fd7
parent9bc09ea9c6d0cf311c508e6e0a4dce8df9855384 (diff)
downloadpecan-e93b929edf9cdbec6711e5672029981cb303fff3.tar.gz
Add support for Pecan *without* thread local request/response objects
Change-Id: I5a5a05e1f57ef2d8ad64e925c7ffa6907b914273
-rw-r--r--docs/source/contextlocals.rst55
-rw-r--r--docs/source/index.rst1
-rw-r--r--docs/source/routing.rst10
-rw-r--r--pecan/core.py238
-rw-r--r--pecan/hooks.py4
-rw-r--r--pecan/rest.py106
-rw-r--r--pecan/routing.py27
-rw-r--r--pecan/tests/test_base.py26
-rw-r--r--pecan/tests/test_no_thread_locals.py1312
9 files changed, 1650 insertions, 129 deletions
diff --git a/docs/source/contextlocals.rst b/docs/source/contextlocals.rst
new file mode 100644
index 0000000..d97ef7e
--- /dev/null
+++ b/docs/source/contextlocals.rst
@@ -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.
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 8a4ef37..520d0f7 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -38,6 +38,7 @@ Narrative Documentation
secure_controller.rst
hooks.rst
jsonify.rst
+ contextlocals.rst
commands.rst
development.rst
deployment.rst
diff --git a/docs/source/routing.rst b/docs/source/routing.rst
index ec79305..28df5d6 100644
--- a/docs/source/routing.rst
+++ b/docs/source/routing.rst
@@ -270,11 +270,11 @@ a :func:`_route` method will enable you to have total control.
Interacting with the Request and Response Object
------------------------------------------------
-For every HTTP request, Pecan maintains a thread-local reference to the request
-and response object, ``pecan.request`` and ``pecan.response``. These are
-instances of :class:`webob.request.BaseRequest` and
-:class:`webob.response.Response`, respectively, and can be interacted with from
-within Pecan controller code::
+For every HTTP request, Pecan maintains a :ref:`thread-local reference
+<contextlocals>` to the request and response object, ``pecan.request`` and
+``pecan.response``. These are instances of :class:`webob.request.BaseRequest`
+and :class:`webob.response.Response`, respectively, and can be interacted with
+from within Pecan controller code::
@pecan.expose()
def login(self):
diff --git a/pecan/core.py b/pecan/core.py
index 6a096b5..44bad0e 100644
--- a/pecan/core.py
+++ b/pecan/core.py
@@ -27,10 +27,31 @@ state = None
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):
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):
- obj = getattr(state, key)
+ try:
+ obj = getattr(state, key)
+ except AttributeError:
+ raise self.explanation_
return getattr(obj, attr)
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={},
- add_slash=False):
+ add_slash=False, request=None):
'''
Perform a redirect, either internal or external. An internal redirect
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 headers: Any HTTP headers to send with the response, as a
dictionary.
+ :param request: The :class:`webob.request.BaseRequest` instance to use.
'''
+ request = request or state.request
if add_slash:
if location is None:
- split_url = list(urlparse.urlsplit(state.request.url))
- new_proto = state.request.environ.get(
+ split_url = list(urlparse.urlsplit(request.url))
+ new_proto = request.environ.get(
'HTTP_X_FORWARDED_PROTO', split_url[0]
)
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)
-def render(template, namespace):
+def render(template, namespace, app=None):
'''
Render the specified template using the Pecan rendering framework
with the specified template namespace as a dictionary. Useful in a
@@ -136,9 +159,10 @@ def render(template, namespace):
``@expose``.
:param namespace: The namespace to use for rendering the template, as a
dictionary.
+ :param app: The instance of :class:`pecan.Pecan` to use
'''
-
- return state.app.render(template, namespace)
+ app = app or state.app
+ return app.render(template, namespace)
def load_app(config, **kwargs):
@@ -165,31 +189,7 @@ def load_app(config, **kwargs):
)
-class Pecan(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.
- '''
+class PecanBase(object):
SIMPLEST_CONTENT_TYPES = (
['text/html'],
@@ -201,9 +201,6 @@ class Pecan(object):
custom_renderers={}, extra_template_vars={},
force_canonical=True, guess_content_type_from_ext=True,
context_local_factory=None, **kw):
-
- self.init_context_local(context_local_factory)
-
if isinstance(root, six.string_types):
root = self.__translate_root__(root)
@@ -223,12 +220,6 @@ class Pecan(object):
self.force_canonical = force_canonical
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):
'''
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 path: The path to look up on this node.
'''
-
path = path.split('/')[1:]
try:
- node, remainder = lookup_controller(node, path)
+ node, remainder = lookup_controller(node, path, req)
return node, remainder
except NonCanonicalPath as e:
if self.force_canonical and \
@@ -276,7 +266,7 @@ class Pecan(object):
(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
def determine_hooks(self, controller=None):
@@ -299,7 +289,7 @@ class Pecan(object):
)
return self.hooks
- def handle_hooks(self, hook_type, *args):
+ def handle_hooks(self, hooks, hook_type, *args):
'''
Processes hooks of the specified type.
@@ -307,10 +297,8 @@ class Pecan(object):
``on_error``, and ``on_route``.
:param \*args: Arguments to pass to the hooks.
'''
- if hook_type in ['before', 'on_route']:
- hooks = state.hooks
- else:
- hooks = reversed(state.hooks)
+ if hook_type not in ['before', 'on_route']:
+ hooks = reversed(hooks)
for hook in hooks:
result = getattr(hook, hook_type)(*args)
@@ -319,14 +307,15 @@ class Pecan(object):
if hook_type == 'on_error' and isinstance(result, Response):
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
passed the argument specification for the controller.
'''
args = []
kwargs = dict()
- valid_args = argspec[0][1:]
+ valid_args = argspec.args[1:] # pop off `self`
+ pecan_state = state.request.pecan
def _decode(x):
return unquote_plus(x) if isinstance(x, six.string_types) \
@@ -392,13 +381,12 @@ class Pecan(object):
template = template.split(':')[1]
return renderer.render(template, namespace)
- def handle_request(self, req, resp):
+ def find_controller(self, state):
'''
The main request handler for Pecan applications.
'''
-
# get a sorted list of hooks, by priority (no controller hooks yet)
- state.hooks = self.hooks
+ req = state.request
pecan_state = req.pecan
# 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')
# 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
# by the file extension on the URI
@@ -491,11 +479,8 @@ class Pecan(object):
)
raise exc.HTTPNotFound
- # get a sorted list of hooks, by priority
- state.hooks = self.determine_hooks(controller)
-
# handle "before" hooks
- self.handle_hooks('before', state)
+ self.handle_hooks(self.determine_hooks(controller), 'before', state)
# fetch any parameters
if req.method == 'GET':
@@ -505,13 +490,25 @@ class Pecan(object):
# fetch the arguments for the controller
args, kwargs = self.get_args(
- pecan_state,
+ state,
params,
remainder,
cfg['argspec'],
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
result = controller(*args, **kwargs)
@@ -570,11 +567,10 @@ class Pecan(object):
'''
# create the request and response object
- state.request = req = Request(environ)
- state.response = resp = Response()
- state.hooks = []
- state.app = self
- state.controller = None
+ req = Request(environ)
+ resp = Response()
+ state = RoutingState(req, resp, self)
+ controller = None
# handle the request
try:
@@ -582,7 +578,8 @@ class Pecan(object):
req.context = environ.get('pecan.recursive.context', {})
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:
# if this is an HTTP Exception, set it as the response
if isinstance(e, exc.HTTPException):
@@ -592,7 +589,12 @@ class Pecan(object):
# if this is not an internal redirect, run error hooks
on_error_result = None
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 isinstance(on_error_result, Response):
@@ -602,15 +604,111 @@ class Pecan(object):
raise
finally:
# handle "after" hooks
- self.handle_hooks('after', state)
+ self.handle_hooks(
+ self.determine_hooks(state.controller), 'after', state
+ )
# 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:
+ 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:
- return state.response(environ, start_response)
+ state.hooks = []
+ state.app = self
+ state.controller = None
+ return super(Pecan, self).__call__(environ, start_response)
finally:
- # clean up state
del state.hooks
del state.request
del state.response
del state.controller
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)
diff --git a/pecan/hooks.py b/pecan/hooks.py
index 4ceeb42..c0f687a 100644
--- a/pecan/hooks.py
+++ b/pecan/hooks.py
@@ -4,7 +4,6 @@ from inspect import getmembers
from webob.exc import HTTPFound
from .util import iscontroller, _cfg
-from .routing import lookup_controller
__all__ = [
'PecanHook', 'TransactionHook', 'HookController',
@@ -334,8 +333,7 @@ class RequestViewerHook(PecanHook):
Specific to Pecan (not available in the request object)
'''
path = state.request.pecan['routing_path'].split('/')[1:]
- controller, reminder = lookup_controller(state.app.root, path)
- return controller.__str__().split()[2]
+ return state.controller.__str__().split()[2]
def format_hooks(self, hooks):
'''
diff --git a/pecan/rest.py b/pecan/rest.py
index db955c5..9cc8b35 100644
--- a/pecan/rest.py
+++ b/pecan/rest.py
@@ -1,9 +1,10 @@
from inspect import getargspec, ismethod
+import warnings
from webob import exc
import six
-from .core import abort, request
+from .core import abort
from .decorators import expose
from .routing import lookup_controller, handle_lookup_traversal
from .util import iscontroller
@@ -26,31 +27,41 @@ class RestController(object):
'''
_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()
- def _route(self, args):
+ def _route(self, args, request=None):
'''
Routes a request to the appropriate controller and returns its result.
Performs a bit of validation - refuses to route delete and put actions
via a GET request).
'''
+ if request is None:
+ from pecan import request
# convention uses "_method" to handle browser-unsupported methods
- if request.environ.get('pecan.validation_redirected', False) is True:
- #
- # 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()
+ method = request.params.get('_method', request.method).lower()
# make sure DELETE/PUT requests don't use GET
if request.method == 'GET' and method in ('delete', 'put'):
abort(405)
# check for nested controllers
- result = self._find_sub_controllers(args)
+ result = self._find_sub_controllers(args, request)
if result:
return result
@@ -62,17 +73,17 @@ class RestController(object):
)
try:
- result = handler(method, args)
+ result = handler(method, args, request)
#
# If the signature of the handler does not match the number
# of remaining positional arguments, attempt to handle
# a _lookup method (if it exists)
#
- argspec = getargspec(result[0])
- num_args = len(argspec[0][1:])
+ argspec = self._get_args_for_controller(result[0])
+ num_args = len(argspec)
if num_args < len(args):
- _lookup_result = self._handle_lookup(args)
+ _lookup_result = self._handle_lookup(args, request)
if _lookup_result:
return _lookup_result
except exc.HTTPNotFound:
@@ -80,7 +91,7 @@ class RestController(object):
# If the matching handler results in a 404, attempt to handle
# a _lookup method (if it exists)
#
- _lookup_result = self._handle_lookup(args)
+ _lookup_result = self._handle_lookup(args, request)
if _lookup_result:
return _lookup_result
raise
@@ -88,7 +99,7 @@ class RestController(object):
# return the result
return result
- def _handle_lookup(self, args):
+ def _handle_lookup(self, args, request):
# filter empty strings from the arg list
args = list(six.moves.filter(bool, args))
@@ -97,7 +108,8 @@ class RestController(object):
if args and iscontroller(lookup):
result = handle_lookup_traversal(lookup, args)
if result:
- return lookup_controller(*result)
+ obj, remainder = result
+ return lookup_controller(obj, remainder, request)
def _find_controller(self, *args):
'''
@@ -109,7 +121,7 @@ class RestController(object):
return obj
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
request URI.
@@ -124,31 +136,33 @@ class RestController(object):
return
# get the args to figure out how much to chop off
- args = getargspec(getattr(self, method))
- fixed_args = len(args[0][1:]) - len(
+ args = self._get_args_for_controller(getattr(self, method))
+ fixed_args = len(args) - len(
request.pecan.get('routing_args', [])
)
- var_args = args[1]
+ var_args = getargspec(getattr(self, method)).varargs
# attempt to locate a sub-controller
if var_args:
for i, item in enumerate(remainder):
controller = getattr(self, item, None)
if controller and not ismethod(controller):
- self._set_routing_args(remainder[:i])
- return lookup_controller(controller, remainder[i + 1:])
+ self._set_routing_args(request, remainder[:i])
+ return lookup_controller(controller, remainder[i + 1:],
+ request)
elif fixed_args < len(remainder) and hasattr(
self, remainder[fixed_args]
):
controller = getattr(self, remainder[fixed_args])
if not ismethod(controller):
- self._set_routing_args(remainder[:fixed_args])
+ self._set_routing_args(request, remainder[:fixed_args])
return lookup_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.
'''
@@ -164,11 +178,12 @@ class RestController(object):
abort(405)
sub_controller = getattr(self, remainder[0], None)
if sub_controller:
- return lookup_controller(sub_controller, remainder[1:])
+ return lookup_controller(sub_controller, remainder[1:],
+ request)
abort(404)
- def _handle_get(self, method, remainder):
+ def _handle_get(self, method, remainder, request):
'''
Routes ``GET`` actions to the appropriate controller.
'''
@@ -176,8 +191,8 @@ class RestController(object):
if not remainder or remainder == ['']:
controller = self._find_controller('get_all', 'get')
if controller:
- argspec = getargspec(controller)
- fixed_args = len(argspec.args[1:]) - len(
+ argspec = self._get_args_for_controller(controller)
+ fixed_args = len(argspec) - len(
request.pecan.get('routing_args', [])
)
if len(remainder) < fixed_args:
@@ -194,13 +209,13 @@ class RestController(object):
if controller:
return controller, remainder[:-1]
- match = self._handle_custom_action(method, remainder)
+ match = self._handle_custom_action(method, remainder, request)
if match:
return match
controller = getattr(self, remainder[0], None)
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
controller = self._find_controller('get_one', 'get')
@@ -209,18 +224,18 @@ class RestController(object):
abort(404)
- def _handle_delete(self, method, remainder):
+ def _handle_delete(self, method, remainder, request):
'''
Routes ``DELETE`` actions to the appropriate controller.
'''
if remainder:
- match = self._handle_custom_action(method, remainder)
+ match = self._handle_custom_action(method, remainder, request)
if match:
return match
controller = getattr(self, remainder[0], None)
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
controller = self._find_controller('post_delete', 'delete')
@@ -234,23 +249,24 @@ class RestController(object):
abort(405)
sub_controller = getattr(self, remainder[0], None)
if sub_controller:
- return lookup_controller(sub_controller, remainder[1:])
+ return lookup_controller(sub_controller, remainder[1:],
+ request)
abort(404)
- def _handle_post(self, method, remainder):
+ def _handle_post(self, method, remainder, request):
'''
Routes ``POST`` requests.
'''
# check for custom POST/PUT requests
if remainder:
- match = self._handle_custom_action(method, remainder)
+ match = self._handle_custom_action(method, remainder, request)
if match:
return match
controller = getattr(self, remainder[0], None)
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
controller = self._find_controller(method)
@@ -259,10 +275,10 @@ class RestController(object):
abort(404)
- def _handle_put(self, method, remainder):
- return self._handle_post(method, remainder)
+ def _handle_put(self, method, remainder, request):
+ 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]
if remainder:
if method in ('put', 'delete'):
@@ -281,7 +297,7 @@ class RestController(object):
if controller:
return controller, remainder
- def _set_routing_args(self, args):
+ def _set_routing_args(self, request, args):
'''
Sets default routing arguments.
'''
diff --git a/pecan/routing.py b/pecan/routing.py
index ca8c93f..17a7e40 100644
--- a/pecan/routing.py
+++ b/pecan/routing.py
@@ -1,4 +1,5 @@
import warnings
+from inspect import getargspec
from webob import exc
@@ -23,7 +24,7 @@ class NonCanonicalPath(Exception):
self.remainder = remainder
-def lookup_controller(obj, remainder):
+def lookup_controller(obj, remainder, request):
'''
Traverses the requested url path and returns the appropriate controller
object, including default routes.
@@ -33,7 +34,8 @@ def lookup_controller(obj, remainder):
notfound_handlers = []
while True:
try:
- obj, remainder = find_object(obj, remainder, notfound_handlers)
+ obj, remainder = find_object(obj, remainder, notfound_handlers,
+ request)
handle_security(obj)
return obj, remainder
except (exc.HTTPNotFound, PecanNotFound):
@@ -55,7 +57,8 @@ def lookup_controller(obj, remainder):
and len(obj._pecan['argspec'].args) > 1
):
raise exc.HTTPNotFound
- return lookup_controller(*result)
+ obj_, remainder_ = result
+ return lookup_controller(obj_, remainder_, request)
else:
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
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)
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)
return next_obj, next_remainder
diff --git a/pecan/tests/test_base.py b/pecan/tests/test_base.py
index f4bd64b..81471ec 100644
--- a/pecan/tests/test_base.py
+++ b/pecan/tests/test_base.py
@@ -285,7 +285,7 @@ class TestControllerArguments(PecanTestCase):
)
@expose()
- def _route(self, args):
+ def _route(self, args, request):
if hasattr(self, args[0]):
return getattr(self, args[0]), args[1:]
else:
@@ -1519,3 +1519,27 @@ class TestEngines(PecanTestCase):
r = app.get('/')
assert r.status_int == 200
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
diff --git a/pecan/tests/test_no_thread_locals.py b/pecan/tests/test_no_thread_locals.py
new file mode 100644
index 0000000..e9fcf75
--- /dev/null
+++ b/pecan/tests/test_no_thread_locals.py
@@ -0,0 +1,1312 @@
+from json import dumps
+import warnings
+
+from webtest import TestApp
+from six import b as b_
+from six import u as u_
+import webob
+import mock
+
+from pecan import Pecan, expose, abort
+from pecan.rest import RestController
+from pecan.hooks import PecanHook, HookController
+from pecan.tests import PecanTestCase
+
+
+class TestThreadingLocalUsage(PecanTestCase):
+
+ @property
+ def root(self):
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ assert isinstance(req, webob.BaseRequest)
+ assert isinstance(resp, webob.Response)
+ return 'Hello, World!'
+
+ return RootController
+
+ def test_locals_are_not_used(self):
+ with mock.patch('threading.local', side_effect=AssertionError()):
+
+ app = TestApp(Pecan(self.root(), use_context_locals=False))
+ r = app.get('/')
+ assert r.status_int == 200
+ assert r.body == b_('Hello, World!')
+
+ self.assertRaises(AssertionError, Pecan, self.root)
+
+
+class TestIndexRouting(PecanTestCase):
+
+ @property
+ def app_(self):
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ assert isinstance(req, webob.BaseRequest)
+ assert isinstance(resp, webob.Response)
+ return 'Hello, World!'
+
+ return TestApp(Pecan(RootController(), use_context_locals=False))
+
+ def test_empty_root(self):
+ r = self.app_.get('/')
+ assert r.status_int == 200
+ assert r.body == b_('Hello, World!')
+
+ def test_index(self):
+ r = self.app_.get('/index')
+ assert r.status_int == 200
+ assert r.body == b_('Hello, World!')
+
+ def test_index_html(self):
+ r = self.app_.get('/index.html')
+ assert r.status_int == 200
+ assert r.body == b_('Hello, World!')
+
+
+class TestManualResponse(PecanTestCase):
+
+ def test_manual_response(self):
+
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ resp = webob.Response(resp.environ)
+ resp.body = b_('Hello, World!')
+ return resp
+
+ app = TestApp(Pecan(RootController(), use_context_locals=False))
+ r = app.get('/')
+ assert r.body == b_('Hello, World!'), r.body
+
+
+class TestDispatch(PecanTestCase):
+
+ @property
+ def app_(self):
+ class SubSubController(object):
+ @expose()
+ def index(self, req, resp):
+ assert isinstance(req, webob.BaseRequest)
+ assert isinstance(resp, webob.Response)
+ return '/sub/sub/'
+
+ @expose()
+ def deeper(self, req, resp):
+ assert isinstance(req, webob.BaseRequest)
+ assert isinstance(resp, webob.Response)
+ return '/sub/sub/deeper'
+
+ class SubController(object):
+ @expose()
+ def index(self, req, resp):
+ assert isinstance(req, webob.BaseRequest)
+ assert isinstance(resp, webob.Response)
+ return '/sub/'
+
+ @expose()
+ def deeper(self, req, resp):
+ assert isinstance(req, webob.BaseRequest)
+ assert isinstance(resp, webob.Response)
+ return '/sub/deeper'
+
+ sub = SubSubController()
+
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ assert isinstance(req, webob.BaseRequest)
+ assert isinstance(resp, webob.Response)
+ return '/'
+
+ @expose()
+ def deeper(self, req, resp):
+ assert isinstance(req, webob.BaseRequest)
+ assert isinstance(resp, webob.Response)
+ return '/deeper'
+
+ sub = SubController()
+
+ return TestApp(Pecan(RootController(), use_context_locals=False))
+
+ def test_index(self):
+ r = self.app_.get('/')
+ assert r.status_int == 200
+ assert r.body == b_('/')
+
+ def test_one_level(self):
+ r = self.app_.get('/deeper')
+ assert r.status_int == 200
+ assert r.body == b_('/deeper')
+
+ def test_one_level_with_trailing(self):
+ r = self.app_.get('/sub/')
+ assert r.status_int == 200
+ assert r.body == b_('/sub/')
+
+ def test_two_levels(self):
+ r = self.app_.get('/sub/deeper')
+ assert r.status_int == 200
+ assert r.body == b_('/sub/deeper')
+
+ def test_two_levels_with_trailing(self):
+ r = self.app_.get('/sub/sub/')
+ assert r.status_int == 200
+
+ def test_three_levels(self):
+ r = self.app_.get('/sub/sub/deeper')
+ assert r.status_int == 200
+ assert r.body == b_('/sub/sub/deeper')
+
+
+class TestLookups(PecanTestCase):
+
+ @property
+ def app_(self):
+ class LookupController(object):
+ def __init__(self, someID):
+ self.someID = someID
+
+ @expose()
+ def index(self, req, resp):
+ return '/%s' % self.someID
+
+ @expose()
+ def name(self, req, resp):
+ return '/%s/name' % self.someID
+
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ return '/'
+
+ @expose()
+ def _lookup(self, someID, *remainder):
+ return LookupController(someID), remainder
+
+ return TestApp(Pecan(RootController(), use_context_locals=False))
+
+ def test_index(self):
+ r = self.app_.get('/')
+ assert r.status_int == 200
+ assert r.body == b_('/')
+
+ def test_lookup(self):
+ r = self.app_.get('/100/')
+ assert r.status_int == 200
+ assert r.body == b_('/100')
+
+ def test_lookup_with_method(self):
+ r = self.app_.get('/100/name')
+ assert r.status_int == 200
+ assert r.body == b_('/100/name')
+
+ def test_lookup_with_wrong_argspec(self):
+ class RootController(object):
+ @expose()
+ def _lookup(self, someID):
+ return 'Bad arg spec' # pragma: nocover
+
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ app = TestApp(Pecan(RootController(), use_context_locals=False))
+ r = app.get('/foo/bar', expect_errors=True)
+ assert r.status_int == 404
+
+
+class TestCanonicalLookups(PecanTestCase):
+
+ @property
+ def app_(self):
+ class LookupController(object):
+ def __init__(self, someID):
+ self.someID = someID
+
+ @expose()
+ def index(self, req, resp):
+ return self.someID
+
+ class UserController(object):
+ @expose()
+ def _lookup(self, someID, *remainder):
+ return LookupController(someID), remainder
+
+ class RootController(object):
+ users = UserController()
+
+ return TestApp(Pecan(RootController(), use_context_locals=False))
+
+ def test_canonical_lookup(self):
+ assert self.app_.get('/users', expect_errors=404).status_int == 404
+ assert self.app_.get('/users/', expect_errors=404).status_int == 404
+ assert self.app_.get('/users/100').status_int == 302
+ assert self.app_.get('/users/100/').body == b_('100')
+
+
+class TestControllerArguments(PecanTestCase):
+
+ @property
+ def app_(self):
+ class RootController(object):
+ @expose()
+ def index(self, req, resp, id):
+ return 'index: %s' % id
+
+ @expose()
+ def multiple(self, req, resp, one, two):
+ return 'multiple: %s, %s' % (one, two)
+
+ @expose()
+ def optional(self, req, resp, id=None):
+ return 'optional: %s' % str(id)
+
+ @expose()
+ def multiple_optional(self, req, resp, one=None, two=None,
+ three=None):
+ return 'multiple_optional: %s, %s, %s' % (one, two, three)
+
+ @expose()
+ def variable_args(self, req, resp, *args):
+ return 'variable_args: %s' % ', '.join(args)
+
+ @expose()
+ def variable_kwargs(self, req, resp, **kwargs):
+ data = [
+ '%s=%s' % (key, kwargs[key])
+ for key in sorted(kwargs.keys())
+ ]
+ return 'variable_kwargs: %s' % ', '.join(data)
+
+ @expose()
+ def variable_all(self, req, resp, *args, **kwargs):
+ data = [
+ '%s=%s' % (key, kwargs[key])
+ for key in sorted(kwargs.keys())
+ ]
+ return 'variable_all: %s' % ', '.join(list(args) + data)
+
+ @expose()
+ def eater(self, req, resp, id, dummy=None, *args, **kwargs):
+ data = [
+ '%s=%s' % (key, kwargs[key])
+ for key in sorted(kwargs.keys())
+ ]
+ return 'eater: %s, %s, %s' % (
+ id,
+ dummy,
+ ', '.join(list(args) + data)
+ )
+
+ @expose()
+ def _route(self, args, request):
+ if hasattr(self, args[0]):
+ return getattr(self, args[0]), args[1:]
+ else:
+ return self.index, args
+
+ return TestApp(Pecan(RootController(), use_context_locals=False))
+
+ def test_required_argument(self):
+ try:
+ r = self.app_.get('/')
+ assert r.status_int != 200 # pragma: nocover
+ except Exception as ex:
+ assert type(ex) == TypeError
+ assert ex.args[0] in (
+ "index() takes exactly 4 arguments (3 given)",
+ "index() missing 1 required positional argument: 'id'"
+ ) # this messaging changed in Python 3.3
+
+ def test_single_argument(self):
+ r = self.app_.get('/1')
+ assert r.status_int == 200
+ assert r.body == b_('index: 1')
+
+ def test_single_argument_with_encoded_url(self):
+ r = self.app_.get('/This%20is%20a%20test%21')
+ assert r.status_int == 200
+ assert r.body == b_('index: This is a test!')
+
+ def test_two_arguments(self):
+ r = self.app_.get('/1/dummy', status=404)
+ assert r.status_int == 404
+
+ def test_keyword_argument(self):
+ r = self.app_.get('/?id=2')
+ assert r.status_int == 200
+ assert r.body == b_('index: 2')
+
+ def test_keyword_argument_with_encoded_url(self):
+ r = self.app_.get('/?id=This%20is%20a%20test%21')
+ assert r.status_int == 200
+ assert r.body == b_('index: This is a test!')
+
+ def test_argument_and_keyword_argument(self):
+ r = self.app_.get('/3?id=three')
+ assert r.status_int == 200
+ assert r.body == b_('index: 3')
+
+ def test_encoded_argument_and_keyword_argument(self):
+ r = self.app_.get('/This%20is%20a%20test%21?id=three')
+ assert r.status_int == 200
+ assert r.body == b_('index: This is a test!')
+
+ def test_explicit_kwargs(self):
+ r = self.app_.post('/', {'id': '4'})
+ assert r.status_int == 200
+ assert r.body == b_('index: 4')
+
+ def test_path_with_explicit_kwargs(self):
+ r = self.app_.post('/4', {'id': 'four'})
+ assert r.status_int == 200
+ assert r.body == b_('index: 4')
+
+ def test_multiple_kwargs(self):
+ r = self.app_.get('/?id=5&dummy=dummy')
+ assert r.status_int == 200
+ assert r.body == b_('index: 5')
+
+ def test_kwargs_from_root(self):
+ r = self.app_.post('/', {'id': '6', 'dummy': 'dummy'})
+ assert r.status_int == 200
+ assert r.body == b_('index: 6')
+
+ # multiple args
+
+ def test_multiple_positional_arguments(self):
+ r = self.app_.get('/multiple/one/two')
+ assert r.status_int == 200
+ assert r.body == b_('multiple: one, two')
+
+ def test_multiple_positional_arguments_with_url_encode(self):
+ r = self.app_.get('/multiple/One%20/Two%21')
+ assert r.status_int == 200
+ assert r.body == b_('multiple: One , Two!')
+
+ def test_multiple_positional_arguments_with_kwargs(self):
+ r = self.app_.get('/multiple?one=three&two=four')
+ assert r.status_int == 200
+ assert r.body == b_('multiple: three, four')
+
+ def test_multiple_positional_arguments_with_url_encoded_kwargs(self):
+ r = self.app_.get('/multiple?one=Three%20&two=Four%20%21')
+ assert r.status_int == 200
+ assert r.body == b_('multiple: Three , Four !')
+
+ def test_positional_args_with_dictionary_kwargs(self):
+ r = self.app_.post('/multiple', {'one': 'five', 'two': 'six'})
+ assert r.status_int == 200
+ assert r.body == b_('multiple: five, six')
+
+ def test_positional_args_with_url_encoded_dictionary_kwargs(self):
+ r = self.app_.post('/multiple', {'one': 'Five%20', 'two': 'Six%20%21'})
+ assert r.status_int == 200
+ assert r.body == b_('multiple: Five%20, Six%20%21')
+
+ # optional arg
+ def test_optional_arg(self):
+ r = self.app_.get('/optional')
+ assert r.status_int == 200
+ assert r.body == b_('optional: None')
+
+ def test_multiple_optional(self):
+ r = self.app_.get('/optional/1')
+ assert r.status_int == 200
+ assert r.body == b_('optional: 1')
+
+ def test_multiple_optional_url_encoded(self):
+ r = self.app_.get('/optional/Some%20Number')
+ assert r.status_int == 200
+ assert r.body == b_('optional: Some Number')
+
+ def test_multiple_optional_missing(self):
+ r = self.app_.get('/optional/2/dummy', status=404)
+ assert r.status_int == 404
+
+ def test_multiple_with_kwargs(self):
+ r = self.app_.get('/optional?id=2')
+ assert r.status_int == 200
+ assert r.body == b_('optional: 2')
+
+ def test_multiple_with_url_encoded_kwargs(self):
+ r = self.app_.get('/optional?id=Some%20Number')
+ assert r.status_int == 200
+ assert r.body == b_('optional: Some Number')
+
+ def test_multiple_args_with_url_encoded_kwargs(self):
+ r = self.app_.get('/optional/3?id=three')
+ assert r.status_int == 200
+ assert r.body == b_('optional: 3')
+
+ def test_url_encoded_positional_args(self):
+ r = self.app_.get('/optional/Some%20Number?id=three')
+ assert r.status_int == 200
+ assert r.body == b_('optional: Some Number')
+
+ def test_optional_arg_with_kwargs(self):
+ r = self.app_.post('/optional', {'id': '4'})
+ assert r.status_int == 200
+ assert r.body == b_('optional: 4')
+
+ def test_optional_arg_with_url_encoded_kwargs(self):
+ r = self.app_.post('/optional', {'id': 'Some%20Number'})
+ assert r.status_int == 200
+ assert r.body == b_('optional: Some%20Number')
+
+ def test_multiple_positional_arguments_with_dictionary_kwargs(self):
+ r = self.app_.post('/optional/5', {'id': 'five'})
+ assert r.status_int == 200
+ assert r.body == b_('optional: 5')
+
+ def test_multiple_positional_url_encoded_arguments_with_kwargs(self):
+ r = self.app_.post('/optional/Some%20Number', {'id': 'five'})
+ assert r.status_int == 200
+ assert r.body == b_('optional: Some Number')
+
+ def test_optional_arg_with_multiple_kwargs(self):
+ r = self.app_.get('/optional?id=6&dummy=dummy')
+ assert r.status_int == 200
+ assert r.body == b_('optional: 6')
+
+ def test_optional_arg_with_multiple_url_encoded_kwargs(self):
+ r = self.app_.get('/optional?id=Some%20Number&dummy=dummy')
+ assert r.status_int == 200
+ assert r.body == b_('optional: Some Number')
+
+ def test_optional_arg_with_multiple_dictionary_kwargs(self):
+ r = self.app_.post('/optional', {'id': '7', 'dummy': 'dummy'})
+ assert r.status_int == 200
+ assert r.body == b_('optional: 7')
+
+ def test_optional_arg_with_multiple_url_encoded_dictionary_kwargs(self):
+ r = self.app_.post('/optional', {
+ 'id': 'Some%20Number',
+ 'dummy': 'dummy'
+ })
+ assert r.status_int == 200
+ assert r.body == b_('optional: Some%20Number')
+
+ # multiple optional args
+
+ def test_multiple_optional_positional_args(self):
+ r = self.app_.get('/multiple_optional')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: None, None, None')
+
+ def test_multiple_optional_positional_args_one_arg(self):
+ r = self.app_.get('/multiple_optional/1')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: 1, None, None')
+
+ def test_multiple_optional_positional_args_one_url_encoded_arg(self):
+ r = self.app_.get('/multiple_optional/One%21')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: One!, None, None')
+
+ def test_multiple_optional_positional_args_all_args(self):
+ r = self.app_.get('/multiple_optional/1/2/3')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: 1, 2, 3')
+
+ def test_multiple_optional_positional_args_all_url_encoded_args(self):
+ r = self.app_.get('/multiple_optional/One%21/Two%21/Three%21')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: One!, Two!, Three!')
+
+ def test_multiple_optional_positional_args_too_many_args(self):
+ r = self.app_.get('/multiple_optional/1/2/3/dummy', status=404)
+ assert r.status_int == 404
+
+ def test_multiple_optional_positional_args_with_kwargs(self):
+ r = self.app_.get('/multiple_optional?one=1')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: 1, None, None')
+
+ def test_multiple_optional_positional_args_with_url_encoded_kwargs(self):
+ r = self.app_.get('/multiple_optional?one=One%21')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: One!, None, None')
+
+ def test_multiple_optional_positional_args_with_string_kwargs(self):
+ r = self.app_.get('/multiple_optional/1?one=one')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: 1, None, None')
+
+ def test_multiple_optional_positional_args_with_encoded_str_kwargs(self):
+ r = self.app_.get('/multiple_optional/One%21?one=one')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: One!, None, None')
+
+ def test_multiple_optional_positional_args_with_dict_kwargs(self):
+ r = self.app_.post('/multiple_optional', {'one': '1'})
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: 1, None, None')
+
+ def test_multiple_optional_positional_args_with_encoded_dict_kwargs(self):
+ r = self.app_.post('/multiple_optional', {'one': 'One%21'})
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: One%21, None, None')
+
+ def test_multiple_optional_positional_args_and_dict_kwargs(self):
+ r = self.app_.post('/multiple_optional/1', {'one': 'one'})
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: 1, None, None')
+
+ def test_multiple_optional_encoded_positional_args_and_dict_kwargs(self):
+ r = self.app_.post('/multiple_optional/One%21', {'one': 'one'})
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: One!, None, None')
+
+ def test_multiple_optional_args_with_multiple_kwargs(self):
+ r = self.app_.get('/multiple_optional?one=1&two=2&three=3&four=4')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: 1, 2, 3')
+
+ def test_multiple_optional_args_with_multiple_encoded_kwargs(self):
+ r = self.app_.get(
+ '/multiple_optional?one=One%21&two=Two%21&three=Three%21&four=4'
+ )
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: One!, Two!, Three!')
+
+ def test_multiple_optional_args_with_multiple_dict_kwargs(self):
+ r = self.app_.post(
+ '/multiple_optional',
+ {'one': '1', 'two': '2', 'three': '3', 'four': '4'}
+ )
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: 1, 2, 3')
+
+ def test_multiple_optional_args_with_multiple_encoded_dict_kwargs(self):
+ r = self.app_.post(
+ '/multiple_optional',
+ {
+ 'one': 'One%21',
+ 'two': 'Two%21',
+ 'three': 'Three%21',
+ 'four': '4'
+ }
+ )
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: One%21, Two%21, Three%21')
+
+ def test_multiple_optional_args_with_last_kwarg(self):
+ r = self.app_.get('/multiple_optional?three=3')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: None, None, 3')
+
+ def test_multiple_optional_args_with_last_encoded_kwarg(self):
+ r = self.app_.get('/multiple_optional?three=Three%21')
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: None, None, Three!')
+
+ def test_multiple_optional_args_with_middle_arg(self):
+ r = self.app_.get('/multiple_optional', {'two': '2'})
+ assert r.status_int == 200
+ assert r.body == b_('multiple_optional: None, 2, None')
+
+ def test_variable_args(self):
+ r = self.app_.get('/variable_args')
+ assert r.status_int == 200
+ assert r.body == b_('variable_args: ')
+
+ def test_multiple_variable_args(self):
+ r = self.app_.get('/variable_args/1/dummy')
+ assert r.status_int == 200
+ assert r.body == b_('variable_args: 1, dummy')
+
+ def test_multiple_encoded_variable_args(self):
+ r = self.app_.get('/variable_args/Testing%20One%20Two/Three%21')
+ assert r.status_int == 200
+ assert r.body == b_('variable_args: Testing One Two, Three!')
+
+ def test_variable_args_with_kwargs(self):
+ r = self.app_.get('/variable_args?id=2&dummy=dummy')
+ assert r.status_int == 200
+ assert r.body == b_('variable_args: ')
+
+ def test_variable_args_with_dict_kwargs(self):
+ r = self.app_.post('/variable_args', {'id': '3', 'dummy': 'dummy'})
+ assert r.status_int == 200
+ assert r.body == b_('variable_args: ')
+
+ def test_variable_kwargs(self):
+ r = self.app_.get('/variable_kwargs')
+ assert r.status_int == 200
+ assert r.body == b_('variable_kwargs: ')
+
+ def test_multiple_variable_kwargs(self):
+ r = self.app_.get('/variable_kwargs/1/dummy', status=404)
+ assert r.status_int == 404
+
+ def test_multiple_variable_kwargs_with_explicit_kwargs(self):
+ r = self.app_.get('/variable_kwargs?id=2&dummy=dummy')
+ assert r.status_int == 200
+ assert r.body == b_('variable_kwargs: dummy=dummy, id=2')
+
+ def test_multiple_variable_kwargs_with_explicit_encoded_kwargs(self):
+ r = self.app_.get(
+ '/variable_kwargs?id=Two%21&dummy=This%20is%20a%20test'
+ )
+ assert r.status_int == 200
+ assert r.body == b_('variable_kwargs: dummy=This is a test, id=Two!')
+
+ def test_multiple_variable_kwargs_with_dict_kwargs(self):
+ r = self.app_.post('/variable_kwargs', {'id': '3', 'dummy': 'dummy'})
+ assert r.status_int == 200
+ assert r.body == b_('variable_kwargs: dummy=dummy, id=3')
+
+ def test_multiple_variable_kwargs_with_encoded_dict_kwargs(self):
+ r = self.app_.post(
+ '/variable_kwargs',
+ {'id': 'Three%21', 'dummy': 'This%20is%20a%20test'}
+ )
+ assert r.status_int == 200
+ result = 'variable_kwargs: dummy=This%20is%20a%20test, id=Three%21'
+ assert r.body == b_(result)
+
+ def test_variable_all(self):
+ r = self.app_.get('/variable_all')
+ assert r.status_int == 200
+ assert r.body == b_('variable_all: ')
+
+ def test_variable_all_with_one_extra(self):
+ r = self.app_.get('/variable_all/1')
+ assert r.status_int == 200
+ assert r.body == b_('variable_all: 1')
+
+ def test_variable_all_with_two_extras(self):
+ r = self.app_.get('/variable_all/2/dummy')
+ assert r.status_int == 200
+ assert r.body == b_('variable_all: 2, dummy')
+
+ def test_variable_mixed(self):
+ r = self.app_.get('/variable_all/3?month=1&day=12')
+ assert r.status_int == 200
+ assert r.body == b_('variable_all: 3, day=12, month=1')
+
+ def test_variable_mixed_explicit(self):
+ r = self.app_.get('/variable_all/4?id=four&month=1&day=12')
+ assert r.status_int == 200
+ assert r.body == b_('variable_all: 4, day=12, id=four, month=1')
+
+ def test_variable_post(self):
+ r = self.app_.post('/variable_all/5/dummy')
+ assert r.status_int == 200
+ assert r.body == b_('variable_all: 5, dummy')
+
+ def test_variable_post_with_kwargs(self):
+ r = self.app_.post('/variable_all/6', {'month': '1', 'day': '12'})
+ assert r.status_int == 200
+ assert r.body == b_('variable_all: 6, day=12, month=1')
+
+ def test_variable_post_mixed(self):
+ r = self.app_.post(
+ '/variable_all/7',
+ {'id': 'seven', 'month': '1', 'day': '12'}
+ )
+ assert r.status_int == 200
+ assert r.body == b_('variable_all: 7, day=12, id=seven, month=1')
+
+ def test_no_remainder(self):
+ try:
+ r = self.app_.get('/eater')
+ assert r.status_int != 200 # pragma: nocover
+ except Exception as ex:
+ assert type(ex) == TypeError
+ assert ex.args[0] in (
+ "eater() takes at least 4 arguments (3 given)",
+ "eater() missing 1 required positional argument: 'id'"
+ ) # this messaging changed in Python 3.3
+
+ def test_one_remainder(self):
+ r = self.app_.get('/eater/1')
+ assert r.status_int == 200
+ assert r.body == b_('eater: 1, None, ')
+
+ def test_two_remainders(self):
+ r = self.app_.get('/eater/2/dummy')
+ assert r.status_int == 200
+ assert r.body == b_('eater: 2, dummy, ')
+
+ def test_many_remainders(self):
+ r = self.app_.get('/eater/3/dummy/foo/bar')
+ assert r.status_int == 200
+ assert r.body == b_('eater: 3, dummy, foo, bar')
+
+ def test_remainder_with_kwargs(self):
+ r = self.app_.get('/eater/4?month=1&day=12')
+ assert r.status_int == 200
+ assert r.body == b_('eater: 4, None, day=12, month=1')
+
+ def test_remainder_with_many_kwargs(self):
+ r = self.app_.get('/eater/5?id=five&month=1&day=12&dummy=dummy')
+ assert r.status_int == 200
+ assert r.body == b_('eater: 5, dummy, day=12, month=1')
+
+ def test_post_remainder(self):
+ r = self.app_.post('/eater/6')
+ assert r.status_int == 200
+ assert r.body == b_('eater: 6, None, ')
+
+ def test_post_three_remainders(self):
+ r = self.app_.post('/eater/7/dummy')
+ assert r.status_int == 200
+ assert r.body == b_('eater: 7, dummy, ')
+
+ def test_post_many_remainders(self):
+ r = self.app_.post('/eater/8/dummy/foo/bar')
+ assert r.status_int == 200
+ assert r.body == b_('eater: 8, dummy, foo, bar')
+
+ def test_post_remainder_with_kwargs(self):
+ r = self.app_.post('/eater/9', {'month': '1', 'day': '12'})
+ assert r.status_int == 200
+ assert r.body == b_('eater: 9, None, day=12, month=1')
+
+ def test_post_many_remainders_with_many_kwargs(self):
+ r = self.app_.post(
+ '/eater/10',
+ {'id': 'ten', 'month': '1', 'day': '12', 'dummy': 'dummy'}
+ )
+ assert r.status_int == 200
+ assert r.body == b_('eater: 10, dummy, day=12, month=1')
+
+
+class TestRestController(PecanTestCase):
+
+ @property
+ def app_(self):
+
+ class OthersController(object):
+
+ @expose()
+ def index(self, req, resp):
+ return 'OTHERS'
+
+ @expose()
+ def echo(self, req, resp, value):
+ return str(value)
+
+ class ThingsController(RestController):
+ data = ['zero', 'one', 'two', 'three']
+
+ _custom_actions = {'count': ['GET'], 'length': ['GET', 'POST']}
+
+ others = OthersController()
+
+ @expose()
+ def get_one(self, req, resp, id):
+ return self.data[int(id)]
+
+ @expose('json')
+ def get_all(self, req, resp):
+ return dict(items=self.data)
+
+ @expose()
+ def length(self, req, resp, id, value=None):
+ length = len(self.data[int(id)])
+ if value:
+ length += len(value)
+ return str(length)
+
+ @expose()
+ def post(self, req, resp, value):
+ self.data.append(value)
+ resp.status = 302
+ return 'CREATED'
+
+ @expose()
+ def edit(self, req, resp, id):
+ return 'EDIT %s' % self.data[int(id)]
+
+ @expose()
+ def put(self, req, resp, id, value):
+ self.data[int(id)] = value
+ return 'UPDATED'
+
+ @expose()
+ def get_delete(self, req, resp, id):
+ return 'DELETE %s' % self.data[int(id)]
+
+ @expose()
+ def delete(self, req, resp, id):
+ del self.data[int(id)]
+ return 'DELETED'
+
+ @expose()
+ def reset(self, req, resp):
+ return 'RESET'
+
+ @expose()
+ def post_options(self, req, resp):
+ return 'OPTIONS'
+
+ @expose()
+ def options(self, req, resp):
+ abort(500)
+
+ @expose()
+ def other(self, req, resp):
+ abort(500)
+
+ class RootController(object):
+ things = ThingsController()
+
+ # create the app
+ return TestApp(Pecan(RootController(), use_context_locals=False))
+
+ def test_get_all(self):
+ r = self.app_.get('/things')
+ assert r.status_int == 200
+ assert r.body == b_(dumps(dict(items=['zero', 'one', 'two', 'three'])))
+
+ def test_get_one(self):
+ for i, value in enumerate(['zero', 'one', 'two', 'three']):
+ r = self.app_.get('/things/%d' % i)
+ assert r.status_int == 200
+ assert r.body == b_(value)
+
+ def test_post(self):
+ r = self.app_.post('/things', {'value': 'four'})
+ assert r.status_int == 302
+ assert r.body == b_('CREATED')
+
+ def test_custom_action(self):
+ r = self.app_.get('/things/3/edit')
+ assert r.status_int == 200
+ assert r.body == b_('EDIT three')
+
+ def test_put(self):
+ r = self.app_.put('/things/3', {'value': 'THREE!'})
+ assert r.status_int == 200
+ assert r.body == b_('UPDATED')
+
+ def test_put_with_method_parameter_and_get(self):
+ r = self.app_.get('/things/3?_method=put', {'value': 'X'}, status=405)
+ assert r.status_int == 405
+
+ def test_put_with_method_parameter_and_post(self):
+ r = self.app_.post('/things/3?_method=put', {'value': 'THREE!'})
+ assert r.status_int == 200
+ assert r.body == b_('UPDATED')
+
+ def test_get_delete(self):
+ r = self.app_.get('/things/3/delete')
+ assert r.status_int == 200
+ assert r.body == b_('DELETE three')
+
+ def test_delete_method(self):
+ r = self.app_.delete('/things/3')
+ assert r.status_int == 200
+ assert r.body == b_('DELETED')
+
+ def test_delete_with_method_parameter(self):
+ r = self.app_.get('/things/3?_method=DELETE', status=405)
+ assert r.status_int == 405
+
+ def test_delete_with_method_parameter_and_post(self):
+ r = self.app_.post('/things/3?_method=DELETE')
+ assert r.status_int == 200
+ assert r.body == b_('DELETED')
+
+ def test_custom_method_type(self):
+ r = self.app_.request('/things', method='RESET')
+ assert r.status_int == 200
+ assert r.body == b_('RESET')
+
+ def test_custom_method_type_with_method_parameter(self):
+ r = self.app_.get('/things?_method=RESET')
+ assert r.status_int == 200
+ assert r.body == b_('RESET')
+
+ def test_options(self):
+ r = self.app_.request('/things', method='OPTIONS')
+ assert r.status_int == 200
+ assert r.body == b_('OPTIONS')
+
+ def test_options_with_method_parameter(self):
+ r = self.app_.post('/things', {'_method': 'OPTIONS'})
+ assert r.status_int == 200
+ assert r.body == b_('OPTIONS')
+
+ def test_other_custom_action(self):
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ r = self.app_.request('/things/other', method='MISC', status=405)
+ assert r.status_int == 405
+
+ def test_other_custom_action_with_method_parameter(self):
+ r = self.app_.post('/things/other', {'_method': 'MISC'}, status=405)
+ assert r.status_int == 405
+
+ def test_nested_controller_with_trailing_slash(self):
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ r = self.app_.request('/things/others/', method='MISC')
+ assert r.status_int == 200
+ assert r.body == b_('OTHERS')
+
+ def test_nested_controller_without_trailing_slash(self):
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ r = self.app_.request('/things/others', method='MISC', status=302)
+ assert r.status_int == 302
+
+ def test_invalid_custom_action(self):
+ r = self.app_.get('/things?_method=BAD', status=404)
+ assert r.status_int == 404
+
+ def test_named_action(self):
+ # test custom "GET" request "length"
+ r = self.app_.get('/things/1/length')
+ assert r.status_int == 200
+ assert r.body == b_(str(len('one')))
+
+ def test_named_nested_action(self):
+ # test custom "GET" request through subcontroller
+ r = self.app_.get('/things/others/echo?value=test')
+ assert r.status_int == 200
+ assert r.body == b_('test')
+
+ def test_nested_post(self):
+ # test custom "POST" request through subcontroller
+ r = self.app_.post('/things/others/echo', {'value': 'test'})
+ assert r.status_int == 200
+ assert r.body == b_('test')
+
+
+class TestHooks(PecanTestCase):
+
+ def test_basic_single_hook(self):
+ run_hook = []
+
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ run_hook.append('inside')
+ return 'Hello, World!'
+
+ class SimpleHook(PecanHook):
+ def on_route(self, state):
+ run_hook.append('on_route')
+
+ def before(self, state):
+ run_hook.append('before')
+
+ def after(self, state):
+ run_hook.append('after')
+
+ def on_error(self, state, e):
+ run_hook.append('error')
+
+ app = TestApp(Pecan(
+ RootController(),
+ hooks=[SimpleHook()],
+ use_context_locals=False
+ ))
+ response = app.get('/')
+ assert response.status_int == 200
+ assert response.body == b_('Hello, World!')
+
+ assert len(run_hook) == 4
+ assert run_hook[0] == 'on_route'
+ assert run_hook[1] == 'before'
+ assert run_hook[2] == 'inside'
+ assert run_hook[3] == 'after'
+
+ def test_basic_multi_hook(self):
+ run_hook = []
+
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ run_hook.append('inside')
+ return 'Hello, World!'
+
+ class SimpleHook(PecanHook):
+ def __init__(self, id):
+ self.id = str(id)
+
+ def on_route(self, state):
+ run_hook.append('on_route' + self.id)
+
+ def before(self, state):
+ run_hook.append('before' + self.id)
+
+ def after(self, state):
+ run_hook.append('after' + self.id)
+
+ def on_error(self, state, e):
+ run_hook.append('error' + self.id)
+
+ app = TestApp(Pecan(RootController(), hooks=[
+ SimpleHook(1), SimpleHook(2), SimpleHook(3)
+ ], use_context_locals=False))
+ response = app.get('/')
+ assert response.status_int == 200
+ assert response.body == b_('Hello, World!')
+
+ assert len(run_hook) == 10
+ assert run_hook[0] == 'on_route1'
+ assert run_hook[1] == 'on_route2'
+ assert run_hook[2] == 'on_route3'
+ assert run_hook[3] == 'before1'
+ assert run_hook[4] == 'before2'
+ assert run_hook[5] == 'before3'
+ assert run_hook[6] == 'inside'
+ assert run_hook[7] == 'after3'
+ assert run_hook[8] == 'after2'
+ assert run_hook[9] == 'after1'
+
+ def test_partial_hooks(self):
+ run_hook = []
+
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ run_hook.append('inside')
+ return 'Hello World!'
+
+ @expose()
+ def causeerror(self, req, resp):
+ return [][1]
+
+ class ErrorHook(PecanHook):
+ def on_error(self, state, e):
+ run_hook.append('error')
+
+ class OnRouteHook(PecanHook):
+ def on_route(self, state):
+ run_hook.append('on_route')
+
+ app = TestApp(Pecan(RootController(), hooks=[
+ ErrorHook(), OnRouteHook()
+ ], use_context_locals=False))
+
+ response = app.get('/')
+ assert response.status_int == 200
+ assert response.body == b_('Hello World!')
+
+ assert len(run_hook) == 2
+ assert run_hook[0] == 'on_route'
+ assert run_hook[1] == 'inside'
+
+ run_hook = []
+ try:
+ response = app.get('/causeerror')
+ except Exception as e:
+ assert isinstance(e, IndexError)
+
+ assert len(run_hook) == 2
+ assert run_hook[0] == 'on_route'
+ assert run_hook[1] == 'error'
+
+ def test_on_error_response_hook(self):
+ run_hook = []
+
+ class RootController(object):
+ @expose()
+ def causeerror(self, req, resp):
+ return [][1]
+
+ class ErrorHook(PecanHook):
+ def on_error(self, state, e):
+ run_hook.append('error')
+
+ r = webob.Response()
+ r.text = u_('on_error')
+
+ return r
+
+ app = TestApp(Pecan(RootController(), hooks=[
+ ErrorHook()
+ ], use_context_locals=False))
+
+ response = app.get('/causeerror')
+
+ assert len(run_hook) == 1
+ assert run_hook[0] == 'error'
+ assert response.text == 'on_error'
+
+ def test_prioritized_hooks(self):
+ run_hook = []
+
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ run_hook.append('inside')
+ return 'Hello, World!'
+
+ class SimpleHook(PecanHook):
+ def __init__(self, id, priority=None):
+ self.id = str(id)
+ if priority:
+ self.priority = priority
+
+ def on_route(self, state):
+ run_hook.append('on_route' + self.id)
+
+ def before(self, state):
+ run_hook.append('before' + self.id)
+
+ def after(self, state):
+ run_hook.append('after' + self.id)
+
+ def on_error(self, state, e):
+ run_hook.append('error' + self.id)
+
+ papp = Pecan(RootController(), hooks=[
+ SimpleHook(1, 3), SimpleHook(2, 2), SimpleHook(3, 1)
+ ], use_context_locals=False)
+ app = TestApp(papp)
+ response = app.get('/')
+ assert response.status_int == 200
+ assert response.body == b_('Hello, World!')
+
+ assert len(run_hook) == 10
+ assert run_hook[0] == 'on_route3'
+ assert run_hook[1] == 'on_route2'
+ assert run_hook[2] == 'on_route1'
+ assert run_hook[3] == 'before3'
+ assert run_hook[4] == 'before2'
+ assert run_hook[5] == 'before1'
+ assert run_hook[6] == 'inside'
+ assert run_hook[7] == 'after1'
+ assert run_hook[8] == 'after2'
+ assert run_hook[9] == 'after3'
+
+ def test_basic_isolated_hook(self):
+ run_hook = []
+
+ class SimpleHook(PecanHook):
+ def on_route(self, state):
+ run_hook.append('on_route')
+
+ def before(self, state):
+ run_hook.append('before')
+
+ def after(self, state):
+ run_hook.append('after')
+
+ def on_error(self, state, e):
+ run_hook.append('error')
+
+ class SubSubController(object):
+ @expose()
+ def index(self, req, resp):
+ run_hook.append('inside_sub_sub')
+ return 'Deep inside here!'
+
+ class SubController(HookController):
+ __hooks__ = [SimpleHook()]
+
+ @expose()
+ def index(self, req, resp):
+ run_hook.append('inside_sub')
+ return 'Inside here!'
+
+ sub = SubSubController()
+
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ run_hook.append('inside')
+ return 'Hello, World!'
+
+ sub = SubController()
+
+ app = TestApp(Pecan(RootController(), use_context_locals=False))
+ response = app.get('/')
+ assert response.status_int == 200
+ assert response.body == b_('Hello, World!')
+
+ assert len(run_hook) == 1
+ assert run_hook[0] == 'inside'
+
+ run_hook = []
+
+ response = app.get('/sub/')
+ assert response.status_int == 200
+ assert response.body == b_('Inside here!')
+
+ assert len(run_hook) == 3
+ assert run_hook[0] == 'before'
+ assert run_hook[1] == 'inside_sub'
+ assert run_hook[2] == 'after'
+
+ run_hook = []
+ response = app.get('/sub/sub/')
+ assert response.status_int == 200
+ assert response.body == b_('Deep inside here!')
+
+ assert len(run_hook) == 3
+ assert run_hook[0] == 'before'
+ assert run_hook[1] == 'inside_sub_sub'
+ assert run_hook[2] == 'after'
+
+ def test_isolated_hook_with_global_hook(self):
+ run_hook = []
+
+ class SimpleHook(PecanHook):
+ def __init__(self, id):
+ self.id = str(id)
+
+ def on_route(self, state):
+ run_hook.append('on_route' + self.id)
+
+ def before(self, state):
+ run_hook.append('before' + self.id)
+
+ def after(self, state):
+ run_hook.append('after' + self.id)
+
+ def on_error(self, state, e):
+ run_hook.append('error' + self.id)
+
+ class SubController(HookController):
+ __hooks__ = [SimpleHook(2)]
+
+ @expose()
+ def index(self, req, resp):
+ run_hook.append('inside_sub')
+ return 'Inside here!'
+
+ class RootController(object):
+ @expose()
+ def index(self, req, resp):
+ run_hook.append('inside')
+ return 'Hello, World!'
+
+ sub = SubController()
+
+ app = TestApp(Pecan(
+ RootController(),
+ hooks=[SimpleHook(1)],
+ use_context_locals=False
+ ))
+ response = app.get('/')
+ assert response.status_int == 200
+ assert response.body == b_('Hello, World!')
+
+ assert len(run_hook) == 4
+ assert run_hook[0] == 'on_route1'
+ assert run_hook[1] == 'before1'
+ assert run_hook[2] == 'inside'
+ assert run_hook[3] == 'after1'
+
+ run_hook = []
+
+ response = app.get('/sub/')
+ assert response.status_int == 200
+ assert response.body == b_('Inside here!')
+
+ assert len(run_hook) == 6
+ assert run_hook[0] == 'on_route1'
+ assert run_hook[1] == 'before2'
+ assert run_hook[2] == 'before1'
+ assert run_hook[3] == 'inside_sub'
+ assert run_hook[4] == 'after1'
+ assert run_hook[5] == 'after2'