Welcome, guest | Sign In | My Account | Store | Cart

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.
Python, 209 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/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()

I wrote the MethodDispatcher to make it easier to port CherryPy applications to the Tornado framework. It makes it so that you can migrate your classes being served via CherryPy to Tornado with trivial modifications. Having said that, the MethodDispatcher can also significantly simplify the process of writing Tornado applications. It also improves code readability by making class methods more Pythonic and natural: URLs map directly to method names and the methods themselves are passed arguments like any other Python code (no need to use get_argument()).

Update: Fixed a bug where arguments with slashes (/) in them were getting included in the method variable (oops).

Rev 5: Made it so that URLs that didn't match any existing method would return a 404 error (lines 149 and 150).