├── .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 | --------------------------------------------------------------------------------