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
|
secure_controller.rst
|
||||||
hooks.rst
|
hooks.rst
|
||||||
jsonify.rst
|
jsonify.rst
|
||||||
|
contextlocals.rst
|
||||||
commands.rst
|
commands.rst
|
||||||
development.rst
|
development.rst
|
||||||
deployment.rst
|
deployment.rst
|
||||||
|
|||||||
@@ -270,11 +270,11 @@ a :func:`_route` method will enable you to have total control.
|
|||||||
Interacting with the Request and Response Object
|
Interacting with the Request and Response Object
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
|
|
||||||
For every HTTP request, Pecan maintains a thread-local reference to the request
|
For every HTTP request, Pecan maintains a :ref:`thread-local reference
|
||||||
and response object, ``pecan.request`` and ``pecan.response``. These are
|
<contextlocals>` to the request and response object, ``pecan.request`` and
|
||||||
instances of :class:`webob.request.BaseRequest` and
|
``pecan.response``. These are instances of :class:`webob.request.BaseRequest`
|
||||||
:class:`webob.response.Response`, respectively, and can be interacted with from
|
and :class:`webob.response.Response`, respectively, and can be interacted with
|
||||||
within Pecan controller code::
|
from within Pecan controller code::
|
||||||
|
|
||||||
@pecan.expose()
|
@pecan.expose()
|
||||||
def login(self):
|
def login(self):
|
||||||
|
|||||||
238
pecan/core.py
238
pecan/core.py
@@ -27,10 +27,31 @@ state = None
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RoutingState(object):
|
||||||
|
|
||||||
|
def __init__(self, request, response, app, hooks=[], controller=None):
|
||||||
|
self.request = request
|
||||||
|
self.response = response
|
||||||
|
self.app = app
|
||||||
|
self.hooks = hooks
|
||||||
|
self.controller = controller
|
||||||
|
|
||||||
|
|
||||||
def proxy(key):
|
def proxy(key):
|
||||||
class ObjectProxy(object):
|
class ObjectProxy(object):
|
||||||
|
|
||||||
|
explanation_ = AttributeError(
|
||||||
|
"`pecan.state` is not bound to a context-local context.\n"
|
||||||
|
"Ensure that you're accessing `pecan.request` or `pecan.response` "
|
||||||
|
"from within the context of a WSGI `__call__` and that "
|
||||||
|
"`use_context_locals` = True."
|
||||||
|
)
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
obj = getattr(state, key)
|
try:
|
||||||
|
obj = getattr(state, key)
|
||||||
|
except AttributeError:
|
||||||
|
raise self.explanation_
|
||||||
return getattr(obj, attr)
|
return getattr(obj, attr)
|
||||||
|
|
||||||
def __setattr__(self, attr, value):
|
def __setattr__(self, attr, value):
|
||||||
@@ -87,7 +108,7 @@ def abort(status_code=None, detail='', headers=None, comment=None, **kw):
|
|||||||
|
|
||||||
|
|
||||||
def redirect(location=None, internal=False, code=None, headers={},
|
def redirect(location=None, internal=False, code=None, headers={},
|
||||||
add_slash=False):
|
add_slash=False, request=None):
|
||||||
'''
|
'''
|
||||||
Perform a redirect, either internal or external. An internal redirect
|
Perform a redirect, either internal or external. An internal redirect
|
||||||
performs the redirect server-side, while the external redirect utilizes
|
performs the redirect server-side, while the external redirect utilizes
|
||||||
@@ -99,12 +120,14 @@ def redirect(location=None, internal=False, code=None, headers={},
|
|||||||
:param code: The HTTP status code to use for the redirect. Defaults to 302.
|
:param code: The HTTP status code to use for the redirect. Defaults to 302.
|
||||||
:param headers: Any HTTP headers to send with the response, as a
|
:param headers: Any HTTP headers to send with the response, as a
|
||||||
dictionary.
|
dictionary.
|
||||||
|
:param request: The :class:`webob.request.BaseRequest` instance to use.
|
||||||
'''
|
'''
|
||||||
|
request = request or state.request
|
||||||
|
|
||||||
if add_slash:
|
if add_slash:
|
||||||
if location is None:
|
if location is None:
|
||||||
split_url = list(urlparse.urlsplit(state.request.url))
|
split_url = list(urlparse.urlsplit(request.url))
|
||||||
new_proto = state.request.environ.get(
|
new_proto = request.environ.get(
|
||||||
'HTTP_X_FORWARDED_PROTO', split_url[0]
|
'HTTP_X_FORWARDED_PROTO', split_url[0]
|
||||||
)
|
)
|
||||||
split_url[0] = new_proto
|
split_url[0] = new_proto
|
||||||
@@ -126,7 +149,7 @@ def redirect(location=None, internal=False, code=None, headers={},
|
|||||||
raise exc.status_map[code](location=location, headers=headers)
|
raise exc.status_map[code](location=location, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
def render(template, namespace):
|
def render(template, namespace, app=None):
|
||||||
'''
|
'''
|
||||||
Render the specified template using the Pecan rendering framework
|
Render the specified template using the Pecan rendering framework
|
||||||
with the specified template namespace as a dictionary. Useful in a
|
with the specified template namespace as a dictionary. Useful in a
|
||||||
@@ -136,9 +159,10 @@ def render(template, namespace):
|
|||||||
``@expose``.
|
``@expose``.
|
||||||
:param namespace: The namespace to use for rendering the template, as a
|
:param namespace: The namespace to use for rendering the template, as a
|
||||||
dictionary.
|
dictionary.
|
||||||
|
:param app: The instance of :class:`pecan.Pecan` to use
|
||||||
'''
|
'''
|
||||||
|
app = app or state.app
|
||||||
return state.app.render(template, namespace)
|
return app.render(template, namespace)
|
||||||
|
|
||||||
|
|
||||||
def load_app(config, **kwargs):
|
def load_app(config, **kwargs):
|
||||||
@@ -165,31 +189,7 @@ def load_app(config, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Pecan(object):
|
class PecanBase(object):
|
||||||
'''
|
|
||||||
Base Pecan application object. Generally created using ``pecan.make_app``,
|
|
||||||
rather than being created manually.
|
|
||||||
|
|
||||||
Creates a Pecan application instance, which is a WSGI application.
|
|
||||||
|
|
||||||
:param root: A string representing a root controller object (e.g.,
|
|
||||||
"myapp.controller.root.RootController")
|
|
||||||
:param default_renderer: The default template rendering engine to use.
|
|
||||||
Defaults to mako.
|
|
||||||
:param template_path: A relative file system path (from the project root)
|
|
||||||
where template files live. Defaults to 'templates'.
|
|
||||||
:param hooks: A callable which returns a list of
|
|
||||||
:class:`pecan.hooks.PecanHook`
|
|
||||||
:param custom_renderers: Custom renderer objects, as a dictionary keyed
|
|
||||||
by engine name.
|
|
||||||
:param extra_template_vars: Any variables to inject into the template
|
|
||||||
namespace automatically.
|
|
||||||
:param force_canonical: A boolean indicating if this project should
|
|
||||||
require canonical URLs.
|
|
||||||
:param guess_content_type_from_ext: A boolean indicating if this project
|
|
||||||
should use the extension in the URL for guessing
|
|
||||||
the content type to return.
|
|
||||||
'''
|
|
||||||
|
|
||||||
SIMPLEST_CONTENT_TYPES = (
|
SIMPLEST_CONTENT_TYPES = (
|
||||||
['text/html'],
|
['text/html'],
|
||||||
@@ -201,9 +201,6 @@ class Pecan(object):
|
|||||||
custom_renderers={}, extra_template_vars={},
|
custom_renderers={}, extra_template_vars={},
|
||||||
force_canonical=True, guess_content_type_from_ext=True,
|
force_canonical=True, guess_content_type_from_ext=True,
|
||||||
context_local_factory=None, **kw):
|
context_local_factory=None, **kw):
|
||||||
|
|
||||||
self.init_context_local(context_local_factory)
|
|
||||||
|
|
||||||
if isinstance(root, six.string_types):
|
if isinstance(root, six.string_types):
|
||||||
root = self.__translate_root__(root)
|
root = self.__translate_root__(root)
|
||||||
|
|
||||||
@@ -223,12 +220,6 @@ class Pecan(object):
|
|||||||
self.force_canonical = force_canonical
|
self.force_canonical = force_canonical
|
||||||
self.guess_content_type_from_ext = guess_content_type_from_ext
|
self.guess_content_type_from_ext = guess_content_type_from_ext
|
||||||
|
|
||||||
def init_context_local(self, local_factory):
|
|
||||||
global state
|
|
||||||
if local_factory is None:
|
|
||||||
from threading import local as local_factory
|
|
||||||
state = local_factory()
|
|
||||||
|
|
||||||
def __translate_root__(self, item):
|
def __translate_root__(self, item):
|
||||||
'''
|
'''
|
||||||
Creates a root controller instance from a string root, e.g.,
|
Creates a root controller instance from a string root, e.g.,
|
||||||
@@ -259,10 +250,9 @@ class Pecan(object):
|
|||||||
:param node: The node, such as a root controller object.
|
:param node: The node, such as a root controller object.
|
||||||
:param path: The path to look up on this node.
|
:param path: The path to look up on this node.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
path = path.split('/')[1:]
|
path = path.split('/')[1:]
|
||||||
try:
|
try:
|
||||||
node, remainder = lookup_controller(node, path)
|
node, remainder = lookup_controller(node, path, req)
|
||||||
return node, remainder
|
return node, remainder
|
||||||
except NonCanonicalPath as e:
|
except NonCanonicalPath as e:
|
||||||
if self.force_canonical and \
|
if self.force_canonical and \
|
||||||
@@ -276,7 +266,7 @@ class Pecan(object):
|
|||||||
(req.pecan['routing_path'],
|
(req.pecan['routing_path'],
|
||||||
req.pecan['routing_path'])
|
req.pecan['routing_path'])
|
||||||
)
|
)
|
||||||
redirect(code=302, add_slash=True)
|
redirect(code=302, add_slash=True, request=req)
|
||||||
return e.controller, e.remainder
|
return e.controller, e.remainder
|
||||||
|
|
||||||
def determine_hooks(self, controller=None):
|
def determine_hooks(self, controller=None):
|
||||||
@@ -299,7 +289,7 @@ class Pecan(object):
|
|||||||
)
|
)
|
||||||
return self.hooks
|
return self.hooks
|
||||||
|
|
||||||
def handle_hooks(self, hook_type, *args):
|
def handle_hooks(self, hooks, hook_type, *args):
|
||||||
'''
|
'''
|
||||||
Processes hooks of the specified type.
|
Processes hooks of the specified type.
|
||||||
|
|
||||||
@@ -307,10 +297,8 @@ class Pecan(object):
|
|||||||
``on_error``, and ``on_route``.
|
``on_error``, and ``on_route``.
|
||||||
:param \*args: Arguments to pass to the hooks.
|
:param \*args: Arguments to pass to the hooks.
|
||||||
'''
|
'''
|
||||||
if hook_type in ['before', 'on_route']:
|
if hook_type not in ['before', 'on_route']:
|
||||||
hooks = state.hooks
|
hooks = reversed(hooks)
|
||||||
else:
|
|
||||||
hooks = reversed(state.hooks)
|
|
||||||
|
|
||||||
for hook in hooks:
|
for hook in hooks:
|
||||||
result = getattr(hook, hook_type)(*args)
|
result = getattr(hook, hook_type)(*args)
|
||||||
@@ -319,14 +307,15 @@ class Pecan(object):
|
|||||||
if hook_type == 'on_error' and isinstance(result, Response):
|
if hook_type == 'on_error' and isinstance(result, Response):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_args(self, pecan_state, all_params, remainder, argspec, im_self):
|
def get_args(self, state, all_params, remainder, argspec, im_self):
|
||||||
'''
|
'''
|
||||||
Determines the arguments for a controller based upon parameters
|
Determines the arguments for a controller based upon parameters
|
||||||
passed the argument specification for the controller.
|
passed the argument specification for the controller.
|
||||||
'''
|
'''
|
||||||
args = []
|
args = []
|
||||||
kwargs = dict()
|
kwargs = dict()
|
||||||
valid_args = argspec[0][1:]
|
valid_args = argspec.args[1:] # pop off `self`
|
||||||
|
pecan_state = state.request.pecan
|
||||||
|
|
||||||
def _decode(x):
|
def _decode(x):
|
||||||
return unquote_plus(x) if isinstance(x, six.string_types) \
|
return unquote_plus(x) if isinstance(x, six.string_types) \
|
||||||
@@ -392,13 +381,12 @@ class Pecan(object):
|
|||||||
template = template.split(':')[1]
|
template = template.split(':')[1]
|
||||||
return renderer.render(template, namespace)
|
return renderer.render(template, namespace)
|
||||||
|
|
||||||
def handle_request(self, req, resp):
|
def find_controller(self, state):
|
||||||
'''
|
'''
|
||||||
The main request handler for Pecan applications.
|
The main request handler for Pecan applications.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# get a sorted list of hooks, by priority (no controller hooks yet)
|
# get a sorted list of hooks, by priority (no controller hooks yet)
|
||||||
state.hooks = self.hooks
|
req = state.request
|
||||||
pecan_state = req.pecan
|
pecan_state = req.pecan
|
||||||
|
|
||||||
# store the routing path for the current application to allow hooks to
|
# store the routing path for the current application to allow hooks to
|
||||||
@@ -406,7 +394,7 @@ class Pecan(object):
|
|||||||
pecan_state['routing_path'] = path = req.encget('PATH_INFO')
|
pecan_state['routing_path'] = path = req.encget('PATH_INFO')
|
||||||
|
|
||||||
# handle "on_route" hooks
|
# handle "on_route" hooks
|
||||||
self.handle_hooks('on_route', state)
|
self.handle_hooks(self.hooks, 'on_route', state)
|
||||||
|
|
||||||
# lookup the controller, respecting content-type as requested
|
# lookup the controller, respecting content-type as requested
|
||||||
# by the file extension on the URI
|
# by the file extension on the URI
|
||||||
@@ -491,11 +479,8 @@ class Pecan(object):
|
|||||||
)
|
)
|
||||||
raise exc.HTTPNotFound
|
raise exc.HTTPNotFound
|
||||||
|
|
||||||
# get a sorted list of hooks, by priority
|
|
||||||
state.hooks = self.determine_hooks(controller)
|
|
||||||
|
|
||||||
# handle "before" hooks
|
# handle "before" hooks
|
||||||
self.handle_hooks('before', state)
|
self.handle_hooks(self.determine_hooks(controller), 'before', state)
|
||||||
|
|
||||||
# fetch any parameters
|
# fetch any parameters
|
||||||
if req.method == 'GET':
|
if req.method == 'GET':
|
||||||
@@ -505,13 +490,25 @@ class Pecan(object):
|
|||||||
|
|
||||||
# fetch the arguments for the controller
|
# fetch the arguments for the controller
|
||||||
args, kwargs = self.get_args(
|
args, kwargs = self.get_args(
|
||||||
pecan_state,
|
state,
|
||||||
params,
|
params,
|
||||||
remainder,
|
remainder,
|
||||||
cfg['argspec'],
|
cfg['argspec'],
|
||||||
im_self
|
im_self
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return controller, args, kwargs
|
||||||
|
|
||||||
|
def invoke_controller(self, controller, args, kwargs, state):
|
||||||
|
'''
|
||||||
|
The main request handler for Pecan applications.
|
||||||
|
'''
|
||||||
|
cfg = _cfg(controller)
|
||||||
|
content_types = cfg.get('content_types', {})
|
||||||
|
req = state.request
|
||||||
|
resp = state.response
|
||||||
|
pecan_state = req.pecan
|
||||||
|
|
||||||
# get the result from the controller
|
# get the result from the controller
|
||||||
result = controller(*args, **kwargs)
|
result = controller(*args, **kwargs)
|
||||||
|
|
||||||
@@ -570,11 +567,10 @@ class Pecan(object):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
# create the request and response object
|
# create the request and response object
|
||||||
state.request = req = Request(environ)
|
req = Request(environ)
|
||||||
state.response = resp = Response()
|
resp = Response()
|
||||||
state.hooks = []
|
state = RoutingState(req, resp, self)
|
||||||
state.app = self
|
controller = None
|
||||||
state.controller = None
|
|
||||||
|
|
||||||
# handle the request
|
# handle the request
|
||||||
try:
|
try:
|
||||||
@@ -582,7 +578,8 @@ class Pecan(object):
|
|||||||
req.context = environ.get('pecan.recursive.context', {})
|
req.context = environ.get('pecan.recursive.context', {})
|
||||||
req.pecan = dict(content_type=None)
|
req.pecan = dict(content_type=None)
|
||||||
|
|
||||||
self.handle_request(req, resp)
|
controller, args, kwargs = self.find_controller(state)
|
||||||
|
self.invoke_controller(controller, args, kwargs, state)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# if this is an HTTP Exception, set it as the response
|
# if this is an HTTP Exception, set it as the response
|
||||||
if isinstance(e, exc.HTTPException):
|
if isinstance(e, exc.HTTPException):
|
||||||
@@ -592,7 +589,12 @@ class Pecan(object):
|
|||||||
# if this is not an internal redirect, run error hooks
|
# if this is not an internal redirect, run error hooks
|
||||||
on_error_result = None
|
on_error_result = None
|
||||||
if not isinstance(e, ForwardRequestException):
|
if not isinstance(e, ForwardRequestException):
|
||||||
on_error_result = self.handle_hooks('on_error', state, e)
|
on_error_result = self.handle_hooks(
|
||||||
|
self.determine_hooks(state.controller),
|
||||||
|
'on_error',
|
||||||
|
state,
|
||||||
|
e
|
||||||
|
)
|
||||||
|
|
||||||
# if the on_error handler returned a Response, use it.
|
# if the on_error handler returned a Response, use it.
|
||||||
if isinstance(on_error_result, Response):
|
if isinstance(on_error_result, Response):
|
||||||
@@ -602,15 +604,111 @@ class Pecan(object):
|
|||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
# handle "after" hooks
|
# handle "after" hooks
|
||||||
self.handle_hooks('after', state)
|
self.handle_hooks(
|
||||||
|
self.determine_hooks(state.controller), 'after', state
|
||||||
|
)
|
||||||
|
|
||||||
# get the response
|
# get the response
|
||||||
|
return state.response(environ, start_response)
|
||||||
|
|
||||||
|
|
||||||
|
class ExplicitPecan(PecanBase):
|
||||||
|
|
||||||
|
def get_args(self, state, all_params, remainder, argspec, im_self):
|
||||||
|
# When comparing the argspec of the method to GET/POST params,
|
||||||
|
# ignore the implicit (req, resp) at the beginning of the function
|
||||||
|
# signature
|
||||||
|
signature_error = TypeError(
|
||||||
|
'When `use_context_locals` is `False`, pecan passes an explicit '
|
||||||
|
'reference to the request and response as the first two arguments '
|
||||||
|
'to the controller.\nChange the `%s.%s.%s` signature to accept '
|
||||||
|
'exactly 2 initial arguments (req, resp)' % (
|
||||||
|
state.controller.__self__.__class__.__module__,
|
||||||
|
state.controller.__self__.__class__.__name__,
|
||||||
|
state.controller.__name__
|
||||||
|
)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
return state.response(environ, start_response)
|
positional = argspec.args[:]
|
||||||
|
positional.pop(1) # req
|
||||||
|
positional.pop(1) # resp
|
||||||
|
argspec = argspec._replace(args=positional)
|
||||||
|
except IndexError:
|
||||||
|
raise signature_error
|
||||||
|
|
||||||
|
args, kwargs = super(ExplicitPecan, self).get_args(
|
||||||
|
state, all_params, remainder, argspec, im_self
|
||||||
|
)
|
||||||
|
args = [state.request, state.response] + args
|
||||||
|
return args, kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class Pecan(PecanBase):
|
||||||
|
'''
|
||||||
|
Pecan application object. Generally created using ``pecan.make_app``,
|
||||||
|
rather than being created manually.
|
||||||
|
|
||||||
|
Creates a Pecan application instance, which is a WSGI application.
|
||||||
|
|
||||||
|
:param root: A string representing a root controller object (e.g.,
|
||||||
|
"myapp.controller.root.RootController")
|
||||||
|
:param default_renderer: The default template rendering engine to use.
|
||||||
|
Defaults to mako.
|
||||||
|
:param template_path: A relative file system path (from the project root)
|
||||||
|
where template files live. Defaults to 'templates'.
|
||||||
|
:param hooks: A callable which returns a list of
|
||||||
|
:class:`pecan.hooks.PecanHook`
|
||||||
|
:param custom_renderers: Custom renderer objects, as a dictionary keyed
|
||||||
|
by engine name.
|
||||||
|
:param extra_template_vars: Any variables to inject into the template
|
||||||
|
namespace automatically.
|
||||||
|
:param force_canonical: A boolean indicating if this project should
|
||||||
|
require canonical URLs.
|
||||||
|
:param guess_content_type_from_ext: A boolean indicating if this project
|
||||||
|
should use the extension in the URL for guessing
|
||||||
|
the content type to return.
|
||||||
|
:param use_context_locals: When `True`, `pecan.request` and
|
||||||
|
`pecan.response` will be available as
|
||||||
|
thread-local references.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kw):
|
||||||
|
if kw.get('use_context_locals') is False:
|
||||||
|
self = super(Pecan, cls).__new__(ExplicitPecan, *args, **kw)
|
||||||
|
self.__init__(*args, **kw)
|
||||||
|
return self
|
||||||
|
return super(Pecan, cls).__new__(cls)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
self.init_context_local(kw.get('context_local_factory'))
|
||||||
|
super(Pecan, self).__init__(*args, **kw)
|
||||||
|
|
||||||
|
def __call__(self, environ, start_response):
|
||||||
|
try:
|
||||||
|
state.hooks = []
|
||||||
|
state.app = self
|
||||||
|
state.controller = None
|
||||||
|
return super(Pecan, self).__call__(environ, start_response)
|
||||||
finally:
|
finally:
|
||||||
# clean up state
|
|
||||||
del state.hooks
|
del state.hooks
|
||||||
del state.request
|
del state.request
|
||||||
del state.response
|
del state.response
|
||||||
del state.controller
|
del state.controller
|
||||||
del state.app
|
del state.app
|
||||||
|
|
||||||
|
def init_context_local(self, local_factory):
|
||||||
|
global state
|
||||||
|
if local_factory is None:
|
||||||
|
from threading import local as local_factory
|
||||||
|
state = local_factory()
|
||||||
|
|
||||||
|
def find_controller(self, _state):
|
||||||
|
state.request = _state.request
|
||||||
|
state.response = _state.response
|
||||||
|
controller, args, kw = super(Pecan, self).find_controller(_state)
|
||||||
|
state.controller = controller
|
||||||
|
return controller, args, kw
|
||||||
|
|
||||||
|
def handle_hooks(self, hooks, *args, **kw):
|
||||||
|
state.hooks = hooks
|
||||||
|
return super(Pecan, self).handle_hooks(hooks, *args, **kw)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from inspect import getmembers
|
|||||||
from webob.exc import HTTPFound
|
from webob.exc import HTTPFound
|
||||||
|
|
||||||
from .util import iscontroller, _cfg
|
from .util import iscontroller, _cfg
|
||||||
from .routing import lookup_controller
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'PecanHook', 'TransactionHook', 'HookController',
|
'PecanHook', 'TransactionHook', 'HookController',
|
||||||
@@ -334,8 +333,7 @@ class RequestViewerHook(PecanHook):
|
|||||||
Specific to Pecan (not available in the request object)
|
Specific to Pecan (not available in the request object)
|
||||||
'''
|
'''
|
||||||
path = state.request.pecan['routing_path'].split('/')[1:]
|
path = state.request.pecan['routing_path'].split('/')[1:]
|
||||||
controller, reminder = lookup_controller(state.app.root, path)
|
return state.controller.__str__().split()[2]
|
||||||
return controller.__str__().split()[2]
|
|
||||||
|
|
||||||
def format_hooks(self, hooks):
|
def format_hooks(self, hooks):
|
||||||
'''
|
'''
|
||||||
|
|||||||
106
pecan/rest.py
106
pecan/rest.py
@@ -1,9 +1,10 @@
|
|||||||
from inspect import getargspec, ismethod
|
from inspect import getargspec, ismethod
|
||||||
|
import warnings
|
||||||
|
|
||||||
from webob import exc
|
from webob import exc
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from .core import abort, request
|
from .core import abort
|
||||||
from .decorators import expose
|
from .decorators import expose
|
||||||
from .routing import lookup_controller, handle_lookup_traversal
|
from .routing import lookup_controller, handle_lookup_traversal
|
||||||
from .util import iscontroller
|
from .util import iscontroller
|
||||||
@@ -26,31 +27,41 @@ class RestController(object):
|
|||||||
'''
|
'''
|
||||||
_custom_actions = {}
|
_custom_actions = {}
|
||||||
|
|
||||||
|
def _get_args_for_controller(self, controller):
|
||||||
|
"""
|
||||||
|
Retrieve the arguments we actually care about. For Pecan applications
|
||||||
|
that utilize thread locals, we should truncate the first argument,
|
||||||
|
`self`. For applications that explicitly pass request/response
|
||||||
|
references as the first controller arguments, we should truncate the
|
||||||
|
first three arguments, `self, req, resp`.
|
||||||
|
"""
|
||||||
|
argspec = getargspec(controller)
|
||||||
|
from pecan import request
|
||||||
|
try:
|
||||||
|
request.path
|
||||||
|
except AttributeError:
|
||||||
|
return argspec.args[3:]
|
||||||
|
return argspec.args[1:]
|
||||||
|
|
||||||
@expose()
|
@expose()
|
||||||
def _route(self, args):
|
def _route(self, args, request=None):
|
||||||
'''
|
'''
|
||||||
Routes a request to the appropriate controller and returns its result.
|
Routes a request to the appropriate controller and returns its result.
|
||||||
|
|
||||||
Performs a bit of validation - refuses to route delete and put actions
|
Performs a bit of validation - refuses to route delete and put actions
|
||||||
via a GET request).
|
via a GET request).
|
||||||
'''
|
'''
|
||||||
|
if request is None:
|
||||||
|
from pecan import request
|
||||||
# convention uses "_method" to handle browser-unsupported methods
|
# convention uses "_method" to handle browser-unsupported methods
|
||||||
if request.environ.get('pecan.validation_redirected', False) is True:
|
method = request.params.get('_method', request.method).lower()
|
||||||
#
|
|
||||||
# If the request has been internally redirected due to a validation
|
|
||||||
# exception, we want the request method to be enforced as GET, not
|
|
||||||
# the `_method` param which may have been passed for REST support.
|
|
||||||
#
|
|
||||||
method = request.method.lower()
|
|
||||||
else:
|
|
||||||
method = request.params.get('_method', request.method).lower()
|
|
||||||
|
|
||||||
# make sure DELETE/PUT requests don't use GET
|
# make sure DELETE/PUT requests don't use GET
|
||||||
if request.method == 'GET' and method in ('delete', 'put'):
|
if request.method == 'GET' and method in ('delete', 'put'):
|
||||||
abort(405)
|
abort(405)
|
||||||
|
|
||||||
# check for nested controllers
|
# check for nested controllers
|
||||||
result = self._find_sub_controllers(args)
|
result = self._find_sub_controllers(args, request)
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -62,17 +73,17 @@ class RestController(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = handler(method, args)
|
result = handler(method, args, request)
|
||||||
|
|
||||||
#
|
#
|
||||||
# If the signature of the handler does not match the number
|
# If the signature of the handler does not match the number
|
||||||
# of remaining positional arguments, attempt to handle
|
# of remaining positional arguments, attempt to handle
|
||||||
# a _lookup method (if it exists)
|
# a _lookup method (if it exists)
|
||||||
#
|
#
|
||||||
argspec = getargspec(result[0])
|
argspec = self._get_args_for_controller(result[0])
|
||||||
num_args = len(argspec[0][1:])
|
num_args = len(argspec)
|
||||||
if num_args < len(args):
|
if num_args < len(args):
|
||||||
_lookup_result = self._handle_lookup(args)
|
_lookup_result = self._handle_lookup(args, request)
|
||||||
if _lookup_result:
|
if _lookup_result:
|
||||||
return _lookup_result
|
return _lookup_result
|
||||||
except exc.HTTPNotFound:
|
except exc.HTTPNotFound:
|
||||||
@@ -80,7 +91,7 @@ class RestController(object):
|
|||||||
# If the matching handler results in a 404, attempt to handle
|
# If the matching handler results in a 404, attempt to handle
|
||||||
# a _lookup method (if it exists)
|
# a _lookup method (if it exists)
|
||||||
#
|
#
|
||||||
_lookup_result = self._handle_lookup(args)
|
_lookup_result = self._handle_lookup(args, request)
|
||||||
if _lookup_result:
|
if _lookup_result:
|
||||||
return _lookup_result
|
return _lookup_result
|
||||||
raise
|
raise
|
||||||
@@ -88,7 +99,7 @@ class RestController(object):
|
|||||||
# return the result
|
# return the result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _handle_lookup(self, args):
|
def _handle_lookup(self, args, request):
|
||||||
# filter empty strings from the arg list
|
# filter empty strings from the arg list
|
||||||
args = list(six.moves.filter(bool, args))
|
args = list(six.moves.filter(bool, args))
|
||||||
|
|
||||||
@@ -97,7 +108,8 @@ class RestController(object):
|
|||||||
if args and iscontroller(lookup):
|
if args and iscontroller(lookup):
|
||||||
result = handle_lookup_traversal(lookup, args)
|
result = handle_lookup_traversal(lookup, args)
|
||||||
if result:
|
if result:
|
||||||
return lookup_controller(*result)
|
obj, remainder = result
|
||||||
|
return lookup_controller(obj, remainder, request)
|
||||||
|
|
||||||
def _find_controller(self, *args):
|
def _find_controller(self, *args):
|
||||||
'''
|
'''
|
||||||
@@ -109,7 +121,7 @@ class RestController(object):
|
|||||||
return obj
|
return obj
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _find_sub_controllers(self, remainder):
|
def _find_sub_controllers(self, remainder, request):
|
||||||
'''
|
'''
|
||||||
Identifies the correct controller to route to by analyzing the
|
Identifies the correct controller to route to by analyzing the
|
||||||
request URI.
|
request URI.
|
||||||
@@ -124,31 +136,33 @@ class RestController(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# get the args to figure out how much to chop off
|
# get the args to figure out how much to chop off
|
||||||
args = getargspec(getattr(self, method))
|
args = self._get_args_for_controller(getattr(self, method))
|
||||||
fixed_args = len(args[0][1:]) - len(
|
fixed_args = len(args) - len(
|
||||||
request.pecan.get('routing_args', [])
|
request.pecan.get('routing_args', [])
|
||||||
)
|
)
|
||||||
var_args = args[1]
|
var_args = getargspec(getattr(self, method)).varargs
|
||||||
|
|
||||||
# attempt to locate a sub-controller
|
# attempt to locate a sub-controller
|
||||||
if var_args:
|
if var_args:
|
||||||
for i, item in enumerate(remainder):
|
for i, item in enumerate(remainder):
|
||||||
controller = getattr(self, item, None)
|
controller = getattr(self, item, None)
|
||||||
if controller and not ismethod(controller):
|
if controller and not ismethod(controller):
|
||||||
self._set_routing_args(remainder[:i])
|
self._set_routing_args(request, remainder[:i])
|
||||||
return lookup_controller(controller, remainder[i + 1:])
|
return lookup_controller(controller, remainder[i + 1:],
|
||||||
|
request)
|
||||||
elif fixed_args < len(remainder) and hasattr(
|
elif fixed_args < len(remainder) and hasattr(
|
||||||
self, remainder[fixed_args]
|
self, remainder[fixed_args]
|
||||||
):
|
):
|
||||||
controller = getattr(self, remainder[fixed_args])
|
controller = getattr(self, remainder[fixed_args])
|
||||||
if not ismethod(controller):
|
if not ismethod(controller):
|
||||||
self._set_routing_args(remainder[:fixed_args])
|
self._set_routing_args(request, remainder[:fixed_args])
|
||||||
return lookup_controller(
|
return lookup_controller(
|
||||||
controller,
|
controller,
|
||||||
remainder[fixed_args + 1:]
|
remainder[fixed_args + 1:],
|
||||||
|
request
|
||||||
)
|
)
|
||||||
|
|
||||||
def _handle_unknown_method(self, method, remainder):
|
def _handle_unknown_method(self, method, remainder, request):
|
||||||
'''
|
'''
|
||||||
Routes undefined actions (like RESET) to the appropriate controller.
|
Routes undefined actions (like RESET) to the appropriate controller.
|
||||||
'''
|
'''
|
||||||
@@ -164,11 +178,12 @@ class RestController(object):
|
|||||||
abort(405)
|
abort(405)
|
||||||
sub_controller = getattr(self, remainder[0], None)
|
sub_controller = getattr(self, remainder[0], None)
|
||||||
if sub_controller:
|
if sub_controller:
|
||||||
return lookup_controller(sub_controller, remainder[1:])
|
return lookup_controller(sub_controller, remainder[1:],
|
||||||
|
request)
|
||||||
|
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
def _handle_get(self, method, remainder):
|
def _handle_get(self, method, remainder, request):
|
||||||
'''
|
'''
|
||||||
Routes ``GET`` actions to the appropriate controller.
|
Routes ``GET`` actions to the appropriate controller.
|
||||||
'''
|
'''
|
||||||
@@ -176,8 +191,8 @@ class RestController(object):
|
|||||||
if not remainder or remainder == ['']:
|
if not remainder or remainder == ['']:
|
||||||
controller = self._find_controller('get_all', 'get')
|
controller = self._find_controller('get_all', 'get')
|
||||||
if controller:
|
if controller:
|
||||||
argspec = getargspec(controller)
|
argspec = self._get_args_for_controller(controller)
|
||||||
fixed_args = len(argspec.args[1:]) - len(
|
fixed_args = len(argspec) - len(
|
||||||
request.pecan.get('routing_args', [])
|
request.pecan.get('routing_args', [])
|
||||||
)
|
)
|
||||||
if len(remainder) < fixed_args:
|
if len(remainder) < fixed_args:
|
||||||
@@ -194,13 +209,13 @@ class RestController(object):
|
|||||||
if controller:
|
if controller:
|
||||||
return controller, remainder[:-1]
|
return controller, remainder[:-1]
|
||||||
|
|
||||||
match = self._handle_custom_action(method, remainder)
|
match = self._handle_custom_action(method, remainder, request)
|
||||||
if match:
|
if match:
|
||||||
return match
|
return match
|
||||||
|
|
||||||
controller = getattr(self, remainder[0], None)
|
controller = getattr(self, remainder[0], None)
|
||||||
if controller and not ismethod(controller):
|
if controller and not ismethod(controller):
|
||||||
return lookup_controller(controller, remainder[1:])
|
return lookup_controller(controller, remainder[1:], request)
|
||||||
|
|
||||||
# finally, check for the regular get_one/get requests
|
# finally, check for the regular get_one/get requests
|
||||||
controller = self._find_controller('get_one', 'get')
|
controller = self._find_controller('get_one', 'get')
|
||||||
@@ -209,18 +224,18 @@ class RestController(object):
|
|||||||
|
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
def _handle_delete(self, method, remainder):
|
def _handle_delete(self, method, remainder, request):
|
||||||
'''
|
'''
|
||||||
Routes ``DELETE`` actions to the appropriate controller.
|
Routes ``DELETE`` actions to the appropriate controller.
|
||||||
'''
|
'''
|
||||||
if remainder:
|
if remainder:
|
||||||
match = self._handle_custom_action(method, remainder)
|
match = self._handle_custom_action(method, remainder, request)
|
||||||
if match:
|
if match:
|
||||||
return match
|
return match
|
||||||
|
|
||||||
controller = getattr(self, remainder[0], None)
|
controller = getattr(self, remainder[0], None)
|
||||||
if controller and not ismethod(controller):
|
if controller and not ismethod(controller):
|
||||||
return lookup_controller(controller, remainder[1:])
|
return lookup_controller(controller, remainder[1:], request)
|
||||||
|
|
||||||
# check for post_delete/delete requests first
|
# check for post_delete/delete requests first
|
||||||
controller = self._find_controller('post_delete', 'delete')
|
controller = self._find_controller('post_delete', 'delete')
|
||||||
@@ -234,23 +249,24 @@ class RestController(object):
|
|||||||
abort(405)
|
abort(405)
|
||||||
sub_controller = getattr(self, remainder[0], None)
|
sub_controller = getattr(self, remainder[0], None)
|
||||||
if sub_controller:
|
if sub_controller:
|
||||||
return lookup_controller(sub_controller, remainder[1:])
|
return lookup_controller(sub_controller, remainder[1:],
|
||||||
|
request)
|
||||||
|
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
def _handle_post(self, method, remainder):
|
def _handle_post(self, method, remainder, request):
|
||||||
'''
|
'''
|
||||||
Routes ``POST`` requests.
|
Routes ``POST`` requests.
|
||||||
'''
|
'''
|
||||||
# check for custom POST/PUT requests
|
# check for custom POST/PUT requests
|
||||||
if remainder:
|
if remainder:
|
||||||
match = self._handle_custom_action(method, remainder)
|
match = self._handle_custom_action(method, remainder, request)
|
||||||
if match:
|
if match:
|
||||||
return match
|
return match
|
||||||
|
|
||||||
controller = getattr(self, remainder[0], None)
|
controller = getattr(self, remainder[0], None)
|
||||||
if controller and not ismethod(controller):
|
if controller and not ismethod(controller):
|
||||||
return lookup_controller(controller, remainder[1:])
|
return lookup_controller(controller, remainder[1:], request)
|
||||||
|
|
||||||
# check for regular POST/PUT requests
|
# check for regular POST/PUT requests
|
||||||
controller = self._find_controller(method)
|
controller = self._find_controller(method)
|
||||||
@@ -259,10 +275,10 @@ class RestController(object):
|
|||||||
|
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
def _handle_put(self, method, remainder):
|
def _handle_put(self, method, remainder, request):
|
||||||
return self._handle_post(method, remainder)
|
return self._handle_post(method, remainder, request)
|
||||||
|
|
||||||
def _handle_custom_action(self, method, remainder):
|
def _handle_custom_action(self, method, remainder, request):
|
||||||
remainder = [r for r in remainder if r]
|
remainder = [r for r in remainder if r]
|
||||||
if remainder:
|
if remainder:
|
||||||
if method in ('put', 'delete'):
|
if method in ('put', 'delete'):
|
||||||
@@ -281,7 +297,7 @@ class RestController(object):
|
|||||||
if controller:
|
if controller:
|
||||||
return controller, remainder
|
return controller, remainder
|
||||||
|
|
||||||
def _set_routing_args(self, args):
|
def _set_routing_args(self, request, args):
|
||||||
'''
|
'''
|
||||||
Sets default routing arguments.
|
Sets default routing arguments.
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import warnings
|
import warnings
|
||||||
|
from inspect import getargspec
|
||||||
|
|
||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ class NonCanonicalPath(Exception):
|
|||||||
self.remainder = remainder
|
self.remainder = remainder
|
||||||
|
|
||||||
|
|
||||||
def lookup_controller(obj, remainder):
|
def lookup_controller(obj, remainder, request):
|
||||||
'''
|
'''
|
||||||
Traverses the requested url path and returns the appropriate controller
|
Traverses the requested url path and returns the appropriate controller
|
||||||
object, including default routes.
|
object, including default routes.
|
||||||
@@ -33,7 +34,8 @@ def lookup_controller(obj, remainder):
|
|||||||
notfound_handlers = []
|
notfound_handlers = []
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
obj, remainder = find_object(obj, remainder, notfound_handlers)
|
obj, remainder = find_object(obj, remainder, notfound_handlers,
|
||||||
|
request)
|
||||||
handle_security(obj)
|
handle_security(obj)
|
||||||
return obj, remainder
|
return obj, remainder
|
||||||
except (exc.HTTPNotFound, PecanNotFound):
|
except (exc.HTTPNotFound, PecanNotFound):
|
||||||
@@ -55,7 +57,8 @@ def lookup_controller(obj, remainder):
|
|||||||
and len(obj._pecan['argspec'].args) > 1
|
and len(obj._pecan['argspec'].args) > 1
|
||||||
):
|
):
|
||||||
raise exc.HTTPNotFound
|
raise exc.HTTPNotFound
|
||||||
return lookup_controller(*result)
|
obj_, remainder_ = result
|
||||||
|
return lookup_controller(obj_, remainder_, request)
|
||||||
else:
|
else:
|
||||||
raise exc.HTTPNotFound
|
raise exc.HTTPNotFound
|
||||||
|
|
||||||
@@ -77,7 +80,7 @@ def handle_lookup_traversal(obj, args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def find_object(obj, remainder, notfound_handlers):
|
def find_object(obj, remainder, notfound_handlers, request):
|
||||||
'''
|
'''
|
||||||
'Walks' the url path in search of an action for which a controller is
|
'Walks' the url path in search of an action for which a controller is
|
||||||
implemented and returns that controller object along with what's left
|
implemented and returns that controller object along with what's left
|
||||||
@@ -114,7 +117,21 @@ def find_object(obj, remainder, notfound_handlers):
|
|||||||
|
|
||||||
route = getattr(obj, '_route', None)
|
route = getattr(obj, '_route', None)
|
||||||
if iscontroller(route):
|
if iscontroller(route):
|
||||||
next_obj, next_remainder = route(remainder)
|
if len(getargspec(route).args) == 2:
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"The function signature for %s.%s._route is changing "
|
||||||
|
"in the next version of pecan.\nPlease update to: "
|
||||||
|
"`def _route(self, args, request)`." % (
|
||||||
|
obj.__class__.__module__,
|
||||||
|
obj.__class__.__name__
|
||||||
|
)
|
||||||
|
),
|
||||||
|
DeprecationWarning
|
||||||
|
)
|
||||||
|
next_obj, next_remainder = route(remainder)
|
||||||
|
else:
|
||||||
|
next_obj, next_remainder = route(remainder, request)
|
||||||
cross_boundary(route, next_obj)
|
cross_boundary(route, next_obj)
|
||||||
return next_obj, next_remainder
|
return next_obj, next_remainder
|
||||||
|
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ class TestControllerArguments(PecanTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@expose()
|
@expose()
|
||||||
def _route(self, args):
|
def _route(self, args, request):
|
||||||
if hasattr(self, args[0]):
|
if hasattr(self, args[0]):
|
||||||
return getattr(self, args[0]), args[1:]
|
return getattr(self, args[0]), args[1:]
|
||||||
else:
|
else:
|
||||||
@@ -1519,3 +1519,27 @@ class TestEngines(PecanTestCase):
|
|||||||
r = app.get('/')
|
r = app.get('/')
|
||||||
assert r.status_int == 200
|
assert r.status_int == 200
|
||||||
assert b_("<h1>Hello, Jonathan!</h1>") in r.body
|
assert b_("<h1>Hello, Jonathan!</h1>") in r.body
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeprecatedRouteMethod(PecanTestCase):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app_(self):
|
||||||
|
class RootController(object):
|
||||||
|
|
||||||
|
@expose()
|
||||||
|
def index(self, *args):
|
||||||
|
return ', '.join(args)
|
||||||
|
|
||||||
|
@expose()
|
||||||
|
def _route(self, args):
|
||||||
|
return self.index, args
|
||||||
|
|
||||||
|
return TestApp(Pecan(RootController()))
|
||||||
|
|
||||||
|
def test_required_argument(self):
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
|
r = self.app_.get('/foo/bar/')
|
||||||
|
assert r.status_int == 200
|
||||||
|
assert b_('foo, bar') in r.body
|
||||||
|
|||||||
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