summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/source/changes.rst5
-rw-r--r--docs/source/deployment.rst80
-rw-r--r--docs/source/hooks.rst6
-rw-r--r--docs/source/routing.rst58
-rw-r--r--pecan/core.py17
-rw-r--r--pecan/routing.py7
-rw-r--r--pecan/tests/test_base.py29
-rw-r--r--pecan/tests/test_hooks.py30
-rw-r--r--setup.py2
9 files changed, 208 insertions, 26 deletions
diff --git a/docs/source/changes.rst b/docs/source/changes.rst
index 8ca46aa..c0a21ba 100644
--- a/docs/source/changes.rst
+++ b/docs/source/changes.rst
@@ -1,3 +1,8 @@
+0.3.1
+=====
+* ``on_error`` hooks can now return a Pecan Response objects.
+* Minor documentation and release tooling updates.
+
0.3.0
=====
* Pecan now supports Python 2.6, 2.7, 3.2, and 3.3.
diff --git a/docs/source/deployment.rst b/docs/source/deployment.rst
index b95ab39..9cc1fec 100644
--- a/docs/source/deployment.rst
+++ b/docs/source/deployment.rst
@@ -11,7 +11,7 @@ probably vary.
.. ::
- While Pecan comes packaged with a simple server *for development use*
+ While Pecan comes packaged with a simple server *for development use*
(``pecan serve``), using a *production-ready* server similar to the ones
described in this document is **very highly encouraged**.
@@ -82,7 +82,7 @@ Considerations for Static Files
-------------------------------
Pecan comes with static file serving (e.g., CSS, Javascript, images)
-middleware which is **not** recommended for use in production.
+middleware which is **not** recommended for use in production.
In production, Pecan doesn't serve media files itself; it leaves that job to
whichever web server you choose.
@@ -93,7 +93,7 @@ performance reasons). There are several popular ways to accomplish this. Here
are two:
1. Set up a proxy server (such as `nginx <http://nginx.org/en>`__, `cherokee
- <http://www.cherokee-project.com>`__, or `lighttpd
+ <http://www.cherokee-project.com>`__, :ref:`cherrypy`, or `lighttpd
<http://www.lighttpd.net/>`__) to serve static files and proxy application
requests through to your WSGI application:
@@ -204,3 +204,77 @@ Pecan's default project::
$ pecan create simpleapp && cd simpleapp
$ python setup.py develop
$ gunicorn_pecan config.py
+
+
+.. _cherrypy:
+
+CherryPy
+++++++++
+
+`CherryPy <http://cherrypy.org/>`__ offers a pure Python HTTP/1.1-compliant WSGI
+thread-pooled web server. It can support Pecan applications easily and even
+serve static files like a production server would do.
+
+The examples that follow are geared towards using CherryPy as the server in
+charge of handling a Pecan app along with serving static files.
+
+::
+
+ $ pip install cherrypy
+ $ pecan create simpleapp && cd simpleapp
+ $ python setup.py develop
+
+To run with CherryPy, the easiest approach is to create a script in the root of
+the project (alongside ``setup.py``), so that we can describe how our example
+application should be served. This is how the script (named ``run.py``) looks::
+
+ import os
+ import cherrypy
+ from cherrypy import wsgiserver
+
+ from pecan import deploy
+
+ simpleapp_wsgi_app = deploy('/path/to/production_config.py')
+
+ public_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'public'))
+
+ # A dummy class for our Root object
+ # necessary for some CherryPy machinery
+ class Root(object):
+ pass
+
+ def make_static_config(static_dir_name):
+ """
+ All custom static configurations are set here, since most are common, it
+ makes sense to generate them just once.
+ """
+ static_path = os.path.join('/', static_dir_name)
+ path = os.path.join(public_path, static_dir_name)
+ configuration = {
+ static_path: {
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': path
+ }
+ }
+ return cherrypy.tree.mount(Root(), '/', config=configuration)
+
+ # Assuming your app has media on diferent paths, like 'css', and 'images'
+ application = wsgiserver.WSGIPathInfoDispatcher({
+ '/': simpleapp_wsgi_app,
+ '/css': make_static_config('css'),
+ '/images': make_static_config('images')
+ }
+ )
+
+ server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 8080), application,
+ server_name='simpleapp')
+
+ try:
+ server.start()
+ except KeyboardInterrupt:
+ print "Terminating server..."
+ server.stop()
+
+To start the server, simply call it with the Python executable::
+
+ $ python run.py
diff --git a/docs/source/hooks.rst b/docs/source/hooks.rst
index 3185355..b7c01ce 100644
--- a/docs/source/hooks.rst
+++ b/docs/source/hooks.rst
@@ -46,8 +46,10 @@ object which includes useful information, such as
the request and response objects, and which controller was selected by
Pecan's routing.
-:func:`on_error` is passed a shared state object **and** the original exception.
-
+:func:`on_error` is passed a shared state object **and** the original exception. If
+an :func:`on_error` handler returns a Response object, this response will be returned
+to the end user and no furthur :func:`on_error` hooks will be executed.
+
Attaching Hooks
---------------
diff --git a/docs/source/routing.rst b/docs/source/routing.rst
index 9c02a63..3a40241 100644
--- a/docs/source/routing.rst
+++ b/docs/source/routing.rst
@@ -8,7 +8,7 @@ HTTP request to a controller, and then the method to call.
Object-dispatch begins by splitting the path into a list of components
and then walking an object path, starting at the root controller. You
can imagine your application's controllers as a tree of objects
-(branches of the object tree map directly to URL paths).
+(branches of the object tree map directly to URL paths).
Let's look at a simple bookstore application:
@@ -90,7 +90,7 @@ type of the response body.
generic = False
)
def hello(self):
- return 'Hello World'
+ return 'Hello World'
Let's look at an example using ``template`` and ``content_type``:
@@ -114,21 +114,21 @@ arguments.
@expose('json')
The first tells Pecan to serialize the response namespace using JSON
-serialization when the client requests ``/hello.json``.
+serialization when the client requests ``/hello.json``.
::
@expose('text_template.mako', content_type='text/plain')
The second tells Pecan to use the ``text_template.mako`` template file when the
-client requests ``/hello.txt``.
+client requests ``/hello.txt``.
::
@expose('html_template.mako')
-The third tells Pecan to use the ``html_template.mako`` template file when the
-client requests ``/hello.html``. If the client requests ``/hello``, Pecan will
+The third tells Pecan to use the ``html_template.mako`` template file when the
+client requests ``/hello.html``. If the client requests ``/hello``, Pecan will
use the ``text/html`` content type by default.
.. seealso::
@@ -141,10 +141,10 @@ Pecan's Routing Algorithm
-------------------------
Sometimes, the standard object-dispatch routing isn't adequate to properly
-route a URL to a controller. Pecan provides several ways to short-circuit
+route a URL to a controller. Pecan provides several ways to short-circuit
the object-dispatch system to process URLs with more control, including the
special :func:`_lookup`, :func:`_default`, and :func:`_route` methods. Defining these
-methods on your controller objects provides additional flexibility for
+methods on your controller objects provides additional flexibility for
processing all or part of a URL.
@@ -189,7 +189,7 @@ Use the utility function :func:`abort` to raise HTTP errors.
Routing to Subcontrollers with ``_lookup``
------------------------------------------
-The :func:`_lookup` special method provides a way to process a portion of a URL,
+The :func:`_lookup` special method provides a way to process a portion of a URL,
and then return a new controller object to route to for the remainder.
A :func:`_lookup` method may accept one or more arguments, segments
@@ -232,7 +232,7 @@ where ``primary_key == 8``.
Falling Back with ``_default``
------------------------------
-The :func:`_default` method is called as a last resort when no other controller
+The :func:`_default` method is called as a last resort when no other controller
methods match the URL via standard object-dispatch.
::
@@ -253,16 +253,16 @@ methods match the URL via standard object-dispatch.
return 'I cannot say hello in that language'
-In the example above, a request to ``/spanish`` would route to
+In the example above, a request to ``/spanish`` would route to
:func:`RootController._default`.
-
+
Defining Customized Routing with ``_route``
-------------------------------------------
-The :func:`_route` method allows a controller to completely override the routing
+The :func:`_route` method allows a controller to completely override the routing
mechanism of Pecan. Pecan itself uses the :func:`_route` method to implement its
-:class:`RestController`. If you want to design an alternative routing system on
+:class:`RestController`. If you want to design an alternative routing system on
top of Pecan, defining a base controller class that defines a :func:`_route` method
will enable you to have total control.
@@ -329,12 +329,38 @@ The same effect can be achieved with HTTP ``POST`` body variables:
$ curl -X POST "http://localhost:8080/" -H "Content-Type: application/x-www-form-urlencoded" -d "arg=foo"
foo
+Handling File Uploads
+---------------------
+
+Pecan makes it easy to handle file uploads via standard multipart forms. Simply
+define your form with a file input:
+
+.. code-block:: html
+
+ <form action="/upload" method="POST" enctype="multipart/form-data">
+ <input type="file" name="file" />
+ <button type="submit">Upload</button>
+ </form>
+
+You can then read the uploaded file off of the request object in your
+application's controller:
+
+::
+
+ from pecan import expose, request
+
+ class RootController(object):
+ @expose()
+ def upload(self):
+ assert isinstance(request.POST['file'], cgi.FieldStorage)
+ data = request.POST['file'].file.read()
+
Helper Functions
----------------
Pecan also provides several useful helper functions for moving between
-different routes. The :func:`redirect` function allows you to issue internal or
-``HTTP 302`` redirects.
+different routes. The :func:`redirect` function allows you to issue internal or
+``HTTP 302`` redirects.
.. seealso::
diff --git a/pecan/core.py b/pecan/core.py
index 8f6e32f..0aad76a 100644
--- a/pecan/core.py
+++ b/pecan/core.py
@@ -304,7 +304,11 @@ class Pecan(object):
hooks = reversed(state.hooks)
for hook in hooks:
- getattr(hook, hook_type)(*args)
+ result = getattr(hook, hook_type)(*args)
+ # on_error hooks can choose to return a Response, which will
+ # be used instead of the standard error pages.
+ if hook_type == 'on_error' and isinstance(result, Response):
+ return result
def get_args(self, req, all_params, remainder, argspec, im_self):
'''
@@ -569,11 +573,16 @@ class Pecan(object):
environ['pecan.original_exception'] = e
# if this is not an internal redirect, run error hooks
+ on_error_result = None
if not isinstance(e, ForwardRequestException):
- self.handle_hooks('on_error', state, e)
+ on_error_result = self.handle_hooks('on_error', state, e)
- if not isinstance(e, exc.HTTPException):
- raise
+ # if the on_error handler returned a Response, use it.
+ if isinstance(on_error_result, Response):
+ state.response = on_error_result
+ else:
+ if not isinstance(e, exc.HTTPException):
+ raise
finally:
# handle "after" hooks
self.handle_hooks('after', state)
diff --git a/pecan/routing.py b/pecan/routing.py
index 31cbf92..fc1a7d4 100644
--- a/pecan/routing.py
+++ b/pecan/routing.py
@@ -46,6 +46,13 @@ def lookup_controller(obj, url_path):
# traversal
result = handle_lookup_traversal(obj, remainder)
if result:
+ # If no arguments are passed to the _lookup, yet the
+ # argspec requires at least one, raise a 404
+ if (
+ remainder == ['']
+ and len(obj._pecan['argspec'].args) > 1
+ ):
+ raise
return lookup_controller(*result)
else:
raise exc.HTTPNotFound
diff --git a/pecan/tests/test_base.py b/pecan/tests/test_base.py
index 37751b7..e5ec6af 100644
--- a/pecan/tests/test_base.py
+++ b/pecan/tests/test_base.py
@@ -182,6 +182,35 @@ class TestLookups(PecanTestCase):
assert r.status_int == 404
+class TestCanonicalLookups(PecanTestCase):
+
+ @property
+ def app_(self):
+ class LookupController(object):
+ def __init__(self, someID):
+ self.someID = someID
+
+ @expose()
+ def index(self):
+ return self.someID
+
+ class UserController(object):
+ @expose()
+ def _lookup(self, someID, *remainder):
+ return LookupController(someID), remainder
+
+ class RootController(object):
+ users = UserController()
+
+ return TestApp(Pecan(RootController()))
+
+ def test_canonical_lookup(self):
+ assert self.app_.get('/users', expect_errors=404).status_int == 404
+ assert self.app_.get('/users/', expect_errors=404).status_int == 404
+ assert self.app_.get('/users/100').status_int == 302
+ assert self.app_.get('/users/100/').body == b_('100')
+
+
class TestControllerArguments(PecanTestCase):
@property
diff --git a/pecan/tests/test_hooks.py b/pecan/tests/test_hooks.py
index 9c3192e..44963bf 100644
--- a/pecan/tests/test_hooks.py
+++ b/pecan/tests/test_hooks.py
@@ -1,7 +1,10 @@
from webtest import TestApp
from six import b as b_
+from six import u as u_
from six.moves import cStringIO as StringIO
+from webob import Response
+
from pecan import make_app, expose, redirect, abort
from pecan.hooks import (
PecanHook, TransactionHook, HookController, RequestViewerHook
@@ -133,6 +136,33 @@ class TestHooks(PecanTestCase):
assert run_hook[0] == 'on_route'
assert run_hook[1] == 'error'
+ def test_on_error_response_hook(self):
+ run_hook = []
+
+ class RootController(object):
+ @expose()
+ def causeerror(self):
+ return [][1]
+
+ class ErrorHook(PecanHook):
+ def on_error(self, state, e):
+ run_hook.append('error')
+
+ r = Response()
+ r.text = u_('on_error')
+
+ return r
+
+ app = TestApp(make_app(RootController(), hooks=[
+ ErrorHook()
+ ]))
+
+ response = app.get('/causeerror')
+
+ assert len(run_hook) == 1
+ assert run_hook[0] == 'error'
+ assert response.text == 'on_error'
+
def test_prioritized_hooks(self):
run_hook = []
diff --git a/setup.py b/setup.py
index 102c0cf..b84d063 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ import sys
from setuptools import setup, find_packages
-version = '0.3.0'
+version = '0.3.1'
#
# determine requirements