Welcome, guest | Sign In | My Account | Store | Cart
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2009 Dan McDougall
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

# Meta
__version__ = '1.0'
__license__ = "Apache License, Version 2.0"
__version_info__ = (1, 0)
__author__ = 'Dan McDougall <YouKnowWho@YouKnowWhat.com>'

"""
Tornado MethodDispatcher
========================
The MethodDispatcher is a subclass of tornado.web.RequestHandler that will use
the methods contained in subclasses of MethodDispatcher to handle requests.  In
other words, instead of having to make a new RequestHandler class for every URL
in your application you can subclass MethodDispatcher and use the methods
contained therein *as* your URLs.

The MethodDispatcher also adds the convenience of automatically passing
arguments to your class methods.  So there is no need to use Tornado's
get_argument() method.

Example
-------
To demonstrate the advantages of using MethodDispatcher I'll present a standard
Tornado app with multiple URLs and re-write it using MethodDispatcher...

The standard Tornado way
------------------------
    class Foo(tornado.web.RequestHandler):
        def get(self):
            self.write('foo')

    class Bar(tornado.web.RequestHandler):
        def get(self):
            self.write('bar')

    class SimonSays(tornado.web.RequestHandler):
        def get(self):
            say = self.get_argument("say")
            self.write('Simon says, %s' % `say`)

    application = tornado.web.Application([
        (r"/foo", Foo),
        (r"/bar", Bar),
        (r"/simonsays", SimonSays),
    ])

The MethodDispatcher way
------------------------
    class FooBar(MethodDispatcher):
        def foo(self):
            self.write("foo")

        def bar(self):
            self.write("bar")

        def simonsays(self, say):
            self.write("Simon Says, %s" % `say`)

    application = tornado.web.Application([
        (r"/.*", FooBar)
    ])

Notes
-----
As you can see from the above example, using the MethodDispatcher can
significantly reduce the complexity of Tornado applications.  Here's some other
things to keep in mind when using the MethodDispatcher:

 * MethodDispatcher will ignore any methods that begin with an underscore (_).
   This prevents builtins and private methods from being exposed to the web.
 * The '/' path is special: It always maps to self.index().
 * MethodDispatcher does not require that your methods distinquish between GET
   and POST requests.  Whether a GET or POST is performed the matching method
   will be called with any passed arguments or POSTed data.  Because of the way
   this works you should not use get() and post() in your MethodDispatcher
   subclasses unless you want to override this functionality.
 * When an argument is passed with a single value (/simonsays?say=hello) the
   value passed to the argument will be de-listed.  In other words, it will be
   passed to your method like so:  {'say': 'hello'}.  This overrides the
   default Tornado behavior which would return the value as a list:
   {'say': ['hello']}.  If more than one value is passed MethodDispatcher will
   use the default behavior.
"""

import tornado.web

def delist_arguments(args):
    """
    Takes a dictionary, 'args' and de-lists any single-item lists then
    returns the resulting dictionary.

    In other words, {'foo': ['bar']} would become {'foo': 'bar'}
    """
    for arg, value in args.items():
        if len(value) == 1:
            args[arg] = value[0]
    return args

class MethodDispatcher(tornado.web.RequestHandler):
    """
    Subclasss this to have all of your class's methods exposed to the web
    for both GET and POST requests.  Class methods that start with an
    underscore (_) will be ignored.
    """

    def _dispatch(self):
        """
        Load up the requested URL if it matches one of our own methods.
        Skip methods that start with an underscore (_).
        """
        args = None
        # Sanitize argument lists:
        if self.request.arguments:
            args = delist_arguments(self.request.arguments)
        # Special index method handler:
        if self.request.uri.endswith('/'):
            func = getattr(self, 'index', None)
            if args:
                return func(**args)
            else:
                return func()
        path = self.request.uri.split('?')[0]
        method = path.split('/')[-1]
        if not method.startswith('_'):
            func = getattr(self, method, None)
            if func:
                if args:
                    return func(**args)
                else:
                    return func()
            else:
                raise tornado.web.HTTPError(404)
        else:
            raise tornado.web.HTTPError(404)

    def get(self):
        """Returns self._dispatch()"""
        return self._dispatch()

    def post(self):
        """Returns self._dispatch()"""
        return self._dispatch()

class TestMethodDispatcher(MethodDispatcher):
    """
    This class demonstrates how to use MethodDispatcher() to load URLs based
    on the names of the methods in a class.  It also demonstrates the special
    index handler and how arguments are passed to methods.
    """
    def index(self):
        self.write(
            "The special index handler has been called.<br />"
            "Try <a href='testing?foo=bar&say=hello'>"
            "testing?foo=bar&say=hello</a>"
        )

    def testing(self, **kwargs):
        self.write("testing() got the following arguments: %s" % `kwargs`)

class TestApplication(tornado.web.Application):
    """
    This Tornado Application container demonstrates how to include a
    MethodDispatcher class in your app via a catch-all regular expression.
    """
    def __init__(self):
        handlers = [
            # Basic catch-all URL handler:
            (r"/.*", TestMethodDispatcher),
            # You can also use a MethodDispatcher in sub-paths:
            (r"/test/.*", TestMethodDispatcher)
        ]
        tornado.web.Application.__init__(self, handlers)

def test_method_dispatcher():
    """
    This function can be used to test that the MethodDispatcher is working
    properly. It is called automatically when this script is executed directly.
    """
    import logging
    from tornado.ioloop import IOLoop
    from tornado.httpserver import HTTPServer
    from tornado.options import define, options, parse_command_line
    define("port", default=8888, help="Run on the given port", type=int)
    parse_command_line()
    logging.info(
        "Test Server Listening on http://0.0.0.0:%s/" % options.port
    )
    http_server = HTTPServer(TestApplication())
    http_server.listen(options.port)
    IOLoop.instance().start()

if __name__ == "__main__":
    test_method_dispatcher()

History

  • revision 5 (14 years ago)
  • previous revisions are not available