Add support for Pecan *without* thread local request/response objects
Change-Id: I5a5a05e1f57ef2d8ad64e925c7ffa6907b914273
This commit is contained in:
		
							
								
								
									
										55
									
								
								docs/source/contextlocals.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								docs/source/contextlocals.rst
									
									
									
									
									
										Normal 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. | ||||
| @@ -38,6 +38,7 @@ Narrative Documentation | ||||
|    secure_controller.rst | ||||
|    hooks.rst | ||||
|    jsonify.rst | ||||
|    contextlocals.rst | ||||
|    commands.rst | ||||
|    development.rst | ||||
|    deployment.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): | ||||
|   | ||||
							
								
								
									
										238
									
								
								pecan/core.py
									
									
									
									
									
								
							
							
						
						
									
										238
									
								
								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: | ||||
|             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: | ||||
|             # 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) | ||||
|   | ||||
| @@ -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): | ||||
|         ''' | ||||
|   | ||||
							
								
								
									
										106
									
								
								pecan/rest.py
									
									
									
									
									
								
							
							
						
						
									
										106
									
								
								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. | ||||
|         ''' | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										1312
									
								
								pecan/tests/test_no_thread_locals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1312
									
								
								pecan/tests/test_no_thread_locals.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user
	 Ryan Petrello
					Ryan Petrello