Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a router.request_context context manager #3086

Merged
merged 1 commit into from
Jun 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()