diff options
| author | Chris McDonough <chrism@plope.com> | 2010-12-27 16:25:15 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2010-12-27 16:25:15 -0500 |
| commit | 90a327b2cd9b9e6b27688dadcdf8125f091f242d (patch) | |
| tree | e32b2e8f06e1eaa3e473c9a177f675fd5659951e | |
| parent | e8db031e8cd22affb65254539ae210f64d37f36e (diff) | |
| download | pyramid-90a327b2cd9b9e6b27688dadcdf8125f091f242d.tar.gz pyramid-90a327b2cd9b9e6b27688dadcdf8125f091f242d.tar.bz2 pyramid-90a327b2cd9b9e6b27688dadcdf8125f091f242d.zip | |
- Add ``paster proute`` command which displays a summary of the routing
table. See the narrative documentation section within the "URL Dispatch"
chapter entitled "Displaying All Application Routes".
- Added narrative documentation section within the "URL Dispatch" chapter
entitled "Displaying All Application Routes" (for ``paster proutes``
command).
| -rw-r--r-- | CHANGES.txt | 8 | ||||
| -rw-r--r-- | docs/narr/project.rst | 2 | ||||
| -rw-r--r-- | docs/narr/urldispatch.rst | 38 | ||||
| -rw-r--r-- | pyramid/paster.py | 109 | ||||
| -rw-r--r-- | pyramid/tests/test_config.py | 8 | ||||
| -rw-r--r-- | pyramid/tests/test_paster.py | 134 | ||||
| -rw-r--r-- | setup.py | 1 |
7 files changed, 281 insertions, 19 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index e58a1dc76..cba3a9800 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -25,6 +25,10 @@ Features arguments to add_route work by raising an exception during configuration if view-related arguments exist but no ``view`` argument is passed. +- Add ``paster proute`` command which displays a summary of the routing + table. See the narrative documentation section within the "URL Dispatch" + chapter entitled "Displaying All Application Routes". + Paster Templates ---------------- @@ -81,6 +85,10 @@ Documentation - Merge "Static Assets" chapter into the "Assets" chapter. +- Added narrative documentation section within the "URL Dispatch" chapter + entitled "Displaying All Application Routes" (for ``paster proutes`` + command). + 1.0a7 (2010-12-20) ================== diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 36f2d6975..55a2711f3 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -256,6 +256,8 @@ create`` -generated project. Within a project generated by the single: IPython single: paster pshell +.. _interactive_shell: + The Interactive Shell --------------------- diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 76eca454d..4c601340f 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -1231,6 +1231,44 @@ which you started the application from. For example: See :ref:`environment_chapter` for more information about how, and where to set these values. +.. index:: + pair: routes; printing + single: paster proutes + +Displaying All Application Routes +--------------------------------- + +You can use the ``paster proutes`` command in a terminal window to print a +summary of routes related to your application. Much like the ``paster +pshell`` command (see :ref:`interactive shell`), the ``paster proutes`` +command accepts two arguments. The first argument to ``proutes`` is the path +to your application's ``.ini`` file. The second is the ``app`` section name +inside the ``.ini`` file which points to your application. + +For example: + +.. code-block:: text + :linenos: + + [chrism@thinko MyProject]$ ../bin/paster proutes development.ini MyProject + Name Pattern View + ---- ------- ---- + home / <function my_view> + home2 / <function my_view> + another /another None + static/ static/*subpath <static_view object> + catchall /*subpath <function static_view> + +``paster proutes`` generates a table. The table has three columns: a Name +name column, a Pattern column, and a View column. The items listed in the +Name column are route names, the items listen in the Pattern column are route +patterns, and the items listed in the View column are representations of the +view callable that will be invoked when a request matches the associated +route pattern. The view column may show ``None`` if no associated view +callable could be found. If no routes are configured within your +application, nothing will be printed to the console when ``paster proutes`` +is executed. + References ---------- diff --git a/pyramid/paster.py b/pyramid/paster.py index c8bc36e80..57dac6b32 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -63,7 +63,22 @@ def get_app(config_file, name, loadapp=loadapp): return app _marker = object() -class PShellCommand(Command): + +class PCommand(Command): + get_app = staticmethod(get_app) # hook point + get_root = staticmethod(get_root) # hook point + group_name = 'pyramid' + interact = (interact,) # for testing + loadapp = (loadapp,) # for testing + verbose = 3 + + def __init__(self, *arg, **kw): + # needs to be in constructor to support Jython (used to be at class + # scope as ``usage = '\n' + __doc__``. + self.usage = '\n' + self.__doc__ + Command.__init__(self, *arg, **kw) + +class PShellCommand(PCommand): """Open an interactive shell with a :app:`Pyramid` app loaded. This command accepts two positional arguments: @@ -88,7 +103,6 @@ class PShellCommand(Command): min_args = 2 max_args = 2 - group_name = 'pyramid' parser = Command.standard_parser(simulate=True) parser.add_option('-d', '--disable-ipython', @@ -96,18 +110,6 @@ class PShellCommand(Command): dest='disable_ipython', help="Don't use IPython even if it is available") - interact = (interact,) # for testing - loadapp = (loadapp,) # for testing - get_app = staticmethod(get_app) # hook point - get_root = staticmethod(get_root) # hook point - verbose = 3 - - def __init__(self, *arg, **kw): - # needs to be in constructor to support Jython (used to be at class - # scope as ``usage = '\n' + __doc__``. - self.usage = '\n' + self.__doc__ - Command.__init__(self, *arg, **kw) - def command(self, IPShell=_marker): if IPShell is _marker: try: #pragma no cover @@ -136,3 +138,82 @@ class PShellCommand(Command): closer() BFGShellCommand = PShellCommand # b/w compat forever + +class PRoutesCommand(PCommand): + """Print all URL dispatch routes used by a Pyramid application in the + order in which they are evaluated. Each route includes the name of the + route, the pattern of the route, and the view callable which will be + invoked when the route is matched. + + This command accepts two positional arguments: + + ``config_file`` -- specifies the PasteDeploy config file to use + for the interactive shell. + + ``section_name`` -- specifies the section name in the PasteDeploy + config file that represents the application. + + Example:: + + $ paster proutes myapp.ini main + + .. note:: You should use a ``section_name`` that refers to the + actual ``app`` section in the config file that points at + your Pyramid app without any middleware wrapping, or this + command will almost certainly fail. + """ + summary = "Print all URL dispatch routes related to a Pyramid application" + min_args = 2 + max_args = 2 + stdout = sys.stdout + + parser = Command.standard_parser(simulate=True) + + def _get_mapper(self, app): + from pyramid.config import Configurator + registry = app.registry + config = Configurator(registry = registry) + return config.get_routes_mapper() + + def out(self, msg): # pragma: no cover + print msg + + def command(self): + from pyramid.request import Request + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + from zope.interface import Interface + from zope.interface import providedBy + config_file, section_name = self.args + app = self.get_app(config_file, section_name, loadapp=self.loadapp[0]) + registry = app.registry + mapper = self._get_mapper(app) + if mapper is not None: + routes = mapper.get_routes() + fmt = '%-15s %-30s %-25s' + if not routes: + return + self.out(fmt % ('Name', 'Pattern', 'View')) + self.out( + fmt % ('-'*len('Name'), '-'*len('Pattern'), '-'*len('View'))) + for route in routes: + request_iface = registry.queryUtility(IRouteRequest, + name=route.name) + view_callable = None + if request_iface is not None: + if route.factory is None: + context_iface = Interface + else: + request = Request.blank('/') + inst = route.factory(request) + context_iface = providedBy(inst) + # try with factory instance as context; views registered for + # a more general interface will be found if the context + # iface is very specific + view_callable = registry.adapters.lookup( + (IViewClassifier, request_iface, context_iface), + IView, name='', default=None) + self.out(fmt % (route.name, route.pattern, view_callable)) + + diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 84e8289be..c129b21ae 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -2338,7 +2338,7 @@ class ConfiguratorTests(unittest.TestCase): config.add_route('name', '/pattern', view_attr='abc') except ConfigurationError: pass - else: + else: # pragma: no cover raise AssertionError def test_add_route_no_view_with_view_context(self): @@ -2348,7 +2348,7 @@ class ConfiguratorTests(unittest.TestCase): config.add_route('name', '/pattern', view_context=DummyContext) except ConfigurationError: pass - else: + else: # pragma: no cover raise AssertionError def test_add_route_no_view_with_view_permission(self): @@ -2358,7 +2358,7 @@ class ConfiguratorTests(unittest.TestCase): config.add_route('name', '/pattern', view_permission='edit') except ConfigurationError: pass - else: + else: # pragma: no cover raise AssertionError def test_add_route_no_view_with_view_renderer(self): @@ -2368,7 +2368,7 @@ class ConfiguratorTests(unittest.TestCase): config.add_route('name', '/pattern', view_renderer='json') except ConfigurationError: pass - else: + else: # pragma: no cover raise AssertionError def test__override_not_yet_registered(self): diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index e2478ac4f..a5f613f8e 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -111,6 +111,123 @@ class TestPShellCommand(unittest.TestCase): self.failUnless(interact.banner) self.assertEqual(apps, [app]) +class TestPRoutesCommand(unittest.TestCase): + def _getTargetClass(self): + from pyramid.paster import PRoutesCommand + return PRoutesCommand + + def _makeOne(self): + return self._getTargetClass()('proutes') + + def test_no_routes(self): + command = self._makeOne() + mapper = DummyMapper() + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + app = DummyApp() + loadapp = DummyLoadApp(app) + command.loadapp = (loadapp,) + command.args = ('/foo/bar/myapp.ini', 'myapp') + result = command.command() + self.assertEqual(result, None) + self.assertEqual(L, []) + + def test_single_route_no_views_registered(self): + command = self._makeOne() + route = DummyRoute('a', '/a') + mapper = DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + app = DummyApp() + loadapp = DummyLoadApp(app) + command.loadapp = (loadapp,) + command.args = ('/foo/bar/myapp.ini', 'myapp') + result = command.command() + self.assertEqual(result, None) + self.assertEqual(len(L), 3) + self.assertEqual(L[-1].split(), ['a', '/a', 'None']) + + def test_single_route_one_view_registered(self): + from zope.interface import Interface + from pyramid.registry import Registry + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + registry = Registry() + def view():pass + class IMyRoute(Interface): + pass + registry.registerAdapter(view, + (IViewClassifier, IMyRoute, Interface), + IView, '') + registry.registerUtility(IMyRoute, IRouteRequest, name='a') + command = self._makeOne() + route = DummyRoute('a', '/a') + mapper = DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + app = DummyApp() + app.registry = registry + loadapp = DummyLoadApp(app) + command.loadapp = (loadapp,) + command.args = ('/foo/bar/myapp.ini', 'myapp') + result = command.command() + self.assertEqual(result, None) + self.assertEqual(len(L), 3) + self.assertEqual(L[-1].split()[:4], ['a', '/a', '<function', 'view']) + + def test_single_route_one_view_registered_with_factory(self): + from zope.interface import Interface + from zope.interface import implements + from pyramid.registry import Registry + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + registry = Registry() + def view():pass + class IMyRoot(Interface): + pass + class Root(object): + implements(IMyRoot) + class IMyRoute(Interface): + pass + registry.registerAdapter(view, + (IViewClassifier, IMyRoute, IMyRoot), + IView, '') + registry.registerUtility(IMyRoute, IRouteRequest, name='a') + command = self._makeOne() + def factory(request): + return Root() + route = DummyRoute('a', '/a', factory=factory) + mapper = DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + app = DummyApp() + app.registry = registry + loadapp = DummyLoadApp(app) + command.loadapp = (loadapp,) + command.args = ('/foo/bar/myapp.ini', 'myapp') + result = command.command() + self.assertEqual(result, None) + self.assertEqual(len(L), 3) + self.assertEqual(L[-1].split()[:4], ['a', '/a', '<function', 'view']) + + def test__get_mapper(self): + from pyramid.registry import Registry + from pyramid.urldispatch import RoutesMapper + command = self._makeOne() + registry = Registry() + class App: pass + app = App() + app.registry = registry + result = command._get_mapper(app) + self.assertEqual(result.__class__, RoutesMapper) + + class TestGetApp(unittest.TestCase): def _callFUT(self, config_file, section_name, loadapp): from pyramid.paster import get_app @@ -125,6 +242,8 @@ class TestGetApp(unittest.TestCase): self.assertEqual(loadapp.section_name, 'myapp') self.assertEqual(loadapp.relative_to, os.getcwd()) self.assertEqual(result, app) + + class Dummy: pass @@ -149,7 +268,7 @@ class DummyIPShell(object): dummy_root = Dummy() class DummyRegistry(object): - def queryUtility(self, iface, default=None): + def queryUtility(self, iface, default=None, name=''): return default dummy_registry = DummyRegistry() @@ -188,3 +307,16 @@ class DummyThreadLocalManager: def pop(self): self.popped.append(True) +class DummyMapper(object): + def __init__(self, *routes): + self.routes = routes + + def get_routes(self): + return self.routes + +class DummyRoute(object): + def __init__(self, name, pattern, factory=None): + self.name = name + self.pattern = pattern + self.factory = factory + @@ -88,6 +88,7 @@ setup(name='pyramid', pylons_sqla=pyramid.paster:PylonsSQLAlchemyProjectTemplate [paste.paster_command] pshell=pyramid.paster:PShellCommand + proutes=pyramid.paster:PRoutesCommand [console_scripts] bfg2pyramid = pyramid.fixers.fix_bfg_imports:main """ |
