diff options
-rw-r--r-- | docs/source/changes.rst | 5 | ||||
-rw-r--r-- | docs/source/deployment.rst | 80 | ||||
-rw-r--r-- | docs/source/hooks.rst | 6 | ||||
-rw-r--r-- | docs/source/routing.rst | 58 | ||||
-rw-r--r-- | pecan/core.py | 17 | ||||
-rw-r--r-- | pecan/routing.py | 7 | ||||
-rw-r--r-- | pecan/tests/test_base.py | 29 | ||||
-rw-r--r-- | pecan/tests/test_hooks.py | 30 | ||||
-rw-r--r-- | setup.py | 2 |
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 = [] @@ -2,7 +2,7 @@ import sys from setuptools import setup, find_packages -version = '0.3.0' +version = '0.3.1' # # determine requirements |