Skip to content

Commit

Permalink
Merge pull request #3086 from mmerickel/set-execution-context
Browse files Browse the repository at this point in the history
add a router.request_context context manager
  • Loading branch information
mmerickel authored Jun 20, 2017
2 parents 983216c + 8226534 commit 22e61f1
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 79 deletions.
50 changes: 40 additions & 10 deletions pyramid/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,25 +679,52 @@ def __call__(context, request):
"""

class IRouter(Interface):
""" WSGI application which routes requests to 'view' code based on
a view registry."""
"""
WSGI application which routes requests to 'view' code based on
a view registry.
"""
registry = Attribute(
"""Component architecture registry local to this application.""")

def make_request(environ):
def request_context(environ):
"""
Create a new request object.
Create a new request context from a WSGI environ.
The request context is used to push/pop the threadlocals required
when processing the request. It also contains an initialized
:class:`pyramid.interfaces.IRequest` instance using the registered
:class:`pyramid.interfaces.IRequestFactory`. The context may be
used as a context manager to control the threadlocal lifecycle:
.. code-block:: python
with router.request_context(environ) as request:
...
Alternatively, the context may be used without the ``with`` statement
by manually invoking its ``begin()`` and ``end()`` methods.
.. code-block:: python
ctx = router.request_context(environ)
request = ctx.begin()
try:
...
finally:
ctx.end()
This method initializes a new :class:`pyramid.interfaces.IRequest`
object using the application's
:class:`pyramid.interfaces.IRequestFactory`.
"""

def invoke_request(request):
"""
Invoke the :app:`Pyramid` request pipeline.
See :ref:`router_chapter` for information on the request pipeline.
The output should be a :class:`pyramid.interfaces.IResponse` object
or a raised exception.
"""

class IExecutionPolicy(Interface):
Expand All @@ -716,13 +743,16 @@ def __call__(environ, router):
object or an exception that will be handled by WSGI middleware.
The default execution policy simply creates a request and sends it
through the pipeline:
through the pipeline, attempting to render any exception that escapes:
.. code-block:: python
def simple_execution_policy(environ, router):
request = router.make_request(environ)
return router.invoke_request(request)
with router.request_context(environ) as request:
try:
return router.invoke_request(request)
except Exception:
return request.invoke_exception_view(reraise=True)
"""

class ISettings(IDict):
Expand Down
81 changes: 41 additions & 40 deletions pyramid/router.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
from zope.interface import (
implementer,
providedBy,
Expand All @@ -25,12 +24,11 @@
BeforeTraversal,
)

from pyramid.compat import reraise
from pyramid.httpexceptions import HTTPNotFound
from pyramid.request import Request
from pyramid.view import _call_view
from pyramid.request import apply_request_extensions
from pyramid.threadlocal import manager
from pyramid.threadlocal import RequestContext

