├── .gitignore
├── README.md
├── requirements.dev
├── setup.cfg
├── setup.py
├── tests
├── generic_1.html
├── generic_2.html
└── test_routes.py
└── tornroutes
└── __init__.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # git-ls-files --others --exclude-from=.git/info/exclude
2 | # Lines that start with '#' are comments.
3 | # For a project mostly in C, the following would be a good set of
4 | # exclude patterns (uncomment them if you want to use them):
5 | # *.[oa]
6 | # *~
7 | .DS_Store
8 | *.swp
9 | *.pyc
10 |
11 | build/*
12 | venv
13 | MANIFEST
14 | dist
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tornroutes
2 |
3 | Provides a route decorator for the Tornado framework.
4 |
5 | ## Installation
6 |
7 | This package is available via pip with `pip install tornroutes` for the stable
8 | version.
9 |
10 | You can also install the latest source (also usually very stable) by the
11 | following:
12 |
13 | ```
14 | pip install -e git+git://github.com/nod/tornroutes.git#egg=tornroutes
15 | ```
16 |
17 | ## Testing
18 |
19 | Pretty well tested. You can run them with `nosetests` if you have nose
20 | installed.
21 |
22 | The following run in the base directory of the repo will run the tests:
23 |
24 | ```bash
25 | virtualenv venv
26 | source venv/bin/activate
27 | pip install -r requirements.txt
28 | nosetests
29 | ```
30 |
31 | ## Usage
32 |
33 | The best source of information is the comments in tornroutes/__init__.py.
34 |
35 | ### simple example
36 |
37 | ```python
38 | import tornado.web
39 | from tornroutes import route
40 |
41 | @route('/blah')
42 | class SomeHandler(tornado.web.RequestHandler):
43 | pass
44 |
45 | t = tornado.web.Application(route.get_routes(), {'some app': 'settings'}
46 | ```
47 |
48 | ### `generic_route` example
49 |
50 | Example carried over from above, if you have a template at `generic.html` and
51 | you want it to get rendered at a certain uri, do the following:
52 |
53 | ```python
54 | generic_route('/generic/?', 'generic.html')
55 | ```
56 |
57 | ### `authed_generic_route` example
58 |
59 | Often, tornado projects end up defining something like `BaseHandler` that
60 | extends `tornado.web.RequestHandler` and defines methods necessary for
61 | authentication.
62 |
63 | This handler might look like:
64 |
65 | ```python
66 | class BaseHandler(RequestHandler):
67 | def get_current_user(self):
68 | """ do stuff here to authenticate the user """
69 | return None # NONE SHALL PASS
70 | ```
71 |
72 | Which allows us to provide authenticated generic routes:
73 |
74 | ```python
75 | from tornroutes import authed_generic_route
76 |
77 | authed_generic_route('/locked', 'some_locked_template.html', BaseHandler)
78 | ```
79 |
80 | Now, you'll have a uri of `/locked` being answered with a render of
81 | `some_locked_template.html` if the user is authenticated, otherwise, they get
82 | redirected to the value set at `settings.login_url`.
83 |
84 |
85 |
--------------------------------------------------------------------------------
/requirements.dev:
--------------------------------------------------------------------------------
1 | nose
2 | tornado
3 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from distutils.core import setup
2 | VERSION = "0.5.1"
3 | setup(
4 | name = 'tornroutes',
5 | version = VERSION,
6 | description = 'Tornado Web Route Decorator',
7 | author = 'Jeremy Kelley',
8 | author_email = 'jeremy@33ad.org',
9 | url = 'https://github.com/nod/tornroutes',
10 | download_url = 'https://github.com/nod/tornroutes/tarball/0.5',
11 | license = "http://www.apache.org/licenses/LICENSE-2.0",
12 | packages = ['tornroutes'],
13 | keywords = ['tornado', 'routes', 'http']
14 | )
15 |
16 |
--------------------------------------------------------------------------------
/tests/generic_1.html:
--------------------------------------------------------------------------------
1 | generic template
2 |
--------------------------------------------------------------------------------
/tests/generic_2.html:
--------------------------------------------------------------------------------
1 | 2nd generic template
2 |
--------------------------------------------------------------------------------
/tests/test_routes.py:
--------------------------------------------------------------------------------
1 |
2 | import os.path
3 | import sys
4 | import unittest
5 | import json
6 |
7 | import tornado.web
8 | from tornado.testing import AsyncHTTPTestCase
9 |
10 | # make sure we get our local tornroutes before anything else
11 | sys.path = [os.path.abspath(os.path.dirname(__file__))] + sys.path
12 |
13 | from tornroutes import (
14 | route, route_redirect,
15 | generic_route, authed_generic_route
16 | )
17 |
18 | # NOTE - right now, the route_redirect function is not tested.
19 |
20 |
21 | class RouteTests(unittest.TestCase):
22 |
23 | def setUp(self):
24 | route._routes = [] # reach in and ensure this is clean
25 | @route('/xyz')
26 | class XyzFake(object):
27 | pass
28 |
29 | route_redirect('/redir_elsewhere', '/abc')
30 |
31 | @route('/abc', name='abc')
32 | class AbcFake(object):
33 | pass
34 |
35 | route_redirect('/other_redir', '/abc', name='other')
36 |
37 |
38 | def test_num_routes(self):
39 | self.assertTrue( len(route.get_routes()) == 4 ) # 2 routes + 2 redir
40 |
41 | def test_routes_ordering(self):
42 | # our third handler's url route should be '/abc'
43 | self.assertTrue( route.get_routes()[2].reverse() == '/abc' )
44 |
45 | def test_routes_name(self):
46 | # our first handler's url route should be '/xyz'
47 | t = tornado.web.Application(route.get_routes(), {})
48 | self.assertTrue( t.reverse_url('abc') )
49 | self.assertTrue( t.reverse_url('other') )
50 |
51 |
52 | class ParameterizedRouteTests(AsyncHTTPTestCase):
53 |
54 | def get_app(self):
55 | return tornado.web.Application(route.get_routes())
56 |
57 | @classmethod
58 | def setUpClass(cls):
59 | route._routes = []
60 |
61 | @route(r'/redirect/(?P\w+)', name='param')
62 | class ParamRedirectHandler(tornado.web.RequestHandler):
63 | def get(self, param):
64 | self.redirect(self.reverse_url(param))
65 |
66 | @route('/abc', name='abc')
67 | class AbcFake(object):
68 | pass
69 |
70 | def test_param_passed(self):
71 | response = self.fetch('/redirect/abc', follow_redirects=False)
72 | assert response.code == 302, "Parameter passed through to handler"
73 |
74 |
75 | class GenericRouteTests(unittest.TestCase):
76 |
77 | def setUp(self):
78 | route._routes = [] # clean things out just in case
79 |
80 | def test_generic_routes_default_handler(self):
81 | generic_route('/something', 'some_template.html')
82 | assert len(route.get_routes()) == 1
83 |
84 |
85 | class BogonAuthedHandler(tornado.web.RequestHandler):
86 | """
87 | used in testing authed_generic_route(...)
88 | """
89 | def get_current_user(self):
90 | return None
91 |
92 |
93 | class TestGenericRoute(AsyncHTTPTestCase):
94 |
95 | def get_app(self):
96 | return tornado.web.Application(
97 | route.get_routes(),
98 | template_path = os.path.dirname(__file__),
99 | login_url = '/faked_for_authed_generic',
100 | )
101 |
102 | @classmethod
103 | def setUpClass(cls):
104 | route._routes = []
105 |
106 | # must be done here prior to get_app being called
107 | generic_route('/generic', 'generic_1.html')
108 | generic_route('/other', 'generic_2.html')
109 | authed_generic_route('/locked', 'generic_1.html', BogonAuthedHandler)
110 |
111 | def test_authed_generic(self):
112 | response = self.fetch('/locked', follow_redirects=False)
113 | assert response.code == 302, "Didn't redirect to login page"
114 |
115 | def test_generic_render(self):
116 | generic_1 = open(
117 | os.path.join( os.path.dirname(__file__), 'generic_1.html')
118 | ).read()
119 | generic_2 = open(
120 | os.path.join( os.path.dirname(__file__), 'generic_2.html')
121 | ).read()
122 |
123 | response = self.fetch('/generic')
124 | assert bytearray(generic_1.strip(), encoding='utf-8') == response.body.strip()
125 | assert response.code == 200
126 |
127 | response = self.fetch('/other')
128 | assert bytearray(generic_2.strip(), encoding='utf-8') == response.body.strip()
129 | assert response.code == 200
130 |
131 |
132 | class KeywordArgsRouteTests(AsyncHTTPTestCase):
133 | def get_app(self):
134 | return tornado.web.Application(route.get_routes())
135 |
136 | @classmethod
137 | def setUpClass(cls):
138 | route._routes = []
139 |
140 | @route('/abc', name='abc', kwargs={'data': 'test'})
141 | class KeywordArgsHandler(tornado.web.RequestHandler):
142 | def initialize(self, **kwargs):
143 | self.data = kwargs.get('data', None)
144 |
145 | def get(self):
146 | self.write({'abc': {'data': self.data}})
147 |
148 |
149 | def test_num_routes(self):
150 | self.assertTrue( len(route.get_routes()) == 1 )
151 |
152 | def test_kwargs_passed(self):
153 | response = self.fetch('/abc', follow_redirects=False)
154 | assert response.code == 200
155 | assert json.loads(response.body) == {"abc": {"data": "test"}}
156 |
--------------------------------------------------------------------------------
/tornroutes/__init__.py:
--------------------------------------------------------------------------------
1 | import tornado.web
2 |
3 | class route(object):
4 | """
5 | decorates RequestHandlers and builds up a list of routables handlers
6 |
7 | Tech Notes (or "What the *@# is really happening here?")
8 | --------------------------------------------------------
9 |
10 | Everytime @route('...') is called, we instantiate a new route object which
11 | saves off the passed in URI. Then, since it's a decorator, the function is
12 | passed to the route.__call__ method as an argument. We save a reference to
13 | that handler with our uri in our class level routes list then return that
14 | class to be instantiated as normal.
15 |
16 | Later, we can call the classmethod route.get_routes to return that list of
17 | tuples which can be handed directly to the tornado.web.Application
18 | instantiation.
19 |
20 | Example
21 | -------
22 |
23 | ```python
24 | @route('/some/path')
25 | class SomeRequestHandler(RequestHandler):
26 | def get(self):
27 | goto = self.reverse_url('other')
28 | self.redirect(goto)
29 |
30 | # so you can do myapp.reverse_url('other')
31 | @route('/some/other/path', name='other')
32 | class SomeOtherRequestHandler(RequestHandler):
33 | def get(self):
34 | goto = self.reverse_url('SomeRequestHandler')
35 | self.redirect(goto)
36 |
37 | # for passing uri parameters
38 | @route(r'/some/(?P\w+)/path')
39 | class SomeParameterizedRequestHandler(RequestHandler):
40 | def get(self, parameterized):
41 | goto = self.reverse_url(parameterized)
42 | self.redirect(goto)
43 |
44 | my_routes = route.get_routes()
45 | ```
46 |
47 | Credit
48 | -------
49 | Jeremy Kelley - initial work
50 | Peter Bengtsson - redirects, named routes and improved comments
51 | Ben Darnell - general awesomeness
52 | """
53 |
54 | _routes = []
55 |
56 | def __init__(self, uri, name=None, kwargs={}):
57 | self._uri = uri
58 | self.name = name
59 | self.kwargs = kwargs
60 |
61 | def __call__(self, _handler):
62 | """gets called when we class decorate"""
63 | name = self.name or _handler.__name__
64 | self._routes.append(tornado.web.url(self._uri, _handler, self.kwargs, name=name))
65 | return _handler
66 |
67 | @classmethod
68 | def get_routes(cls):
69 | return cls._routes
70 |
71 | # route_redirect provided by Peter Bengtsson via the Tornado mailing list
72 | # and then improved by Ben Darnell.
73 | # Use it as follows to redirect other paths into your decorated handler.
74 | #
75 | # from routes import route, route_redirect
76 | # route_redirect('/smartphone$', '/smartphone/')
77 | # route_redirect('/iphone/$', '/smartphone/iphone/', name='iphone_shortcut')
78 | # @route('/smartphone/$')
79 | # class SmartphoneHandler(RequestHandler):
80 | # def get(self):
81 | # ...
82 | def route_redirect(from_, to, name=None):
83 | route._routes.append(tornado.web.url(
84 | from_,
85 | tornado.web.RedirectHandler,
86 | dict(url=to),
87 | name=name ))
88 |
89 | # maps a template to a route.
90 | def generic_route(uri, template, handler = None):
91 | h_ = handler or tornado.web.RequestHandler
92 | @route(uri, name=uri)
93 | class generic_handler(h_):
94 | _template = template
95 | def get(self):
96 | return self.render(self._template)
97 | return generic_handler
98 |
99 | def authed_generic_route(uri, template, handler):
100 | """
101 | Provides authenticated mapping of template render to route.
102 |
103 | :param: uri: the route path
104 | :param: template: the template path to render
105 | :param: handler: a subclass of tornado.web.RequestHandler that provides all
106 | the necessary methods for resolving current_user
107 | """
108 | @route(uri, name=uri)
109 | class authed_handler(handler):
110 | _template = template
111 | @tornado.web.authenticated
112 | def get(self):
113 | return self.render(self._template)
114 | return authed_handler
115 |
--------------------------------------------------------------------------------