diff --git a/falcon/cmd/print_routes.py b/falcon/cmd/print_routes.py new file mode 100644 index 0000000..795e0bd --- /dev/null +++ b/falcon/cmd/print_routes.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# Copyright 2013 by Rackspace Hosting, Inc. +# +# 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. +""" +Script that prints out the routes of an API instance. +""" + +from __future__ import print_function + +import inspect + +import falcon + + +def print_routes(api, verbose=False): # pragma: no cover + """ + Initial call. + + :param api: The falcon.API or callable that returns an instance to look at. + :type api: falcon.API or callable + :param verbose: If the output should be verbose. + :type verbose: bool + """ + traverse(api._router._roots, verbose=verbose) + + +def traverse(roots, parent='', verbose=False): + """ + Recursive call which also handles printing output. + + :param api: The falcon.API or callable that returns an instance to look at. + :type api: falcon.API or callable + :param parent: The parent uri path to the current iteration. + :type parent: str + :param verbose: If the output should be verbose. + :type verbose: bool + """ + for root in roots: + if root.method_map: + print('->', parent + '/' + root.raw_segment) + if verbose: + for method, func in root.method_map.items(): + if func.__name__ != 'method_not_allowed': + print('-->{0} {1}:{2}'.format( + method, + inspect.getsourcefile(func), + inspect.getsourcelines(func)[1])) + if root.children: + traverse(root.children, parent + '/' + root.raw_segment, verbose) + + +def main(): + """ + Main entrypoint. + """ + import argparse + + parser = argparse.ArgumentParser( + description='Example: print-api-routes myprogram:app') + parser.add_argument( + '-v', '--verbose', action='store_true', + help='Prints out information for each method.') + parser.add_argument( + 'api_module', + help='The module and api to inspect. Example: myapp.somemodule:api', + ) + args = parser.parse_args() + + try: + module, instance = args.api_module.split(':', 1) + except ValueError: + parser.error( + 'The api_module must include a colon between ' + 'the module and instnace') + api = getattr(__import__(module, fromlist=[True]), instance) + if not isinstance(api, falcon.API): + if callable(api): + api = api() + if not isinstance(api, falcon.API): + parser.error( + '{0} did not return a falcon.API instance'.format( + args.api_module)) + else: + parser.error( + 'The instance must be of falcon.API or be ' + 'a callable without args that returns falcon.API') + print_routes(api, verbose=args.verbose) + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 2bff901..f9752ba 100644 --- a/setup.py +++ b/setup.py @@ -111,7 +111,8 @@ setup( tests_require=['nose', 'ddt', 'testtools', 'requests', 'pyyaml'], entry_points={ 'console_scripts': [ - 'falcon-bench = falcon.cmd.bench:main' + 'falcon-bench = falcon.cmd.bench:main', + 'falcon-print-routes = falcon.cmd.print_routes:main' ] } ) diff --git a/tests/test_cmd_print_api.py b/tests/test_cmd_print_api.py new file mode 100644 index 0000000..1947674 --- /dev/null +++ b/tests/test_cmd_print_api.py @@ -0,0 +1,52 @@ +import sys +import testtools + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +from falcon import API +from falcon.cmd import print_routes + + +_api = API() +_api.add_route('/test', None) + +STDOUT = sys.stdout + + +class TestPrintRoutes(testtools.TestCase): + + def setUp(self): + """Capture stdout""" + super(TestPrintRoutes, self).setUp() + self.output = StringIO() + sys.stdout = self.output + + def tearDown(self): + """Reset stdout""" + super(TestPrintRoutes, self).tearDown() + self.output.close() + del self.output + sys.stdout = STDOUT + + def test_traverse_with_verbose(self): + """Ensure traverse finds the proper routes and adds verbose output.""" + print_routes.traverse( + _api._router._roots, + verbose=True) + + route, options = self.output.getvalue().strip().split('\n') + self.assertEquals('-> /test', route) + self.assertTrue('OPTIONS' in options) + self.assertTrue('falcon/falcon/responders.py:' in options) + + def test_traverse(self): + """Ensure traverse finds the proper routes.""" + print_routes.traverse( + _api._router._roots, + verbose=False) + + route = self.output.getvalue().strip() + self.assertEquals('-> /test', route)