from pyramid.traversal import (
DefaultRootFactory,
Expand All @@ -43,8 +41,6 @@ class Router(object):
debug_notfound = False
debug_routematch = False

threadlocal_manager = manager

def __init__(self, registry):
q = registry.queryUtility
self.logger = q(IDebugLogger)
Expand Down Expand Up @@ -195,16 +191,35 @@ def invoke_subrequest(self, request, use_tweens=False):
extensions = self.request_extensions
if extensions is not None:
apply_request_extensions(request, extensions=extensions)
return self.invoke_request(request, _use_tweens=use_tweens)
with RequestContext(request):
return self.invoke_request(request, _use_tweens=use_tweens)

def make_request(self, environ):
def request_context(self, environ):
"""
Configure a request object for use by the router.
Create a new request context from a WSGI environ.
The request context is used to push/pop the threadlocals required
when processing the request. It also contains an initialized
:class:`pyramid.interfaces.IRequest` instance using the registered
:class:`pyramid.interfaces.IRequestFactory`. The context may be
used as a context manager to control the threadlocal lifecycle:
.. code-block:: python
with router.request_context(environ) as request:
...
The request is created using the configured
:class:`pyramid.interfaces.IRequestFactory` and will have any
configured request methods / properties added that were set by
:meth:`pyramid.config.Configurator.add_request_method`.
Alternatively, the context may be used without the ``with`` statement
by manually invoking its ``begin()`` and ``end()`` methods.
.. code-block:: python
ctx = router.request_context(environ)
request = ctx.begin()
try:
...
finally:
ctx.end()
"""
request = self.request_factory(environ)
Expand All @@ -213,7 +228,7 @@ def make_request(self, environ):
extensions = self.request_extensions
if extensions is not None:
apply_request_extensions(request, extensions=extensions)
return request
return RequestContext(request)

def invoke_request(self, request, _use_tweens=True):
"""
Expand All @@ -222,35 +237,27 @@ def invoke_request(self, request, _use_tweens=True):
"""
registry = self.registry
has_listeners = self.registry.has_listeners
notify = self.registry.notify
threadlocals = {'registry': registry, 'request': request}
manager = self.threadlocal_manager
manager.push(threadlocals)
has_listeners = registry.has_listeners
notify = registry.notify

if _use_tweens:
handle_request = self.handle_request
else:
handle_request = self.orig_handle_request

try:
response = handle_request(request)

try:
response = handle_request(request)

if request.response_callbacks:
request._process_response_callbacks(response)
if request.response_callbacks:
request._process_response_callbacks(response)

has_listeners and notify(NewResponse(request, response))
has_listeners and notify(NewResponse(request, response))

return response

finally:
if request.finished_callbacks:
request._process_finished_callbacks()
return response

finally:
manager.pop()
if request.finished_callbacks:
request._process_finished_callbacks()

def __call__(self, environ, start_response):
"""
Expand All @@ -264,14 +271,8 @@ def __call__(self, environ, start_response):
return response(environ, start_response)

def default_execution_policy(environ, router):
request = router.make_request(environ)
try:
return router.invoke_request(request)
except Exception:
exc_info = sys.exc_info()
with router.request_context(environ) as request:
try:
return request.invoke_exception_view(exc_info)
except HTTPNotFound:
reraise(*exc_info)
finally:
del exc_info # avoid local ref cycle
return router.invoke_request(request)
except Exception:
return request.invoke_exception_view(reraise=True)
69 changes: 42 additions & 27 deletions pyramid/tests/test_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,22 +641,6 @@ def listener(event):
result = router(environ, start_response)
self.assertEqual(result, exception_response.app_iter)

def test_call_pushes_and_pops_threadlocal_manager(self):
from pyramid.interfaces import IViewClassifier
context = DummyContext()
self._registerTraverserFactory(context)
response = DummyResponse()
response.app_iter = ['Hello world']
view = DummyView(response)
environ = self._makeEnviron()
self._registerView(view, '', IViewClassifier, None, None)
router = self._makeOne()
start_response = DummyStartResponse()
router.threadlocal_manager = DummyThreadLocalManager()
router(environ, start_response)
self.assertEqual(len(router.threadlocal_manager.pushed), 1)
self.assertEqual(len(router.threadlocal_manager.popped), 1)

def test_call_route_matches_and_has_factory(self):
from pyramid.interfaces import IViewClassifier
logger = self._registerLogger()
Expand Down Expand Up @@ -1311,6 +1295,48 @@ class Exception2(Exception):
result = router(environ, start_response)
self.assertEqual(result, ["Hello, world"])

def test_request_context_with_statement(self):
from pyramid.threadlocal import get_current_request
from pyramid.interfaces import IExecutionPolicy
from pyramid.request import Request
from pyramid.response import Response
registry = self.config.registry
result = []
def dummy_policy(environ, router):
with router.request_context(environ):
result.append(get_current_request())
result.append(get_current_request())
return Response(status=200, body=b'foo')
registry.registerUtility(dummy_policy, IExecutionPolicy)
router = self._makeOne()
resp = Request.blank('/test_path').get_response(router)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.body, b'foo')
self.assertEqual(result[0].path_info, '/test_path')
self.assertEqual(result[1], None)

def test_request_context_manually(self):
from pyramid.threadlocal import get_current_request
from pyramid.interfaces import IExecutionPolicy
from pyramid.request import Request
from pyramid.response import Response
registry = self.config.registry
result = []
def dummy_policy(environ, router):
ctx = router.request_context(environ)
ctx.begin()
result.append(get_current_request())
ctx.end()
result.append(get_current_request())
return Response(status=200, body=b'foo')
registry.registerUtility(dummy_policy, IExecutionPolicy)
router = self._makeOne()
resp = Request.blank('/test_path').get_response(router)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.body, b'foo')
self.assertEqual(result[0].path_info, '/test_path')
self.assertEqual(result[1], None)

class DummyPredicate(object):
def __call__(self, info, request):
return True
Expand Down Expand Up @@ -1362,17 +1388,6 @@ def __call__(self, environ, start_response):
start_response(self.status, self.headerlist)
return self.app_iter

class DummyThreadLocalManager:
def __init__(self):
self.pushed = []
self.popped = []

def push(self, val):
self.pushed.append(val)

def pop(self):
self.popped.append(True)

class DummyAuthenticationPolicy:
pass

Expand Down
27 changes: 25 additions & 2 deletions pyramid/threadlocal.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,48 @@ def defaults():
manager = ThreadLocalManager(default=defaults)

def get_current_request():
"""Return the currently active request or ``None`` if no request
"""
Return the currently active request or ``None`` if no request
is currently active.
This function should be used *extremely sparingly*, usually only
in unit testing code. It's almost always usually a mistake to use
``get_current_request`` outside a testing context because its
usage makes it possible to write code that can be neither easily
tested nor scripted.
"""
return manager.get()['request']

def get_current_registry(context=None): # context required by getSiteManager API
"""Return the currently active :term:`application registry` or the
"""
Return the currently active :term:`application registry` or the
global application registry if no request is currently active.
This function should be used *extremely sparingly*, usually only
in unit testing code. It's almost always usually a mistake to use
``get_current_registry`` outside a testing context because its
usage makes it possible to write code that can be neither easily
tested nor scripted.
"""
return manager.get()['registry']

class RequestContext(object):
def __init__(self, request):
self.request = request

def begin(self):
request = self.request
registry = request.registry
manager.push({'registry': registry, 'request': request})
return request

def end(self):
manager.pop()

def __enter__(self):
return self.begin()

def __exit__(self, *args):
self.end()

0 comments on commit 22e61f1

Please sign in to comment.