summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2010-12-27 16:25:15 -0500
committerChris McDonough <chrism@plope.com>2010-12-27 16:25:15 -0500
commit90a327b2cd9b9e6b27688dadcdf8125f091f242d (patch)
treee32b2e8f06e1eaa3e473c9a177f675fd5659951e
parente8db031e8cd22affb65254539ae210f64d37f36e (diff)
downloadpyramid-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.txt8
-rw-r--r--docs/narr/project.rst2
-rw-r--r--docs/narr/urldispatch.rst38
-rw-r--r--pyramid/paster.py109
-rw-r--r--pyramid/tests/test_config.py8
-rw-r--r--pyramid/tests/test_paster.py134
-rw-r--r--setup.py1
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
+
diff --git a/setup.py b/setup.py
index f698f06ae..d58641487 100644
--- a/setup.py
+++ b/setup.py
@@ -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
"""